Skip to content

builtin: fix Boehm interior pointers and modal GC aborts on Windows (… - #28914

Open
quaesitor-scientiam wants to merge 2 commits into
vlang:masterfrom
quaesitor-scientiam:fix-28896-boehm-leak-closure-free
Open

quaesitor-scientiam wants to merge 2 commits into
vlang:masterfrom
quaesitor-scientiam:fix-28896-boehm-leak-closure-free

Conversation

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor

…fix #28896)

V reaches array data through pointers into the middle of a Boehm block: a managed array points one header past the block start, and a slice points anywhere inside its owner's block. Boehm honours such pointers from the stack in any case, but from heap objects only when interior-pointer recognition is on, or at registered displacements.

Every libgc V compiles from source enables ALL_INTERIOR_POINTERS. The prebuilt libgc.a linked on Windows with tcc does not (GC_get_all_interior_pointers() returns 0), so a collection freed array blocks that heap objects still used:

Call GC_set_all_interior_pointers(1) before GC_INIT, unless a host already initialized the collector, so every build behaves like the ones V compiles from source. Also register the array-header displacement through the GC_REGISTER_DISPLACEMENT macro, which under GC_DEBUG maps to GC_debug_register_displacement and covers the debug header too; gc.h says the debugging variant must be used when debugging allocation is done. That keeps V's arrays safe when a host initialized the collector without interior pointers.

On Windows, Boehm reports a fatal error in a modal "Fatal error in GC" message box and waits for someone to dismiss it, so any fatal GC error hung a console program or a test run instead of failing it. Its other output (error details, leak reports) went to an <exe>.gc.log file, not stderr. Make Windows behave like Linux and macOS:

  • Build the bundled gc.c with NO_MSGBOX_ON_ERROR and CONSOLE_LOG, so the libgc V compiles from source (gcc, clang, msvc) writes everything to stderr and shows no message box.
  • The prebuilt libgc.a linked with tcc cannot be rebuilt from here, so also replace Boehm's abort handler on Windows. V's handler prints the message on stderr and still calls the default handler with a nil message for the rest of its work (skipping the at-exit leak collection, GC_LOOP_ON_ABORT); Boehm then ends the process. It is not installed when a host initialized the collector first.

 vlang#28896)

V reaches array data through pointers into the middle of a Boehm block: a
managed array points one header past the block start, and a slice points
anywhere inside its owner's block. Boehm honours such pointers from the stack
in any case, but from heap objects only when interior-pointer recognition is
on, or at registered displacements.

Every libgc V compiles from source enables ALL_INTERIOR_POINTERS. The prebuilt
libgc.a linked on Windows with tcc does not (GC_get_all_interior_pointers()
returns 0), so a collection freed array blocks that heap objects still used:

- `-gc boehm_leak` defines GC_DEBUG, which also puts a debug header in front
  of every object. The one displacement V registered (`+ sizeof(voidptr)`,
  for the array header) went through the plain GC_register_displacement, so
  the real offset was never registered. Collections dropped live lifetime
  bookkeeping arrays in builtin.closure, and the next free() of one aborted
  with "Invalid pointer passed to free()" in a modal message box, hanging
  closure_lifetime_api_test.v (vlang#28896).
- In every Boehm mode, a slice kept only by a heap object lost its owner's
  block at the next collection while the slice was still in use.

Call GC_set_all_interior_pointers(1) before GC_INIT, unless a host already
initialized the collector, so every build behaves like the ones V compiles
from source. Also register the array-header displacement through the
GC_REGISTER_DISPLACEMENT macro, which under GC_DEBUG maps to
GC_debug_register_displacement and covers the debug header too; gc.h says
the debugging variant must be used when debugging allocation is done. That
keeps V's arrays safe when a host initialized the collector without
interior pointers.

On Windows, Boehm reports a fatal error in a modal "Fatal error in GC"
message box and waits for someone to dismiss it, so any fatal GC error hung
a console program or a test run instead of failing it. Its other output
(error details, leak reports) went to an `<exe>.gc.log` file, not stderr.
Make Windows behave like Linux and macOS:

- Build the bundled gc.c with NO_MSGBOX_ON_ERROR and CONSOLE_LOG, so the
  libgc V compiles from source (gcc, clang, msvc) writes everything to
  stderr and shows no message box.
- The prebuilt libgc.a linked with tcc cannot be rebuilt from here, so also
  replace Boehm's abort handler on Windows. V's handler prints the message
  on stderr and still calls the default handler with a nil message for the
  rest of its work (skipping the at-exit leak collection, GC_LOOP_ON_ABORT);
  Boehm then ends the process. It is not installed when a host initialized
  the collector first.

Co-Authored-By: WOZCODE <contact@withwoz.com>
@quaesitor-scientiam quaesitor-scientiam self-assigned this Sep 24, 2026

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all five changed files at 47d84822482b6504edf75efd678f5cb508076eba, ignoring CI as requested. One finding is attached inline: the new abort handler suppresses its fatal diagnostic when GC_LOOP_ON_ABORT is enabled.

Validation: built this PR on Linux; all three new Boehm regression tests passed. Also ran closure_lifetime_api_test.v, which failed in test_closure_lifetime_freestanding_no_std_object_compile because the compiler rejects -freestanding; that command and test are unchanged by this PR. Windows execution was not available. The inline finding was reproduced separately with the same callback ordering in a small C harness linked against Boehm.

Comment thread vlib/builtin/builtin_d_gcboehm.c.v Outdated
fn internal_gc_abort_to_stderr(const_msg &char) {
// With a nil message the default handler only disables the at-exit leak
// collection (and honours GC_LOOP_ON_ABORT); it shows no message box.
C.v_gc_call_abort_func(gc_boehm_default_abort_func, unsafe { nil })

Copy link
Copy Markdown
Member

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_ABORT is set, Boehm's GC_default_on_abort(NULL) still enters its infinite debugging loop (thirdparty/libgc/gc.c:27419–27429). Consequently this call never reaches the fprintf/fflush below, 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.

Copy link
Copy Markdown
Contributor Author

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_stderr now prints and flushes the message first and only then chains to the saved handler with a nil message. The at-exit-collection flag and GC_LOOP_ON_ABORT behave as before, just after the diagnostic, which is the same order GC_default_on_abort uses.

I reproduced it on Windows before changing anything. With GC_LOOP_ON_ABORT=1 the aborting child kept spinning with an empty stderr when built with tcc (the prebuilt libgc.a). Built with gcc (gc.c from source), stderr only had Boehm's own Invalid pointer passed to free(): <pointer> log line; the handler's line was missing in both cases.

gc_boehm_abort_reports_to_stderr_test.v has a new test for this. It runs the child with GC_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.

… handler

With GC_LOOP_ON_ABORT set, Boehm's default abort handler never returns: it
spins so that a debugger can be attached (thirdparty/libgc/gc.c:27419). The
Windows handler added for vlang#28896 called it before printing, so in that mode
the fatal message was lost: stderr stayed empty with the prebuilt libgc.a
that tcc links, and only Boehm's own log line with the pointer appended was
left with the libgc built from source. Print and flush first, then chain,
in the same order as Boehm's own handler.

gc_boehm_abort_reports_to_stderr_test.v gets a second test that runs the
aborting child with GC_LOOP_ON_ABORT=1, waits for the handler's bare message
line and then kills the spinning child. Before this change it failed with tcc
(empty stderr) and with gcc (only Boehm's line); it now passes with both on
Windows, and on Linux.

From vlang#28914 pullrequestreview-5309842842.

Co-Authored-By: WOZCODE <contact@withwoz.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants