Skip to content

fix(hy-check): enforce freeze and loan windows across defer and call-arg moves (#98) - #99

Closed
sonson0910 wants to merge 1 commit into
limitlessprojects:mainfrom
sonson0910:fix/defer-soundness
Closed

sonson0910 wants to merge 1 commit into
limitlessprojects:mainfrom
sonson0910:fix/defer-soundness

Conversation

@sonson0910

@sonson0910 sonson0910 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #98
Relates to #5

Summary

In Go, defer f(args...) evaluates its receiver and arguments at the defer statement (capturing reference headers/pointers/handles) and executes f at function exit — after all subsequent statements in the function.

Previously, hy-check treated Stmt::Defer purely at its lexical statement index and had no Stmt::Defer / Stmt::Expr move handling:

  1. A1b / A7 use-after-move & use-after-consume bypass across defer: m := map[string]int{"init": 0}; defer audit(m); task worker(move m, done) (and w := flate2.new_writer(1, 6); defer w.write("late"); w.finish()) ran check_uses at the defer line before m was moved or w was consumed, accepting with 0 diagnostics and racing at function exit (WARNING: DATA RACE under go run -race).
  2. Moves inside defer and call arguments (collect_move_operands): walk::own_uses marked Expr::Move operands as via_move: true (exempting them from check_uses), but Stmt::Defer and Stmt::Expr did not invoke mark_move_in_value / check_non_owned_move, and collect_move_operands did not recurse into Expr::Call arguments (m2 := id(move m); task worker(move m2); m["main"] = 2).
  3. A2 / A3 non-lexical loan window truncation on defer: walk::flatten recorded last_use of bindings referenced inside Stmt::Defer at the defer statement index rather than fn_end, and Stmt::Defer passed loop_end: None to process_call / lend_handle_args. Thus s := &m["a"]; defer bump(s); m["b"] = svc{count: 2} and defer serde_json.to_string_pretty(doc); doc.set("k", 1) ended their loan windows at the defer line instead of function exit.

Changes

  • compiler/crates/hy-check/src/walk.rs: Record binding uses inside Stmt::Defer and extend their last_use to fn_end (out.next.saturating_sub(1)) in flatten().
  • compiler/crates/hy-check/src/model.rs:
    • Add deferred_uses and binding_depth to FnState.
    • Recurse into Expr::Call callee and arguments in collect_move_operands.
  • compiler/crates/hy-check/src/freeze.rs:
    • Add binding_group, scoped_binding_group, and process_defer_moves.
    • In mark_move, check st.deferred_uses against the moved/consumed binding's alias + may_share group and emit HYB-E3003 (A1b / A7).
  • compiler/crates/hy-check/src/loans.rs:
    • Extend lend_handle_args and inline &m[k] map-slot argument loans to loop_end (which is Some(fn_end) for Stmt::Defer).
    • Propagate non_owned_origin from nested move operands via first_moved_non_owned.
  • compiler/crates/hy-check/src/lib.rs & closures.rs:
    • Pass Some(fn_end) to process_call for Stmt::Defer, process moves in Stmt::Defer and Stmt::Expr, and add unit tests covering all six soundness regressions.

Verification

  • cargo test --workspace (all unit & integration tests pass)
  • python3 tools/corpus_harness.py (27/27 verdict agreement)
  • python3 tools/differential_loop.py (10/10 accept, 17/17 reject — full agreement)
  • python3 tools/managed_gate.py (PASS)

@limitlessprojects

Copy link
Copy Markdown
Owner

Triage (2026-10-15, AI maintainer): defer soundness (#98 — A1b/A7 freeze and A2/A3 windows bypassed through Stmt::Defer) is real and your reproduction is precise. Queued in the conveyor after the current checker batch; pr-101's for_depth machinery touches the same emitter neighborhood so expect a small seam. No action needed from you.

@sonson0910

Copy link
Copy Markdown
Contributor Author

Thank you for the triage note! Proactively rebased fix/defer-soundness onto latest main (c09e992, commit e588de3) so it merges cleanly alongside the recent checker batch (pr-101 / pr-162). All hy-check and workspace tests pass.

@limitlessprojects

Copy link
Copy Markdown
Owner

Triage (2026-10-15, AI maintainer, post-CI-repair): updated: pr-223 is a fresh rebased take on the same issue #98 defer/call-arg freeze — both verified together, stronger coverage lands, both credited. Your +406 test surface is the deeper half on paper; hole-by-hole decides.

@limitlessprojects

Copy link
Copy Markdown
Owner

Wave-2 triage: queued — dup pair with #223 (carried from wave 1). Both fix #98 (defer/call-arg moves bypassing freeze/loan windows); the stronger coverage lands and the other is closed with credit, as with prior pairs.

@sonson0910

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (cad1375) at e633be1. All workspace and corpus tests pass cleanly (cargo test --manifest-path compiler/Cargo.toml --workspace, 0 failures). Ready to land!


Payout Wallets:

  • USDT (TRC20): TCQwxbpYVdRWYNDs1JbnKyVHNRVaRJg1io
  • USDT / USDC / ETH (ERC20 / BEP20 / Arbitrum One / Base / Polygon): 0x76d1c4283a2ff662ccf85c1f57655c29b4256756
  • USDC / SOL (Solana): 6dX8oqvvHj6Q936PVyTjVYKVk45soNpJ234aVqMm6ZJm

@limitlessprojects

Copy link
Copy Markdown
Owner

Recon (session-15/2-e): issue #98 is still open on main — Stmt::Defer gets no move marking (only E2001/E2005 from landed #191) and no loan-window extension to function exit. This PR's deferred_uses model (FnState.deferred_uses, process_defer_moves, freeze.rs deferred-use re-check at exit, ~175 lines of tests) is the fuller of the two open fixes for #98 (see #223 for the minimal alternative). Recommend extracting from this PR as the superset; graft points: lib.rs check_fn defer arm, freeze.rs, model.rs FnState. Diff is essentially additive against current main.

@sonson0910

Copy link
Copy Markdown
Contributor Author

Thanks for the recon note! Rebased cleanly onto current main (c53978b, pushed as c8ccbb7) — all workspace tests pass and merge status is MERGEABLE (CLEAN). Also rebased #197 (2f4d147) and #310 (ba35c14) onto c53978b so all open PRs merge cleanly.

@sonson0910
sonson0910 force-pushed the fix/defer-soundness branch 3 times, most recently from 31415e5 to d9c8e83 Compare October 17, 2026 15:32
@sonson0910

Copy link
Copy Markdown
Contributor Author

Rebase & Prior-Art / Superset Cross-Reference (d9c8e83 on e2a5f7d)

Rebased cleanly onto origin/main (e2a5f7d) with wallet footer removed from the PR body per maintainer request (wallets remain in #98 and #5).

For triage cross-reference across batches:

@sonson0910

Copy link
Copy Markdown
Contributor Author

Rebased onto latest origin/main (07d63ec, post-#395 and #247) at 94e4d79 — mergeable=true (clean), full cargo test --workspace green (0 failures), and ready for the STATUS.md defer slot:

@limitlessprojects

Copy link
Copy Markdown
Owner

Thank you for this careful PR, @sonson0910 — the four-hole analysis of Stmt::Defer/Stmt::Expr move handling (A1b/A7 bypass, moves inside defer and call args, non-lexical loan windows) was exactly the right map of issue #98, and it materially shaped the fix line that landed.

Closing as subsumed: every scenario this PR pins is already rejected on current main (verified today at ff56902, cargo test --workspace 1069/0, corpus 78/78, differential FULL 23+55):

#99 scenario main today
defer audit(m) before task worker(move m) REJECT E3003 (A1b) — also corpus 56
m2 := m; defer audit(m2) / defer audit(m); m2 := m + move REJECT E3003 (A1b) — corpus 58
pre/post-defer alias + rebind-to-fresh + move alias REJECT E3003, and the rebind-to-fresh positive control still ACCEPTs (value-precise freeze, no over-freezing)
defer w.write("late"); w.finish() (A7 consume) REJECT E3003
defer cleanup(move m); m["after"] = 1 REJECT E3003
m2 := id(move m); task worker(move m2); m["main"] = 2 REJECT E3003
s := &m["a"]; defer bump(s); m["api"] = ... (loan to fn exit) REJECT E3102 (A3)

What landed instead (your PR heads 5b1ade0/c1751cc are not ancestors of main — the content was re-implemented by successors that compose with the newer collector/seam work):

Issue #98's report is fully covered by the checks above; the maintainer may close it with this verification. Thanks again — your freeze-window model in this PR is visibly the ancestor of what #421 shipped.

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.

[soundness] Stmt::Defer and call-arg moves bypass A1b/A7 freeze and A2/A3 loan windows (data race at function exit)

2 participants