Fix H2: shared reference-graph guard for standalone walkers (review #006)
Cyclic or over-deep $ref graphs stack-overflowed the three standalone schema walkers (OffsetMap::compute, LayoutBuilder::new, materialize_aligned) — SIGABRT on probe, parity-preserved from 0.2.0. - New src/walk_guard.rs: check_ref_graph() — one bounded walk over the reachable reference graph (depth cap 128 matching the plan compilers, path-scoped cycle set; diamonds allowed, cycles and 201-def chains rejected with the plan compilers' error wording) - All three walkers run the guard at entry, before any recursion; materialize_aligned's is defense-in-depth (a cyclic doc can no longer produce an OffsetMap, but mismatched doc/map inputs must still fail cleanly) - Behavioral side effect, net-positive: the guard eagerly parses every reachable def, so an invalid non-root def now surfaces at LayoutBuilder::new instead of build() — four H3 tests updated to expect the same Schema error earlier - Test family: 12 new tests (walk_guard, offset_map, layout_builder, materialize) covering self/two-def/composite-carrier cycles, deep chains, and diamond non-rejection; no stack-overflow reproducers in-tree per the review's Methodology warning - Stale "walkers have no cycle guard" statements updated in validation.md, 030 plan, ADR-012, and the engine gate comment Verified: 501 tests green (423 + 17 + 34 + 15 + 12 + 2 ignored), clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
This commit is contained in:
1 parent
05a2a42983
commit
5e74b991ac
10 files changed
+555
-32
No files matched your search
@@ -497,11 +497,12 @@ aligned reads/writes over identical bytes.
|
||||
`Schema` error instead of a stack overflow. The plan compile runs
|
||||
*before* the layout build in `AlkTypeEngine::compile`, making it the
|
||||
engine's reference-graph gate — `LayoutBuilder`/`OffsetMap`
|
||||
struct-recursion has no cycle guard and previously could recurse
|
||||
struct-recursion had no cycle guard and previously could recurse
|
||||
unboundedly on such a document (a pre-existing untrusted-schema
|
||||
hazard, surfaced by the phase-7 `compile_rejects_cyclic_ref_graph`
|
||||
test). Hardening the layout walkers' own recursion is a separate
|
||||
cleanup, not required while the gate holds in the engine path.
|
||||
test). (Resolved since: review #006 H2 added the shared
|
||||
`walk_guard::check_ref_graph` guard at every standalone walker entry,
|
||||
so the trust boundary no longer depends on the engine path.)
|
||||
- **Fingerprinting enables downstream uses.** Cross-run plan caching,
|
||||
`alkcall` schema handshake, and schema-version diagnostics all
|
||||
become possible without further API work — across `ReadPlan`,
|
||||
|
||||
@@ -293,11 +293,12 @@ The expensive work happens once at schema load time:
|
||||
2. Parse the root struct's `"endian"` annotation.
|
||||
3. Compile the `ValidationPlan` — the value-domain constraint tree,
|
||||
with eager `$ref` resolution. Its compile walk rejects cyclic `$ref`
|
||||
graphs with a clean `Schema` error *before* the layout computation:
|
||||
the layout walkers' struct/union recursion has no cycle guard, so
|
||||
ordering the plan compile first is what keeps a self-referential
|
||||
(malicious or accidental) document a handleable error, not a stack
|
||||
overflow.
|
||||
graphs with a clean `Schema` error *before* the layout computation.
|
||||
(The layout walkers now also guard themselves — each standalone
|
||||
entry point runs the shared reference-graph check
|
||||
(`walk_guard::check_ref_graph`, review #006 H2) — so the plan-first
|
||||
ordering is belt-and-suspenders at engine compile, and the trust
|
||||
boundary no longer depends on the call path.)
|
||||
4. Compute the layout (`LayoutBuilder` for packed, `OffsetMap` for
|
||||
aligned).
|
||||
5. If `json_schema` is `Some`, build the standard
|
||||
|
||||
@@ -735,9 +735,11 @@ are trait derives, `fingerprint` is an inherent method).
|
||||
> - **Engine:** `Arc<ValidationPlan>` built at `compile` in both modes;
|
||||
> new accessor `validation_plan()`. `validate_bytes` walks the plan.
|
||||
> **Bonus:** `ValidationPlan::compile` runs before the layout build and
|
||||
> serves as the engine's cyclic-`$ref` gate (the layout walkers have no
|
||||
> cycle guard; a cyclic doc used to be a stack-overflow hazard there —
|
||||
> now a clean `Schema` error, see ADR-012 Consequences).
|
||||
> serves as the engine's cyclic-`$ref` gate. (As of the review #006 H2
|
||||
> fix, the layout walkers also carry their own guard —
|
||||
> `walk_guard::check_ref_graph` runs at each standalone entry — so this
|
||||
> ordering is now belt-and-suspenders rather than the only defense; the
|
||||
> plan text below predates that fix.)
|
||||
> - **Remaining phase-7 bench work** (a `validate_bytes`-stream bench
|
||||
> in alktty) moves with the bench work into phase 8; a spot check
|
||||
> during development measured plan-validate at ~0.2 µs/call vs ~0.6
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: in-progress (H1, L1, H3, L5, L6 resolved 2026-09-02)
|
||||
status: in-progress (H1, L1, H3, L5, L6, H2 resolved 2026-09-02)
|
||||
last_updated: 2026-09-02
|
||||
reviewed_artifacts:
|
||||
- src/read_plan.rs
|
||||
@@ -95,11 +95,15 @@ sub-types, `fingerprint()` methods; `Hash` on `Endian`/
|
||||
|
||||
| Severity | Count | Status |
|
||||
|----------|------:|--------|
|
||||
| High | 3 (H1, H2, H3) | H1, H3 resolved 2026-09-02 |
|
||||
| High | 3 (H1, H2, H3) | H1, H2, H3 resolved 2026-09-02 |
|
||||
| Medium | 4 (M1, M2, M3, M4) | open |
|
||||
| Low | 6 (L1–L6) | L1, L5, L6 resolved 2026-09-02 |
|
||||
| Nit | 2 (N1, N2) | open |
|
||||
|
||||
All three Highs are now resolved. The remaining work is Medium/Low/Nit:
|
||||
M1+M2 (same file, one session), M3 (trivial), M4 (per-fix posture), and
|
||||
L2/L3/L4/N1/N2 opportunistic.
|
||||
|
||||
The three Highs are adversarial-input crashes (H1, H2) and a
|
||||
cross-consumer wire-layout convention gap (H3) — all three are the
|
||||
exact class of problem AGENTS.md §3 exists for, given that `alkcall`
|
||||
@@ -124,6 +128,12 @@ them became materially easier to hit with the 0.3.0 surface.
|
||||
written, L5 doc added, L6 roundtrip test added. 488 tests green,
|
||||
clippy `-D warnings` clean, wasm build green, `cargo doc --no-deps`
|
||||
zero warnings.
|
||||
- **H2 (2026-09-02):** resolved — see the "Resolution (2026-09-02)"
|
||||
block at the end of the H2 finding. Shared one-shot reference-graph
|
||||
guard (`walk_guard::check_ref_graph`) added and run at the entry of
|
||||
all three standalone walkers; full cyclic/deep-nesting/diamond test
|
||||
family added. 501 tests green, clippy `-D warnings` clean, wasm build
|
||||
green, `cargo doc --no-deps` zero warnings.
|
||||
|
||||
---
|
||||
|
||||
@@ -316,6 +326,62 @@ calls require pre-validated documents — but that re-introduces the
|
||||
explicitly retired for `ReadPlan`. The shared guard is the right fix;
|
||||
the doc note is the minimum acceptable one.
|
||||
|
||||
**Resolution (2026-09-02) — shared one-shot graph guard, all three
|
||||
walkers gated:**
|
||||
|
||||
1. **New module `src/walk_guard.rs`** (private, `pub(crate)`) with
|
||||
`check_ref_graph(&BastDoc)` — a single bounded walk over the
|
||||
reachable reference graph that rejects depth > 128
|
||||
(`MAX_GRAPH_DEPTH`, matching the plan compilers'
|
||||
`MAX_COMPILE_DEPTH`) and any `$ref` cycle, using the same
|
||||
path-scoped cycle-set semantics the plan compilers use (diamond
|
||||
refs compile; only genuine cycles trip). The walk covers every
|
||||
carrier shape: inline structs recurse into fields, named defs are
|
||||
entered via `resolve_ref`, union mappings and shared `fields` are
|
||||
both checked, and arrays/records are seen through to their
|
||||
element/value types (so a cycle behind an array-of-`$ref` hop is
|
||||
caught — the probe-verified gap shape). Error text mirrors the plan
|
||||
compilers' wording ("cyclic $ref through…", "compile depth
|
||||
exceeded…") so downstream matching sees one shape.
|
||||
|
||||
2. **All three standalone walkers run the guard at entry**, before any
|
||||
recursion: `OffsetMap::compute`, `LayoutBuilder::new`, and
|
||||
`materialize_aligned` (the last as defense-in-depth — a cyclic doc
|
||||
can no longer produce an `OffsetMap`, but a mismatched
|
||||
doc/map pairing must still fail with a clean `Schema` error, not
|
||||
overflow). `AlkTypeEngine::compile`'s ValidationPlan gate is
|
||||
unchanged and now redundant-but-harmless; its doc comment is
|
||||
updated to say so. The full shared-guard recommendation was taken
|
||||
(not the minimum doc note).
|
||||
|
||||
3. **Behavioral side effect, net-positive:** `check_ref_graph`
|
||||
resolves every reachable def eagerly (via `resolve_ref`), which
|
||||
parses each def's full shape — so an invalid *non-root* def (e.g. a
|
||||
`$defs` union that re-declares a shared field, or whose discriminator
|
||||
field is missing/non-first) now surfaces at `LayoutBuilder::new`
|
||||
instead of at `build()`. Four pre-existing H3 tests asserted the old
|
||||
lazy-parse timing ("root parses, fails at build"); they were updated
|
||||
to expect the same `Schema` error at `new()`. Earlier rejection of
|
||||
the same malformed schemas — strictly better for untrusted input,
|
||||
no accepted schema's behavior changed.
|
||||
|
||||
4. **Test family added (12 tests):** in `walk_guard.rs` (self-cycle,
|
||||
two-def cycle, diamond allowed, 201-def deep chain → clean depth
|
||||
error), in `offset_map.rs` (cycle rejected at compute, two-def
|
||||
cycle, cycle behind inline-struct + array hop, diamond still
|
||||
computes), in `layout_builder.rs` (cycle rejected at `new`, two-def
|
||||
cycle, diamond still builds), and in `materialize.rs` (cyclic doc
|
||||
+ unrelated map → guard fires before offset lookup, diamond still
|
||||
materializes). No stack-overflow reproducers in the default suite —
|
||||
the tests assert the clean-error half only (the overflow itself was
|
||||
probe-verified SIGABRT in the review session; see the Methodology
|
||||
warning about running it).
|
||||
|
||||
Verified: 501 tests green (423 crate + 17 + 34 + 15 + 12 + 2
|
||||
pre-existing ignored), `cargo clippy --all-targets -- -D warnings`
|
||||
clean, `cargo build --target wasm32-unknown-unknown --release` green,
|
||||
`cargo doc --no-deps` zero warnings.
|
||||
|
||||
### H3. Field-name-discriminator unions: builder, reader, and materializer disagree on layout and on discriminator position
|
||||
|
||||
**Files**: `src/layout_builder.rs:485-527` (write side),
|
||||
@@ -861,9 +927,8 @@ Worth recording, because the findings shouldn't eclipse it:
|
||||
see the resolution block on the finding).
|
||||
2. ~~**H3**~~ **resolved 2026-09-02** (with L5 + L6; see the
|
||||
resolution block on the finding).
|
||||
3. **H2** — shared walk guard (or the minimum doc note if the full
|
||||
guard is judged too invasive for 0.3.x), plus the cyclic-schema
|
||||
tests for all three walkers.
|
||||
3. ~~**H2** — shared walk guard~~ **resolved 2026-09-02** (see the
|
||||
resolution block on the finding).
|
||||
4. **M1 + M2** — same file, same test family; do together.
|
||||
5. **M3** — trivial deletion (or the feature decision, if kept).
|
||||
6. **M4** — ongoing: per-fix coverage extension as recommended above;
|
||||
|
||||
Reference in new issue
Block a user