Skip to content

SESSION-EMPTY-OBJECT: refuse {} in the JSON preflight so a malformed document cannot abort the host - #390

Open
tamashi095 wants to merge 4 commits into
mainfrom
sonnet/387-empty-object-panic
Open

SESSION-EMPTY-OBJECT: refuse {} in the JSON preflight so a malformed document cannot abort the host#390
tamashi095 wants to merge 4 commits into
mainfrom
sonnet/387-empty-object-panic

Conversation

@tamashi095

@tamashi095 tamashi095 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • crates/session/src/json_preflight.rs: reject an empty JSON object {} anywhere in the document, before the typed walk and before json-syntax's Value tree is built, with a proper source span, DiagnosticCode::JsonSyntax, and message "empty JSON object {} is never a valid V1 schema value". This is option 1 from session aborts the host on untrusted JSON containing an empty object {} (parser CodeMap volume bug, reachable from C API and wasm boot) #387.
  • Confirmed {} is never legal V1: docs/SESSION_SCHEMA_V1.md -- "Every object rejects unknown keys and every field is explicit, including empty arrays"; every schema object has required keys, so an object with none can never validate. Empty arrays stay legal and untouched (array() is unmodified).
  • Root cause (upstream, not fixed here): json-syntax 0.12.5 never calls parser.end_fragment(i) for an empty object, so the reserved CodeMap entry keeps span = p..p, volume = 0. IterMapped::next then advances 2 + volume instead of 2 + 1, so every object member after an empty object is read one CodeMap slot off. session::parse::Parser::keys (crates/session/src/parse.rs:100) hits Option::unwrap() on the resulting None at json-syntax-0.12.5/src/object/mod.rs:795:67. The workspace's release profile is panic = "abort" (Cargo.toml:132), so this aborts the host process / traps the AudioWorklet rather than returning a diagnostic, for any document reachable through miso_engine_v1_compile_session or miso_engine_web_v1_boot.
  • Reused the existing DiagnosticCode::JsonSyntax ("json.syntax") rather than adding a new code: it is exactly the code the preflight's other two refusals (duplicate key, depth-129) already use, and tools/session-validator/src/lib.rs documents (and tests) that json.syntax is produced only by the grammar stage and is always returned alone -- our new refusal preserves that invariant (it also short-circuits the whole document, same as the other two). No conformance fixture or diagnostic-code enumeration needed updating (there is no registry doc or enumerated-code list beyond the DiagnosticCode enum itself and docs/SESSION_SCHEMA_V1.md's prose) -- but docs/SESSION_SCHEMA_V1.md's own prose describing the preflight did need updating, and initially wasn't: see "Documentation and comment corrections" below, added after verifier review.
  • Factored the three refusal sites' (duplicate key, depth, now empty-object) identical path-materialization loop into one Scanner::current_path helper in the same file, to avoid a third copy.
  • Re-pinned the shipped AudioWorklet artifact (crates/session ships in the wasm build) and regenerated the qualification lineage, exactly as JSON-SYNTAX: replace the jstrict fork with upstream json-syntax in crates/session #385 did, against the latest origin/main (65d83af5, includes TOOLING-DEDUPE: remove the private SHA-256 and the incomplete JSON escaper, fold duplicated helpers into bench-support #386) -- twice (see "Documentation and comment corrections").

Documentation and comment corrections (verifier round 1: REQUEST CHANGES)

