fix(mir): reject unsafe actor-state resource overwrite#2664
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Overwriting a
Resourceor leakingIoHandle(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-spawnthe 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): passedcargo 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 greenmake checked-mir-verify checked-mir-golden: OK (40 fixtures × 2 stages byte-identical)cargo clippy -p hew-mir -p hew-cli --release: cleanOut of scope
String/Bytes/collection-typed actor-state fields — unaffected, already handled correctly.Fixes #2654