Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/8206-gc-store-site-claim-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**GC store-site inventory now verifies its claims, not its comments (#8185).** A `GC_STORE_AUDIT` marker was a trusted comment: deleting a write barrier while leaving its `BARRIERED` marker in place was a clean pass, and #8183 recorded that exactly this defect is invisible to every runtime probe. `scripts/gc_store_site_inventory.py` now binds each claim class to evidence:

- **`BARRIERED` in perry-codegen** is bound to an IR witness. Every stem-labelled barrier-emitter call site must pass a literal stem, the stem set must equal `VERIFIED_BARRIER_STEMS` in the new per-PR lib test `crates/perry-codegen/src/expr/barrier_stem_census_tests.rs` (drift in either direction fails lint), and the census test compiles a probe per stem, asserting — for **every** instance of the gate in the emitted IR — a `cond_br` into `<stem>.barrier.<n>`, the `js_write_barrier_slot` call inside that block, and the branch predicate walked by def-chain (so `br i1 true` with the predicate left dead fails). Four IR-surgery sabotages (delete the call / hard-wire the gate / move the call / bypass the gate) run in the suite against every stem. New witness: `idxset.inbounds` previously had no IR test. A `BARRIERED` marker in an unbound codegen file fails lint, and the scanner gains the `slot_ptr`/`root_slot` dest hints so deleting the shared emitters' own markers is visible.
- **`BARRIERED`/`EXTERNAL_BARRIERED` in perry-runtime/perry-stdlib** (rustc-compiled, no perry IR) are verified against source structure: a barrier-primitive or chain-verified discharge-helper call between the marker and the end of its enclosing function, with every registered helper re-verified each run to reach a primitive through the call graph. All 107 current markers verify; granularity (enclosing function) is printed.
- **`ROOT`/`INIT`/`POINTER_FREE`/`STACK`** stay human-audited and are declared `UNVERIFIED` in the summary on every run instead of silently trusted.

Rot exits 2 (the `gc_rekeyed_key_tables.py` discipline): missing/empty registry, dark witness module, or any scan under its floor never reads as a clean pass. `--self-test` plants fifteen shapes, each of which must be adjudicated — one of them (V-P10) caught a marker-window bug in this PR's own first draft. Real-tree sabotage matrix (seven arms, each red then restored green) recorded in the PR. `docs/src/internals/gc-rooting-invariant.md`'s marker section now describes the enforced chain instead of the open ask.
644 changes: 644 additions & 0 deletions crates/perry-codegen/src/expr/barrier_stem_census_tests.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/class_field_barrier_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ fn probe_module() -> Module {
m
}

fn ir() -> String {
pub(super) fn ir() -> String {
String::from_utf8(compile_module(&probe_module(), ir_opts()).expect("module compiles"))
.expect("LLVM IR should be UTF-8")
}
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/index_set_barrier_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ fn probe_module() -> Module {
m
}

fn ir() -> String {
pub(super) fn ir() -> String {
String::from_utf8(compile_module(&probe_module(), ir_opts()).expect("module compiles"))
.expect("LLVM IR should be UTF-8")
}
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ pub(crate) use write_barrier::{
#[cfg(test)]
mod array_push_guard_tests;
#[cfg(test)]
mod barrier_stem_census_tests;
#[cfg(test)]
mod class_field_barrier_tests;
mod dispatch;
#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/expr/write_barrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ pub(crate) fn emit_write_barrier_slot_value_and_generation_tested(
}

pub(crate) fn emit_root_nanbox_store_on_block(blk: &mut LlBlock, value: &str, root_slot: &str) {
// GC_STORE_AUDIT(ROOT): module-global slot registered as a mutable GC
// root; the root-barrier call below covers incremental marking.
blk.store(DOUBLE, value, root_slot);
let value_bits = blk.bitcast_double_to_i64(value);
blk.call_void("js_write_barrier_root_nanbox", &[(I64, &value_bits)]);
Expand All @@ -322,6 +324,7 @@ pub(crate) fn emit_root_heap_word_store_on_block(
value_bits: &str,
root_slot: &str,
) {
// GC_STORE_AUDIT(ROOT): registered mutable GC root slot; root barrier below.
blk.store(I64, value_bits, root_slot);
blk.call_void("js_write_barrier_root_heap_word", &[(I64, value_bits)]);
}
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/expr/write_pic_barrier_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ fn write_pic_ir(name: &str, value: Expr) -> String {
))
}

/// #8185: the census (`barrier_stem_census_tests`) runs its uniform floor on
/// this file's pointer-possible probe — same fixture, distinct module name.
pub(super) fn census_put_pic_ir() -> String {
write_pic_ir("census_put_pic", Expr::LocalGet(VALUE))
}

/// Emitted block labels carry a per-function numeric suffix
/// (`put.pic.gc_bookkeeping.60`), so a label matches a stem when it IS the
/// stem or the stem followed by `.` and digits only.
Expand Down
51 changes: 42 additions & 9 deletions docs/src/internals/gc-rooting-invariant.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,15 +525,48 @@ The classes are `BARRIERED`, `EXTERNAL_BARRIERED`, `ROOT`, `INIT`,
the first-party store sites and fails when one has no marker — so a **new**
store site cannot land with the question unanswered.

Be precise about what that buys, because the name oversells it: the script
audits the **comment claim**, not the emitted IR. It proves somebody wrote down
a verdict next to the store. It cannot prove the verdict is true, and it cannot
notice when a later edit falsifies one — deleting a barrier while leaving its
`GC_STORE_AUDIT(BARRIERED)` comment in place is a clean pass. Teaching it to
check the claim against emitted IR would turn this section from a review
convention into a gate; that is #8185's open long-term ask and is not a small
change. Until then the marker is documentation and the `--lib` IR test is the
gate.
Since #8185 landed its second half, the script verifies the **claim**, not
just the comment, for the two classes where a false claim strands objects:

- **`BARRIERED` in `perry-codegen`** is bound to an IR witness. Every call to
the stem-taking barrier emitters (`emit_write_barrier_slot_generation_tested`,
`…_value_and_generation_tested`, `emit_jsvalue_slot_store_pointer_tested`)
must pass a string-literal stem, and the census in
`crates/perry-codegen/src/expr/barrier_stem_census_tests.rs`
(`VERIFIED_BARRIER_STEMS`) must list exactly that stem set — the lint script
fails on drift in either direction, on a stem it cannot resolve to a
literal, and on a `BARRIERED` marker in any codegen file not bound to a
census stem. The census test itself (a `--lib` test, so per-PR) compiles a
probe per stem and, for **every instance** of the stem's gate in the emitted
IR, asserts a `cond_br` into `<stem>.barrier.<n>`, the
`js_write_barrier_slot` call inside that block, and the branch predicate
walked by def-chain back to the `GC_FLAG_TENURED` load and the
incremental-count load — so `br i1 true` with the dead predicate left in
place fails, and so does a barrier bypassed in one specialized clone but
intact in another. Four IR-surgery sabotages (delete the call, hard-wire the
gate, move the call out of its block, bypass the gate) run in the suite
against every stem.
- **`BARRIERED` / `EXTERNAL_BARRIERED` in `perry-runtime` / `perry-stdlib`**
are rustc-compiled, so there is no perry-emitted IR; the claim is verified
against source structure instead. From the marker to the end of its
enclosing function there must be a call to a barrier primitive (defined
under `crates/perry-runtime/src/gc/`) or to a registered discharge helper
(`RUNTIME_DISCHARGE_HELPERS` in the script), and every registered helper is
itself re-verified each run to reach a primitive through the call graph —
deleting the barrier *inside* `note_array_slot` turns every marker leaning
on it red. Granularity is the enclosing function (two barriered stores and
one barrier call in the same function still pass), and the script prints
that limit.

What is still trusted: `ROOT`, `INIT`, `POINTER_FREE` and `STACK` verdicts are
human-audited only, and the script says so in its summary on every run —
`UNVERIFIED (human-audited only, by class): …`. A codegen caller that passes
`write_barrier_needed: false` where `true` was meant is a parameterization bug
neither layer catches. If the verifier's own inputs rot — the census file
missing, the registry parsing to zero entries, a scan matching fewer sites
than its floor — the script exits **2** rather than reading as a clean empty
pass (the `gc_rekeyed_key_tables.py` discipline), and its `--self-test` plants
fifteen shapes, each of which must be adjudicated.

## The corpus problem, and the two corpora (#7280)

Expand Down
Loading
Loading