Skip to content

v0.0.128: WASM call translator critical safety fixes (#475 PR 1/2)#566

Merged
aallan merged 3 commits into
mainfrom
claude/fix-475-critical
May 5, 2026
Merged

v0.0.128: WASM call translator critical safety fixes (#475 PR 1/2)#566
aallan merged 3 commits into
mainfrom
claude/fix-475-critical

Conversation

@aallan

@aallan aallan commented May 5, 2026

Copy link
Copy Markdown
Owner

v0.0.128 — three Critical safety fixes from #475 (PR 1 of 2). Closes the memory-read-OOB hole and two correctness bugs that have been outstanding since mid-April.

Closes #475 (partial — Critical findings 1, 2, 3 of 10). The seven Major findings ship as PR 2 to keep this PR's review surface focused on safety-tier work.

Summary

Three pre-existing translator bugs surfaced by CodeRabbit during PR #474's calls.py decomposition review. Each was concentrated in one method, fixable with a small local change, and shares the architectural pattern "validate in i64 before narrowing to i32 — pre-narrow validation lets huge positive i64 values silently turn into in-range-looking i32 values and bypass the check."

Finding Severity Method What was wrong
3 🔴 Critical _translate_char_code No bounds check before i32.load8_u — out-of-range index reads arbitrary memory
2 🔴 Critical _translate_string_slice No clamping at all; i32.wrap_i64 first, then... nothing
1 🔴 Critical _translate_handle_exn Catch-arm result type only inferred for ast.Block bodies — expression-bodied handlers emit invalid WAT

Each method had a placeholder _ = len_s # reserved for future bounds checking (or equivalent) that documented the gap. The "TODO that passes the linter" pattern is technical debt that takes external review (Rabbit, in this case) to surface; lesson noted in the commit message.

Fix shape

Finding 3 (_translate_char_code): bounds check operates in i64 (idx < 0 || idx >= len_s_i64) and traps with unreachable before narrowing to i32.

Finding 2 (_translate_string_slice): clamp in i64 space (via the new _clamp_i64_to_range_then_wrap helper that widens len_s to i64, clamps via nested if/else, then wraps) before narrowing. Negative starts clamp to 0; ends past length clamp to length; swapped indices produce empty slices.

Finding 1 (_translate_handle_exn): replace the isinstance(clause.body, ast.Block) guard with _infer_expr_wasm_type(clause.body) which handles all ast.Expr types uniformly (including ast.Block at line 163 of inference.py).

Shared infrastructure

New _clamp_i64_to_range_then_wrap(max_local_i64) -> list[str] helper on CallsStringsMixin. Used by both _translate_string_slice and _translate_char_code. Will promote to a shared location when PR 2 fixes finding 4 (array_slice same shape, in calls_arrays.py).

Tests

13 new tests across three classes, all passing:

  • TestCharCodeBoundsCheck475 (5) — in-range / negative / at-length / huge-positive / last-valid-index
  • TestStringSliceClampBefore475 (5) — normal / negative-start / end-beyond-length / swapped / huge-positive
  • TestExpressionBodiedExnHandler475 (3) — Option-returning, Int-returning, trap-path catch arms

Documentation

Test plan

  • pytest tests/test_codegen.py::TestCharCodeBoundsCheck475 tests/test_codegen.py::TestStringSliceClampBefore475 tests/test_codegen.py::TestExpressionBodiedExnHandler475 — 13 pass
  • pytest tests/ -q --ignore=tests/test_browser.py — 3,545 pass, 14 skipped (no regressions)
  • mypy vera/ — clean
  • python scripts/check_doc_counts.py — 3,658 tests, 82 conformance, 33 examples
  • python scripts/check_limitations_sync.py — 33 / 15 / 2
  • python scripts/check_version_sync.py — v0.0.128 across 4 files
  • python scripts/check_spec_examples.py — all parseable spec blocks pass
  • All pre-commit hooks pass (including site-assets regen — required two attempts as is the pattern)

Co-Authored-By: Claude noreply@anthropic.invalid

Summary by CodeRabbit

  • Bug Fixes
    • Fixed critical WASM memory-safety issues in string operations and corrected exception-handler translation so expression-bodied handlers emit correct WASM types.
  • Tests
    • Added regression tests covering string slicing, character-code bounds and exception-handler type inference.
  • Documentation
    • Updated changelogs, history, README, roadmap, known-issues and testing metrics for v0.0.128; noted shared infrastructure improvements.
  • Chore
    • Project version bumped to v0.0.128.

Closes the three Critical findings from #475 (filed during PR #474's
calls.py decomposition review) — all real safety / correctness holes
that have been sitting since mid-April.  The seven Major findings
ship separately as PR 2 to keep this PR's review surface focused on
the safety-tier fixes.

Fixes
-----

vera/wasm/calls_strings.py `_translate_char_code` (#475 finding 3,
🔴 Critical, memory-safety hole)
- Pre-fix: no bounds check before `i32.load8_u`.  The placeholder
  `_ = len_s  # reserved for future bounds checking` documented
  the gap.  Out-of-range indices read arbitrary WASM linear
  memory at `ptr_s + (wrapped index)` — a real memory-safety
  issue.
- Post-fix: bounds check operates in i64 space (`idx < 0 ||
  idx >= len_s_i64`) and traps with `unreachable` *before*
  narrowing to i32.  The in-i64 ordering matters: huge positive
  i64 values would otherwise wrap to small in-range-looking i32
  values and silently bypass the check.
- Tests: TestCharCodeBoundsCheck475 (5 cases) — in-range,
  negative, at-length, huge-positive, last-valid-index.

vera/wasm/calls_strings.py `_translate_string_slice` (#475
finding 2, 🔴 Critical, silently-wrong output)
- Pre-fix: no clamping at all.  The same `_ = len_s` placeholder
  pattern.  Indices were narrowed via `i32.wrap_i64` first; large
  positive i64 values silently turned into negative i32 values
  and drove the byte-copy loop into out-of-range memory or
  produced garbled output.
- Post-fix: clamp in i64 space (via the new
  `_clamp_i64_to_range_then_wrap` helper that widens len_s to
  i64, clamps via nested if/else, then wraps) before narrowing.
  Negative starts clamp to 0; ends past length clamp to length;
  swapped indices produce empty slices; huge positive indices
  clamp to length cleanly.
- Tests: TestStringSliceClampBefore475 (5 cases) — normal slice,
  negative start, end-beyond-length, swapped indices, huge
  positive start.

vera/wasm/calls_handlers.py `_translate_handle_exn` (#475
finding 1, 🔴 Critical, invalid-WAT)
- Pre-fix: catch-arm result type was inferred only when
  `clause.body` was an `ast.Block`.  Expression-bodied handlers
  (e.g. `throw(@string) -> None`, `throw(@int) -> @Int.0 + 1`)
  left `result_wt = None` and the emitted WAT omitted the
  `(result T)` annotation, producing invalid WAT that failed
  validation when the body type was anything other than Unit.
- Post-fix: use `_infer_expr_wasm_type` (which handles all Expr
  types including `ast.Block` at line 163 of inference.py) for
  both the catch clause and the body.  Clean three-line change.
- Tests: TestExpressionBodiedExnHandler475 (3 cases) —
  `Option`-returning, `Int`-returning, trap-path catch arms.

Shared infrastructure
---------------------

vera/wasm/calls_strings.py
- New `_clamp_i64_to_range_then_wrap(max_local_i64) -> list[str]`
  helper on `CallsStringsMixin`.  Emits the canonical "clamp i64
  to [0, max] then wrap to i32" WAT sequence using a fresh i64
  temp local.  Used by both `_translate_string_slice` and
  `_translate_char_code`.  Will be promoted to a shared location
  (probably `helpers.py`) when PR 2 fixes finding 4 (`array_slice`
  same shape, in `calls_arrays.py`).

Version bump v0.0.127 → v0.0.128
--------------------------------

- vera/__init__.py: 0.0.127 → 0.0.128
- pyproject.toml: 0.0.127 → 0.0.128
- README.md: 127 releases → 128, 3,638 → 3,658 tests
- docs/index.html: header version badge
- uv.lock: regenerated (one-line vera version line)

CHANGELOG.md
- New [0.0.128] - 2026-05-05 release section.  Includes the three
  Critical fixes under ### Fixed, the new shared helper under
  ### Shared infrastructure, and the previously-Unreleased
  byte-arithmetic spec note from #565 / #551 closure under
  ### Documentation (rolled in per the standard release
  treadmill).
- New [0.0.128] link reference; bumped [Unreleased] base.

HISTORY.md
- Single-line v0.0.128 row per the hardened format
  ("**WASM call translator critical safety fixes** ([#475]())")
  — one bolded outcome clause, one link, no em-dashes, no
  secondary clauses.

KNOWN_ISSUES.md
- #475 row narrowed to the seven Major findings remaining;
  Critical findings struck from the description.

ROADMAP.md
- Campaign tracker (line 25): append "v0.0.128 shipped PR 1 of
  2 for #475" with named Critical findings; count stays at
  "seven remain" because #475 is still open with Major findings.
- Priority table row 1: rewritten to "PR 2 of 2 — seven Major
  findings" with each Major finding briefly named.  #475 stays
  at row 1 (reflecting "Major correctness debt still
  outstanding").
- "By the numbers" footer: 3,645 → 3,658 tests.
- TESTING.md test counts updated:
  * Tests headline: 3,645 → 3,658 (3,644 passed, 14 skipped).
  * test_codegen.py row: 998 → 1,011 / 14,131 → 14,432.

Verification
------------
- 13 new tests in TestExpressionBodiedExnHandler475 +
  TestStringSliceClampBefore475 + TestCharCodeBoundsCheck475
  all pass.
- 3,545 in the full pytest suite (excluding browser parity);
  no regressions.
- mypy clean; doc-counts / limitations-sync / version-sync all
  green.

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

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

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: 041ed277-07a8-4b03-baf2-7804f27e9bfa

📥 Commits

Reviewing files that changed from the base of the PR and between b010b4d and b4f8370.

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

📝 Walkthrough

Walkthrough

Fixes three critical WASM call-translator issues: clamp string slice indices in i64 before narrowing, perform i64-space bounds checks for string char-code before load, and ensure expression-bodied Exn<E> handler clauses infer/emit correct WASM result types; adds shared i64-clamping helper and regression tests.

Changes

WASM Call Translator Safety Fixes

Layer / File(s) Summary
Shared Infrastructure
vera/wasm/calls_strings.py
Add CallsStringsMixin._clamp_i64_to_range_then_wrap to clamp an i64 value to [0, max] in i64 space then i32.wrap_i64.
String Safety — slice
vera/wasm/calls_strings.py
_translate_string_slice widens len_s, start, end to i64, clamps indices in i64 space via the new helper, narrows to i32, and enforces end >= start before computing slice length.
String Safety — char code
vera/wasm/calls_strings.py
_translate_char_code widens len_s and index to i64, emits unreachable when idx < 0 or idx >= len_s (i64), then narrows index to i32 for i32.load8_u.
Handler Type Inference
vera/wasm/calls_handlers.py
_translate_handle_exn now uses _infer_expr_wasm_type for clause bodies (including expression-bodied clauses) and for fallback expr.body, ensuring correct (result ...) emission when required.
Tests / Release metadata
tests/test_codegen.py, CHANGELOG.md, HISTORY.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, TESTING.md, pyproject.toml, vera/__init__.py
Add regression tests TestExpressionBodiedExnHandler475, TestStringSliceClampBefore475, TestCharCodeBoundsCheck475. Bump version to 0.0.128 and update changelog/metadata/docs; mark #475 progress and adjust test/release counts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • aallan/vera#476: Related follow-up that updates KNOWN_ISSUES.md and tracks further fixes for issue #475.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main change: version 0.0.128 release with three critical WASM call translator safety fixes addressing issue #475 PR 1/2.
Linked Issues check ✅ Passed All three Critical objectives from issue #475 are fully addressed: expression-bodied exception handler type inference fixed [calls_handlers.py], string_slice i64-space clamping implemented [calls_strings.py], and char_code bounds checking added [calls_strings.py].
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the three Critical findings from #475. Version bumps and documentation updates appropriately track the progress; no unrelated refactoring or feature work present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 claude/fix-475-critical

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

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.94%. Comparing base (588d710) to head (b4f8370).

Files with missing lines Patch % Lines
vera/wasm/calls_handlers.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #566   +/-   ##
=======================================
  Coverage   90.93%   90.94%           
=======================================
  Files          59       59           
  Lines       22388    22418   +30     
  Branches      259      259           
=======================================
+ Hits        20358    20387   +29     
- Misses       2023     2024    +1     
  Partials        7        7           
Flag Coverage Δ
javascript 56.83% <ø> (ø)
python 94.80% <97.50%> (+<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 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: 3

🤖 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 `@CHANGELOG.md`:
- Line 15: The changelog is inaccurate: in vera/wasm/calls_handlers.py within
_translate_handle_exn the body type is still inferred using
_infer_block_result_type(expr.body); update the implementation to call
_infer_expr_wasm_type for the handler body as well (i.e., replace the use of
_infer_block_result_type on expr.body with _infer_expr_wasm_type) so the code
matches the release note; ensure the change affects both the catch clause and
the handler body inference sites in _translate_handle_exn.

In `@ROADMAP.md`:
- Line 283: Update the release-count footer string "127 tagged releases (as of
v0.0.127)" in ROADMAP.md to "128 tagged releases (as of v0.0.128)" and make the
matching edits in HISTORY.md (there are three roll-up/footer locations called
out in the comment) so the tagged-release total and the version token (v0.0.127
→ v0.0.128) are incremented consistently across all instances.

In `@vera/wasm/calls_strings.py`:
- Around line 245-247: The source string local (ptr_s) must be rooted before
performing the subsequent call $alloc to avoid GC reclaiming a heap string;
update the code that builds the instructions (the block that appends s_instrs
then "local.set {len_s}" and "local.set {ptr_s}") to insert a call to
gc_shadow_push for ptr_s immediately before emitting the "call $alloc"
instruction and ensure you emit the matching gc_shadow_pop after the
allocation/copy completes; do the same change for the similar sequence around
the other block (the one referenced at lines 283-287) so every heap pointer live
across call $alloc is pushed to the GC shadow (use the existing
gc_shadow_push/gc_shadow_pop helpers).
🪄 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: 6ff7ae15-0b57-4188-822d-93382f2399b4

📥 Commits

Reviewing files that changed from the base of the PR and between 588d710 and d78d2af.

⛔ Files ignored due to path filters (6)
  • 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/**
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (11)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/wasm/calls_handlers.py
  • vera/wasm/calls_strings.py

Comment thread CHANGELOG.md
Comment thread ROADMAP.md Outdated
Comment thread vera/wasm/calls_strings.py
Three findings; one functional bug, one accuracy fix, one
release-count drift across two files.

vera/wasm/calls_strings.py `_translate_string_slice` — GC root for ptr_s
(finding 3, real GC bug)
- Pre-fix: `ptr_s` (the source string pointer) was set into a WAT
  local at line ~247, then `call $alloc` at line ~285 could
  trigger GC.  The conservative collector only sees pointers
  stored on the GC shadow stack via `gc_shadow_push`, NOT those
  living solely in WAT locals — so during a GC fired by the
  destination allocation, the source string could be reclaimed
  while still in use by the byte-copy loop at line ~301.  Sibling
  sites in `calls_arrays.py` (e.g. `_translate_array_map` at
  line 672) push the source pointer on entry; this site missed
  the convention.
- Post-fix: insert `gc_shadow_push(ptr_s)` immediately after the
  ptr/len destructure, before any subsequent allocation.  Auto-pop
  at function exit via the standard $gc_sp save/restore in the
  function epilogue (no `gc_shadow_pop` helper exists; pushes
  unwind together when the function returns).
- Inline comment documents the rooting requirement so a future
  reader sees why the push is there.

vera/wasm/calls_handlers.py `_translate_handle_exn` — body inference
also uses _infer_expr_wasm_type (finding 1, accuracy fix)
- Pre-this-fix the catch-clause inference used the right helper
  but the body-fallback still used `_infer_block_result_type`,
  even though the CHANGELOG text claimed both call sites were
  updated.  `expr.body` is statically typed `Block` (ast.py:481)
  so the two helpers produce identical output for it — no
  functional difference — but the inconsistency between code and
  release note was real.
- Post-fix: both inference sites now call `_infer_expr_wasm_type`.
  Comment noting the type guarantee added so a future reader
  doesn't unify them differently.

ROADMAP.md / HISTORY.md — release-count strings (finding 2)
- ROADMAP.md line 3: "Vera v0.0.127 delivers..." → "v0.0.128"
- ROADMAP.md line 283: "127 tagged releases (as of v0.0.127)" →
  "128 tagged releases (as of v0.0.128)"
- HISTORY.md line 295: by-the-numbers table column header
  v0.0.127 (29 Apr) → v0.0.128 (5 May).  Row "Tests" already
  bumped 3,638 → 3,658 in the prior PR; release count moves
  with it.
- HISTORY.md line 305: footer "127 tagged releases, 49 active
  development days" → "128, 54" (49 + 5 days from 2026-04-29
  to 2026-05-05).

Verification
- 13 TestExpressionBodiedExnHandler475 / TestStringSliceClampBefore475
  / TestCharCodeBoundsCheck475 tests still pass after the
  ptr_s root + handler-body helper switch.
- mypy clean; doc-counts / limitations-sync / version-sync all
  green.

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

@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.

Caution

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

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

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

Update the header development-day count to match the footer.

Line 3 states "across 48 active development days" whilst line 305 (the roll-up footer) shows "54 active development days". Both describe the same timeline ("from initial commit through Stage 11"), so the counts must agree. The footer count (54) is authoritative as it was updated in this PR; line 3 should be updated to match.

📅 Proposed fix
-How the Vera compiler was built, from initial commit through Stage 11, across 48 active development days.
+How the Vera compiler was built, from initial commit through Stage 11, across 54 active development days.
🤖 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 `@HISTORY.md` at line 3, Update the header sentence that reads "How the Vera
compiler was built, from initial commit through Stage 11, across 48 active
development days." to use the authoritative footer count of 54 active
development days so it matches the footer; locate the header string in
HISTORY.md and change "48" to "54".
🤖 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.

Outside diff comments:
In `@HISTORY.md`:
- Line 3: Update the header sentence that reads "How the Vera compiler was
built, from initial commit through Stage 11, across 48 active development days."
to use the authoritative footer count of 54 active development days so it
matches the footer; locate the header string in HISTORY.md and change "48" to
"54".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f07a8dc-3888-4d6e-8252-b230861aa96f

📥 Commits

Reviewing files that changed from the base of the PR and between d78d2af and b010b4d.

📒 Files selected for processing (4)
  • HISTORY.md
  • ROADMAP.md
  • vera/wasm/calls_handlers.py
  • vera/wasm/calls_strings.py

…drift

Single-line fix.  HISTORY.md line 3 (header) said "48 active
development days"; line 305 (footer) said "54".  Pre-existing
drift (header was 48, footer 49 at v0.0.127); my v0.0.128 update
bumped the footer to 54 to match the new release date but didn't
catch the header.

HISTORY.md
- Line 3: 48 → 54 to match the authoritative footer count.

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

aallan commented May 5, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai (re: HISTORY.md outside-diff finding) Verified — pre-existing drift, header was 48, footer 49 at v0.0.127; my v0.0.128 update bumped the footer to 54 to match the new release date but didn't catch the header. Fixed in commit b4f8370: line 3 "48 active development days" → "54" so header and footer agree.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Tip

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

@aallan: Thanks for the context on the pre-existing drift — good catch tracking that the header/footer were already mismatched before this PR. The fix in b4f8370 (header 4854) brings both counts into agreement. ✓

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.

Pre-existing bugs in WASM call translators (surfaced by #474 review)

1 participant