Skip to content

fix(mir): reject unsafe actor-state resource overwrite#2664

Merged
slepp merged 4 commits into
mainfrom
fix/2654-actor-resource-replace
Jul 15, 2026
Merged

fix(mir): reject unsafe actor-state resource overwrite#2664
slepp merged 4 commits into
mainfrom
fix/2654-actor-resource-replace

Conversation

@slepp

@slepp slepp commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Overwriting a Resource or leaking IoHandle (Stream/Sink/Generator/CancellationToken) field in actor state is now rejected at compile time instead of silently dropping the old handle without closing it. The compiler's actor-state store lowering had no release-before-store for these owned-handle kinds, so a reassignment in a handler would leak the previously held handle; that path now fails closed with a diagnostic that tells you to re-spawn the actor instead of reassigning the field. Assigning a final resource value that is only ever closed by the actor's shutdown drop remains fully supported and unaffected.

Verification

  • make ci-preflight (pre-push hook, exact HEAD): passed
  • cargo test -p hew-cli --release --test actor_state_resource_field_leak_oracle: 6/6 pass (Resource + IoHandle reassignment refused; positive-control exact stdout; leaks(1) exact-zero; MallocScribble poisoned-allocator triple; single-close IR check)
  • cargo test -p hew-mir --release: all suites green
  • make checked-mir-verify checked-mir-golden: OK (40 fixtures × 2 stages byte-identical)
  • cargo clippy -p hew-mir -p hew-cli --release: clean

Out of scope

  • Release-before-store semantics for String/Bytes/collection-typed actor-state fields — unaffected, already handled correctly.
  • General record-field (non-actor-state) reassignment and COW behavior — untouched by this change.

Fixes #2654

slepp added 4 commits July 15, 2026 03:59
…le gate

Add `AggregateOwner { Record, ActorState }` to the RAII owned-handle
aggregate refusal (`MirCheck::OwnedHandleAggregateDoubleFree` /
`MirDiagnosticKind::OwnedHandleAggregateExtractionUnsupported`) so the
user-facing remediation wording can distinguish a plain record from an
actor's state field. Existing record/aggregate producer sites carry
`AggregateOwner::Record` (behaviour-preserving); the actor-state
remediation wording ("re-`spawn` the actor") is wired in the CLI
renderer ahead of its producer.

No behavioural change: all existing findings render identically; the
dump strings gain an `owner=` field (no golden churn — these fire only
on rejected programs, absent from the checked-mir corpus).
)

An `ActorStateFieldStore` (`self.dq = src`) over a state field whose
drop runs a real close had NO release-before-store in codegen
(`lower_actor_state_field_store` no-release arm), so the previous handle
leaked (its `close` never ran) while the actor's shutdown drop
(`__hew_state_drop_<A>`) double-owned the freshly-stored handle. The
structurally identical record store (`h.dq = src`) was already
fail-closed rejected; this closes the last silent owned-handle-leak
asymmetry between the two aggregate-store instructions.

Add `detect_actor_state_resource_overwrite`, the actor-state sibling of
the record `RecordFieldStore` overwrite arm. It reuses the actor's
authoritative per-field classification (`ActorLayout::
state_field_clone_kinds` — the same vector the drop-body synthesis
consumes) so the refusal fences exactly the kinds whose drop it names:
`Resource` and the pointer-backed `IoHandle` kinds (Stream/Sink/
Generator/CancellationToken). Kinds with an existing release-before-store
(String/Bytes/Vec/HashMap/HashSet), a no-op drop (IoHandle::Connection),
or no close (no-close OpaqueHandle, BitCopy) are NOT gated — they do not
leak on overwrite. The gate is overwrite-only; it never inspects
`ActorStateFieldLoad`, so reads (`self.dq.method()`) stay legal.

Fail-closed is the acceptable failure direction here: over-refusal is a
compile error, never a double-free (LESSONS boundary-fail-closed,
raii-null-after-move, lifecycle-symmetry). A safe release-before-store
(retain-to-compare + source-slot null-after-move) is deferred to RAII-2.

Regression scan (examples/ std/ tests/): no accepted program declares —
let alone reassigns — an actor-state IO-handle field; the IoHandle arm is
a fail-closed boundary ahead of the capability (such fields are also
blocked upstream: not Send, and rejected by the supervisor-restart clone
helper).
Sibling of `raii1_record_resource_field_leak_oracle.rs` for the
actor-state store instruction. Negative cases: a `#[resource]` and a
`Stream<T>` actor-state field reassignment are both refused with the
actor-state overwrite diagnostic (naming the field/handle, remediation
= re-`spawn`). Positive control (a never-reassigned `var dq: Dq` field):
exactly-once close by exact stdout, `0 leaks for 0 total leaked bytes`
under leaks(1) over a real heap deque, a clean run under the poisoned-
allocator triple, and IR showing a single `Dq::close` + null-after-close
in `__hew_state_drop_Keeper`. Pins both the refusal and the working path
so neither the leak nor a broken teardown can regress.
State the record/actor-state store parity in plain present-tense
technical wording. Comment-only; no behavior change.
@slepp slepp merged commit 86570a7 into main Jul 15, 2026
12 checks passed
@slepp slepp deleted the fix/2654-actor-resource-replace branch July 15, 2026 15:11
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.

codegen: release or reject overwritten actor-state resources

1 participant