The fix itself was verified correct (186 {} placements, 48 scanner attacks, fuzz clean, all CI job commands green locally); the verifier's findings were documentation and PR-truthfulness only, addressed in two follow-up commits:

  1. docs/SESSION_SCHEMA_V1.md did not mention the empty-object refusal at all, even though its opening paragraph already documents the sibling duplicate-key and depth-129 preflight refusals. Added two sentences there naming the code (json.syntax), the path/span shape (over the value's {...} bytes), the json-syntax 0.12.5 CodeMap defect it works around, and that empty arrays are unaffected.
  2. crates/session/src/json_preflight.rs:31-32's comment said this pass "only returns the two refusals for which the value parser does not expose the required contract information" -- stale from before the empty-object refusal existed, and actually wrong for it: the value parser would diagnose an empty object correctly on its own (missing schema fields etc.); the refusal exists to keep the corrupted CodeMap state from ever being reached, not because of a contract-information gap. Rewritten to name all three refusals accurately.
  3. The issue asked to add {} to the session_parse fuzz corpus seeds; the original PR did not wire this into CI. Added fixtures/session/v1/fuzz-seeds/empty-object.json (canonical.json with render_profile set to {}) and appended it to .github/workflows/fuzz.yml's session_parse job -seed_inputs= list (libFuzzer accepts a comma-separated list). Verified directly: running the compiled session_parse fuzz binary once against this exact seed on this branch executes cleanly with no crash; the identical byte content already reproduced the pre-fix panic in this PR's own session-level repro (captured before implementing the fix) and is how the JSON-SYNTAX: replace the jstrict fork with upstream json-syntax in crates/session #385 verifier found session aborts the host on untrusted JSON containing an empty object {} (parser CodeMap volume bug, reachable from C API and wasm boot) #387 in the first place -- so this seed would crash main's session_parse target on its first execution, and CI will now catch a regression here going forward.
  4. Option 3 was misquoted in the PR's original "Seen, not done": session aborts the host on untrusted JSON containing an empty object {} (parser CodeMap volume bug, reachable from C API and wasm boot) #387 says upstreaming the json-syntax fix is "in addition to 1, not instead" -- i.e. still owed, not forbidden. My original PR body incorrectly framed it as if the issue itself excluded it; that exclusion was actually my own task scope for this PR, not the issue's position. Corrected below; the coordinator will open the follow-up issue for the upstream PR + [patch].
  5. Unplanned re-pin: editing json_preflight.rs's comment (item 2) shifts every subsequent line number in that real, shipped source file, including its one slicing panic site (self.source[start..self.cursor] in Scanner::string). Rust embeds a core::panic::Location (file/line/column) constant at that call site regardless of the release profile's -C strip=debuginfo (that strip level removes DWARF sections, not these compiled-in panic-location strings), so a comment-only edit to a shipped file still moves the wasm artifact's bytes. Confirmed by rebuilding: digest changed 439ba6b5... -> f1f27d8d... at the identical byte count (2,635,719) -- a debug-metadata-only delta, not a behavior change -- so the artifact was re-pinned and the qualification lineage regenerated a second time, against this branch's actual final commits.
  6. Per the coordinator's explicit instruction, crates/capi/tests/resource_lifecycle.rs is untouched. exported_c_candidates_replay_render_and_both_destroy_orders_balance_exactly (left 2613, right 2610) already fails on main itself since JSON-SYNTAX: replace the jstrict fork with upstream json-syntax in crates/session #385 and is being diagnosed separately; it is unrelated to this PR and out of scope here.

Rows closed (#387)

  • The fuzz artifact and every {} placement now produce a diagnostic, not a panic, through parse_session_json, miso_engine_v1_compile_session, and miso_engine_web_v1_boot. See Before/after.
  • cargo test --locked --workspace count matches main plus the new regression tests (see Gate output / workspace counts below).
  • Fuzz session_parse ran 15 minutes with {} seeded: no crashes (17,652,390 executions), and {} is now a permanent CI seed (fuzz.yml's -seed_inputs=), not just a one-off local run.
  • The wasm artifact re-pin and browser lineage are refreshed, against this branch's final head.

Gate output

Original round, rerun fresh on 816957af (rebased onto origin/main 65d83af5):

$ cargo test --locked -p session
running 13 tests (json_grammar.rs) ... 13 passed; 0 failed  (was 9; +4 new: empty_object_refuses_at_every_placement_instead_of_corrupting_the_code_map,
    canonical_minimal_empty_render_profile_reports_syntax_not_a_bogus_numeric_range,
    fuzz_crash_52d9c906_empty_render_profile_diagnoses_instead_of_aborting,
    minimal_two_member_object_with_an_empty_first_value_diagnoses_instead_of_aborting)
... every other session test file unchanged and green (allocation_budget, canonical_schema,
    diagnostic_parity, invalid_matrix, json_contract_artifacts, json_unicode, render_mode_tiers,
    sample_rate_tiers, scale_transaction, strict_unknowns, token_tables, visit_model)

$ cargo test --locked -p capi
running 30 tests (ffi::tests, was 29; +1 new: empty_object_document_is_diagnosed_not_aborted) ... ok
running 3 tests (resource_lifecycle.rs) ... ok

$ cargo test --locked -p host-web
running 62 tests (tests::*, was 61; +1 new: empty_object_document_boots_to_a_diagnostic_not_a_trap) ... ok
running 1 test (boot_transient_budget.rs) ... ok

$ bash scripts/check-session-policy.sh
session policy: ok

$ bash scripts/check-realtime-policy.sh
realtime policy: ok (7 marked regions)

$ bash scripts/check-workspace-policy.sh
workspace policy: ok

$ cargo clippy --locked -p session -p capi -p host-web --all-targets -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) -- clean, zero warnings

$ cargo fmt --all -- --check
(no output, exit 0)

$ git diff --check
(no output, exit 0)

Verifier-requested round, rerun fresh on the final head (a52bdae0):

$ cargo test --locked -p session
[same 13/13 in json_grammar.rs, all session test files green; unaffected by comment/doc/fixture changes]

$ bash scripts/check-session-policy.sh
session policy: ok

$ RUSTDOCFLAGS="-D warnings" cargo doc --locked -p session -p capi -p host-web --no-deps
Documenting session v0.1.0
Documenting host-web v0.1.0
Documenting capi v0.1.0
Finished `dev` profile [unoptimized + debuginfo] target(s) -- clean, zero warnings
(no scripts/check-*docs*.sh exists in this repo; grepped scripts/ for "doc" -- none found)

$ cargo fmt --all -- --check
(no output, exit 0)

$ git diff --check
(no output, exit 0)

$ cargo clippy --locked -p session -p capi -p host-web --all-targets -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) -- clean, zero warnings

$ cargo test --locked -p capi
[unchanged, 30+3 passed]

$ cargo test --locked -p host-web
[unchanged, 62+1 passed]

Wasm/qualification, both rounds (<dir> rebuilt fresh each time):

$ bash scripts/build-web-audioworklet.sh <dir>
[round 1: builds clean against 439ba6b5...; round 2: builds clean against f1f27d8d..., same 2,635,719 bytes]

$ bash scripts/check-web-audioworklet.sh <dir>
... web boot budget high-water gate passed; mismatches: 0
web AudioWorklet static/object checks passed

$ python3 scripts/check-browser-expected-resources.py
browser-correctness expected.json resource rows and digests agree with the built simd128 module,
  and the native witness agrees where the rows are target-independent
browser expected-resources self-test passed (26 red mutations)

$ npm run qualify -- --artifacts <dir> --browser all --record-matrix \
    --candidate-commit 12973933896823e30f5ad2c447d3f52455916b79 --self-test-mutations
session identities: 3 qualification documents declare their fed PCM
artifact set: the exact 6-file shipped set is pinned
chromium: all qualification gates passed (151.0.7922.34)
firefox: all qualification gates passed (153.0)
webkit: all qualification gates passed (26.5)
recorded results.json and ../BROWSER_DEPLOYMENT_MATRIX.md

$ npm run qualify -- --artifacts <dir> --browser all --check-matrix --self-test-mutations
chromium: all qualification gates passed (151.0.7922.34)
firefox: all qualification gates passed (153.0)
webkit: all qualification gates passed (26.5)

Fuzz (cargo-fuzz 0.13.2 installed; +nightly-2026-08-20 toolchain present):

$ export RUSTFLAGS='-C target-feature=+avx2,+fma'
$ cargo +nightly-2026-08-20 fuzz run session_parse target/ci/session-fuzz/parse -- \
    -max_total_time=900 -seed=1387387 fuzz/corpus/session_parse
[fuzz/corpus/session_parse seeded with 5 files: the canonical.json fixture, the three sed
 variants (render_profile/output_profile/a track's builtins each replaced by {}), and the
 minimal {"a":{},"b":1}]
Done 17652390 runs in 901 second(s)

$ ./target/x86_64-unknown-linux-gnu/release/session_parse fixtures/session/v1/fuzz-seeds/empty-object.json
Running: fixtures/session/v1/fuzz-seeds/empty-object.json
Executed fixtures/session/v1/fuzz-seeds/empty-object.json in 0 ms
[single-input smoke test of the now-committed CI seed: no crash]

17,652,390 executions, 0 crashes, 0 new artifacts (fuzz/artifacts/session_parse/ empty). ~19,600 exec/s sustained. The general mutation corpus directory (fuzz/corpus/session_parse/, 2,150 files after the run) is not committed, matching this workspace's existing convention for this target (only the curated protocol golden seeds under fuzz/corpus/protocol_* are checked in). What is now committed and wired into CI is the one deliberate {} seed (fixtures/session/v1/fuzz-seeds/empty-object.json), appended to fuzz.yml's -seed_inputs= for session_parse.

Workspace-wide counts (unchanged by the verifier-requested round -- no test-affecting files touched, only comments/docs/a fixture/a workflow/the pin):

cargo test --locked --workspace (branch, 816957af, on origin/main 65d83af5):
  1551 passed; 0 failed; 24 ignored; 273 result summaries

cargo test --locked --workspace (fresh origin/main worktree, 65d83af5):
  1545 passed; 0 failed; 24 ignored; 273 result summaries

Delta: +6 passed, 0 failed, 0 ignored -- exactly the 4 (session) + 1 (capi) + 1 (host-web) new
tests named above.

Before/after

Session-level ({"render_profile": {}} substituted into fixtures/session/v1/canonical.json, unmodified pre-fix tree):

thread 'main' panicked at .../json-syntax-0.12.5/src/object/mod.rs:795:67:
called `Option::unwrap()` on a `None` value
stack backtrace:
   ...
   8: <session::parse::Parser>::keys
             at ./crates/session/src/parse.rs:100:22
   9: session::parse::parse_track
  10: session::parse::parse_list::<session::model::Track>
  11: session::parse::parse_root
  12: session::parse::parse_session_json

After:

diagnostics (1):
  json.syntax at $.render_profile: empty JSON object {} is never a valid V1 schema value

canonical-minimal.json (before, unmodified tree -- the bogus diagnostics the issue named):

diagnostics (3):
  numeric.out_of_schema_range at $.output_profile.channels: expected an unsigned integer JSON number (span 199..209)
  schema.missing_field at $.render_profile.id: required key is absent (span 151..151)
  schema.missing_field at $.render_profile.mode: required key is absent (span 151..151)

After:

diagnostics (1):
  json.syntax at $.render_profile: empty JSON object {} is never a valid V1 schema value

(one clear diagnostic at the real fault, not a bogus range error plus two degenerate zero-width spans)

C API (miso_engine_v1_compile_session, nine-track fixture, render_profile emptied):

  • Before: process abort (same panic as above, reached through compile_children -> parse_host_session -> parse_session_json; catch_result's catch_unwind cannot contain an abort under panic = "abort").
  • After: RESULT_COMPILE_REJECTED, diagnostics bytes json.syntax\t$.render_profile\n. Verified in ffi::tests::empty_object_document_is_diagnosed_not_aborted.

Wasm boot path (AudioWorkletEngineHost::boot / miso_engine_web_v1_boot, one-track fixture, render_profile emptied):

  • Before: the same panic, which traps the AudioWorklet module under the shipped --release (panic = "abort") build.
  • After: RESULT_REFUSED_DOCUMENT, diagnostic json.syntax\t$.render_profile\n, through both the safe AudioWorkletEngineHost::boot call and the raw C ABI miso_engine_web_v1_boot entry point. Verified in tests::empty_object_document_boots_to_a_diagnostic_not_a_trap.

Wasm artifact digest and size (scripts/build-web-audioworklet.sh, miso-engine-v1-audio-worklet.simd128.wasm):

point digest bytes
before (origin/main 65d83af5, pre-#387) 22e4c25cba7f97b66db720ad8ac8cf653de0afcabe84101693f4fa166b90d4e6 2,636,176
after round 1 (816957af) 439ba6b513c5b1c9c2d1ff462d5cdd43136b0d70df540d4a21c238886d165958 2,635,719
after round 2, final (a52bdae0) f1f27d8daa4c9f0828c9a3b2ca939bfdb507d041db6d7bc2a63ec6c826c1f74d 2,635,719

Round 1: -457 bytes from the new preflight branch (a handful of straight-line instructions, smaller than whatever code-size noise the change otherwise perturbed in the fat-LTO build). Round 2: 0 byte-count change, digest-only -- see "Documentation and comment corrections" item 5 for why a comment edit still moves a compiled panic-location constant.

Seen, not done

  • Upstream json-syntax still has the bug (src/parse/object.rs:26-29 never calls end_fragment for the empty-object branch; the empty-array branch does). No upstream release exists to bump to. Per session aborts the host on untrusted JSON containing an empty object {} (parser CodeMap volume bug, reachable from C API and wasm boot) #387 itself, the upstream PR + a [patch] (option 3) is "in addition to 1, not instead" -- i.e. still owed, not excluded by the issue. It is not done in this PR because the coordinator scoped this PR to option 1 only and will open the follow-up issue for it. The bug is fully neutralized for this workspace by the preflight refusal in the meantime, since {} can never reach the parser's Value tree from parse_session_json any more, but a future direct consumer of json_syntax::Value::parse_str outside session's preflight boundary would still hit it.
  • Option 2 (trusting volume == 0 as 1 in the typed walk) not implemented, per this PR's scope (not touching parse.rs's typed walk).
  • The literal fuzz artifact crash-52d9c906ce5ad7f1d1e67dad91b13ec69e2caab5 named in the issue was not present on disk in this worktree (checked fuzz/artifacts/, the scratch directories named in the task, and did a filesystem-wide search). A content-equivalent input was found at the JSON-SYNTAX: replace the jstrict fork with upstream json-syntax in crates/session #385 verifier's own scratch path (render_profile reduced to whitespace-only content, still an empty object under the JSON grammar) and is used verbatim as the fuzz_crash_52d9c906_empty_render_profile_diagnoses_instead_of_aborting regression test and as the committed CI fuzz seed; it reproduces the identical pre-fix panic (verified on the unmodified tree before implementing the fix).
  • Render-path/bit-identity/benchmark gates: not applicable. session, capi, and host-web are control-plane-only, and this PR touches no render arithmetic (docs/REALTIME_DEPENDENCY_POLICY.md's "Issue 004 control-plane parser dependencies" section already establishes session is not render-reachable).
  • crates/capi/tests/resource_lifecycle.rs's exported_c_candidates_replay_render_and_both_destroy_orders_balance_exactly failure (left 2613, right 2610) predates this PR (present on main since JSON-SYNTAX: replace the jstrict fork with upstream json-syntax in crates/session #385) and is out of scope here per the coordinator's explicit instruction; it is being diagnosed separately.

Skipped

  • Nothing else was skipped; every "Done when" item and every requested gate ran, across both review rounds.

Closes #387

🤖 Generated with Claude Code

https://claude.ai/code/session_01EwL1uTcxsmopHtamG6bMko


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

tamashi095 and others added 4 commits September 4, 2026 18:01
…document cannot abort the host

json-syntax 0.12.5 (and jstrict 0.14.0 before it) never calls `end_fragment` for an empty JSON
object, so the reserved `CodeMap` entry keeps `volume = 0` and `IterMapped::next` reads every later
member one slot off. `session::parse::Parser::keys` hits `Option::unwrap()` on the resulting `None`
(`json-syntax-0.12.5/src/object/mod.rs:795:67`), reachable from untrusted input through the C API
(`miso_engine_v1_compile_session`) and the wasm boot path (`miso_engine_web_v1_boot`). The
workspace's release profile is `panic = "abort"`, so this aborts the host process / traps the
AudioWorklet rather than returning a diagnostic.

No V1 schema position ever admits `{}` (every object rejects unknown keys and requires every field
explicit; empty arrays are legal, empty objects never are). This implements option 1 from #387: a
preflight refusal in `crates/session/src/json_preflight.rs` that rejects an empty object anywhere
in the document, before the typed walk and before the dependency's `Value` tree is built, with a
proper source span and `DiagnosticCode::JsonSyntax` -- the same code the existing duplicate-key and
depth-129 preflight refusals already use, since a `json.syntax` diagnostic is documented (see
`tools/session-validator/src/lib.rs`) to be produced only by the grammar stage and returned alone.
No new diagnostic code was needed. Factored the three refusal sites' path materialization into one
`Scanner::current_path` helper.

Options 2 (trusting `volume == 0` in the typed walk) and 3 (patching json-syntax upstream) are not
done here, per the issue's ordering; upstream still has the bug.

Tests:
- `crates/session/tests/json_grammar.rs`: every `{}` placement in `canonical.json`
  (`render_profile`, `output_profile`, a track's `builtins`) refuses with an exact span and path;
  `canonical-minimal.json` with an empty `render_profile` reports `json.syntax` at `$.render_profile`
  instead of the previously bogus `numeric.out_of_schema_range` at `$.output_profile.channels` with
  degenerate `151..151` spans; a document content-equivalent to the fuzz crash the #385 verifier
  found (`crash-52d9c906ce5ad7f1d1e67dad91b13ec69e2caab5`, not present on disk in this worktree) no
  longer panics; the minimal `{"a":{},"b":1}` CodeMap illustration from the issue.
- `crates/capi/src/ffi.rs` (`ffi::tests::empty_object_document_is_diagnosed_not_aborted`):
  `miso_engine_v1_compile_session` on the nine-track fixture with an empty `render_profile` returns
  `RESULT_COMPILE_REJECTED` with `json.syntax\t$.render_profile\n`, not an abort.
- `hosts/host-web/src/tests.rs` (`tests::empty_object_document_boots_to_a_diagnostic_not_a_trap`):
  `AudioWorkletEngineHost::boot` and the raw `miso_engine_web_v1_boot` C ABI entry both return
  `RESULT_REFUSED_DOCUMENT` with the same diagnostic instead of trapping.

Re-pinned the AudioWorklet artifact (`crates/session` ships in the wasm build):
`22e4c25cba7f97b66db720ad8ac8cf653de0afcabe84101693f4fa166b90d4e6` ->
`439ba6b513c5b1c9c2d1ff462d5cdd43136b0d70df540d4a21c238886d165958`
(`MISO_ENGINE_WEB_AUDIOWORKLET_REPIN=1 bash scripts/build-web-audioworklet.sh <dir>`).

Commands run:
- cargo test --locked -p session
- cargo test --locked -p capi
- cargo test --locked -p host-web
- bash scripts/check-session-policy.sh
- bash scripts/check-realtime-policy.sh
- bash scripts/check-workspace-policy.sh
- bash scripts/build-web-audioworklet.sh <dir>
- bash scripts/check-web-audioworklet.sh <dir>
- python3 scripts/check-browser-expected-resources.py
- cargo clippy --locked -p session -p capi -p host-web --all-targets -- -D warnings
- cargo fmt --all -- --check
- git diff --check

Refs #387

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwL1uTcxsmopHtamG6bMko
- npm run qualify -- --artifacts <dir> --browser all --record-matrix
  --candidate-commit 90109f6
  --self-test-mutations: chromium 151.0.7922.34, firefox 153.0, webkit 26.5
  all passed.
- npm run qualify -- --artifacts <dir> --browser all --check-matrix
  --self-test-mutations: PASS against the recorded results.json.

candidateCommit fc01c53... -> 90109f6... (this branch's #387 fix commit);
wasmSha256 22e4c25c... -> 439ba6b5... (matches the re-pinned artifact digest
recorded in the parent commit).

Refs #387

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwL1uTcxsmopHtamG6bMko
…comment, and seed the CI fuzz corpus

Verifier feedback on PR #390 (REQUEST CHANGES, documentation and PR-truthfulness only; the fix
itself was verified correct: 186 {} placements, 48 scanner attacks, fuzz clean, all CI job
commands green locally):

1. docs/SESSION_SCHEMA_V1.md: added the empty-object refusal to the preflight paragraph that
   already documents the duplicate-key and depth-129 refusals -- code, path/span shape, and the
   json-syntax 0.12.5 CodeMap defect it works around (issue #387). Empty arrays are called out as
   unaffected.
2. crates/session/src/json_preflight.rs:31-32: the comment claimed this pass "only returns the two
   refusals for which the value parser does not expose the required contract information." That
   was already inaccurate for the empty-object refusal added in the parent commit: the value
   parser normally *would* diagnose an empty object correctly (missing schema fields etc.); this
   preflight refuses it first specifically to keep the corrupted json-syntax CodeMap state (no
   `end_fragment` call on the empty-object branch) from ever being reached. Rewritten to name all
   three refusals and state that reason accurately.
3. .github/workflows/fuzz.yml:117 / fixtures/session/v1/fuzz-seeds/empty-object.json: the issue
   asked to add `{}` to the session_parse fuzz corpus seeds. Added a small committed fixture
   (canonical.json with render_profile set to {}) and appended it to the session_parse job's
   `-seed_inputs=` list (libFuzzer accepts a comma-separated list). Verified directly: running the
   compiled session_parse fuzz binary once against this exact seed on this branch executes cleanly
   (no crash); the identical byte content was already proven to panic session::parse::Parser::keys
   on the unmodified pre-#387 tree (session-level repro captured before implementing the fix, and
   independently by the #385 verifier's own fuzzer, which is how #387 was found in the first
   place) -- so this seed would crash main's session_parse target on its first execution.

Re-pin note (not requested, but required): editing json_preflight.rs's comment shifts every
subsequent line number in that file, including its one slicing panic site
(`self.source[start..self.cursor]` in `Scanner::string`). Rust embeds a `core::panic::Location`
(file/line/column) constant at that call site regardless of the release profile's `-C
strip=debuginfo` (that strip level removes DWARF sections, not these compiled-in panic-location
strings), so a comment-only edit to a shipped source file still moves the wasm artifact's bytes.
Confirmed by rebuilding: digest changed 439ba6b5... -> f1f27d8d... at the identical byte count
(2,635,719), i.e. exactly a debug-metadata-only delta, not a behavior change. Re-pinned the
artifact and regenerated the qualification lineage against this commit's own hash, same as the
prior two commits did.

Per the coordinator's explicit instruction, crates/capi/tests/resource_lifecycle.rs is untouched;
its pre-existing, already-diagnosed-separately failure on main since #385 is out of scope here.

Commands run:
- cargo test --locked -p session
- bash scripts/check-session-policy.sh
- RUSTDOCFLAGS="-D warnings" cargo doc --locked -p session -p capi -p host-web --no-deps
- cargo fmt --all -- --check
- git diff --check
- cargo clippy --locked -p session -p capi -p host-web --all-targets -- -D warnings
- bash scripts/build-web-audioworklet.sh <dir> (re-pin, then normal build against the new pin)
- bash scripts/check-web-audioworklet.sh <dir>
- python3 scripts/check-browser-expected-resources.py
- ./target/x86_64-unknown-linux-gnu/release/session_parse fixtures/session/v1/fuzz-seeds/empty-object.json (single-input run, no crash)

Refs #387

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwL1uTcxsmopHtamG6bMko
… commit

The prior commit only edits comments/docs/a fuzz fixture/a workflow file, but one of those edits
(crates/session/src/json_preflight.rs's comment) is inside a real shipped source file. Shifting
its line numbers moved the compiled `core::panic::Location` constant for
`Scanner::string`'s one slicing panic site, which changed the wasm artifact's bytes even though no
behavior changed (2,635,719 bytes before and after; only the digest moved). See that commit's
message for the full explanation.

- MISO_ENGINE_WEB_AUDIOWORKLET_REPIN=1 bash scripts/build-web-audioworklet.sh <dir>: produced
  f1f27d8daa4c9f0828c9a3b2ca939bfdb507d041db6d7bc2a63ec6c826c1f74d (was
  439ba6b513c5b1c9c2d1ff462d5cdd43136b0d70df540d4a21c238886d165958).
- bash scripts/build-web-audioworklet.sh <dir> (normal build against the new pin): PASS, same
  2,635,719 bytes.
- bash scripts/check-web-audioworklet.sh <dir>: PASS.
- python3 scripts/check-browser-expected-resources.py: PASS.
- npm run qualify -- --artifacts <dir> --browser all --record-matrix
  --candidate-commit 1297393 --self-test-mutations: chromium
  151.0.7922.34, firefox 153.0, webkit 26.5 all passed.
- npm run qualify -- --artifacts <dir> --browser all --check-matrix --self-test-mutations: PASS
  against the recorded results.json.

Refs #387

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwL1uTcxsmopHtamG6bMko
tamashi095 added a commit that referenced this pull request Sep 4, 2026
…acle arms (#392)

## Summary

`crates/capi/tests/resource_lifecycle.rs`'s
`exported_c_candidates_replay_render_and_both_destroy_orders_balance_exactly` asserts an exact
allocation-call count against an exact deallocation-call count inside an armed window. After #385
replaced `jstrict` with upstream `json-syntax 0.12.5`, every object parse indexes through
`hashbrown` 0.12's `DefaultHashBuilder`, which is `ahash` 0.7's `RandomState`. Its **first**
construction in a process boxes three `once_cell::race::OnceBox` statics (`RAND_SOURCE`, its inner
`Box<dyn RandomSource>`, and the `SEEDS` array: 8 + 16 + 64 = 88 bytes) that live until process
exit and belong to no capi owner. `ahash`'s `build.rs` forces the `runtime-rng` feature on every
hosted target, so no Cargo feature removes it.

Those 88 bytes land on whichever thread parses the first `json-syntax` object in the process.
Under the parallel test harness that's a race between this file's three tests; on the CI runner
(4 vCPU) `exported_c_candidates_replay_render_and_both_destroy_orders_balance_exactly` loses the
race and gets charged for an allocation with no matching deallocation inside its own window
(`left: 2613 right: 2610`, deterministic with `RUST_TEST_THREADS=1`).

**Fix (test-only):** parse a trivial JSON object once, before `begin()` arms the allocator
oracle, so the lazy `ahash` statics are always initialized before any window starts observing.
Uses `{"warm":0}`, not `{}` — PR #390 (landing soon) makes the preflight refuse an empty object
before the parser runs, which would defeat the warm-up; `{"warm":0}` reaches
`json_syntax::Value::parse_str` and constructs the hasher regardless of what the preflight or
schema does with it afterward.

**Shipped caps are unaffected.** They are explicit row sums computed at compile time
(`crates/capi/src/runtime/compile.rs:185`), not derived from any allocator oracle, and this PR
touches no shipped file — confirmed by rebuilding the AudioWorklet artifact and diffing its digest
against the committed pin (unchanged, see Gate output).

Also adds one sentence to `docs/REALTIME_DEPENDENCY_POLICY.md`'s #385 session-parser entry
recording this lazy-allocation behavior, so a future reader of that policy (or a future allocator
oracle) isn't surprised by it again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session aborts the host on untrusted JSON containing an empty object {} (parser CodeMap volume bug, reachable from C API and wasm boot)

1 participant