Skip to content

v.parser.parse_file() permanently breaks spawn + os.execute() on macOS (~10s hang)Β #26633

@ylluminate

Description

@ylluminate

Describe the bug

Note: This affects the V1 parser (vlib/v/parser/) only. The V2 parser (vlib/v2/parser/) is NOT affected - it does not use g_timers and has been tested to confirm no fork() poisoning occurs.

Calling v.parser.parse_file() permanently breaks all subsequent spawn + os.execute() combinations on macOS. After parse_file() runs, any spawned thread that calls os.execute() hangs for approximately 10 seconds before returning.

This affects any V tool built on the V1 parser that combines AST parsing with subprocess execution (linters, formatters, dev tools).

Reproduction Steps

Minimal test case - save as /tmp/v1_parser_fork_test.v:

import os
import time
import v.ast
import v.parser
import v.pref

fn run_fmt(path string) string {
	r := os.execute('v fmt -verify ${path} 2>&1')
	return '${r.exit_code}'
}

fn main() {
	path := '/tmp/v1_parser_fork_test.v'

	// spawn + os.execute BEFORE parse_file - fast
	sw := time.new_stopwatch()
	t := spawn run_fmt(path)
	result := t.wait()
	println('fmt BEFORE parse_file: ${sw.elapsed().milliseconds()}ms result=${result}')

	// call parse_file (V1 parser)
	mut tbl := ast.new_table()
	prefs := pref.new_preferences()
	_ := parser.parse_file(path, mut tbl, .skip_comments, prefs)

	// spawn + os.execute AFTER parse_file - ~10s hang
	sw2 := time.new_stopwatch()
	t2 := spawn run_fmt(path)
	result2 := t2.wait()
	println('fmt AFTER parse_file: ${sw2.elapsed().milliseconds()}ms result=${result2}')
}

Build and run:

v -o /tmp/v1_parser_fork_test /tmp/v1_parser_fork_test.v && /tmp/v1_parser_fork_test

Expected Behavior

Both calls should complete in similar time (~15-50ms).

Current Behavior

fmt BEFORE parse_file: 13ms result=0
fmt AFTER parse_file: 10095ms result=0

The second call hangs for ~10 seconds - consistently.

V2 Parser: Confirmed NOT Affected

Same test pattern using v2.parser:

fmt BEFORE v2 parse: 17ms result=0
v2 parse took: 0ms
fmt AFTER v2 parse: 13ms result=0

V2's parser does not trigger this issue because it does not use the g_timers global.

Possible Solution

Root Cause Analysis

v.parser.parse_file() calls util.timing_start('PARSE') which initializes the g_timers global (vlib/v/util/timers.v:9). This global contains a shared map field which compiles to a pthread_rwlock_t backed by Mach semaphores on macOS.

Once this rwlock is initialized, fork() (used by C.popen() inside os.execute()) in a multi-threaded context becomes unsafe - the child process inherits a broken state where the Mach semaphore ports from the parent's rwlock are never released. The ~10s delay is consistent with a macOS kernel timeout on orphaned Mach semaphore ports.

Key observations:

  • V1 parser only - V2 parser does not use g_timers and is confirmed safe
  • Just importing v.parser is safe - the poison only occurs when parse_file() is called
  • Both -gc none and default Boehm GC are affected - this is a fork() issue, not a GC issue
  • The effect is permanent for the lifetime of the process - once poisoned, all subsequent spawn + os.execute combinations hang
  • macOS-specific (the Mach semaphore backing for pthread_rwlock_t)

Workaround

Run all spawn + os.execute() cycles before calling parse_file():

// 1. Launch and complete ALL subprocesses first
t_compile := spawn run_v_compile(path)
t_fmt := spawn run_v_fmt(path)
compile_result := t_compile.wait()
fmt_result := t_fmt.wait()

// 2. THEN parse - no more forks needed after this
mut tbl := ast.new_table()
prefs := pref.new_preferences()
tree := parser.parse_file(path, mut tbl, .skip_comments, prefs)

Alternative: use v2.parser which does not trigger this issue.

Additional Information/Context

V version

0.5.0 849edf4

Environment details (OS name and version, etc.)

  • macOS (Darwin 25.3.0, Apple Silicon)
  • V 0.5.0 (tested with latest from v up)
  • Affects both -gc none and default GC
  • Affects both -prod and debug builds

Note

You can use the πŸ‘ reaction to increase the issue's priority for developers.

Please note that only the πŸ‘ reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugThis tag is applied to issues which reports bugs.OS: MacBugs/feature requests, that are specific to Mac OS.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions