fix(minvmd,minimald): subnet validation, attached_count lifecycle, tokio::fs - #537
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a distinct ChangesNetworking Switch Hardening
Sequence Diagram(s)sequenceDiagram
participant Caller
participant GvproxySwitch
participant PtaskAttachment
participant GvproxyProcess
Caller->>GvproxySwitch: attach_ptask()
GvproxySwitch->>GvproxySwitch: attached_count.fetch_add(1)
GvproxySwitch-->>Caller: PtaskAttachment { pid, stopping, attached_count }
Note over Caller: ptask runs...
Caller->>GvproxySwitch: detach_ptask(attachment)
GvproxySwitch->>PtaskAttachment: drop(attachment)
PtaskAttachment->>PtaskAttachment: attached_count.fetch_sub(1)
alt count reaches 0
PtaskAttachment->>PtaskAttachment: stopping.swap(true)
PtaskAttachment->>GvproxyProcess: kill(pid, SIGTERM)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minvmd/src/net.rs`:
- Around line 376-395: The Drop implementation for PtaskAttachment has a
PID-reuse hazard where signal_child can be called on a recycled PID if
GvproxySwitch::stop() has already terminated and reaped the child. Guard the
signal_child call within the prev == 1 block by atomically checking and claiming
the teardown using the shared stopping flag. Before calling signal_child in the
Drop implementation, atomically compare-and-swap stopping from false to true,
and only proceed with the signal_child call and log message if this drop
instance successfully claimed the teardown. This prevents multiple drops or
concurrent stop operations from sending SIGTERM to a potentially recycled PID.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 960fc75e-9ce0-44ab-9d8f-f91b0e3b06b9
📒 Files selected for processing (3)
crates/minimald/src/net/mod.rscrates/minvmd/Cargo.tomlcrates/minvmd/src/net.rs
|
Revise claim for head 579fc38. |
|
Auto-revise 1 of 3. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
`PtaskAttachment::Drop` sent SIGTERM whenever the attachment count reached zero, even when `GvproxySwitch::stop()` (or `GvproxySwitch::Drop`) had already terminated and reaped the child. A handle that outlives `stop()` — e.g. `b` in `attach_assigns_unique_sequential_ips`, dropped after `switch.stop().await` — would then signal a PID the OS may have recycled, delivering SIGTERM to an unrelated process. Gate the signal on this drop being the first to claim teardown: `prev == 1 && !stopping.swap(true, AcqRel)`. `swap` returns the prior value, so once `stop()` or another drop has set `stopping`, the signal is skipped. This is also race-free between a concurrent drop and stop, since only one caller can flip `false -> true`. Resolves the CodeRabbit and sdd-review PID-reuse findings on #537. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Commit pushed:
|
This comment has been minimized.
This comment has been minimized.
|
Commit pushed:
|
Address review comments on PR #537. - Add subnet_new_rejects_prefix_32 covering the /32 boundary the prefix validation already rejects: for /32 span=1, so host(0) is the network address and every host() call returns None — the exact pathology the validation guards. - Add a debug_assert that attached_count was non-zero before the fetch_sub in PtaskAttachment::drop, catching an accidental extra drop in debug builds instead of wrapping the counter to u32::MAX. - Relax detach_ptask to take &self (it touches no switch state) and fix the PtaskAttachment doc to state that every drop path logs the detach, not only an explicit detach_ptask call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the three open review threads in commit
The HIGH PID-reuse SIGTERM guard (r3457043606) was already addressed on this branch (the Pre-PR gate (scoped to the changed crate):
|
This comment has been minimized.
This comment has been minimized.
|
Commit pushed:
|
|
Addressed the teardown-signalling review feedback in a follow-up commit (
Gate:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by sdd-review for issue #537
Comments that could not be inline-anchored
crates/minvmd/src/net.rs:325
MEDIUM — Correctness
stop() sends SIGTERM unconditionally after stopping.store(true, Ordering::Release), with no swap guard. GvproxySwitch::Drop was hardened in this PR to use if !self.stopping.swap(true, Ordering::AcqRel) so it skips SIGKILL when PtaskAttachment::Drop already claimed teardown. stop() has the same exposure and was not updated consistently.
Scenario: a caller drops the last PtaskAttachment (R1.4 path — SIGTERM sent, process exits, supervisor task reaps …
crates/minvmd/src/net.rs:303
LOW — Correctness (documentation)
"it touches no switch state" is inaccurate. Dropping attachment decrements attached_count (an Arc<AtomicU32> shared with the parent switch) and, when the count reaches zero, atomically swaps stopping and sends SIGTERM to the switch process. Both are observable changes to switch-wide state.
The intended explanation is that GvproxySwitch's own struct fields are not directly mutated through &self. Consider wording like: "it mutates no `GvproxySwi…
|
Auto-resolve conflict 1 of 3. |
|
/revise resolve the merge conflict with main: merge main into this branch (or rebase onto it) and resolve the conflicts, then push to this PR branch. The conflict appeared after sibling PR #547 merged; most collisions are append-only unions in shared files (the crate manifest, the lib module / pub use list) — take the union of both sides. |
|
Auto-resolve conflict 2 of 3. |
|
/revise resolve the merge conflict with main: merge main into this branch (or rebase onto it) and resolve the conflicts, then push to this PR branch. The conflict appeared after sibling PR #546 merged; most collisions are append-only unions in shared files (the crate manifest, the lib module / pub use list) — take the union of both sides. |
`PtaskAttachment::Drop` sent SIGTERM whenever the attachment count reached zero, even when `GvproxySwitch::stop()` (or `GvproxySwitch::Drop`) had already terminated and reaped the child. A handle that outlives `stop()` — e.g. `b` in `attach_assigns_unique_sequential_ips`, dropped after `switch.stop().await` — would then signal a PID the OS may have recycled, delivering SIGTERM to an unrelated process. Gate the signal on this drop being the first to claim teardown: `prev == 1 && !stopping.swap(true, AcqRel)`. `swap` returns the prior value, so once `stop()` or another drop has set `stopping`, the signal is skipped. This is also race-free between a concurrent drop and stop, since only one caller can flip `false -> true`. Resolves the CodeRabbit and sdd-review PID-reuse findings on #537. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d67089b to
b7c436d
Compare
Address review comments on PR #537. - Add subnet_new_rejects_prefix_32 covering the /32 boundary the prefix validation already rejects: for /32 span=1, so host(0) is the network address and every host() call returns None — the exact pathology the validation guards. - Add a debug_assert that attached_count was non-zero before the fetch_sub in PtaskAttachment::drop, catching an accidental extra drop in debug builds instead of wrapping the counter to u32::MAX. - Relax detach_ptask to take &self (it touches no switch state) and fix the PtaskAttachment doc to state that every drop path logs the detach, not only an explicit detach_ptask call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…kio::fs minvmd/net.rs: - SwitchSubnet::new is now fallible (Result<Self, SwitchSubnetError>); rejects prefixes outside 1..=30 with the new SwitchSubnetError::InvalidPrefix variant instead of silently constructing a subnet where host() always returns None (prefix 0 overflows the host-bit shift; prefix 31/32 leave no valid host index). - Wire attached_count (Arc<AtomicU32>) shared between GvproxySwitch and each PtaskAttachment. attach_ptask increments it; PtaskAttachment::Drop decrements it and delivers SIGTERM to gvproxy when the count reaches zero, implementing R1.4 "stop when the last own-IP PTask exits". detach_ptask now consumes the attachment so the RAII Drop fires in one place. - Add unit tests: SwitchSubnet::new rejects prefix 0, 31, 33; accepts 1..=30; last_ptask_detach_terminates_switch verifies the switch terminates on the final detach. - SwitchExit::recv already documents that None covers both intentional teardown and supervision-task failure (no further change needed). minimald/net/mod.rs: - Add NetError::InvalidPrefix(u8): distinct from SubnetExhausted, which indicates a valid-but-exhausted subnet. SwitchSubnet::new now returns InvalidPrefix for a prefix outside 8..=29 instead of the misleading SubnetExhausted variant. - Convert blocking std::fs to tokio::fs on all async GvproxySwitch paths: write_config (now async), stale-socket cleanup in ensure_running, and the socket cleanup in stop. Caller in attach awaits write_config. - Update tests: subnet_rejects_overly_narrow_prefix and subnet_rejects_overly_wide_prefix now assert NetError::InvalidPrefix. Refs #526. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The InvalidPrefix error variant added thiserror.workspace to minvmd's Cargo.toml but Cargo.lock was not regenerated, so `cargo fetch --locked` in the test job failed (exit 101). Add the missing thiserror entry to minvmd's locked dependencies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
`PtaskAttachment::Drop` sent SIGTERM whenever the attachment count reached zero, even when `GvproxySwitch::stop()` (or `GvproxySwitch::Drop`) had already terminated and reaped the child. A handle that outlives `stop()` — e.g. `b` in `attach_assigns_unique_sequential_ips`, dropped after `switch.stop().await` — would then signal a PID the OS may have recycled, delivering SIGTERM to an unrelated process. Gate the signal on this drop being the first to claim teardown: `prev == 1 && !stopping.swap(true, AcqRel)`. `swap` returns the prior value, so once `stop()` or another drop has set `stopping`, the signal is skipped. This is also race-free between a concurrent drop and stop, since only one caller can flip `false -> true`. Resolves the CodeRabbit and sdd-review PID-reuse findings on #537. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PR's base commit added `thiserror.workspace = true` to `crates/minvmd/Cargo.toml` for the new `SwitchSubnetError` enum but left `Cargo.lock` unregenerated (the originating run could not reach crates.io to refresh it). Record the resolved `thiserror 2.0.18` entry under the `minvmd` package so a `--locked` / `--frozen` build resolves cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review comments on PR #537. - Add subnet_new_rejects_prefix_32 covering the /32 boundary the prefix validation already rejects: for /32 span=1, so host(0) is the network address and every host() call returns None — the exact pathology the validation guards. - Add a debug_assert that attached_count was non-zero before the fetch_sub in PtaskAttachment::drop, catching an accidental extra drop in debug builds instead of wrapping the counter to u32::MAX. - Relax detach_ptask to take &self (it touches no switch state) and fix the PtaskAttachment doc to state that every drop path logs the detach, not only an explicit detach_ptask call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the last-attachment SIGTERM guard on GvproxySwitch::Drop so it only SIGKILLs when it is the first to claim teardown via stopping.swap; an already-claimed teardown means the child is reaped and its PID may be recycled. Document the residual independent-crash recycled-PID window the swap guard cannot close, add #[must_use] to PtaskAttachment (dropping it may terminate the switch), and correct the now-stale teardown comment in last_ptask_detach_terminates_switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b7c436d to
0c3c9a2
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
stop() set stopping via store and always signalled, unlike the swap guards on GvproxySwitch::Drop and the last-detach PtaskAttachment::Drop. If a last-detach drop already claimed teardown and sent SIGTERM, the child may be reaped and its PID recycled before stop() runs; the unconditional SIGTERM/SIGKILL could then hit an unrelated process. Use swap for symmetry and skip signalling when teardown was already claimed, awaiting only the supervisor for the exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
|
Revise claim for head f3372ab. |
|
Auto-revise 2 of 3. |
sdd-validate findings — implementation boundaryPR: fix(minvmd,minimald): subnet validation, attached_count lifecycle, tokio::fs Gate 1 — Proof artifacts re-executed and passingInfo — Infrastructure limit; deferred to consumer CI Both proof artifacts require
Note: the check-runs and commit-status APIs both return 403 for this integration token ( Gate 2 — Changed files within task scopeWarning — Warning — All other changed files ( Gate 3 — No real credentials in the diffClean. No secrets, tokens, or credentials found in the diff. ResultNo Blocker findings. Two Warnings (build artifact files outside explicit scope, both necessary consequences of the Lifecycle: Issue #526 already carries sdd-validate · implementation boundary · pass 6 of 6 on this PR Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "index.crates.io"See Network Configuration for more information.
|
There was a problem hiding this comment.
Generated by sdd-review for issue #537
Comments that could not be inline-anchored
crates/minvmd/src/net.rs:582
LOW — Correctness
When already_claimed=true (a PtaskAttachment::Drop already claimed teardown and sent SIGTERM) and term_timeout elapses before the supervisor finishes, the code correctly skips SIGKILL to avoid signalling a recycled PID — but then falls through to let _ = supervisor.await with no time bound. If gvproxy hangs on SIGTERM, stop() blocks indefinitely in this path.
The prior review thread (run 28007952721) reasoned that "the timeout never fires" when `already_claimed…
Summary
Addresses the in-sandbox-verifiable hardening items from #526:
crates/minvmd/src/net.rsSwitchSubnetError::InvalidPrefix—SwitchSubnet::newis now fallible. Prefixes outside1..=30return a typedSwitchSubnetError::InvalidPrefix(prefix)instead of silently constructing a subnet where everyhost()call returnsNone(prefix 0 overflows the host-bit shift; prefix 31/32 leave no valid host index).attached_countlifecycle (R1.4) —GvproxySwitchnow carries anArc<AtomicU32>counter shared with everyPtaskAttachment.attach_ptaskincrements it;PtaskAttachment::Dropdecrements it and deliversSIGTERMto gvproxy when the count reaches zero, implementing R1.4 "stop when the last own-IP PTask exits".detach_ptaskis now a consuming method so the RAII drop fires in one place.SwitchExit::recvalready documentsNonecovering both intentional teardown and supervision-task failure — no further change needed.crates/minimald/src/net/mod.rsNetError::InvalidPrefix(u8)— distinct fromSubnetExhausted.SwitchSubnet::newnow returnsInvalidPrefixfor a prefix outside8..=29instead of the misleadingSubnetExhaustedvariant (a rejected prefix was never valid, not exhausted).tokio::fsconversion —write_config(nowasync), the stale-socket cleanup inensure_running, and the socket cleanup instopall usetokio::fsinstead of blockingstd::fs.Proof artifacts
cargo test -p minvmd -p minimald— new testssubnet_new_rejects_zero_prefix,subnet_new_rejects_prefix_31,subnet_new_rejects_prefix_above_32,subnet_new_accepts_valid_prefixes, andlast_ptask_detach_terminates_switchfail on base (no validation / no lifecycle), pass after this PR.cargo clippy -p minvmd -p minimald --all-targets -- -D warnings— no blocking-in-async lint on the convertedtokio::fspaths.Closes #526 (in-sandbox scope). The hardware relay items moved to #535.
🤖 Generated with [Claude Code]((claude.com/redacted)
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
index.crates.ioSee Network Configuration for more information.
Summary by CodeRabbit