From 9c02311f097658430cafceb2bae86ca0b68958a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 16:03:16 +0200 Subject: [PATCH] docs(rfc): score rooting-by-construction against four bugs found after it was written #7341's quarantine produced 31 real stale-pointer bugs; four were fixed (#7373-#7376). None existed when the RFC's 'would it have caught the real bugs' table was written, so they are the strongest available calibration. It would have caught ONE of the four. #7375 (await polling a moved promise) is squarely in scope and would have been caught completely -- notably it had survived a comment explicitly reasoning about the surrounding hazard, which is the RFC's own central argument. The other three lived in perry-runtime, which this RFC does not govern. Recording that honestly rather than only the win, because the ratio is the useful part: it says where the remaining risk lives. Three of four were layer 3, where RuntimeHandleScope exists (675 uses) but is optional. The sharper finding is that all four were the SAME defect shape -- ordering a root relative to a collection point, never a missing root. That is exactly what a Raw that dies at the next &mut emit enforces, so the design generalises; the open question is whether layer 3 needs the same discipline rather than whether codegen does. Also notes the sample is favourable: the eight catches left open in #7341 are caller-side, and Raw<'e> does not cross a function boundary either. No code change -- the RFC says step 1 wants a quiet tree, and expr/ is under edit by #7375. --- .../internals/rfc-rooting-by-construction.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/src/internals/rfc-rooting-by-construction.md b/docs/src/internals/rfc-rooting-by-construction.md index 4b094b89ee..16176f40b9 100644 --- a/docs/src/internals/rfc-rooting-by-construction.md +++ b/docs/src/internals/rfc-rooting-by-construction.md @@ -155,6 +155,47 @@ Rust and become the single source of truth both consume. Four of five by construction, the fifth by making the frame own its own slot numbering. That is a strong enough result to justify the work. +### Re-tested against four bugs found after this RFC was written (2026-08-04) + +#7341's from-space quarantine produced 31 real stale-pointer bugs and four of +them were fixed (#7373–#7376). Scoring them against this proposal is the +strongest available calibration, because none of them existed when the table +above was written. + +| bug | layer | shape | caught? | +|---|---|---|---| +| **#7375** `await` polls a moved promise | 1 (codegen) | promise unboxed, pump helpers emitted, promise reused | **Yes.** The pump calls are `&mut` emits, so the `Raw` is dead at the back-edge. The author must hold a `Rooted` and `get()` per block — which is exactly the fix that landed. | +| **#7373** `JSON.parse` reads a moved input | 3 (runtime) | slice derived, `gc_check_trigger()`, slice used | **No.** Rust locals in `perry-runtime`; this RFC governs `perry-codegen` lowering only. | +| **#7374** `RegExp` flags stored stale | 3 (runtime) | string allocated, `gc_malloc`, pre-collection pointer stored | **No.** Same reason. | +| **#7376** `Symbol` description stored stale | 3 (runtime) | identical to #7374 | **No.** Same reason. | + +**One of four.** That is not an argument against the RFC — the one it catches it +catches completely, and #7375 had survived a code comment explicitly reasoning +about the surrounding hazard ("unbox the promise in each block that uses it", +which solves LLVM dominance and not GC movement). It is an argument about +**where the remaining risk lives**: three of the four were layer 3, which this +mechanism cannot reach by construction. + +**The sharper finding is that all four were the same defect shape.** Every one +had rooting already — the pattern's root, the receiver's root, the per-block +unbox, the registry for registered symbols. What was missing in each case was +**ordering the root relative to the collection point**. That is precisely the +property this RFC enforces: a `Raw` that dies at the next `&mut` emit makes +"used after a collection point" unrepresentable rather than reviewable. + +So the design generalises, and the open question this evidence raises is not +whether to adopt it for codegen, but whether layer 3 needs the same discipline +in `perry-runtime` — where `RuntimeHandleScope` exists (675 uses across 169 +files) but is **optional**, and where three of these four bugs lived. A handle +type that made the raw pointer unusable across an allocating call would have +caught all three by the same mechanism. + +**Caveat on the sample.** These four are the clusters that were *tractable* from +a backtrace. The eight catches left open in #7341 fault on an argument that is +already stale on entry, so they are caller-side and this RFC would not catch +them either — the value crosses a function boundary, and `Raw<'e>` does not +survive one. Counting them would make the ratio worse, not better. + ## Migration cost The honest number is large but the distribution is favourable.