From f75b0ca033b8805c51a84c6f09587e9061f46eef Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 21 Sep 2026 09:45:54 +0700 Subject: [PATCH] A constructor naming a declaration is what clang could not sanitize --dev --sanitize did not compile. Any program, at any optimisation level: clang 20's AddressSanitizer module pass segfaults on the module, and the message it leaves behind is its own crash backtrace rather than anything about the program. Reduced here to five lines of IR, and the trigger is narrow -- @llvm.global_ctors naming a function that the module only *declares*. Both of ours are declarations, because both are C in the runtime. This predates the crash-handler lane and is not its doing: the one-entry table, with flan_dev_reg_enable alone in it, crashes the same way, and that entry has been emitted since the registry was armed from a constructor. What the lane did was add a second declaration to a shape that was already crashing, where nothing built the combination to notice. So the table names a local definition that calls the two, which is the shape clang emits for its own constructors, and it costs a call once before main. The ordering it fixes was unspecified before -- two entries at one priority -- and arming the registry before installing the handler is the order that was wanted. What it unblocks is the reason to care. flan_dev_crash_enable yields to ASan through a weak __asan_init so the two do not both own SIGSEGV, and that line could not have run, because the build it guards would not link. It does now: a dev build of dev-segv.flan at -O0 under ASan takes the fault, ASan reports it with the frame, and the handler stays out of the way instead of parking. No alias covers the combination, so that check was made by hand. --- lib/emit.ml | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/emit.ml b/lib/emit.ml index a0cf554..67763c6 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -4404,18 +4404,37 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = []) handler has to be installed before any program code can fault. Only in a dev build — the constructor is emitted here and nowhere else — so a release build dies exactly the way it always did. *) - Buffer.add_string m.out - "@llvm.global_ctors = appending global [2 x { i32, ptr, ptr }] \ - [{ i32, ptr, ptr } { i32 65535, ptr @flan_dev_reg_enable, ptr null }, \ - { i32, ptr, ptr } { i32 65535, ptr @flan_dev_crash_enable, ptr null }]\n"; + (* Both of them behind one constructor defined here, rather than named + directly in the table, and that shape is load-bearing rather than + tidiness. [llvm.global_ctors] naming a *declaration* — which both of + these are, they are C in the runtime — crashes clang 20's + AddressSanitizer module pass outright, so `--dev --sanitize` could not + compile any program at all; the whole combination was unreachable, and + that includes the [__asan_init] yield in [flan_dev_crash_enable] that + is the one thing keeping the crash handler out of ASan's way. Reduced + to five lines of IR here; it is an upstream crash and nobody has filed + it. A local definition in the table is what clang itself emits for its + own constructors, and it costs a call before main. + The ordering the wrapper now fixes was unspecified before — two entries + at one priority — and arming the registry before the handler is the + order that was wanted anyway. *) (* Declared here rather than in the preamble, which is the one place a - declare is worth gating: this lane then adds no dev-only text at all to - a release module, and the only line it does add there — + declare is worth gating: the crash-handler lane then adds no dev-only + text at all to a release module, and the only line it does add there — [flan_bytes_dup] — is a function release builds really call, since (bytes s) allocates in every build. The neighbouring - [flan_dev_reg_enable] stays in the preamble ungated; it predates this - and moving it is not this lane's to make. *) + [flan_dev_reg_enable] stays in the preamble ungated; it predates that + lane and moving it was not its to make. *) Buffer.add_string m.out "declare void @flan_dev_crash_enable()\n"; + Buffer.add_string m.out + "define internal void @\"flan.dev.ctor\"() {\n\ + \ call void @flan_dev_reg_enable()\n\ + \ call void @flan_dev_crash_enable()\n\ + \ ret void\n\ + }\n"; + Buffer.add_string m.out + "@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] \ + [{ i32, ptr, ptr } { i32 65535, ptr @\"flan.dev.ctor\", ptr null }]\n"; Buffer.add_char m.out '\n' end; List.iter (emit_global m ~hidden) p.Tast.globals;