Skip to content

Fix #692: host-walker GC rooting — release v0.0.158#693

Merged
aallan merged 5 commits into
mainfrom
fix-692-host-walker-gc-rooting
May 22, 2026
Merged

Fix #692: host-walker GC rooting — release v0.0.158#693
aallan merged 5 commits into
mainfrom
fix-692-host-walker-gc-rooting

Conversation

@aallan

@aallan aallan commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #692.

html_parse, json_parse, and md_parse no longer trap with Out-of-bounds memory access on inputs large enough to pressure GC during host-side tree marshalling. Root cause: missing-shadow-stack rooting in the three host walkers — same #570 / #515 / #593 bug class but on the host side rather than WAT-emitted user code. Python-held intermediate pointers (arr_ptr / name_ptr / wrapper_ptr) were invisible to the conservative GC scan; if a sub-walk triggered $gc_collect, those blocks were reclaimed and the next write into them corrupted the free list. Concrete trap signature: out-of-bounds access at 0xfffffffd (= 1 - 4 in i32) from inside $alloc's free-list traversal, with 1 being the HtmlText/Result.Err tag that overwrote a freed block's free-list link.

External report (an agent's summarise_urls example running html_parse over the current FAQ.md body) verified empirically; same shape proven in write_json (large nested JArray) and write_md_block (many headings).

Fix design — Option 3 from the issue body (architectural)

The fix follows the three-step architectural option described in #692 rather than the surgical Option 1 (pre-grow memory). Rationale: Option 1 closes only the reported call sites; Option 3 closes the bug class. Each future host walker that allocates more than one WASM object across Python-held pointers will inherit shadow-stack discipline by default.

1. Export $gc_sp and $gc_stack_limit from the emitted WAT module (vera/codegen/assembly.py). Inline export syntax on the global declarations; WAT-side gc_shadow_push helper for user code unchanged.

2. New _ShadowGuard context manager in vera/codegen/api.py.

  • On __enter__ snapshots $gc_sp.
  • guard.push(ptr) writes ptr at [gc_sp] and advances $gc_sp by 4. Re-acquires data_ptr each push (memory may have moved via memory.grow).
  • On __exit__ resets $gc_sp to the snapshot — atomically pops every push in the block, on both success and exception paths. Callers push freely without counting; the outer with is the unwind boundary.
  • Bytes written via the ctypes-pointer interface (buf[sp + i] = b per byte), not struct.pack_into — wasmtime-py's data_ptr returns LP_c_ubyte which lacks the buffer protocol pack_into requires.

3. Thread guard through the host walkers:

  • vera/wasm/html_serde.py::write_html — pushes name_ptr, wrapper_ptr, arr_ptr across element children walks; pushes string body for text/comment branches.
  • vera/wasm/json_serde.py::write_json — pushes arr_ptr for JArray, each val_ptr and wrapper_ptr for JObject (so each iteration's value survives the next sub-walk's GC); reordered JString to allocate the string before the body.
  • vera/wasm/markdown.py::write_md_block / write_md_inline + four array helpers — adopt the fields-first-then-body convention everywhere: allocate child contents first (rooted via guard), then allocate the body last so the body pointer is never held across another alloc.

4. Host import callers wrap the walk + final Result.Ok alloc in a guard:

  • host_html_parse, host_html_query (for html_parse and html_query)
  • host_json_parse
  • host_md_parse

Tests

Test plan

  • pytest tests/ — 3,815 pass (88 conformance, including 4 new GC-rooting regressions)
  • pytest tests/test_browser.py — 99 pass
  • mypy vera/ clean
  • ruff check --select S vera/ clean (the previously-fixed S110 rule plus all other security lints)
  • All 11 doc validators green (conformance, examples, spec, SKILL, README, FAQ, EXAMPLES, HTML, limitations sync, version sync, doc counts)
  • python scripts/check_doc_counts.py — consistent (3,945 tests, 32 files, 88 conformance, 35 examples)
  • python scripts/check_version_sync.py — v0.0.158 across 6 files
  • Empirical regression: vera run on the original FAQ.md reproducer returns Ok (was: trap)
  • Empirical regression: vera run on a 1000-element JSON array returns Ok (was: trap)
  • VERA_EAGER_GC=1 (forces GC on every alloc — most aggressive test) — both reproducers return Ok

Release prep

Per the v0.0.157 lesson (version bump in feature PR, not a follow-up PR):

  • Version 0.0.1570.0.158 in pyproject.toml, vera/__init__.py, README.md, docs/index.html, uv.lock.
  • CHANGELOG.md: [Unreleased][0.0.158] - 2026-05-19 with link references for the new version and the bumped Unreleased pointer.
  • HISTORY.md: new Stage 12 row for v0.0.158; release counter 157 → 158.
  • 87 → 88 conformance-count sweep across CLAUDE.md, AGENTS.md, SKILL.md, TESTING.md, FAQ.md, docs/index.html, ROADMAP.md.
  • Site assets rebuilt (docs/SKILL.md, docs/index.md, docs/llms.txt, docs/llms-full.txt, docs/sitemap.xml).

After merge: single git tag v0.0.158 <merge-commit> + gh release create with the awk-extracted CHANGELOG section.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed a host-side GC-rooting bug that could corrupt memory during HTML, JSON and Markdown parsing under allocation pressure.
  • Documentation

    • Released v0.0.158; updated changelog, history, README, roadmap, testing guide, FAQ and CI docs.
    • Conformance-suite count increased from 87 to 88; manifests and references updated.
  • Tests

    • Added regression tests exercising host-side GC-rooting across HTML, JSON and Markdown parsing.
  • Known Issues

    • Documented a GC reachability bug for certain map/value patterns and a flaky browser-test timeout with workarounds.

Review Change Stack

Closes #692.

html_parse, json_parse, and md_parse no longer trap with
Out-of-bounds memory access on inputs large enough to pressure
GC during host-side tree marshalling.  Root cause: missing-
shadow-stack rooting in vera/wasm/html_serde.py::write_html,
vera/wasm/json_serde.py::write_json, and
vera/wasm/markdown.py::write_md_block/write_md_inline — same
#570 / #515 / #593 bug class but on the host side rather than
WAT-emitted user code.  Python-held intermediate pointers
(arr_ptr / name_ptr / wrapper_ptr) were invisible to the
conservative GC scan; if a sub-walk triggered $gc_collect those
blocks were reclaimed and subsequent writes corrupted the free
list, trapping at 0xfffffffd inside $alloc's free-list
traversal.

Fix design (Option 3 from the issue body — architectural):

1. Export $gc_sp and $gc_stack_limit as globals from the
   emitted WAT module so host imports can manipulate the
   shadow-stack pointer.  Inline export syntax on the globals;
   WAT-side gc_shadow_push helper for user code unchanged.

2. New _ShadowGuard context manager in vera/codegen/api.py.
   On __enter__ snapshots $gc_sp; on __exit__ resets $gc_sp
   to the snapshot — atomically popping every push in the
   block, on both success and exception paths.  Callers push
   freely without counting.

3. Thread guard parameter through write_html, write_json,
   write_md_block, write_md_inline, and the markdown array
   helpers (_write_inline_array, _write_block_array,
   _write_array_of_block_arrays, _write_table_data).  Each
   intermediate alloc pushes its pointer onto the guard
   before any subsequent alloc that could trigger GC.

4. Markdown walkers also adopt the fields-first-then-body
   convention — allocate child contents first (rooted via
   guard), then allocate the body last so the body pointer
   is never held across another alloc.

5. host_html_parse, host_html_query, host_json_parse,
   host_md_parse wrap the walk + final Result.Ok alloc in
   `with _ShadowGuard(caller) as guard:`.

Tests:

- tests/conformance/ch09_host_walker_gc_rooting.vera (run-
  level, 4 sub-tests) for html_parse (500 element siblings),
  json_parse (1000-element number array + 500-element string
  array), and md_parse (200 H1 + paragraph blocks).  Manifest
  entry added — conformance suite now 88 programs.

- tests/test_codegen.py::TestHostWalkerGCRooting692 — 4 in-
  process regression tests for the same scenarios at the
  codegen layer, alongside the existing host-side GC-rooting
  regression classes.

- Updated tests/test_codegen.py::TestWorklistOverflow348
  structural assertion to match the new exported
  $gc_stack_limit WAT shape.

Release prep (per the version-bump-in-feature-PR rule):

- Version 0.0.157 → 0.0.158 in pyproject.toml,
  vera/__init__.py, README.md, docs/index.html, uv.lock.
- CHANGELOG cut: [Unreleased] → [0.0.158] - 2026-05-19 with
  link references for both the new version and the bumped
  Unreleased pointer.
- HISTORY.md row + release counter (157 → 158 tagged).
- 87 → 88 conformance count sweep across CLAUDE.md, AGENTS.md,
  SKILL.md, TESTING.md, FAQ.md, docs/index.html, ROADMAP.md.

Validation: 3,815 pytest pass + 99 browser pass, mypy clean,
ruff S clean, 88 conformance programs pass, 35 examples pass,
all 11 doc validators green, version sync clean across 6
files, doc counts consistent.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.06780% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.90%. Comparing base (8f9e15b) to head (a167d22).

Files with missing lines Patch % Lines
vera/codegen/api.py 87.27% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #693      +/-   ##
==========================================
+ Coverage   90.88%   90.90%   +0.02%     
==========================================
  Files          60       60              
  Lines       23529    23606      +77     
  Branches      259      259              
==========================================
+ Hits        21384    21459      +75     
- Misses       2138     2140       +2     
  Partials        7        7              
Flag Coverage Δ
javascript 57.43% <ø> (ø)
python 94.63% <94.06%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1721ebaf-a83b-46fa-9d19-99771e6da1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9b170bd and a167d22.

📒 Files selected for processing (1)
  • README.md

📝 Walkthrough

Walkthrough

Exports WASM shadow-stack globals and adds a host-side _ShadowGuard; threads the guard through HTML/JSON/Markdown serde writers and host parse/query functions; adds end-to-end regression tests, a conformance manifest entry, and version/docs updates for v0.0.158.

Changes

Host-side GC shadow-stack rooting for tree marshalling

Layer / File(s) Summary
WASM shadow-stack global exports and assembly updates
vera/codegen/assembly.py, tests/test_codegen.py
The generated WASM now exports $gc_sp and $gc_stack_limit; tests assert the gc_stack_limit export form is present.
Host-side _ShadowGuard context manager
vera/codegen/api.py
New _ShadowGuard snapshots/restores gc_sp, provides push() to write pointers onto the shadow stack with bounds checking and restores on exit.
HTML serde guard threading and rooting discipline
vera/wasm/html_serde.py
write_html accepts guard and roots allocated name/attributes/children payloads via guard.push(...) before allocations; recursive calls receive the same guard.
JSON serde guard threading and rooting discipline
vera/wasm/json_serde.py
write_json accepts guard and roots string/array/object intermediate pointers onto the guard before allocating enclosing nodes; recursive calls pass guard.
Markdown serde guard threading and rooting discipline
vera/wasm/markdown.py
write_md_inline, write_md_block, and array/table helpers accept and thread guard; they allocate fields/backings first, push them onto guard, then allocate constructor bodies.
Host parsing and query functions thread shadow guard
vera/codegen/api.py
host_md_parse, host_json_parse, host_html_parse, and host_html_query create/use _ShadowGuard, push intermediate pointers before allocating Result.Ok wrappers, and pass guard into recursive writers.
Regression tests for #692 host walker GC rooting
tests/test_codegen.py
Add TestHostWalkerGCRooting692 with end-to-end IO tests stressing host walkers under GC pressure to assert successful completion and "ok" stdout.
Conformance manifest and version/docs updates
tests/conformance/manifest.json, pyproject.toml, vera/__init__.py, README.md, CHANGELOG.md, HISTORY.md, AGENTS.md, CLAUDE.md, FAQ.md, ROADMAP.md, SKILL.md, TESTING.md, KNOWN_ISSUES.md
Add ch09_host_walker_gc_rooting.vera to the manifest, bump package/version metadata to 0.0.158, update docs and counts to 88 conformance programs, and record changelog/release notes.

Sequence Diagram

sequenceDiagram
  participant Host
  participant ShadowGuard as _ShadowGuard
  participant Serde as SerdeWriter
  participant WASM as WASM_memory
  participant GC as gc_collect

  Host->>ShadowGuard: __enter__ (snapshot gc_sp)
  Host->>Serde: write(..., guard, input)
  Serde->>WASM: alloc(field_size)
  WASM-->>Serde: field_ptr
  Serde->>ShadowGuard: push(field_ptr)
  Serde->>WASM: alloc(body_size)
  WASM->>GC: gc_collect (if memory full)
  GC->>WASM: scan shadow_stack (sees field_ptr)
  GC-->>WASM: mark field_ptr live
  WASM-->>Serde: body_ptr
  Serde->>Serde: recurse with same guard
  Host->>ShadowGuard: __exit__ (restore gc_sp)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • aallan/vera#473: Prior changes to GC shadow-stack layout and bounds behaviour that intersect with this PR's exports.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the primary change: a host-walker GC rooting fix addressing issue #692, culminating in release v0.0.158.
Linked Issues check ✅ Passed All coding objectives from #692 are met: shadow-stack exports added, _ShadowGuard implemented, host walkers (html/json/markdown) threaded with rooting, regression tests added, version bumped.
Out of Scope Changes check ✅ Passed All changes remain within scope of #692 closure: GC rooting implementation, conformance/regression tests, version/documentation updates. Pre-existing Map<K, T_heap> issue (#695) appropriately deferred.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-692-host-walker-gc-rooting

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
TESTING.md (1)

193-193: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the parametrised test count from 435 to 440.

The comment still references the old count (87 programmes × 5 stages = 435 tests). With the new conformance test added, this should be 88 × 5 = 440 tests, matching the table entry at line 82.

📝 Proposed fix
-# Via pytest (parametrized — 435 tests)
+# Via pytest (parametrized — 440 tests)

As per coding guidelines, when updating conformance counts, all literal occurrences of the old number must be updated consistently across the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TESTING.md` at line 193, Update the hard-coded test count in the TESTING.md
header string "# Via pytest (parametrized — 435 tests)" to reflect the new total
of 440 tests (88 programmes × 5 stages) and ensure any other literal occurrences
of "435" in this file are changed to "440" so the textual count matches the
table entry and new conformance test; search for the exact header text and any
standalone "435" tokens in TESTING.md and replace them with "440".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vera/codegen/api.py`:
- Around line 1791-1803: The new guarded marshalling using _ShadowGuard and
write_md_block is currently inside the outer except that converts all exceptions
into a Result.Err(String) for md_parse; update md_parse so that construction/use
of _ShadowGuard and the call to write_md_block (and the call to
_alloc_result_ok_i32 on success) are executed outside the broad
exception-to-Result conversion window so host-side invariant failures (e.g.
shadow-stack overflows raised by _ShadowGuard) propagate as real exceptions
instead of being swallowed; keep only true user/parse error handling inside the
except that produces Result.Err(String), and let any exceptions from
_ShadowGuard/write_md_block escape (or re-raise them) rather than being turned
into a parse error.

In `@vera/wasm/markdown.py`:
- Around line 119-120: The code is re-pushing arr_ptr into the guard after
calling helper functions that already root the same buffer, doubling
shadow-stack usage; remove the redundant guard.push(arr_ptr) calls that
immediately follow calls to _write_inline_array, _write_block_array,
_write_array_of_block_arrays, and _write_table_data so the helpers remain the
single point that roots the backing buffers. Locate each occurrence of
guard.push(arr_ptr) directly after those helper calls (several places mirror the
snippet shown) and delete the duplicate push; keep a guard.push only in callers
that allocate or return an unrooted pointer (i.e., when a helper does not
already push). Ensure no other logic depends on the extra push so the outer
guard still unwinds correctly.

---

Outside diff comments:
In `@TESTING.md`:
- Line 193: Update the hard-coded test count in the TESTING.md header string "#
Via pytest (parametrized — 435 tests)" to reflect the new total of 440 tests (88
programmes × 5 stages) and ensure any other literal occurrences of "435" in this
file are changed to "440" so the textual count matches the table entry and new
conformance test; search for the exact header text and any standalone "435"
tokens in TESTING.md and replace them with "440".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0081842-0212-4f4a-be22-96675287d637

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9e15b and 808a7bb.

