8000 fix(minimald): close the race between the cohort lock and the shim's leaf join (T73) by minimal-foundry[bot] · Pull Request #2595 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

fix(minimald): close the race between the cohort lock and the shim's leaf join (T73) - #2595

Merged
norrietaylor merged 7 commits into
mainfrom
agent/2559-close-the-race-between-the-cohort-lock-and-the-s
Oct 26, 2026
Merged

norrietaylor merged 7 commits into
mainfrom
agent/2559-close-the-race-between-the-cohort-lock-and-the-s

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T73 — Close the race between the cohort lock and the shim's leaf join

slice: S10
kind: task
covers: BEP-018
size: S
est: 60 code, 100 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the daemon log names an injected process that the post-join check moved into the blocked leaf
observability: one debug line per post-join move
layers:
- inject:
- crates/minimald/src/box_runtime.rs: after the shim joins its leaf, Host::command checks the cohort again under the lock and moves the shim into the blocked leaf and records it, or refuses the injection
- crates/minimald/src/box_runtime/tests.rs: the interleaving test
test: cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf
produces: none
consumes: none
proof: A process injected while a blocking member's admit runs ends in the blocked leaf and is recorded.
depends on:
- blocked by #2534


Generated by Claude Code

Proof

A process injected into a host_ip box while a blocking member's admit runs now ends in the cohort's UDP 443 blocked leaf and is recorded under its box. The new test shows the shim and its forked program in the blocked leaf after the post-join check. They go home when the blocking box departs. The daemon log has one debug line per moved pid.

Notes from the agent: The task names Host::command. In the tree that is Host::command_in_session, and the non-test variant carries the change. The shim joins its leaf itself after exec, so the daemon cannot see the join directly. The pid-report thread (report_spawned_pid) now polls the own and blocked leaves for the shim pid, for up to 5 s, until it is there or /proc/<pid> is gone. Then settle_injection runs under the cohort lock. A shim that got the blocked leaf is recorded, as before, but after the join. Before this change it was recorded at once, so a lift between the record and the join left it blocked. A shim that got its own leaf moves, with everything in that leaf, into the blocked leaf when the cohort now blocks for the box, and each move is recorded. If a move fails with an error other than ESRCH, the injection is refused: the daemon SIGKILLs the failed pid and the shim, and logs a warn. When no box-leaf is set, nothing changes. Every injection into a box with a leaf now starts this short poll thread, not only the injections that got the blocked leaf. The poll is a bounded wait. The real command path is cfg(not(test)). The test drives injection_leaf, wait_for_injection_join and settle_injection against StandinMoves. It does not spawn a real shim. I also ran cargo nextest run -p minimald quic443 (6 passed). I did not run the whole crate suite or just clippy-strict.

Tests

line exit tail
cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-b069... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling stdlib v0.0.22 (/workbenc

Verify

min task run agent-verify exited 101 under rustc 1.98.1, which the repository does not pin (1.99.0 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

re/compose.rs:402:8
    |
402 | pub fn deferring_env() -> StoredEnv {
    |        ^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
    |
402 + #[must_use]
403 | pub fn deferring_env() -> StoredEnv {
    |

error: this method could have a `#[must_use]` attribute
    --> crates/boxes/src/core/compose.rs:1019:12
     |
1019 |     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |            ^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
     |
1019 ~     #[must_use]
1020 ~     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1085:5
     |
1085 |     pub fn as_sandbox_path(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1084:5
     |
1084 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use
     = note: `-D clippy::double-must-use` implied by `-D warnings`
     = help: to override `-D warnings` add `#[allow(clippy::double_must_use)]`

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1302:5
     |
1302 |     pub fn host_path(&self) -> &HostAbsPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1301:5
     |
1301 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1309:5
     |
1309 |     pub fn destination(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1308:5
     |
1308 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: could not compile `boxes` (lib) due to 6 previous errors

Base

The base branch moved while the coder worked; it was merged in before the tests ran.

Closes #2559

…leaf (T73)

An admit of a blocking host_ip member that ran between injection_leaf
and the shim's own leaf join found no shim to move, so the injected
process escaped the UDP 443 block. The pid-report thread now waits for
the join, checks the cohort under its lock, moves the shim and what it
forked into the blocked leaf and records them, or stops the injection.
@coderabbitai
coderabbitai Bot commented Oct 26, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6482b3a0-457f-4d01-9fc4-bbefc473d037

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@minimal-foundry
minimal-foundry Bot commented Oct 26, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: every review thread resolved at 91029c9d83ee.

@norrietaylor resolved the last thread, which releases the approval this review had withheld.

Nothing to do.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_runtime.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 3 conditions · the full ruling

The post-join check is in the correct place and records each move as the task asks. Three paths still leave an injected process on unblocked UDP 443 while the cohort blocks. Section 5.3 line 150 forbids that, and section 4.1 line 54 requires an inescapable cgroup placement. C1 makes settle_injection refuse when own is not empty after the move passes, as move_in does. C2 refuses the injection when the join wait times out with the shim alive. C3 stops every unmoved process on a refusal.

Ruled at 3f8b45f against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
When the wait for the shim's join timed out, the check still moved the
shim into the UDP 443 blocked leaf. A late join into the box's own leaf
then undid the move. While the cohort blocks, the check now refuses the
injection, and the daemon stops the shim.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as 9c1af7fd67c7.

line exit tail
cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-f8f3... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling stdlib v0.0.22 (/workbenc

Verify min task run agent-verify exited 101 under rustc 1.98.1, which the repository does not pin (1.99.0 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

re/compose.rs:402:8
    |
402 | pub fn deferring_env() -> StoredEnv {
    |        ^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
    |
402 + #[must_use]
403 | pub fn deferring_env() -> StoredEnv {
    |

error: this method could have a `#[must_use]` attribute
    --> crates/boxes/src/core/compose.rs:1019:12
     |
1019 |     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |            ^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
     |
1019 ~     #[must_use]
1020 ~     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1085:5
     |
1085 |     pub fn as_sandbox_path(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1084:5
     |
1084 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use
     = note: `-D clippy::double-must-use` implied by `-D warnings`
     = help: to override `-D warnings` add `#[allow(clippy::double_must_use)]`

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1302:5
     |
1302 |     pub fn host_path(&self) -> &HostAbsPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1301:5
     |
1301 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1309:5
     |
1309 |     pub fn destination(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1308:5
     |
1308 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: could not compile `boxes` (lib) due to 6 previous errors

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_runtime.rs: settle_injection now takes the join result. A shim that did not join its own leaf in time while the cohort blocks is refused (TimedOut) and killed, not moved. The interleaving test covers this case.

A process injected while a blocking member's admit runs ends in the UDP 443 blocked leaf and is recorded under its box. A shim that does not join its leaf within the wait, while the cohort blocks, is refused and stopped, so it cannot leave the block later.

Notes from the agent: Revise round 1. settle_injection now takes a joined flag from wait_for_injection_join. A shim given the box's own leaf that did not join in time, while the cohort blocks for the box, is refused with a TimedOut error, and the caller kills it. The interleaving test covers that case. When the cohort does not block at the timeout, the injection continues as before. A blocking admit that runs after that timeout and before the late join is still a narrow window, because no later check runs for that shim. clippy -p minimald --all-targets shows no warnings in minimald (the warnings it prints are in crates/boxes, which this change does not touch).

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The earlier head moved a shim that missed its leaf join, and a late join undid the move. On this head, settle_injection refuses that injection while the cohort blocks, and the daemon stops the shim. That keeps the box's cgroup placement inescapable, as §4.1 line 54 requires, and it keeps the cohort block of §5.3 line 150. No conditions remain. One point is for code review, not architecture. When a reaped shim misses the join, the refusal sends SIGKILL to a pid that the kernel can reuse.

Ruled at 9c1af7f against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
The check after the shim's join now refuses when the box's own leaf
is not empty after the last move pass. A refusal carries what stays
in that leaf, and the daemon stops each of those and the shim through
a pidfd, so a reused pid never gets the signal. A shim that is gone
cannot join later, so what it forked is still moved, not refused.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 2 conditions · the full ruling

The compare closes the fork-storm gap and stops what a refused injection leaves, and that part conforms. But pin_pid turns every pidfd_open error into None, and the caller reads None as a gone shim. A live shim that did not join is then moved, and it can rejoin own outside the UDP 443 block. On a refusal it is not stopped. Section 4.1 line 54 says the placement must be inescapable. Make an unknown result refuse and stop, as C1 and C2 say.

Ruled at d352487 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on the runtime's review findings, pushed as d3524873cf33.

line exit tail
cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-e0a6... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling stdlib v0.0.22 (/workbenc

Verify min task run agent-verify exited 101 under rustc 1.98.1, which the repository does not pin (1.99.0 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

re/compose.rs:402:8
    |
402 | pub fn deferring_env() -> StoredEnv {
    |        ^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
    |
402 + #[must_use]
403 | pub fn deferring_env() -> StoredEnv {
    |

error: this method could have a `#[must_use]` attribute
    --> crates/boxes/src/core/compose.rs:1019:12
     |
1019 |     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |            ^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
     |
1019 ~     #[must_use]
1020 ~     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1085:5
     |
1085 |     pub fn as_sandbox_path(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1084:5
     |
1084 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use
     = note: `-D clippy::double-must-use` implied by `-D warnings`
     = help: to override `-D warnings` add `#[allow(clippy::double_must_use)]`

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1302:5
     |
1302 |     pub fn host_path(&self) -> &HostAbsPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1301:5
     |
1301 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1309:5
     |
1309 |     pub fn destination(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1308:5
     |
1308 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: could not compile `boxes` (lib) due to 6 previous errors

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_runtime.rs: settle_injection now reads own again after the last of INJECTION_MOVE_PASSES (8, the same bound as COHORT_MOVE_PASSES, which is private to classifier.rs) and returns InjectionRefused when it is not empty. The test covers this with a fork storm.
  • 👍 valid — crates/minimald/src/box_runtime.rs: The previous round already refuses a live shim that did not join while the cohort blocks, and moves nothing for it. This round kills that shim through the pidfd pinned when its pid arrives. A shim that is gone cannot join later, so what it forked is still moved.
  • 👍 valid — crates/minimald/src/box_runtime.rs: InjectionRefused now carries stranded: what is in own when the check refuses, read under the cohort lock. The caller kills each of them with SIGKILL through stop_stranded and logs the stopped pids.
  • 👍 valid — crates/minimald/src/box_runtime.rs: Same fix as C1: when own is still not empty after the last pass, the check returns InjectionRefused and names those pids, and the caller stops them.
  • 👍 valid — crates/minimald/src/box_runtime.rs: kill(2) is gone. The shim is pinned with pidfd_open when its pid arrives and signalled with pidfd_send_signal. Each stranded pid is pinned and then found in own again before the signal. A shim that is gone counts as unable to join, so it no longer gets a TimedOut refusal.

A process injected into a host-address box while a blocking member's admit runs ends in the UDP 443 blocked leaf, is recorded under the box, and goes home when the block lifts. The daemon log has one debug line per move. When the check cannot move everything (a failed move, a fork storm past the last pass, or a live shim that did not join in time), it refuses the injection. The daemon then stops the shim and each process left in the box's own leaf, so the block never fails open.

Notes from the agent: The reviewer asked for COHORT_MOVE_PASSES. That constant is private in crates/minimald/src/net/classifier.rs, and that file is outside the task's layers. So box_runtime.rs has its own INJECTION_MOVE_PASSES = 8, with the same bound. A follow-up can make the classifier constant pub(crate) and use it here. Residual: the shim's pidfd is opened when the pid-report thread receives the pid. A shim that exits and is reaped between its pre_exec send and that open gives no pidfd, and the check then treats it as gone. No signal goes to a pid that is not pinned. The new stop_stranded check runs a real sleep child and kills it through a pidfd.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 2 of 3 personas ran (correctness, supply-chain); did not complete: security.

Comment thread crates/minimald/src/box_runtime.rs Resolved
pin_pid returns the pidfd_open error. After any other pin error the
shim's liveness comes from /proc, and a refusal stops the shim and each
stranded process by its pid while the own leaf still holds it.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 05e98115b7d4.

line exit tail
cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-3526... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling stdlib v0.0.22 (/workbenc

Verify min task run agent-verify exited 101 under rustc 1.98.1, which the repository does not pin (1.99.0 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

re/compose.rs:402:8
    |
402 | pub fn deferring_env() -> StoredEnv {
    |        ^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
    |
402 + #[must_use]
403 | pub fn deferring_env() -> StoredEnv {
    |

error: this method could have a `#[must_use]` attribute
    --> crates/boxes/src/core/compose.rs:1019:12
     |
1019 |     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |            ^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
     |
1019 ~     #[must_use]
1020 ~     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1085:5
     |
1085 |     pub fn as_sandbox_path(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1084:5
     |
1084 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use
     = note: `-D clippy::double-must-use` implied by `-D warnings`
     = help: to override `-D warnings` add `#[allow(clippy::double_must_use)]`

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1302:5
     |
1302 |     pub fn host_path(&self) -> &HostAbsPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1301:5
     |
1301 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1309:5
     |
1309 |     pub fn destination(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1308:5
     |
1308 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: could not compile `boxes` (lib) due to 6 previous errors

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_runtime.rs: pin_pid now returns io::Result with the pidfd_open error. Callers treat only ESRCH as a gone process.
  • 👍 valid — crates/minimald/src/box_runtime.rs:965: On a pin error other than ESRCH, alive() reads /proc//stat (a zombie is not alive), so a live unpinned shim is not taken as joined.
  • 👍 valid — crates/minimald/src/box_runtime.rs: A refusal with no pidfd and a pin error other than ESRCH now sends kill(pid, SIGKILL) to the shim.
  • 👍 valid — crates/minimald/src/box_runtime.rs: stop_stranded skips a pid only on ESRCH. For any other pin error it reads own again and sends kill(pid, SIGKILL) while own still holds the pid.
  • 👍 valid — crates/minimald/src/box_runtime.rs: A failed pin is now told apart from a gone process. Only ESRCH is gone, and other errors take liveness from /proc. A live shim that did not join therefore times out, and the injection is refused while the cohort blocks.

A process injected into a host-address box while a blocking member's admit runs ends in the UDP 443 blocked leaf and is recorded under the box. When the shim cannot be pinned for a reason other than ESRCH, it is no longer taken as joined. Its liveness comes from /proc, and a refusal stops it, so the block does not fail open.

Notes from the agent: Revise round 3. pin_pid now returns io::Result. Only ESRCH counts as gone. After any other pin error, proc_alive reads /proc//stat and treats a zombie as not alive. On a refusal, kill(2) stops the unpinned shim. stop_stranded skips a pid only on ESRCH. For any other pin error it reads own again and sends kill(2) by pid while own still holds the pid. The interleaving test now also checks pin_pid's ESRCH class and proc_alive on a live, zombie and reaped child. cargo clippy -p minimald --all-targets shows no minimald warnings. I did not run just clippy-strict.

@minimal-foundry minimal-foundry Bot added the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Oct 26, 2026
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/minimald/src/box_runtime.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

The runtime will not revise this pull request again on its own: the diff is +516/-7 (523 lines), past 3x the plan's estimate of 160 lines (est: 60 code, 100 tests, as #2559 reads now), at round 3 (the last trigger was the runtime's review findings). A person decides now. Remove needs-human from this pull request to let /revise <note> or the automatic rounds resume, or finish #2559 by hand.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 1 condition · the full ruling

The compare fixes pin_pid(), and only ESRCH now marks the shim gone. The new /proc fallback breaks the same rule. proc_alive() returns false for any read error, so fd exhaustion makes a live shim look gone. The check then takes the shim as joined, and a later join into the own leaf undoes the move. Section 4.1 line 54 says minimald must make the cgroup placement inescapable. Make the change in C1.

Ruled at 05e9811 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/minimald/src/box_runtime.rs Resolved
@norrietaylor norrietaylor removed the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Oct 26, 2026
Copy link
Copy Markdown
Member

/revise

Round 4 of #2595, on the arch condition at 05e9811 (CHANGES_REQUESTED, C1, thread 4374096638) and one low finding. No round is in flight. The size cap is lifted for this epic, so the diff size is not a reason to stop.

  1. C1 (thread 4374096638), crates/minimald/src/box_runtime.rs proc_alive(). A read error on /proc/<pid>/stat now returns false. EMFILE makes both pin_pid() and this read fail, so a live shim looks gone and the join check takes it as joined, which breaks §4.1 line 54. Return false only for ErrorKind::NotFound, or for a stat whose state is Z or X. Return true for every other error. Add a unit test that feeds the parser a Z state, an R state and a non-NotFound error, and pins false, true and true.
  2. Low, thread 4374096486, command_in_session (line 934). The injection-join wait now runs for every box with a leaf. For a box that the host_ip cohort does not hold, settle_injection returns Ok(0) whatever the wait sees, so the wait only adds up to 5 s. Run the wait only when cohort.injection_leaf(box_id) is Some.
  3. Do not change kill_unpinned in stop_stranded (thread 4374096487). That thread waits on the owner.
  4. Run cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf and the new test, and clippy on minimald with --all-targets.

Generated by Claude Code

A read error on /proc/<pid>/stat other than NotFound, such as EMFILE,
now leaves the shim alive, so the join check does not take a live shim
as joined. The join wait also runs only for a box with host-address
enforcement, the only boxes the host_ip cohort can hold.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a /revise note, pushed as 91029c9d83ee.

line exit tail
cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-acf2... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling stdlib v0.0.22 (/workbenc

Verify min task run agent-verify exited 101 under rustc 1.98.1, which the repository does not pin (1.99.0 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

re/compose.rs:402:8
    |
402 | pub fn deferring_env() -> StoredEnv {
    |        ^^^^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
    |
402 + #[must_use]
403 | pub fn deferring_env() -> StoredEnv {
    |

error: this method could have a `#[must_use]` attribute
    --> crates/boxes/src/core/compose.rs:1019:12
     |
1019 |     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |            ^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#must_use_candidate
help: add the attribute
     |
1019 ~     #[must_use]
1020 ~     pub fn lifecycle_hooks_teardown(&self) -> impl DoubleEndedIterator<Item = &ProvenancedHook> {
     |

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1085:5
     |
1085 |     pub fn as_sandbox_path(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1084:5
     |
1084 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use
     = note: `-D clippy::double-must-use` implied by `-D warnings`
     = help: to override `-D warnings` add `#[allow(clippy::double_must_use)]`

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1302:5
     |
1302 |     pub fn host_path(&self) -> &HostAbsPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1301:5
     |
1301 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: this function has a `#[must_use]` attribute with no message, but returns a type already marked as `#[must_use]`
    --> crates/boxes/src/core/primitives.rs:1309:5
     |
1309 |     pub fn destination(&self) -> &SandboxRelPath {
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
help: remove `must_use`
    --> crates/boxes/src/core/primitives.rs:1308:5
     |
1308 |     #[must_use]
     |     ^^^^^^^^^^^
     = note: alternatively, you may add an explicit reason to the `must_use` attribute
     = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#double_must_use

error: could not compile `boxes` (lib) due to 6 previous errors

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_runtime.rs: proc_alive now returns false only for a NotFound read or a Z/X state, and true for any other error (EMFILE included). The new test a_shim_stat_read_says_gone_only_for_a_zombie_or_a_missing_file pins this.
  • 👍 valid — crates/minimald/src/box_runtime.rs:947: The hook and its wait now run only for a box with host_ip_enforcement, the only boxes the cohort can hold. The gate is not on injection_leaf(box_id).is_some(): an admit after that read and before the join lets the shim escape the block.
  • 👎 invalid — crates/minimald/src/box_runtime.rs:561: No change this round. The PR note keeps kill_unpinned in stop_stranded as it is until the owner decides. The window is real but narrow: it opens only when pidfd_open fails with an error other than ESRCH.

A process injected into a host-address box while a blocking member's admit runs ends in the cohort's UDP 443 blocked leaf and is recorded under its box. The interleaving test pins this. A live shim whose /proc stat read fails with EMFILE is no longer taken as joined.

Notes from the agent: Item 1 (C1): proc_alive now calls a new stat_alive. It returns false only for ErrorKind::NotFound or a Z or X state, and true for every other read error. A stat that cannot be parsed also counts as alive. The new unit test pins Z and X as false, R and S as true, NotFound as false, and EMFILE as true. Item 2: I did not use the note's literal gate, cohort.injection_leaf(box_id).is_some(). That gate reopens the race this task closes. A blocking member's admit can land after the hook reads the cohort and before the shim joins its own leaf. Then settle_injection returns Ok(0), and the shim joins its own leaf after the admit's move, outside the block. The hook now runs only when the box has host_ip_enforcement. join_quic443_cohort admits no box without it, so the cohort never holds such a box and the check cannot change its outcome. For a box outside the cohort, this is the cost the finding names. A tighter gate needs a membership accessor on CohortQuic443 in crates/minimald/src/net/classifier.rs, which is outside this task's layers. Item 3: kill_unpinned in stop_stranded is unchanged, as the note asks. I did not run just clippy-strict.

@minimal-foundry minimal-foundry Bot added the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Oct 26, 2026
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

The runtime will not revise this pull request again on its own: the diff is +557/-7 (564 lines), past 3x the plan's estimate of 160 lines (est: 60 code, 100 tests, as #2559 reads now), at round 1 (the last trigger was a /revise note). A person decides now. Remove needs-human from this pull request to let /revise <note> or the automatic rounds resume, or finish #2559 by hand.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The change conforms to §5.3 line 150 and §4.1 line 54 at this head. A stat read error other than a missing file now leaves an unpinned shim alive. The join check then refuses it while the cohort blocks, and does not take it as joined. The join wait now runs only for a box with host_ip_enforcement. That is the same value that gates cohort membership in join_quic443_cohort, so no cohort member loses the check.

Ruled at 91029c9 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approval withheld

Verdict COMMENT — APPROVE withheld: 12 unresolved threads — minimal-arch[bot] crates/minimald/src/box_runtime.rs, minimal-arch[bot] crates/minimald/src/box_runtime.rs, minimal-arch[bot] crates/minimald/src/box_runtime.rs, minimal-foundry[bot] crates/minimald/src/box_runtime.rs, minimal-arch[bot] crates/minimald/src/box_runtime.rs, …. No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@norrietaylor norrietaylor removed the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Oct 26, 2026
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — every review thread is resolved; @norrietaylor closed the last one. The review of 91029c9d83ee withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 26, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Own review at 91029c9. The race fix pins the shim by pidfd and treats only ESRCH as gone, and falls back to /proc stat for any other pin error, where only NotFound or a Z/X state reads gone. A live shim that did not join is refused while the cohort blocks, and its stranded pids are killed. The join wait runs only for host_ip_enforcement boxes. The pid-reuse window in stop_stranded is accepted by the epic owner (thread 4374096487). No other findings.


Generated by Claude Code

@norrietaylor norrietaylor changed the title T73 — Close the race between the cohort lock and the shim's leaf join fix(minimald): close the race between the cohort lock and the shim's leaf join (T73) Oct 26, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter approval at 91029c9: every merge gate passes at this head (arch PASS at 91029c9, zero open threads, every lane aggregator green). Squash title fix(minimald): close the race between the cohort lock and the shim's leaf join (T73) and the squashed message pass commitlint. Queued for squash merge.


Generated by Claude Code

@norrietaylor
norrietaylor added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
Copy link
Copy Markdown
Member

The merge-queue run for this PR (run 40287898529, tests) failed one test: minimald box_actor::tests::abrupt_client_loss_keeps_task (10.4 s), which passed on this head. Another box_actor timing test failed the same way on #2593 earlier today, and it merged on a re-queue. I filed the pattern as #2609 and am re-queueing this PR once.


Generated by Claude Code

@norrietaylor
norrietaylor added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 78e706f Oct 26, 2026
37 checks passed
@norrietaylor
norrietaylor deleted the agent/2559-close-the-race-between-the-cohort-lock-and-the-s branch October 26, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T73 — Close the race between the cohort lock and the shim's leaf join

1 participant

0