Skip to content

release/v0.5.0 PR-B: close hoist hole on early-exit (Return/Br) patterns#89

Merged
avrabe merged 1 commit intomainfrom
release/v0.5.0-pr-b-hoist-guard-return-br
May 2, 2026
Merged

release/v0.5.0 PR-B: close hoist hole on early-exit (Return/Br) patterns#89
avrabe merged 1 commit intomainfrom
release/v0.5.0-pr-b-hoist-guard-return-br

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 1, 2026

Summary

Fixes the CSE soundness bug discovered on gale_sem_count_take (documented in PR-A's gale measurement report). The v0.4.0 hoist guard only flagged BrIf/BrTable; functions using if (cond) return; end (the canonical Rust early-exit guard) slipped through.

Root cause

Per-pass tracing on a gale-style fixture showed the reordering happens in constant_folding, not in CSE as initially suspected. The instructions_to_terms → rewrite → terms_to_instructions round-trip does not preserve order across early-exit patterns: a function with If { then_body: [..., Return] } followed by straight-line code emerges with the if-block moved to the function tail and the straight-line code (load/sub/store) hoisted to the function head — so the store runs unconditionally, including when the guard would have returned early.

Fix

  • Extend has_dataflow_unsafe_control_flow to flag any nested Return/Br as an early-exit pattern. New helper has_unsafe_in_nested recurses into Block/Loop/If bodies. Top-level Return/Br at the very last position is allowed (function terminator).
  • constant_folding and optimize_advanced_instructions now skip the function entirely when has_dataflow_unsafe_control_flow is true. Previously they fell back to rewrite_pure, which still went through the unsound round-trip.
  • Defense-in-depth guards added to simplify_locals, remove_unused_branches, optimize_added_constants (others were already guarded in v0.4.0).
  • eliminate_dead_code intentionally NOT guarded — it only deletes code after terminators, never reorders. Guarding it would defeat the pass.
  • Dead use_dataflow_env branch removed; the unsafe path is gone, the safe path always uses the dataflow env.

Regression tests

  • test_early_return_guard_prevents_store_hoist — mirrors gale_sem_count_take, asserts I32Eqz appears before I32Store in optimized output. Without this PR: store at index 5, eqz at 7 (hoisted above guard). With this PR: original order preserved.
  • test_default_then_override_across_br_table_preserved (from v0.4.0 PR-C) still passes.

Test plan

  • cargo build --release passes
  • Both regression tests pass
  • All 244 unit tests pass
  • cargo clippy clean
  • Pre-commit hooks pass
  • CI green

v0.5.0 release sequence

PR Theme Status
#88 PR-A: VerificationResult strict mode + gale measurement Open
(this) PR-B: hoist guard for Return/Br + CSE soundness fix Open
- PR-C: Spec-feature corpus fixtures Pending
- PR-D: FusedOptimization.v in BUILD.bazel Pending
- PR-E: Tag v0.5.0 Pending

🤖 Generated with Claude Code

The v0.4.0 has_dataflow_unsafe_control_flow guard only flagged
BrIf/BrTable. The gale v0.4.0 measurement (PR-A) found CSE produced
an unsound store-hoist on gale_sem_count_take where the function
uses `if (eqz) return; end` — an If with Return inside, which
slipped through the guard.

Root-cause investigation (per-pass tracing on the gale-style input)
showed the reordering happens in constant_folding's
instructions_to_terms / terms_to_instructions round-trip, not in
CSE. The terms IR doesn't preserve early-exit ordering: once a
function with an `If { then_body: [..., Return] }` is converted
to terms and back, the if-block can land at the function tail
with the straight-line code (load/sub/store) hoisted to the
function head — so the store now runs unconditionally.

Fix:
- Extend has_dataflow_unsafe_control_flow to flag any nested
  Return/Br as an early-exit pattern. New helper has_unsafe_in_nested
  recurses into Block/Loop/If bodies. Top-level Return/Br at the
  very last position is allowed (function terminator).
- constant_folding and optimize_advanced_instructions now skip
  the function entirely when has_dataflow_unsafe_control_flow is
  true (previously they fell back to rewrite_pure, which still
  went through the round-trip and reordered).
- Same guard added to simplify_locals, code_folding, LICM,
  remove_unused_branches, optimize_added_constants, coalesce_locals,
  and both CSE entry points — defense in depth across the rest
  of optimize_module's pipeline.
- DCE intentionally NOT guarded — it only deletes unreachable code
  after terminators, never reorders, and Return-at-tail is the
  primary reason DCE exists. Guarding it would defeat the pass.
- Remove the dead use_dataflow_env branch from constant_folding /
  optimize_advanced_instructions; the unsafe path is gone, the
  safe path always uses the dataflow env.

Regression test:
- test_early_return_guard_prevents_store_hoist mirrors
  gale_sem_count_take and asserts I32Eqz appears before I32Store
  in the optimized output. Without this PR, the store ends up at
  index 5 and the eqz at 7 (i.e., store hoisted above guard);
  with this PR, the original order is preserved exactly.
- The existing test_default_then_override_across_br_table_preserved
  also still passes — both regressions covered.

Trace: REQ-1, REQ-3, REQ-5, H-9
@avrabe avrabe force-pushed the release/v0.5.0-pr-b-hoist-guard-return-br branch from 06b20a8 to 70756b2 Compare May 2, 2026 14:34
@avrabe avrabe merged commit de9b345 into main May 2, 2026
12 of 18 checks passed
@avrabe avrabe deleted the release/v0.5.0-pr-b-hoist-guard-return-br branch May 2, 2026 14:34
@avrabe avrabe mentioned this pull request May 2, 2026
3 tasks
avrabe added a commit that referenced this pull request May 2, 2026
Bump workspace version 0.4.0 → 0.5.0 and add CHANGELOG section.

This release ships the v0.5.0 audit follow-ups across five PRs:

- #88 PR-A: VerificationResult strict-mode helpers (is_skip,
  skip_reason, verify_or_revert_strict) + gale v0.4.0 measurement
  report documenting the CSE soundness bug on production
  kernel-scheduler code.
- #89 PR-B: Close hoist hole on early-exit (Return/Br) patterns.
  Per-pass tracing showed reordering happens in constant_folding's
  terms-IR roundtrip; the function ends up with the if-guard moved
  to the function tail and the load/store sequence hoisted to the
  function head. Fix: extend has_dataflow_unsafe_control_flow to
  flag nested Return/Br; constant_folding and
  optimize_advanced_instructions skip such functions entirely.
  Defense-in-depth guards on simplify_locals, remove_unused_branches,
  optimize_added_constants. Regression test pinned.
- #90 PR-C: 8 minimal post-MVP wasm fixtures + spec_features.rs
  test harness. Pins the "parser must never panic" contract for
  SIMD, ref types, bulk memory, tail calls, exception handling,
  multi-memory, sign-extension, saturating-trunc.
- #91 PR-D: FusedOptimization.v wired into BUILD.bazel. Closes
  audit D1; the 7 axioms remain (discharge is future work).
- #92 chore: top-level concurrency: block on every workflow.
  Closes the org-wide CI queue backlog; superseded PR runs now
  cancelled in ~30s, runs on main / tags / releases / scheduled
  events never cancelled.

CHANGELOG.md adds a v0.5.0 section documenting all of the above
plus the deferred work for the next release.

Trace: REQ-1, REQ-3, REQ-5, REQ-7, REQ-9, REQ-12, REQ-14
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.

1 participant