The collector's Flan-side roots go under the sanitizers

The sweep's note said there was no Flan program that reached flan_dyn.c, so
the only sanitized run over the collector was dyn_ops.c's -- which pushes
its roots by hand. That note stopped being true when the dyn programs
landed, and it stayed in the file. The gap it left is the one that matters
for the backend lane just committed: a root the *emitter* forgot is a live
object swept, and no amount of C testing can see a mistake the compiler
made. dyn-vec and dyn-defer are in the corpus now -- the second for the
roots that come off on a transfer's path out rather than a return's -- and
so is p13-dyn-collect, which is the only program anywhere that allocates
past flan_dyn.c's one-megabyte floor and therefore the only one under which
a mark and a sweep actually run. Everything else in that list agrees with
ASan by never collecting at all.

p13 lives in spike/x86 because that is the lane that wrote it, so the
alias's deps grew a glob for that directory; it is in this sweep for what it
does and not for where it sits.

What this cannot cover, and the compiler says so itself when asked: there is
no sanitizer pass over hand-written assembly, so --x86 --sanitize is refused
by name. ASan sees the x86 lane's roots only from the collector's side of
the call, never as frame slots. p13 through --x86 under the @x86 sweep is
what stands in for it, and it is a weaker check honestly labelled rather
than a stronger one assumed.

--force @sanitize: clean, and non-empty, which the previous run was not --
an alias satisfied from cache prints nothing and reads exactly like a pass.
dune test --force still green.
This commit is contained in:
Joseph Ferano 2026-09-19 16:01:26 +07:00
parent d722b267e6
commit 92fae67fb3
2 changed files with 35 additions and 7 deletions

View File

@ -145,10 +145,16 @@
(glob_files programs/*.flan)
(glob_files programs/assets/*)
(glob_files programs/assets/edn/*)
; p13-dyn-collect.flan, which lives with the x86 probes because that is the
; lane that wrote it, and is in this sweep because of what it does rather
; than where it is: it is the only program anywhere that allocates past
; flan_dyn.c's one-megabyte floor, so it is the only one under which a mark
; and a sweep actually run. Every other Flan program here agrees with ASan
; by never collecting at all.
(glob_files %{workspace_root}/spike/x86/*.flan)
; The dyn runtime's C main, which is the one thing in this sweep that is not
; a Flan program: flan_dyn.c has no Flan spelling yet. It is also the one
; translation unit here that frees anything, which is what makes it worth a
; sanitized run at all. See [dyn_sweep].
; a Flan program. It is also the translation unit here that frees the most,
; which is what makes it worth a sanitized run at all. See [dyn_sweep].
(file dyn_ops.c))
(action (run ./test_sanitize.exe)))

View File

@ -158,6 +158,23 @@ let corpus =
"programs/printers.flan", [];
"programs/println.flan", [];
"programs/restarts.flan", [];
(* The dyn programs, which reach flan_dyn.c from Flan rather than from the
hand-written C below and the difference is the whole reason they are
here. [dyn_sweep] checks the collector against roots dyn_ops.c pushes
by hand; these check it against the roots the *compiler* emits, which
is the half no C test can reach. A root the emitter forgot is a live
object swept, and that is a use-after-free with the collector's own
hands on it.
[dyn-vec] is the one that builds objects of three kinds; [dyn-defer]
is the one whose roots come off on a transfer's path out rather than a
return's, which is where a pop written on one path only would show.
[p13] is the only program anywhere that allocates past flan_dyn.c's
one-megabyte floor, so it is the only one where a mark and a sweep
actually run everything else in this list agrees with ASan by never
collecting at all. *)
"programs/dyn-vec.flan", [];
"programs/dyn-defer.flan", [];
"../spike/x86/p13-dyn-collect.flan", [];
"programs/sand-headless.flan", [];
"programs/signedness.flan", [];
"programs/slices.flan", [];
@ -194,10 +211,15 @@ let sweep ~checks label =
(try Sys.remove san with Sys_error _ -> ())))
corpus
(* The dyn runtime, under the same two sanitizers. It is not in [corpus] and
cannot be: there is no Flan program that reaches flan_dyn.c yet, so the
thing to build is test/dyn_ops.c against programs/dyn-host.flan the same
pair test_dyn.ml builds, with [sanitize] on.
(* The dyn runtime, under the same two sanitizers, driven from C. There are
Flan programs that reach flan_dyn.c now and three of them are in [corpus]
above this used to say there were none but they are a different
question and not a replacement for this one. They exercise the roots the
*compiler* emits, over the handful of operations a program happens to
write; this exercises every entry point in the header, with the roots
pushed by hand so that the runtime can be wrong on its own. The thing to
build is test/dyn_ops.c against programs/dyn-host.flan the same pair
test_dyn.ml builds, with [sanitize] on.
This is the case the sweep is most likely to have something to say about.
Every other program in the corpus allocates and never frees, which is a