From 7d8a4c7ad99eab45fe533c094238a07d3893bfac Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Thu, 25 Jun 2026 20:27:04 +0100 Subject: [PATCH] fix(runtime): --debug leak tracker crashed with "undefined symbol: atexit" on the native backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The leak tracker registered its end-of-run report via glibc's bare `atexit`. That symbol lives only in the static libc_nonshared.a; it is NOT a dynamic export of libc.so.6. The QBE/cc path linked fine because gcc auto-pulls libc_nonshared.a, but the native backend's linkers (internal ELF linker and the native external-assembler cc invocation) do not, so a --debug build aborted at load time with `symbol lookup error: undefined symbol: atexit`. Register via __cxa_atexit instead — a genuine dynamic libc export that resolves on every link path. Its third argument is the DSO handle; nil registers the handler against the main program. The handler takes one (ignored) argument. Pre-existing bug (reproduces on v0.12.0); affects only --debug builds compiled with --backend native. Verified on all three link paths (QBE/cc, native internal linker, native external assembler) plus the full suite and all four fixpoints. --- compiler/src/main/pascal/runtime.arc.pas | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/main/pascal/runtime.arc.pas b/compiler/src/main/pascal/runtime.arc.pas index 141cb67..ec07ae9 100644 --- a/compiler/src/main/pascal/runtime.arc.pas +++ b/compiler/src/main/pascal/runtime.arc.pas @@ -60,7 +60,15 @@ procedure _AbstractMethodError; procedure _LeakTrackerEnable; procedure _LeakTrackerRegister(UserPtr: Pointer; ClassName: Pointer; UnitName: Pointer; Line: Int64); -procedure _libc_atexit(Fn: Pointer); external name 'atexit'; +{ Register an at-exit handler. Uses __cxa_atexit, NOT atexit: glibc's bare + `atexit` lives only in the static libc_nonshared.a (it is not a dynamic export + of libc.so.6), so a link that does not pull that archive — as the native + backend's internal and external linkers do not — leaves `atexit` unresolved at + load time. __cxa_atexit is a genuine dynamic libc export and resolves on + every link path. Its third argument is the DSO handle; nil registers the + handler against the main program. The handler is passed one (ignored) arg. } +function _libc_cxa_atexit(Fn, Arg, DsoHandle: Pointer): Integer; + external name '__cxa_atexit'; implementation @@ -366,7 +374,7 @@ begin GLTTable := PChar(_BlaiseGetMem(TableSize)); if GLTTable = nil then begin GLTEnabled := False; Exit end; _libc_memset(GLTTable, 0, Int64(TableSize)); - _libc_atexit(Pointer(@_LeakTrackerReport)); + _libc_cxa_atexit(Pointer(@_LeakTrackerReport), nil, nil); end; { ------------------------------------------------------------------ }