Skip to content

fix(borrow): close #554 use-after-move through a callee-returned borrow#595

Merged
hyperpolymath merged 3 commits into
mainfrom
fix/554-callee-returned-borrow
Jun 14, 2026
Merged

fix(borrow): close #554 use-after-move through a callee-returned borrow#595
hyperpolymath merged 3 commits into
mainfrom
fix/554-callee-returned-borrow

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Closes #554 — the borrow checker accepted a use-after-move through a callee-returned borrow (let r = pick(a); consume(a); *r passed the full pipeline, "Type checking passed", exit 0). This was the core operational soundness hole behind the CORE-01 stamp.

Fix

Per-function return-borrow summary (compute_ret_borrow_params): the parameter indices whose borrow flows out via the return value — a returned &p / return p for a ref/mut parameter, directly, via a let-bound ref-local chain, or through an if/match/block tail in return position. At a call site those argument borrows stay live — in every respect like a plain & borrow — claimed into the result binder (NLL-governed) or lingering to block exit. So the probe is now MoveWhileBorrowed, while legitimate NLL reorderings still pass. Scope is surgical: only calls to functions that actually return a borrow of a parameter change behaviour — the entire pre-existing valid corpus is unaffected.

Hardening — two adversarial-verification rounds

A first cut missed four same-family holes; all were found by adversarial multi-agent verification and closed here:

  • aggregate-storage — let t = (pick(a), 0); consume(a); *(t.0)
  • assign-path — r = pick(a)
  • summary missing if/match-arm-tail returns
  • summary missing the return <branch>; statement form (the idiomatic spelling)

Plus a shadowing over-rejection fix.

