-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
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_timersand is confirmed safe - Just importing
v.parseris safe - the poison only occurs whenparse_file()is called - Both
-gc noneand default Boehm GC are affected - this is afork()issue, not a GC issue - The effect is permanent for the lifetime of the process - once poisoned, all subsequent
spawn+os.executecombinations 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
- Using spawn with os.execute inside a thread causes runtime errorΒ #25721 β
spawn+os.executeSIGSEGV with Boehm GC (different symptom, Linux) - os.fork fails on FreeBSD with tccΒ #24710 β FreeBSD fork + GC interaction
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 noneand default GC - Affects both
-prodand 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.