-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
builtin: fix Boehm interior pointers and modal GC aborts on Windows (… #28914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
quaesitor-scientiam
wants to merge
2
commits into
vlang:master
Choose a base branch
from
quaesitor-scientiam:fix-28896-boehm-leak-closure-free
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Regression test for the hang in issue #28896. On Windows, Boehm's default | ||
| // fatal-error handler shows a modal message box and waits for someone to click | ||
| // it, so a console program or a test run stops instead of failing. V installs | ||
| // its own handler, which reports the error on stderr like other platforms do. | ||
| // | ||
| // The child program triggers a deterministic Boehm abort: under | ||
| // `-gc boehm_leak`, GC_FREE of a pointer Boehm does not own aborts with | ||
| // "Invalid pointer passed to free()". The child is run with a deadline, so a | ||
| // modal dialog shows up as a timeout failure rather than a hung test. | ||
| import os | ||
| import time | ||
|
|
||
| const vexe = @VEXE | ||
|
|
||
| const child_deadline = 60 * time.second | ||
|
|
||
| fn test_boehm_abort_is_reported_on_stderr_without_blocking() { | ||
| dir := os.join_path(os.vtmp_dir(), 'v_gc_abort_stderr_${os.getpid()}') | ||
| os.mkdir_all(dir) or { panic(err) } | ||
| defer { | ||
| os.rmdir_all(dir) or {} | ||
| } | ||
| source := os.join_path(dir, 'gc_abort_child.v') | ||
| os.write_file(source, [ | ||
| 'fn C.GC_FREE(voidptr)', | ||
| '', | ||
| 'fn main() {', | ||
| '\tmut not_heap := 0', | ||
| '\tunsafe { C.GC_FREE(voidptr(¬_heap)) }', | ||
| "\tprintln('not reached')", | ||
| '}', | ||
| ].join('\n')) or { panic(err) } | ||
| mut exe := os.join_path(dir, 'gc_abort_child') | ||
| $if windows { | ||
| exe += '.exe' | ||
| } | ||
| build := os.execute('${os.quoted_path(vexe)} -gc boehm_leak -o ${os.quoted_path(exe)} ${os.quoted_path(source)}') | ||
| if build.exit_code != 0 && build.output.contains('libgc') { | ||
| eprintln('skipping: no Boehm GC library available\n${build.output}') | ||
| return | ||
| } | ||
| assert build.exit_code == 0, build.output | ||
|
|
||
| mut child := os.new_process(exe) | ||
| child.set_redirect_stdio() | ||
| child.run() | ||
| started := time.now() | ||
| for child.is_alive() { | ||
| if time.since(started) > child_deadline { | ||
| child.signal_kill() | ||
| child.wait() | ||
| assert false, 'the Boehm abort blocked for more than ${child_deadline} (a modal dialog?)' | ||
| } | ||
| time.sleep(50 * time.millisecond) | ||
| } | ||
| child.wait() | ||
| stdout := child.stdout_slurp() | ||
| stderr := child.stderr_slurp() | ||
| child.close() | ||
| assert child.code != 0, 'the child exited cleanly; stdout: ${stdout}' | ||
| assert !stdout.contains('not reached') | ||
| assert stderr.contains('Invalid pointer passed to free()'), stderr | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Regression test for issue #28896. V reaches array data through pointers into | ||
| // the middle of a Boehm block (past the array header, and anywhere for slices), | ||
| // including from heap objects, so the runtime must have Boehm recognise interior | ||
| // pointers. The prebuilt libgc linked on Windows with tcc leaves that off, and a | ||
| // collection then freed blocks that live arrays and slices still used. | ||
| // | ||
| // This checks the setting itself: whether a particular block survives a | ||
| // collection depends on stale values in the conservatively scanned stack, which | ||
| // can keep a block alive by accident and make such a test pass without the fix. | ||
| fn C.GC_get_all_interior_pointers() int | ||
|
|
||
| fn test_boehm_recognises_interior_pointers() { | ||
| $if gcboehm ? { | ||
| assert C.GC_get_all_interior_pointers() == 1 | ||
| } $else { | ||
| eprintln('skipping: not a Boehm GC build') | ||
| assert true | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // vtest vflags: -gc boehm_leak | ||
| // Regression test for issue #28896. A managed V array points one header past the | ||
| // start of its Boehm block, and `-gc boehm_leak` builds Boehm with GC_DEBUG, | ||
| // which puts its own debug header in front of every object as well. The | ||
| // prebuilt libgc linked on Windows with tcc does not recognise interior pointers | ||
| // by default, so a collection freed array blocks that a heap object still used, | ||
| // and the next free() of one aborted with "Invalid pointer passed to free()". | ||
| fn C.GC_base(voidptr) voidptr | ||
| fn C.GC_size(voidptr) usize | ||
| fn C.GC_gcollect() | ||
| fn C.GC_noop1(u64) | ||
| fn C.GC_get_all_interior_pointers() int | ||
|
|
||
| const block_ints = 2048 | ||
|
|
||
| @[heap] | ||
| struct ArrayHolder { | ||
| mut: | ||
| items []int | ||
| } | ||
|
|
||
| // The array gets a Boehm large block of its own, so a dropped block cannot hide | ||
| // behind live neighbours. | ||
| @[noinline] | ||
| fn new_array_holder() &ArrayHolder { | ||
| mut holder := &ArrayHolder{} | ||
| for i in 0 .. block_ints { | ||
| holder.items << i | ||
| } | ||
| return holder | ||
| } | ||
|
|
||
| // clear_stack_residue overwrites the dead stack below the caller. Boehm scans | ||
| // the stack conservatively, and a stale copy of a pointer into the array block, | ||
| // left behind by building the array, would otherwise keep the block alive by | ||
| // itself and hide the bug. | ||
| @[noinline] | ||
| fn clear_stack_residue() { | ||
| mut scratch := [1024]u64{} | ||
| unsafe { | ||
| C.memset(&scratch[0], 0, sizeof(scratch)) | ||
| // Keeps the compiler from dropping the zeroing as a dead store. | ||
| C.GC_noop1(u64(&scratch[0])) | ||
| } | ||
| } | ||
|
|
||
| @[noinline] | ||
| fn churn_allocations() { | ||
| for i in 0 .. 64 { | ||
| mut junk := []u8{len: 256} | ||
| junk[0] = u8(i) | ||
| } | ||
| } | ||
|
|
||
| // array_block_intact reports whether the holder's array is still backed by the | ||
| // large Boehm block it was allocated in: Boehm must still know a block there, of | ||
| // at least the array's size, starting just before V's data (V's array header | ||
| // plus Boehm's debug header). A dropped block is either unknown to GC_base or | ||
| // has been reused for smaller objects. | ||
| @[noinline] | ||
| fn array_block_intact(holder &ArrayHolder) bool { | ||
| block := unsafe { voidptr(u64(holder.items.data) - u64(holder.items.offset)) } | ||
| base := C.GC_base(block) | ||
| if base == unsafe { nil } { | ||
| return false | ||
| } | ||
| return C.GC_size(base) >= usize(block_ints * sizeof(int)) && u64(block) - u64(base) < 256 | ||
| } | ||
|
|
||
| fn test_boehm_leak_recognises_interior_pointers() { | ||
| $if gcboehm_leak ? { | ||
| assert C.GC_get_all_interior_pointers() == 1 | ||
| } $else { | ||
| eprintln('skipping: not a -gc boehm_leak build') | ||
| assert true | ||
| } | ||
| } | ||
|
|
||
| fn test_boehm_leak_collection_keeps_a_heap_held_array_block() { | ||
| $if gcboehm_leak ? { | ||
| holder := new_array_holder() | ||
| clear_stack_residue() | ||
| for _ in 0 .. 3 { | ||
| churn_allocations() | ||
| C.GC_gcollect() | ||
| } | ||
| // Checked through GC_base rather than by freeing, so a dropped block | ||
| // fails an assertion instead of reaching Boehm's abort. | ||
| assert array_block_intact(holder) | ||
| assert holder.items.flags.has(.managed) | ||
| assert holder.items.len == block_ints | ||
| assert holder.items[0] == 0 | ||
| assert holder.items[block_ints - 1] == block_ints - 1 | ||
| } $else { | ||
| eprintln('skipping: not a -gc boehm_leak build') | ||
| assert true | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Print and flush the fatal message before chaining to the saved handler
When
GC_LOOP_ON_ABORTis set, Boehm'sGC_default_on_abort(NULL)still enters its infinite debugging loop (thirdparty/libgc/gc.c:27419–27429). Consequently this call never reaches thefprintf/fflushbelow, so the new Windows handler suppresses the fatal reason precisely when someone enables the collector's debugging mode. Boehm's original handler prints the message before looping. Please move the stderr output before the saved-handler call; this preserves both the diagnostic and the intentional debugging loop. A C harness using this ordering timed out with empty stderr, while the print-first ordering emitted the message before timing out.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed in commit bbb65c2.
internal_gc_abort_to_stderrnow prints and flushes the message first and only then chains to the saved handler with a nil message. The at-exit-collection flag andGC_LOOP_ON_ABORTbehave as before, just after the diagnostic, which is the same orderGC_default_on_abortuses.I reproduced it on Windows before changing anything. With
GC_LOOP_ON_ABORT=1the aborting child kept spinning with an empty stderr when built with tcc (the prebuiltlibgc.a). Built with gcc (gc.cfrom source), stderr only had Boehm's ownInvalid pointer passed to free(): <pointer>log line; the handler's line was missing in both cases.gc_boehm_abort_reports_to_stderr_test.vhas a new test for this. It runs the child withGC_LOOP_ON_ABORT=1, waits (with a deadline) for the handler's bare message line, then kills the spinning child. It failed with both compilers before the change and passes with both after it. It also passes on Linux, where V leaves Boehm's handler in place.