From 92fae67fb3074fd29891a7fa0e8a22f070af04fc Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 16:01:26 +0700 Subject: [PATCH] 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. --- test/dune | 12 +++++++++--- test/test_sanitize.ml | 30 ++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/test/dune b/test/dune index 143edfb..f004d77 100644 --- a/test/dune +++ b/test/dune @@ -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))) diff --git a/test/test_sanitize.ml b/test/test_sanitize.ml index 72811c4..aa8ff79 100644 --- a/test/test_sanitize.ml +++ b/test/test_sanitize.ml @@ -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