Skip to content

Commit 007fec0

Browse files
avrabeclaude
andauthored
test(vcr-mem): layer-2 honest-fail safety oracle — unbounded recursion refuses a proven budget (#242, #383) (#425)
The msgq/gust tests cover layer-2's PROVEN (finite) path; the soundness of the whole derivation rests on the complementary REFUSAL path working — an unbounded shadow stack must never receive a finite budget, or the image silently under-reserves and overflows on silicon. #421 covers this with synthetic bounds only; this anchors it against REAL scry output. `scripts/repro/recursive_shadow_stack.wat` recurses through the shadow stack (each activation decrements the SP global by a 16 B frame), so the worst-case depth is unbounded. Empirically scry classifies it exactly: sp_global identified, recursive=true, max_stack_bytes=Unbounded. `layer2_unbounded_recursion_refuses_proven_budget_242` (synth-cli main.rs cfg(test)) asserts that scry behaviour AND that `budget_from_bound` never yields a ProvenStackDepth for it — with a fallback it returns AssertedFallback (not proven), without one it honestly refuses. This guards the upstream assumption the entire honest-fail gate depends on: a scry regression that returned a finite bound for an unbounded stack — the one failure mode that would silently under-reserve on silicon — would redden CI. Frozen-safe: scry + wat stay test-only (cfg(test)); production bytes unchanged, no MODULE.bazel pin. Roadmap VCR-MEM-001 records the proven+honest-fail coverage. Verification: `cargo test -p synth-cli --bin synth layer2_` -> 3/3 pass; fmt + clippy -D warnings clean; rivet check zero non-xref errors. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 47820e8 commit 007fec0

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

artifacts/verified-codegen-roadmap.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,19 @@ artifacts:
12041204
fallback). scry stays a DEV-dep exercised only under cfg(test) ⇒ the
12051205
production binary pulls no scry and emits identical bytes. This test is
12061206
the oracle the gated 2b wiring must match.
1207+
LAYER (2) PROVEN-DEPTH + HONEST-FAIL coverage (LANDED 2026-06-22,
1208+
frozen-safe): the proven path is now anchored on BOTH real shadow-stack
1209+
fixtures — msgq (Bytes(32)) and gust_kernel (Bytes(16), the fixture jess
1210+
flashes; proven ≤ flashed 4096, silicon budget sound, #424). The
1211+
soundness-critical REFUSAL path is anchored against real scry output via
1212+
`recursive_shadow_stack.wat` (recurses through the SP global ⇒ scry
1213+
returns recursive=true + Unbounded): `layer2_unbounded_recursion_refuses
1214+
_proven_budget_242` asserts an unbounded depth NEVER yields a
1215+
ProvenStackDepth — only the asserted fallback (if given) or an honest
1216+
refuse. This guards the upstream assumption the entire honest-fail gate
1217+
rests on (scry flags recursion-through-the-shadow-stack as non-finite), so
1218+
a scry regression that returned a finite bound for an unbounded stack —
1219+
the one failure that would silently under-reserve on silicon — reddens CI.
12071220
12081221
- id: VCR-MEM-002
12091222
type: sw-req

crates/synth-cli/src/main.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4291,6 +4291,81 @@ mod tests {
42914291
);
42924292
}
42934293