Residuals (documented in-code; closed properly by Polonius #553)

  • interprocedural-through-call-result
  • branch-merged / copy-out claim — proven &-symmetric (the byte-identical plain-& program behaves identically): a pre-existing lexical limitation, inherited not introduced
  • reassign-old-loan over-rejection (sound direction)

The mechanized proof of this property is tracked separately as #515.

Verification

Docs

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 40 issues detected

Severity Count
🔴 Critical 2
🟠 High 22
🟡 Medium 16

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affinescript-cli/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (2 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affine-vscode/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-vite/src/affine-plugin-improved.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (32 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/wasm_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (29 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/affine_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (2 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/panic.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (1 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/alloc.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath force-pushed the fix/554-callee-returned-borrow branch from 62ddc06 to 5a0566e Compare June 14, 2026 14:30
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 40 issues detected

Severity Count
🔴 Critical 2
🟠 High 22
🟡 Medium 16

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affinescript-cli/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (2 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affine-vscode/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-vite/src/affine-plugin-improved.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (32 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/wasm_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (29 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/affine_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (2 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/panic.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (1 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/alloc.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 14, 2026 14:32
@hyperpolymath hyperpolymath disabled auto-merge June 14, 2026 14:34
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 14, 2026 14:37
@hyperpolymath hyperpolymath disabled auto-merge June 14, 2026 14:40
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 40 issues detected

Severity Count
🔴 Critical 2
🟠 High 22
🟡 Medium 16

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affinescript-cli/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (2 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affine-vscode/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-vite/src/affine-plugin-improved.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (32 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/wasm_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (29 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/affine_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (2 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/panic.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (1 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/alloc.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

hyperpolymath and others added 3 commits June 14, 2026 15:48
A call whose result borrows one of its arguments now registers the
borrow-graph edge that was missing. Each function carries a return-borrow
summary (compute_ret_borrow_params): the parameter indices whose borrow may
flow out via the return value (a returned `&p` / `return p` for a `ref`/`mut`
parameter, directly, via a let-bound ref-local chain, or through an
`if`/`match`/block tail in return position). At a call site those argument
borrows stay live exactly like a plain `&` borrow — claimed into the result
binder (NLL-governed) or lingering to block exit — so
`let r = pick(a); consume(a); *r` is now MoveWhileBorrowed while the
legitimate NLL reorderings still pass. The control (plain double-consume) and
the whole pre-existing valid corpus are unaffected: only calls to functions
that actually return a borrow of a parameter change behaviour.

Hardened across two adversarial-verification rounds: the aggregate-storage,
assign-path, summary-via-arm-tail, and `return`-statement-form holes a first
cut missed were all found and closed. Residuals — interprocedural-through-
call-result; the branch-merged / copy-out claim (proven `&`-symmetric, a
pre-existing lexical limitation inherited not introduced); and a reassign
old-loan over-rejection (sound direction) — are documented in-code and closed
properly by Polonius (#553); the mechanized proof is tracked as #515.

7 regression fixtures + Alcotest cases added; full suite 393/393 green.
ADR-022 reframed (residual = soundness, not precision; status ratified).

Closes #554.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…old-loan

Follow-up hardening on the #554 return-borrow summaries, closing two of the
three documented residuals.

(a) interprocedural-through-call-result — compute_ret_borrow_params now
resolves a returned call result through the callee's summary, and
build_context drives all summaries to a monotone fixpoint (seed empty,
recompute against callees until stable; origins only grow and are bounded by
arity, so it terminates). So `fn wrap(ref x){ let t = pick(x); return t }`
inherits pick's origin and `let r = wrap(a); consume(a); *r` is rejected —
transitively through any wrapper depth, and across self- and mutual-recursion.

(c) reassignment precision — `r = f(b)` reassigning an existing ref-binder now
releases the binder's prior loan (ref-counted by b_id, so a borrow a ref-to-ref
alias still holds is kept) and rebinds to the escaping call result, symmetric
with the plain-& Slice-B reborrow. A later use of the old target is no longer
spuriously rejected.

The remaining residual (b) — the branch-merged / copy-out claim — is
&-symmetric (a pre-existing lexical limitation inherited by the call-result
path, not introduced by it) and is closed properly by Polonius (#553).

Self-checked: ref-to-ref-alias-keeps-borrow, self/mutual recursion terminate,
multi-arg interprocedural origin mapping, NLL precision preserved. Full suite
395/395 green; +2 regression fixtures.

Refs #554.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eassigned-local summary

A third adversarial-verification pass on the #554 return-borrow work surfaced
two more reachable holes (and confirmed the interprocedural fixpoint is robust:
60-level wrapper chains, mutual recursion, and 3-cycles all terminate in ms and
summarise correctly).

- Slice-B `&` reassign (a PRE-EXISTING soundness bug, exposed by the #554 (c)
  block being more correct): `r = &b` ended the binder's old loan
  UNCONDITIONALLY, so `let r2 = r; r = &b; consume(a); *r2` dropped the borrow
  `r2` still aliased and accepted a use-after-move. Now ref-counted by b_id
  (mirroring the (c) block and expire_dead_ref_bindings), so an aliased loan
  survives the reassign. The `&mut` variant (exclusive aliasing) is caught too.

- Reassigned-local summary: the summary walker did not update a reassigned
  ref-local's origins, so `let mut t = pick(y); t = pick(x); return t`
  summarised the stale {y}. It now UNIONs origins on reassignment
  (conservative, flow-insensitive), summarising {x,y}.

Two low-severity residuals are documented in-code (Polonius #553 closes them):
a summary that cannot bootstrap an origin for a base-case-less divergent
recursion (unreachable — the function never returns), and a loop-body reassign
to an outer borrow (&-symmetric, predates #554).

Full suite 397/397 green; +2 regression fixtures (alias-survives-reassign,
reassigned-local-summary).

Refs #554.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath force-pushed the fix/554-callee-returned-borrow branch from f308ae9 to e1831b2 Compare June 14, 2026 14:49
@hyperpolymath hyperpolymath merged commit 9bf8d57 into main Jun 14, 2026
15 of 18 checks passed
@hyperpolymath hyperpolymath deleted the fix/554-callee-returned-borrow branch June 14, 2026 14:49
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 40 issues detected

Severity Count
🔴 Critical 2
🟠 High 22
🟡 Medium 16

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affinescript-cli/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (2 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affine-vscode/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-vite/src/affine-plugin-improved.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (32 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/wasm_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (29 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/affine_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (2 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/panic.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (1 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/alloc.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

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: use-after-move through a callee-returned borrow is accepted end-to-end (post-CORE-01 hole)

1 participant