⛔ Files ignored due to path filters (8)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • docs/sitemap.xml is excluded by !docs/**
  • tests/conformance/ch09_host_walker_gc_rooting.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (18)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • tests/conformance/manifest.json
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/codegen/api.py
  • vera/codegen/assembly.py
  • vera/wasm/html_serde.py
  • vera/wasm/json_serde.py
  • vera/wasm/markdown.py
👮 Files not reviewed due to content moderation or server errors (1)
  • tests/test_codegen.py

Comment thread vera/codegen/api.py Outdated
Comment thread vera/wasm/markdown.py Outdated
1. host_md_parse exception scope (#693 inline #1): the broad
   except Exception was swallowing host-side invariant
   violations (shadow-stack overflow from _ShadowGuard,
   ValueError from write_md_block exhaustive match, internal
   AssertionErrors).  Restructured to put only the parse step
   inside the try; shadow-stack work + write_md_block +
   Result.Ok alloc now sit outside, so invariant violations
   propagate as wasmtime traps rather than being repackaged as
   user-domain parse errors.  Mirrors the narrow-catch pattern
   in host_html_parse and host_json_parse.

2. Redundant guard.push in markdown.py walkers (#693 inline #2):
   the _write_inline_array / _write_block_array /
   _write_array_of_block_arrays / _write_table_data helpers
   already push their backing buffer onto the shadow stack
   internally.  Removed the duplicate guard.push(arr_ptr) calls
   immediately after these helpers in write_md_inline (MdEmph,
   MdStrong, MdLink) and write_md_block (MdParagraph,
   MdHeading, MdBlockQuote, MdList, MdTable, MdDocument).
   Functionally equivalent (the entry IS rooted, just twice)
   but halves shadow-stack consumption from those branches and
   removes the only correct way to overflow the guard on
   pathologically deep walks.  Pushes after alloc_string calls
   are KEPT - alloc_string does not push.

3. TESTING.md "435 tests" -> "440 tests" (outside-diff): the
   pytest-parametrized shell-block comment now matches the
   table entry and the new 88-program x 5-stage total.

4. Tracking: added #694 (Windows test_browser timeout flake) to
   KNOWN_ISSUES.md "Test coverage gaps" with the symptom,
   asymmetry-test diagnostic that proves it is a flake (3.11
   and 3.13 pass on the same Windows runner where 3.12 timed
   out), and the one-line fix (bump timeout=30 to timeout=60 at
   the two sites).  Per the PR-generated-new-issues-must-be-
   tracked-in-the-same-PR rule.

Validation: 3,815 pytest pass + 88 conformance + mypy clean +
all doc validators green + limitations sync consistent.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai also applied the outside-diff TESTING.md finding in 4b8c127: line 193's pytest-parametrized shell-block comment is now # Via pytest (parametrized — 440 tests) to match the conformance suite's new 88 programs × 5 stages total.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Confirmed — 88 × 5 = 440 checks out. The comment is now consistent with the rest of the conformance-count updates in this PR. Thanks for the follow-through.

Four agents in parallel: code-reviewer, pr-test-analyzer,
comment-analyzer, silent-failure-hunter.  Each caught items
the others missed.  All applied.

Critical:

1. host_html_parse had its _ShadowGuard block INSIDE the
   narrow `except (ValueError, TypeError, AttributeError)`,
   while host_md_parse (just restructured per the previous
   CodeRabbit round) and host_json_parse have it OUTSIDE.
   The in-file comment claimed they matched.  They didn't.
   Caught by silent-failure-hunter AND comment-analyzer from
   different angles — same finding via two different paths.
   Restructured host_html_parse to match: parse-only-in-try,
   shadow-stack work + write_html + Result.Ok alloc outside.
   Host-side invariant violations (`_ShadowGuard` overflow,
   `_wrap_handle` RuntimeError from #578, AssertionErrors)
   now propagate as wasmtime traps consistent with the other
   two walkers.

Important:

2. _ShadowGuard.__init__ wraps the gc_sp / gc_stack_limit
   lookups in a try/except that re-raises as a clearer
   RuntimeError naming the missing exports and #692.
   Diagnostic-quality fix — a hand-crafted .wat fixture
   without the GC globals exported would otherwise produce
   a bare "python exception" trap.

3. _ShadowGuard.__exit__ tightened: the `if self._initial_sp
   is not None` guard is now an `assert`.  The only way the
   None case is reachable is if someone calls __exit__
   without going through __enter__ (misuse of the context
   manager outside a `with` block).  The assert pins the
   invariant; permissive `if` was silently allowing misuse.

4. host_html_query comment softened — previous text claimed
   "WASM codegen at the call site shadow-pushes it via the
   standard mechanism", overstating the guard's protection.
   The guard exits before the function returns; the WASM-
   side caller's local-store push is what roots the result
   thereafter.  Safe in practice because no allocation
   happens between return and local-store.  Comment now
   says exactly that.

5. host_md_parse comment phrasing fixed — previously said
   "Mirrors the narrow-catch pattern in host_html_parse and
   host_json_parse" but host_html_parse had different
   structure pre-fix.  Now says "Matches the parse-only-in-
   try structure of host_html_parse and host_json_parse"
   which is structurally accurate post-fix.

6. JObject branch of write_json gains an exception-safety
   note documenting why map_dict partial state is safe to
   discard on mid-loop raise (function exits via the raise
   before map_alloc is called; the val_ptrs are guaranteed-
   invalid after guard exit and a future "rescue" would
   trigger use-after-free).

7. Rooting-contract docstrings added to all four markdown
   array helpers (_write_inline_array, _write_block_array,
   _write_array_of_block_arrays, _write_table_data) so
   future maintainers see the convention from the function
   signature.

8. Markdown helpers' alloc-then-push allocation order is
   now documented as load-bearing — a future "fix" must NOT
   try to push before alloc or wrap the push in try/except.

Test coverage additions (pr-test-analyzer):

9. test_html_query_30_matches — exercises the
   host_html_query _ShadowGuard path that re-walks every
   matched subtree.  Bounded at 30 (rather than 500 like
   the html_parse tests) because each match's recursive
   write_html accumulates pushes within a single guard
   window; empirically 100 overflows the 4096-entry stack
   when combined with the prior html_parse's retained
   pushes.  30 still triggers GC during the walk.

10. test_json_parse_500_key_object — exercises the JObject
    branch of write_json (val_ptr-pushed-per-iteration
    pattern) that motivated the whole fix.

Skipped with reason:

- Pre-existing _alloc_array_of_strings has the same bug
  shape but predates #692 and isn't in scope; file as a
  follow-up issue rather than expand this PR.
- _ShadowGuard unit tests for the exit-on-exception
  contract: the class is a closure inside compile_to_wasm,
  not module-level, so direct unit testing requires
  extracting it.  The behavioural contract is implicitly
  verified by 6 in-process tests + 4 conformance tests all
  passing under VERA_EAGER_GC=1.
- Narrow host_md_parse's `except Exception` further:
  parse_markdown currently has zero raise statements but
  could grow them; keep broad as defensive.
- Dedicated RuntimeError subclass for shadow-stack overflow:
  the message starts with #692 which already carries the
  needed context.

Validation: 3,817 pytest pass (added 2 tests, +2 from 3,815)
+ mypy clean + ruff S clean + 88 conformance pass + 35
examples pass + all doc validators green + version sync
clean + limitations sync consistent.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit: code-reviewer agent findings

Agent reviewed: CLAUDE.md compliance, general code quality, bug detection, release-prep correctness. Ran pytest tests/test_codegen.py::TestHostWalkerGCRooting692 tests/test_codegen.py::TestWorklistOverflow348 (7/7 pass), scripts/check_conformance.py (88/88 pass), grepped for stale 87 references (none found).

Critical / Important

None blocking. The fix correctly identifies and closes the bug class.

Suggestions

Pre-existing similar bug class outside #692 scope. _alloc_array_of_strings at vera/codegen/api.py:1214-1230 has the identical shape — backing_ptr held in a Python local across count sub-allocations of str_ptr via _alloc_string. Called from at least 5 sites (host_args, host_md_extract_code_blocks, Regex.findall, Map.keys, Map.values, Set elements path). Not regressed by this PR (predates it) and not in the bug report, but it's the same shape and would benefit from the same _ShadowGuard treatment.

Comment slightly misleading at host_html_query (vera/codegen/api.py:3203-3206): "WASM codegen at the call site shadow-pushes it via the standard mechanism" overstates the protection. _translate_html_query in vera/wasm/calls_markup.py:93-110 emits just call $vera.html_query with no explicit shadow push; the consuming let/match is what shadow-pushes the result.

  • Verdict: ✅ Applied in 4e43eaa. Comment softened to: "the guard exits before the function returns; the WASM-side caller's local-store push is what roots the result thereafter. Safe in practice because no allocation happens between return and local-store."

Strengths flagged by the agent

  • Round-2 fix is structurally sound. Redundant guard.push(arr_ptr) correctly removed from markdown call sites after helpers that root internally.
  • JString reorder in json_serde.py:103-116 allocates the string first and roots it before the JString wrapper — eliminates the secondary bug shape.
  • WAT exports use inline export syntax — minimal surface change.
  • Module-docstring discipline — all three serde modules explain the guard contract upfront.
  • Release prep correctness: CHANGELOG [0.0.158] mirrors [0.0.157] shape; link references chained correctly; version sync clean across 6 files; HISTORY.md row consistent with the v0.0.156/v0.0.157 longer-form precedent.

Verdict

Ship after dropping the redundant pushes (already done in 4b8c127) and softening the host_html_query comment (done in 4e43eaa). Pre-existing _alloc_array_of_strings follow-up tracked outside this PR.

@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit: pr-test-analyzer agent findings

Agent reviewed: behavioural test coverage on the new GC-rooting fix, conformance manifest wiring, test sizing rationale.

Important gaps

1. JObject branch of write_json has no large-input test (rating 7). vera/wasm/json_serde.py:155-180 now pushes three distinct intermediate pointers per dict iteration plus map_dict is a Python-local-only int dict — the exact conservative-scan-blind-spot pattern that motivated the whole PR. None of the four original tests exercise this branch.

  • Verdict: ✅ Applied in 4e43eaa. Added test_json_parse_500_key_object — 500-key flat JObject like {"k":0, "k":0, ..., "last":0}. Exercises the val_ptr-pushed-per-iteration pattern + wrapper_ptr push + body alloc.

2. host_html_query is touched but untested at scale (rating 6). vera/codegen/api.py:3196-3216 adds a _ShadowGuard around the arr_ptr + per-match write_html loop. Existing tests use trivial 1-3 element inputs.

  • Verdict: ✅ Applied in 4e43eaa. Added test_html_query_30_matches — 30 <p>x</p> siblings under <root>, then html_query("p") to exercise the re-walk path. Bounded at 30 rather than 500: each match's recursive write_html accumulates pushes within a single guard window, and empirically 100 overflows the 4096-entry stack when combined with the prior html_parse's retained pushes. 30 still triggers GC during the walk.

3. _ShadowGuard is not unit-tested directly (rating 5). The four added tests cover the integration path but never the contract: __exit__ resetting $gc_sp on success, __exit__ doing so on exception, the no-push case, overflow detection.

  • Verdict: ⏭️ Skipped with reason. _ShadowGuard is a closure inside compile_to_wasm, not module-level — direct unit testing requires extracting it. The behavioural contract is implicitly verified by 6 in-process tests + 4 conformance tests all passing under VERA_EAGER_GC=1 (forces GC on every alloc, exercising the rooting path maximally). Filing as a follow-up if a regression surfaces.

Test quality issues

  • test_worklist_size_quadrupled_in_wat couples to exact WAT string formatting. Acceptable as-is — it's a structural sentinel — but flag for awareness.

  • Documentation of dropped patterns: the "unclosed tags 100 → 50 → dropped due to recursion limit" design history isn't preserved.

    • Verdict: ⏭️ The conformance test header at lines 23-28 explains the 500-bound choice, and the CHANGELOG at line 23 mentions the recursion limit constraint. Adequate without a separate breadcrumb.

Strengths flagged

  • Test sizes target page-growth rather than absolute memory pressure — making accidental-pass on larger-initial-memory machines unlikely.
  • Each new test has a clear docstring naming the specific intermediate pointers it exercises — DAMP.
  • Class docstring cross-references the WAT-side cousins (TestArrayMapGCRooting570, TestFoldAccumulator515, TestClosureReturnShadowAsymmetry593).
  • Structural WAT assertion correctly pins the new exported $gc_stack_limit shape.
  • Manifest entry correctly placed alongside related ch09 entries.

Verdict

Two important test gaps closed in 4e43eaa (host_html_query + JObject large-input). _ShadowGuard unit-test gap deferred — implicit coverage is strong; explicit unit tests require closure-extraction work outside this PR's scope.

@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit: comment-analyzer agent findings

Agent reviewed: comment accuracy vs implementation, cross-reference correctness, claim-vs-code drift.

Improvement opportunities

1. host_html_query comment overstates protection (vera/codegen/api.py:3203-3206): "WASM codegen at the call site shadow-pushes it via the standard mechanism" — but _translate_html_query in calls_markup.py:93-110 emits just call $vera.html_query with no automatic shadow push of the (arr_ptr, count) return pair. The receiving local push is what roots it.

  • Verdict: ✅ Applied in 4e43eaa. Same finding as code-reviewer's v0.0.5: Type checker #2 from a different angle. Comment now: "the guard exits before the function returns; the WASM-side caller's local-store push is what roots the result thereafter. Safe in practice because no allocation happens between return and local-store, but the guard's protection does NOT extend past the function boundary."

2. host_md_parse "Mirrors the narrow-catch pattern in host_html_parse and host_json_parse" (vera/codegen/api.py:1797-1798): this implies structural identity, but host_html_parse (pre-fix) placed the _ShadowGuard block INSIDE its try while host_md_parse and host_json_parse had it OUTSIDE.

  • Verdict: ✅ Applied in 4e43eaa. Combined with silent-failure-hunter's same finding: fixed host_html_parse to put shadow-stack outside the try (structural match restored), and updated the comment to "Matches the parse-only-in-try structure of host_html_parse and host_json_parse (both narrow their catch around only the parse call, with _ShadowGuard usage outside)" — now structurally accurate.

3. _write_inline_array docstring missing rooting contract (vera/wasm/markdown.py:316-336): one-line docstring doesn't tell a developer reading just the function that backing remains on the shadow stack after return.

  • Verdict: ✅ Applied in 4e43eaa. Added rooting-contract docstrings to all four markdown array helpers (_write_inline_array, _write_block_array, _write_array_of_block_arrays, _write_table_data) so future maintainers see the convention from the function signature.

4. assembly.py:367-368 "$gc_sp is (mut)" phrasing elides WASM-side mutator: "the host advances it to push, resets it to pop" could be read as the host being the only mutator, but $gc_collect and function prologues/epilogues also mutate it.

  • Verdict: ⏭️ Skipped with reason. The agent rated this low ("could infer") and the comment block is already substantial. The WAT-side gc_shadow_push helper documented at vera/wasm/helpers.py:113 makes the WAT-side mutation discoverable. Adding another sentence would tip into over-commenting.

Positive findings

  • _ShadowGuard.push() comment explaining why struct.pack_into doesn't work (LP_c_ubyte lacks buffer protocol) and why data_ptr must be re-acquired on every push — exactly the "non-obvious why" comments should capture.
  • markdown.py module docstring's "allocate fields first, root them, allocate the body last" convention is the architectural invariant future maintainers need.
  • "No second push needed here" comments in markdown branches are well-targeted; without them a reviewer might "fix" the omission.
  • HISTORY.md row matches v0.0.156/v0.0.157 precedent — denser than older Stage 12 rows but not drifted into mini-CHANGELOG shape.
  • #694 entry in KNOWN_ISSUES.md — file:line refs to test_browser.py:160 and :1736 both verified accurate (both contain timeout=30); diagnostic concrete and actionable.

Verdict

Three of four findings applied in 4e43eaa; one skipped as low-value over-commenting. Comments now accurately describe the code on every flagged site.

@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit: silent-failure-hunter agent findings

Agent reviewed: silent failures, exception-scope correctness, masking, resource leaks.

Critical

1. host_html_parse shadow-stack work is INSIDE its narrow except (vera/codegen/api.py:3129-3155): the with-block sits between try: and except (ValueError, TypeError, AttributeError). Silently repackages ValueError from _alloc_map_wrapper_wrap_handle (the #578 _validate_wrap_handle range check), AttributeError from a malformed node dict, TypeError from _write_i32/alloc boundary mismatches. Comment at 3146-3154 promises "we deliberately do NOT catch invariant violations" but the code structure contradicts it.

  • Verdict: ✅ Applied in 4e43eaa. Same finding caught by comment-analyzer from a different angle. Restructured host_html_parse to put parse-only-in-try, shadow-stack work + write_html + Result.Ok alloc OUTSIDE the narrow except. All three host walkers (html, json, md) now structurally match. This was the most material finding of the review.

Important

2. host_md_parse except Exception is broader than necessaryparse_markdown has zero raise statements anywhere in the module today.

  • Verdict: ⏭️ Skipped with reason. Keep broad as defensive: parse_markdown could grow raise statements in future, and a future bug there should still surface as Result.Err. Narrowing now creates a regression risk for marginal cleanup benefit.

3. _ShadowGuard.push() raises bare RuntimeError that becomes "python exception" trap — different diagnostic shape from the WAT-side unreachable overflow.

  • Verdict: ⏭️ Skipped with reason. The message starts with #692: and includes sp= / limit= values, which is enough debugging context. A dedicated HostShadowStackOverflow subclass would be marginal diagnostic-quality improvement for non-zero implementation cost.

4. _ShadowGuard.__init__ raises bare KeyError if globals not exported.

  • Verdict: ✅ Applied in 4e43eaa. Wrapped both caller["gc_sp"] and caller["gc_stack_limit"] lookups in a try/except that re-raises as RuntimeError("#692: host walker requires the module to export $gc_spand$gc_stack_limit ...") so hand-crafted-.wat scenarios get an actionable diagnostic.

5. _ShadowGuard.__exit__'s if self._initial_sp is not None is defensive but unreachable — should be assert.

  • Verdict: ✅ Applied in 4e43eaa. Tightened to assert self._initial_sp is not None. The only way the None case is reachable is misuse outside a with block; the assert pins the invariant.

6. Markdown helpers push backing AFTER alloc — exception between them leaks the block (vera/wasm/markdown.py:329-330, 352-353, 377-378, 402-403, 414-415).

  • Verdict: ✅ Applied in 4e43eaa. Added explicit comment block documenting the alloc-then-push allocation order is load-bearing — a future "fix" must NOT try to push before alloc (impossible without a ptr) or wrap the push in try/except (would break the trap-on-overflow invariant). Confirmed correct semantics: exception propagates, walk abandoned, next GC reclaims the leaked block.

Important (data integrity)

7. JSON JObject loop accumulates val_ptrs in map_dict after pushes are unwound on exception.

  • Verdict: ✅ Applied in 4e43eaa. Added an exception-safety note to the JObject branch documenting: map_dict partial state is safe to discard on mid-loop raise (function exits via the raise BEFORE map_alloc is called; the val_ptrs are guaranteed-invalid after guard exit and a future "rescue" would trigger use-after-free).

Suggestions

8. __exit__'s set_value could mask original body exception (Python's normal exception-chain semantics; minor diagnostic quality).

  • Verdict: ⏭️ Skipped with reason. Theoretical — set_value on a known-valid Global doesn't fail in practice. Wrapping in another try/except adds complexity for a path that doesn't fire.

9. _ShadowGuard._caller could become invalid if leaked — guard always used inside a single host function; no path to leak.

  • Verdict: ⏭️ Skipped with reason. No reachable leak path; theoretical concern.

Positive findings

  • host_json_parse is the gold-standard pattern — narrow catch around _json.loads only, _ShadowGuard block outside. Used as the reference for restructuring host_html_parse.
  • if s_ptr != 0 empty-string guards correctly avoid pushing the GC's "not a heap object" sentinel.
  • Comments throughout the diff explain why each push happens before each alloc with concrete trap signatures.
  • fields-first-then-body convention in markdown is exactly the right invariant — body pointer never has to be rooted.
  • gc_stack_limit is correctly immutable in WAT (no (mut i32)) — host can read but not corrupt.

Verdict

One critical asymmetry fixed (host_html_parse restructure — the headline finding). Five important findings applied; three skipped with documented reasons. All four host walkers now structurally consistent and the stated invariants match the code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/wasm/json_serde.py (1)

180-193: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

JObject still loses reachability of nested Json nodes after the guard unwinds.

The map_dict values here are plain Python ints, and the wrapper written at Lines 188-192 stores only the map handle. Once the caller’s _ShadowGuard exits, the earlier guard.push(val_ptr) roots disappear, so a later GC can reclaim those child Json nodes even though the JObject itself is still live. That leaves _map_store[handle] holding dangling heap pointers for later object traversal/stringify. This needs either a WASM-visible representation for object members or a tracing/rooting scheme for Map<String, Json> entries. Based on learnings: Json values are vera-heap-allocated tagged ADTs, and a Map handle inside JObject is only an internal layout detail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/wasm/json_serde.py` around lines 180 - 193, The JObject creation
currently stores plain Python ints for child Json pointers (built in write_json)
so guard.push(val_ptr) unwinds and children can be GC'd; change the construction
so the Map/Map handle produced by map_alloc retains WASM-visible references to
its member Json nodes (or register them in the heap rooting/tracing structures)
instead of relying on the temporary guard. Concretely, in the block that calls
write_json and then map_alloc: either have map_alloc accept and copy the child
val_ptrs into vera-heap objects (so the resulting wrapper_ptr references live
heap pointers), or create a small wasm-allocated container per member (via
alloc) that stores the child pointer and pass those containers to map_alloc;
alternatively, record the child pointers into _map_store[handle] (or the global
tracer) before popping the guard so JObject (the _TAG_JOBJECT node) holds
reachable roots. Update the code paths around write_json, guard.push, map_alloc,
and the place that writes _TAG_JOBJECT to ensure children remain rooted after
the guard unwinds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_codegen.py`:
- Around line 19226-19227: The test currently uses the private helper _run_io;
replace each assertion that calls _run_io(...) == "ok" (and other direct _run_io
uses) with the approved test helpers: use _check_ok(...) or _verify_ok(...) for
expected-success cases and _check_err(...) or _verify_err(...) for
expected-failure cases. Locate occurrences of _run_io in the test file (symbol:
_run_io) and update each to call the corresponding wrapper helper
(_check_ok/_verify_ok/_check_err/_verify_err) with the same input and expected
outputs, ensuring any existing assertions or exception checks are moved into the
appropriate helper call.

---

Outside diff comments:
In `@vera/wasm/json_serde.py`:
- Around line 180-193: The JObject creation currently stores plain Python ints
for child Json pointers (built in write_json) so guard.push(val_ptr) unwinds and
children can be GC'd; change the construction so the Map/Map handle produced by
map_alloc retains WASM-visible references to its member Json nodes (or register
them in the heap rooting/tracing structures) instead of relying on the temporary
guard. Concretely, in the block that calls write_json and then map_alloc: either
have map_alloc accept and copy the child val_ptrs into vera-heap objects (so the
resulting wrapper_ptr references live heap pointers), or create a small
wasm-allocated container per member (via alloc) that stores the child pointer
and pass those containers to map_alloc; alternatively, record the child pointers
into _map_store[handle] (or the global tracer) before popping the guard so
JObject (the _TAG_JOBJECT node) holds reachable roots. Update the code paths
around write_json, guard.push, map_alloc, and the place that writes _TAG_JOBJECT
to ensure children remain rooted after the guard unwinds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 66525426-e206-497d-a92a-05865793d108

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8c127 and 4e43eaa.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • vera/codegen/api.py
  • vera/wasm/json_serde.py
  • vera/wasm/markdown.py

Comment thread tests/test_codegen.py
CodeRabbit's outside-diff observation on json_serde.py JObject
correctly identifies a real pre-existing architectural bug: Map
values that are heap pointers (Map<K, Json>, Map<K, HtmlNode>,
etc.) are stored in Python-side _map_store but invisible to the
conservative GC scan, so a post-walk gc_collect frees them.

This pre-dates #692 — the #692 fix improved DURING-walk safety
(intermediate val_ptrs pushed to shadow stack via guard) but
does not address POST-walk reachability.  Files as #695 with
reproducer sketch, three candidate fixes (host-side WASM
container per entry / extend GC tracing / serialise into WASM),
and the affected-Map-types breakdown.

Added to KNOWN_ISSUES.md "Bugs" section per the PR-generated-
new-issues-must-be-tracked-in-the-same-PR rule.

CodeRabbit inline finding on tests/test_codegen.py (_run_io ->
_check_ok) is invalid and skipped — _check_ok only type-checks
the program; my regression tests need to ACTUALLY EXECUTE the
code to exercise the GC pressure path that motivated #692.
The reply on the PR explains.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented May 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai outside-diff finding (vera/wasm/json_serde.py JObject children rooting) verified — real architectural bug, but pre-dates #692 and applies to the broader Map<K, T_heap> contract, not just JObject.

Confirmed by inspecting git show $(git merge-base main HEAD):vera/wasm/json_serde.py: the pre-#692 JObject branch had the same map_dict[str(k)] = val_ptr pattern that stores heap pointers as Python ints in _map_store. The #692 fix improved DURING-walk safety by pushing val_ptrs to the shadow stack via guard, but does NOT address POST-walk reachability — after the guard exits (resetting $gc_sp), val_ptrs are once again only reachable from _map_store[handle], which the conservative GC scan can't see (handle is bit-31 tagged per #578, so it's structurally not a heap pointer).

The bug affects all Map<K, T_heap> not just JObject:

  • 🐛 Vulnerable: Map<K, Json> (JObject), Map<K, HtmlNode>, Map<K, Md*>, any user-defined Map<K, T> where T is heap-allocated.
  • ✅ Safe: Map<K, V_inline> (Int/Bool/Float — values stored as Python primitives), Map<K, String> (strings re-encoded into WASM memory at access time, which is why HtmlElement.attrs doesn't have this bug).

Filed as #695 with a reproducer sketch, three candidate fix options (host-side WASM container per entry / extend GC tracing to walk _map_store / serialise values into WASM at insertion), and the affected-Map-types breakdown. Tracked in KNOWN_ISSUES.md per the PR-generated-new-issues-must-be-tracked-in-the-same-PR rule (commit 9b170bd).

Deferring rather than expanding #693 because: (a) the bug is pre-existing, (b) the fix is a substantive architectural change touching the Map contract or the GC tracer, (c) #693 is already at 27 files / +710 lines / 5 commit rounds and adding a Map-contract change muddles the review surface. Closing #692's during-walk-safety in this PR, leaving #695 as a clean follow-up.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Analysis is correct and the triage is clean.

The critical asymmetry you've identified is precise: _ShadowGuard closes the during-walk window (val_ptrs on shadow stack while write_json recurses), but the post-walk window is a structurally different problem — once __exit__ resets $gc_sp, the only remaining reference to each val_ptr is _map_store[handle] where the handle carries the bit-31 ownership tag from #578, which is exactly what prevents the conservative GC from treating it as a heap pointer. That's not a gap in #693's fix; it's a separate invariant the Map contract needs to satisfy.

The affected/safe breakdown is also sound — Map<K, String> escapes because strings are re-materialised from WASM memory at access time rather than stored as raw heap pointers, whereas any Map<K, T_heap> where T is a heap-resident structured value has the same exposure.

Deferral to #695 is the right call. The three candidate options span a meaningful design space (per-entry host container, GC tracer extension, eagerly serialise into WASM at insertion), and picking among them deserves a clean PR without the noise of an already-large diff. Good that the KNOWN_ISSUES.md entry is in this commit so the gap is visible to anyone reviewing #693 in the future.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/693

Timestamp: 2026-05-22T11:25:05.071Z
Learning: In vera/wasm/json_serde.py and the broader Map<K, T_heap> contract: after a _ShadowGuard exits and resets $gc_sp, val_ptrs stored as Python ints in _map_store[handle] are no longer reachable by the conservative GC. The handle carries a bit-31 ownership tag (per `#578`) which prevents the GC from treating it as a heap pointer. This post-walk reachability gap affects Map<K, Json>, Map<K, HtmlNode>, Map<K, Md*>, and any Map<K, T> where T is heap-allocated. Map<K, V_inline> (Int/Bool/Float) and Map<K, String> are safe. Tracked as issue `#695`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

README's "3,949 tests" was set during the v0.0.158 release-
prep commit when I bumped the count to match my then-estimate;
the actual count after all PR work is 3,947.  Caught during
the final pre-merge audit.

Not flagged by check_doc_counts.py because the README's "v0.0.X
- N+ commits, N releases, X tests, Y%" line uses a comma-
separated prose pattern that the script's regex doesn't anchor
on.  Worth adding to the validation in a future PR.

Skip-changelog: stale-count fix, no behaviour change.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan merged commit cd4af3c into main May 22, 2026
24 checks passed
@aallan aallan deleted the fix-692-host-walker-gc-rooting branch May 22, 2026 12:26
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.

html_parse and json_parse trap with Out-of-bounds memory access on inputs that pressure GC during host-side tree walk

1 participant