4294+
/// VCR-MEM-001 layer-2 HONEST-FAIL SAFETY ORACLE (#242, #383): the soundness
4295+
/// of the whole budget derivation rests on an upstream assumption — that scry
4296+
/// returns a NON-finite bound for a stack that can grow without bound, so the
4297+
/// derivation refuses rather than inventing a finite budget that would
4298+
/// silently under-reserve and overflow on silicon. The msgq/gust tests cover
4299+
/// the proven (finite) path; this covers the refusal path against REAL scry
4300+
/// output, guarding the assumption directly.
4301+
///
4302+
/// `recursive_shadow_stack.wat` recurses through the shadow stack (each
4303+
/// activation decrements the SP global by a frame), so the depth is
4304+
/// unbounded. We assert scry detects that (recursive + Unbounded) AND that
4305+
/// `budget_from_bound` never yields a ProvenStackDepth for it — only the
4306+
/// asserted fallback (if given) or an honest refuse.
4307+
///
4308+
/// Frozen-safe: scry + wat stay test-only here; production bytes unchanged.
4309+
#[test]
4310+
fn layer2_unbounded_recursion_refuses_proven_budget_242() {
4311+
use scry_analyze_core::{AnalysisConfig, StackBound, analyze};
4312+
use shadow_budget::{BudgetDecision, BudgetSource, StackDepthBound, budget_from_bound};
4313+
4314+
let wat_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4315+
.join("../../scripts/repro/recursive_shadow_stack.wat");
4316+
let wasm = wat::parse_file(&wat_path).expect("the honest-fail fixture .wat parses");
4317+
4318+
let r = analyze(
4319+
wasm,
4320+
AnalysisConfig {
4321+
widening_threshold: None,
4322+
emit_diagnostics: false,
4323+
taint_policy: None,
4324+
},
4325+
)
4326+
.expect("scry analyzes the recursive Core module");
4327+
4328+
// The upstream assumption the honest-fail gate depends on: recursion
4329+
// through the shadow stack is detected and has NO finite bound.
4330+
assert!(
4331+
r.function_summaries.iter().any(|s| s.recursive),
4332+
"scry detects the shadow-stack recursion"
4333+
);
4334+
assert_eq!(
4335+
r.stack_usage.max_stack_bytes,
4336+
StackBound::Unbounded,
4337+
"recursion through the shadow stack has no finite proven bound"
4338+
);
4339+
4340+
let bound = match r.stack_usage.max_stack_bytes {
4341+
StackBound::Bytes(n) => StackDepthBound::Bytes(n),
4342+
StackBound::Unbounded => StackDepthBound::Unbounded,
4343+
StackBound::Unknown => StackDepthBound::Unknown,
4344+
};
4345+
4346+
// SAFETY (1): with a fallback, the budget is the integrator-ASSERTED one —
4347+
// explicitly NOT ProvenStackDepth. An unbounded stack never gets a proven
4348+
// finite budget; the proof label is reserved for a real finite depth.
4349+
assert_eq!(
4350+
budget_from_bound(bound, 65_536, Some(4096)),
4351+
BudgetDecision::Use {
4352+
bytes: 4096,
4353+
source: BudgetSource::AssertedFallback
4354+
},
4355+
"unbounded depth -> asserted fallback, never ProvenStackDepth"
4356+
);
4357+
4358+
// SAFETY (2): with no fallback, an honest refuse — never an invented
4359+
// number for a stack the analyzer could not bound.
4360+
match budget_from_bound(bound, 65_536, None) {
4361+
BudgetDecision::Refuse(msg) => assert!(
4362+
msg.contains("unbounded"),
4363+
"refusal names the unbounded cause; got: {msg}"
4364+
),
4365+
other => panic!("unbounded + no fallback must refuse, got {other:?}"),
4366+
}
4367+
}
4368+
42944369
/// #235: a dissolved export's non-exported callee must be pulled into the
42954370
/// reachable set (so it lands in the relocatable object), while imports and
42964371
/// unreachable functions stay out.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
;; VCR-MEM-001 layer-2 honest-fail fixture (#242, #383).
2+
;;
3+
;; A function that recurses THROUGH the shadow stack: each activation decrements
4+
;; the stack-pointer global by a 16-byte frame before calling itself, so the
5+
;; worst-case shadow-stack depth grows without bound. This is the case the
6+
;; layer-2 budget derivation MUST refuse — deriving any finite budget for an
7+
;; unbounded stack would silently under-reserve and overflow on silicon.
8+
;;
9+
;; scry classifies this exactly: sp_global identified, function recursive,
10+
;; max_stack_bytes = Unbounded. The test
11+
;; `layer2_unbounded_recursion_refuses_proven_budget_242` asserts that scry
12+
;; behaviour AND that `budget_from_bound` never returns a ProvenStackDepth for
13+
;; it — only the asserted fallback (if given) or an honest refuse.
14+
(module
15+
(global $sp (mut i32) (i32.const 65536))
16+
(memory 1)
17+
(func $recurse (param $n i32)
18+
;; allocate a 16-byte shadow-stack frame
19+
(global.set $sp (i32.sub (global.get $sp) (i32.const 16)))
20+
(if (local.get $n)
21+
(then (call $recurse (i32.sub (local.get $n) (i32.const 1)))))
22+
;; free the frame
23+
(global.set $sp (i32.add (global.get $sp) (i32.const 16))))
24+
(func (export "run") (call $recurse (i32.const 10))))

0 commit comments

Comments
 (0)