Skip to content

alloc_tracker: the leak dump registers first so it runs last, and a frame with no dynsym symbol prints its module and offset - #3966

Merged
borisbat merged 1 commit into
masterfrom
aleksisch/fix/leak-dump-exit-ordering
Sep 8, 2026
Merged

alloc_tracker: the leak dump registers first so it runs last, and a frame with no dynsym symbol prints its module and offset#3966
borisbat merged 1 commit into
masterfrom
aleksisch/fix/leak-dump-exit-ordering

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Behavior change: a POSIX RelWithDebInfo build stops reporting every process-lifetime cache as a leak, and stops exiting 1 on a clean run.

Why. The leak dump runs from an atexit handler that has to be registered FIRST to run LAST. Only MSVC did that, through #pragma init_seg(lib). On GCC and Clang the dump ran before every other static destructor and reported the caches those destructors free: 495 allocations over 50 sites on a hello-world, 1473 over 80 sites with -jit, and _Exit(1) every run. Separately dladdr resolves no symbol for a static or hidden function, and the POSIX frame printer answered ? - about 60 of those 80 stacks could not be read.

What changes.

  • init_priority(101) on the atexit registrar gives GCC and Clang the MSVC ordering, guarded by __has_attribute.
  • print_frame's POSIX arm gains a module-plus-offset tier for the frames dladdr cannot name, instead of answering ?.
  • ~Module_HV calls hv::async::cleanup(). libhv's GlobalThreadPool is a call_once heap singleton that only exitInstance() frees, and nothing called it.
  • The vendored libhv include dir is marked SYSTEM, because hthreadpool.h trips -Wsign-compare under -Werror.

Observable behavior.

  • daslang hello.das: 495 allocs / 50 sites / exit 1 -> no report, exit 0
  • daslang -jit hello.das: 1473 allocs / 583,205 B / 80 sites -> 16 allocs / 106,135 B / 16 sites, all LLVM ManagedStatic plus one libz3 static ctor
  • a frame with no .dynsym symbol: 0x7e6e7e4b39a1 ? -> 0x7e6e7e4b39a1 /path/liblibDaScriptDyn_runtime.so+0xab39a1
  • dastest --test tests/dasHV: driver exit 1 with a 7-site report -> exit 0, 143/143
  • run_tests_interpreter on POSIX RelWithDebInfo: target FAILED -> 13245 passed, 0 failed

Where to look. The registrar and print_frame in src/misc/alloc_tracker.cpp; the risky spot is the ordering change, which moves the dump after every static destructor - safe because getMap and getMutex placement-new into static storage and are never destructed.

Validation, claims, ledger

Validation

  • preflight --full did not complete on this tip. It was 19 minutes into a run against the previous tip when that tip was amended away, so its verdict would have described a commit that no longer exists; it was stopped rather than finished. Run against the pushed tip instead: the full tests suite via dastest (13268 tests, 13245 passed, 0 failed, 0 errors, 23 skipped), tests/dasHV (143/143, driver exit 0), src/misc/REVIEW.das OK, and the leak report itself on both arms. Earlier on the pre-amend tip: ctest -L small 117/123, the 6 red cells pre-existing and unrelated - 4 Not Run for binaries the incremental target never built, watchdog_smoke likewise, and C API jit enable failing on HOST_FAST_MATH unresolvable in modules/dasLLVM/daslib/llvm_jit_run.das:824, which is also red on the nightly linux Release fastmath lane.
  • The ordering mechanism is negative-controlled outside the tree: two TUs with the cache TU linked first: without the attribute the dump precedes the cache free on both g++ 13.3 and clang 20.1, with it the order flips.
  • No CI lane builds the configuration this fixes. DAS_TRACK_ALLOC is $<CONFIG:RelWithDebInfo> only, and the sole RelWithDebInfo job is build_windows_relwithdebinfo_nightly (schedule or dispatch, upstream repo only), where init_seg(lib) already gave the right order. CI cannot re-prove the POSIX arm.

Claims - stated, not tested

  • Darwin and wasm keep the fix. init_priority is accepted by clang for x86_64-apple-macos11, arm64-apple-ios14 and wasm32-unknown-emscripten (syntax-checked per target), but no lane builds RelWithDebInfo there, so the ordering itself is unverified on those targets. A break looks like today's Linux behavior: a full report of process-lifetime caches.
  • MSVC is unchanged, by inspection only: it takes the _MSC_VER arm and DAS_LEAK_DUMP_EARLY_INIT expands empty. Its symbolizer, including kMaxTrustedSymbolOffset, is untouched. Not compiled here.
  • The POSIX arm deliberately does NOT copy the MSVC distant-match guard. On glibc dladdr answers with no symbol rather than a far neighbour - every static function in the reports printed ?, and every symbol it did print carried a small offset - so the guard would never fire, and the pre-existing symbol branch had none either. Darwin's dladdr does return a far preceding symbol, so a misattributed name there is the failure mode; the fix is one comparison, to be added with a case in hand.

Not done

  • 16 sites remain under -jit, all third-party load-time state: LLVM ManagedStatic globals (cl::Option::addArgument, TimerGroup, DebugCounter, initSignalsOptions) and one 40-byte libz3 static ctor. They need LLVMShutdown(), and there is no hook to call it from: modules/dasLLVM/src/dasLLVM.cpp is a stub that does not link LLVM, module_jit.cpp reaches LLVM only through das-passed function pointers, and neither daslang nor daslib has a process-exit hook. LLVMShutdown is bound at modules/dasLLVM/bindings/llvm_func.das:52 with no caller. So -jit in RelWithDebInfo still exits 1.
  • build_windows_relwithdebinfo_nightly is green, so it is not surfacing the libhv leak either, although its ordering is correct and the leak is platform-independent. Its log was not retrievable, so the reason is open.
  • dasHV never calls hv::async::startup(), so its handler pool takes libhv's defaults (min 1, max hardware_concurrency(), 60s idle) and ignores DAS_JOBQUE_THREADS and the core-tier logic in src/misc/job_que.cpp. Unchanged here.
  • skills/memory_leak_detection.md indexes six leak-detection mechanisms and this tracker is not one of them, though it is the only one that changes a process's exit code.

…rame with no dynsym symbol prints its module and offset

The atexit handler is the last one only where init_seg(lib) exists, so on
GCC/Clang it ran before every other static dtor and reported each
process-lifetime cache the runtime frees there: 495 allocations over 50 sites
on a hello-world, 1473 over 80 with -jit. init_priority(101) on the registrar
restores the MSVC ordering, guarded by __has_attribute so a toolchain with
neither keeps today's behavior.

print_frame's POSIX arm printed a bare "?" whenever dladdr found no symbol,
which is every static and hidden-visibility function, and dropped the module
and offset that make the frame recoverable. It now falls through the same three
tiers as the MSVC arm - symbol+offset inside kMaxTrustedSymbolOffset, then
module+offset, then "?".

libhv's GlobalThreadPool is a call_once heap singleton, born on the first
request a handler dispatches and deleted only by exitInstance(), which nothing
called: ~Module_HV now calls hv::async::cleanup(). Including hasync.h needs the
vendored libhv include dir marked SYSTEM - hthreadpool.h trips -Wsign-compare
under -Werror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aleksisch
aleksisch force-pushed the aleksisch/fix/leak-dump-exit-ordering branch from 9b6ab74 to a8a0f51 Compare September 8, 2026 10:05
@borisbat
borisbat merged commit ad19f5f into master Sep 8, 2026
35 checks passed
@borisbat
borisbat deleted the aleksisch/fix/leak-dump-exit-ordering branch September 8, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants