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.
This commit is contained in:
Joseph Ferano 2026-09-21 09:45:54 +07:00
parent 216d1c0ab0
commit f75b0ca033

View File

@ -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;