8000 feat(minimald): reject UDP 443 from a blocked cohort's own leaves (T72) by minimal-foundry[bot] · Pull Request #2601 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

feat(minimald): reject UDP 443 from a blocked cohort's own leaves (T72) - #2601

Merged
norrietaylor merged 14 commits into
mainfrom
agent/2558-reject-udp-443-from-a-blocked-cohort-s-own-leave
Oct 26, 2026
Merged

norrietaylor merged 14 commits into
mainfrom
agent/2558-reject-udp-443-from-a-blocked-cohort-s-own-leave

Conversation

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

T72 — Reject UDP 443 from a blocked cohort's own leaves, sockets opened before the flip included

slice: S10
kind: task
covers: BEP-018
size: M
est: 220 code, 200 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the daemon log names each leaf added to or removed from the blocked set, and min finalize-install names the helper item and why it cannot install it
observability: one info line per set change with the leaf and the cohort's blocking members
layers:
- ruleset:
- scripts/install-host-classifier.sh: render_ruleset declares a set of cgroup v2 leaves (typeof socket cgroupv2 level N) and the UDP 443 reject rule also matches a socket whose leaf is in it, so a socket created in a member's own leaf before the flip is rejected from its next datagram; the install step adds a root-owned helper that only adds or removes one leaf under the delegated boxes slice in that set, refusing any other path, and a sudoers drop-in that lets the login run only that helper with sudo -n (owner's decision 2026-10-26: a narrow helper, not CAP_NET_ADMIN for minimald)
- scripts/install_test.sh: the rendered set and rule, and the helper refusing a path outside the slice
test: cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set
- cohort:
- crates/minimald/src/net/classifier.rs: CohortQuic443 keeps every resident allow member's own leaf in the blocked set while the cohort blocks: all of them when it turns blocked, a joining member's at admit, none once the last blocker leaves; the process moves stay
- crates/minimald/src/box_actor/host.rs: runs the helper through sudo -n for each set change; a refused add fails the launch that needed it, so the block fails closed (arch C1 at cc74186), and a refused remove is logged and retried while the box keeps running
test: cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set
produces: none
consumes: none
proof: A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, and sends again when it lifts.
depends on:
- blocked by #2534

Proof

The classifier table now declares the quic443_blocked_leaves set and rejects UDP 443, with ICMP port unreachable, from any socket whose own box leaf is in that set. While the host_ip cohort blocks, minimald puts the own leaf of every resident allow member in the set through the root-owned helper (sudo -n). It takes each leaf out when the block lifts. A socket a member opened before the flip still has its own leaf as its cgroup, so its next UDP 443 datagram is rejected, and it sends again when the block lifts. The tests show the rendered set and rule, the helper's changes and refusals, and the set following the cohort. No kernel run proves the end-to-end send and stop on this box.

Notes from the agent: Design: the set change goes on the CgroupMoves seam as add_blocked_leaf and remove_blocked_leaf, with no-op defaults. The admit and depart signatures did not change, because box_runtime.rs tests call admit and that file is outside the layers. host.rs adds HostMoves (KernelMoves plus the helper through sudo -n) and uses it on the launch, stop and exit paths. KernelMoves stays in box_runtime.rs adopt_injected and changes no set. That is correct, because an injected process joins the blocked leaf itself. The helper path is /usr/local/lib/minimal/quic443-leaf and the drop-in is /etc/sudoers.d/minimal-quic443-leaf (rule '# ALL=(root) NOPASSWD: ', checked with visudo when visudo is present). A rehearsal writes them only when MINIMAL_OVERRIDE_QUIC443_HELPER and MINIMAL_OVERRIDE_QUIC443_SUDOERS are set. Otherwise it skips them, so the existing stand-in installs are unchanged. The uninstall removes both. The install dies with 'cannot install the QUIC block helper at : ' (or the sudoers equivalent) before it loads the table. Not done: the diagnostics line asks that min finalize-install name the helper item and why it cannot install it. That code is in crates/minimal (resolver.rs), outside the layers. Today finalize-install shows the helper …[truncated]

Tests

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-4ef8... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-5a27... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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.

Residuals

Closes #2558


Generated by Claude Code

A socket keeps the cgroup it was created in. So a host_ip member's
process moved into boxes/quic443-blocked still sent UDP 443 from a
socket it opened in its own leaf before the cohort turned blocked.

- The classifier table declares the quic443_blocked_leaves set of box
  leaves, and the output chain rejects UDP 443 from a socket whose leaf
  is in it, with ICMP port unreachable.
- The install writes a root-owned helper that adds or removes one leaf
  under boxes/allow in that set and refuses every other path, plus a
  sudoers drop-in that lets the delegated account run only it with
  sudo -n.
- CohortQuic443 keeps every resident allow member's own leaf in the set
  while the cohort blocks, and logs one info line per set change. The
  launch and stop paths run the helper through sudo -n, and a refusal
  is logged without failing the box. The process moves stay.
The harness installs the helper under its scratch root, checks the set
and its reject rule in the loaded table, and runs the helper: it changes
one allow-subtree leaf and refuses every other path.
@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: 922cea9f-92a7-46a8-95b7-e4cea1c8feca

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

Reviewed: 1 finding, none blocking at a6472dd48dcc.

Address the comments on the diff.

Clobber: none seen.

@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 — 3 findings, 3 inline.

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

Comment thread crates/minimald/src/net/classifier.rs Resolved
Comment thread scripts/install-host-classifier.sh Resolved
Comment thread scripts/install-host-classifier.sh 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 · 2 conditions · the full ruling

The leaf set and its reject rule close the pre-flip gap that §5.3 line 150 requires closed. One path fails open. When the helper refuses or is missing, sync_leaf_set drops the add and admit returns Ok, so the cohort blocks only in part. C1 makes admit refuse the launch, as NoBlockedLeaf does today. C2 changes the test that expects the fail-open result. The escalation asks the design to record the runtime sudo helper in §7.4.

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

Comment thread crates/minimald/src/net/classifier.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
Comment thread crates/minimald/src/net/classifier.rs Resolved
The cohort keeps a leaf recorded when its remove fails and the leaf still
exists, so the next cohort change retries it. A leaf that is gone is
dropped, because its cgroup id is never reused.

A real install of the QUIC block helper refuses the sudoers drop-in when
visudo is missing, because an unchecked drop-in can break sudo.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as 4964cfdeca58.

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-87ef... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-2be2... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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/net/classifier.rs:798: sync_leaf_set now keeps the leaf recorded on a failed remove while its directory exists, so the next cohort change retries it. A gone leaf is dropped. The test now pins both cases.
  • 👍 valid — scripts/install-host-classifier.sh:723: A real install now refuses the sudoers drop-in and removes the staged copy when visudo is missing. Only a rehearsal, which writes under a scratch root, skips the check.
  • 👍 valid — scripts/install-host-classifier.sh:733: The install fails when visudo is missing outside a rehearsal. The function comment now says that, and says why a rehearsal can skip the check.

A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, because its own leaf goes into the blocked-leaves set. It sends again when the block lifts. A remove that fails is now retried on the next cohort change, so a surviving box is not left blocked.

Notes from the agent: Revise round 1. The sync now keeps a leaf recorded when its remove fails and the leaf directory still exists, and the next cohort change retries it. A leaf whose directory is gone is dropped, because the kernel never reuses a cgroup id and nft cannot resolve the path to delete it. A real install now refuses the sudoers drop-in when visudo is missing. A rehearsal still skips the check, because its drop-in is under a scratch root that sudo does not read. New refusal text: 'cannot install the sudoers rule for the QUIC block helper at : visudo is not installed, so the rule cannot be checked. Install the sudo package, then run this installer again.' I also ran sh scripts/install_test.sh host_classifier_tree_installed (exit 0, 211 passed), cargo clippy -p minimald --all-targets (clean) and cargo fmt -p minimald --check (clean).

@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_WITH_CONDITIONS · 2 conditions · the full ruling

The compare fixes the two fail-open paths. A failed remove is now retried, and a real install refuses a sudoers rule that it cannot check. The set, the reject rule and the root helper conform to §5.3 line 150, §4.1 line 54 and §7.4 line 275. Two conditions stay. C1: the min finalize-install diagnostics item is undone and has no issue, so open one and link it. C2: the new CgroupMoves set methods default to Ok(()). Make them fail closed, and correct the stale sync_leaf_set doc comment.

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

Comment thread crates/minimald/src/net/classifier.rs Resolved
Comment thread crates/minimald/src/net/classifier.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
@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 scripts/install-host-classifier.sh Resolved
A refused add to the blocked-leaves set while the cohort blocks now
returns CohortPlaceError::Unlisted from admit, so the launch fails
instead of leaving a pre-flip socket able to send UDP 443. The
CgroupMoves defaults fail with Unsupported, so a seam that changes no
set never records a leaf as listed.
@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_WITH_CONDITIONS · 2 conditions · the full ruling

The new commit closes the fail-open gap. A refused add to the blocked-leaves set now fails the launch, so the block fails closed as §5.3 line 150 requires. BEP-018 conforms. Two items stay open and need issues. The kernel leg of the proof is not run (C1). The min finalize-install helper diagnostics item is left undone with no issue (C2). Open both issues and link them in the PR body.

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

Comment thread crates/minimald/src/net/classifier.rs Resolved
Comment thread scripts/install-host-classifier.sh Resolved
@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 scripts/install-host-classifier.sh Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-ddf2... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-03fe... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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/net/classifier.rs: sync_leaf_set returns CohortPlaceError::Unlisted for a refused add while the cohort blocks, and admit propagates it after the process moves, so the launch fails.
  • 👍 valid — crates/minimald/src/box_actor/host.rs:25: The run_quic443_leaf_helper doc now says that a refused add fails the launch that needed it, and that only a refused remove leaves the box running.
  • 👍 valid — crates/minimald/src/net/classifier.rs: The test now asserts Err(CohortPlaceError::Unlisted) for homes[0]. Then it departs the flipper, checks that the set is empty, and checks that a retry lists the leaf.
  • 👍 valid — crates/minimald/src/net/classifier.rs:592: The add_blocked_leaf and remove_blocked_leaf defaults now return ErrorKind::Unsupported. The ForkingMoves test seam forwards both calls to its inner stand-in.
  • 👍 valid — crates/minimald/src/net/classifier.rs: The sync_leaf_set comment now says that a refused remove stays recorded for a retry, and that only a leaf that is gone is dropped.
  • 👎 invalid — crates/minimald/src/box_actor/host.rs: A missing helper now fails the session start with the launch refusal, not only a warn!. The finalize-install item is in crates/minimal, outside this task's layers. I have no credential to open an issue, so a person must file it and link it from the PR.
  • 👎 invalid — PRRT_kwDOPdfcxM6wJfgR: The fix belongs in scripts/install.sh, which is outside this task's layers. install-host-classifier.sh --uninstall does remove both files, but the install.sh removal list does not run it. A follow-up must add both paths to that list and to _cls_found. (not posted on the thread: not an open thread on the pull request)

A host_ip cohort member's UDP 443 socket that opened before the cohort turned blocked is in a leaf in the blocked-leaves set, so it stops sending when the block flips and sends again when the block lifts. If the helper refuses to add a leaf, the launch fails and the block does not fail open.

Notes from the agent: Revise round 2. A refused add while the cohort blocks now returns CohortPlaceError::Unlisted from CohortQuic443::admit. The launch then fails through the existing join_quic443_cohort path, which departs the box. The CgroupMoves add and remove defaults now return ErrorKind::Unsupported. Not done, outside the layers: (1) the min finalize-install item that names the helper (crates/minimal) is still open. I have no credential to open an issue, so a person must file it and link it from the PR. (2) The finalize-install removal list in scripts/install.sh does not name /usr/local/lib/minimal/quic443-leaf or /etc/sudoers.d/minimal-quic443-leaf. The --uninstall mode of install-host-classifier.sh does remove both files. just clippy-strict was not run.

@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 scripts/install-host-classifier.sh 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: PASS_WITH_CONDITIONS · 3 conditions · the full ruling

The change conforms to §5.3 line 150 and §4.1 line 54. The head has the same tree as cc741861, so the earlier ruling stands. Three records are open. C1: open a gominimal/arch issue that records the sudo -n helper in §7.4 line 275, because the design names only the one-time step. C2: file the min finalize-install diagnostics gap. C3: file the kernel leg that proves a pre-flip socket stops and starts. Link all three issues from the PR body.

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

Comment thread crates/minimald/src/box_actor/host.rs Resolved
Comment thread scripts/install-host-classifier.sh Resolved
Comment thread scripts/install-host-classifier.sh Resolved
@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 f5ffed1, focused on the new root surface.

  • The sudoers drop-in names one root-owned helper and is checked with visudo -cf before it is moved in. A real install refuses without visudo.
  • The helper takes only add or remove and one leaf name under the allow subtree, matching [A-Za-z0-9._-]. It refuses a symlink or a non-directory on add, and it changes only quic443_blocked_leaves. Its paths and the nft binary are fixed at install.
  • The daemon runs it with sudo -n, so a refused add fails the launch.

Arch conditions C1 and C2 are filed as #2606 and #2607. One thread stays open for the owner: the installer leaves the helper and its rule in place when nft -f fails (4374360491).


Generated by Claude Code

Root runs the helper for the delegated account, so the installer now
fails unless the helper and sudoers directories and their parents are
root-owned and not group or other writable. The staging files come
from mktemp, so a planted staging name or symlink is never written
through.
@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_WITH_CONDITIONS · 2 conditions · the full ruling

The compare makes the install of the root helper safer. The install refuses a helper or sudoers directory that an account other than root can write. It also stages each file with mktemp. Both refusals stop the install, so the change fails closed in the install step of §7.4 line 275. The cohort and ruleset ruling on f5ffed16 stays. C1 (kernel leg, #2606) and C2 (min finalize-install names the helper, #2607) stay open as filed residuals.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 07a484166a3f.

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-9fb3... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-86ea... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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.

toredEnv {
    |        ^^^^^^^^^^^^^
    |
    = 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
warning: build failed, waiting for other jobs to finish...

Review threads the coder judged:

  • 👍 valid — scripts/install-host-classifier.sh:757: install_quic443_helper now fails unless the helper and sudoers directories and all their parents are root-owned and not group or other writable, and it stages both files with mktemp in their directory. install_test.sh pins the refusal for a mode 0777 helper directory.

A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, and sends again when it lifts. This round also makes the install refuse a helper directory that an account other than root can write, so the sudo rule for the helper cannot become a path to root.

Notes from the agent: Revise round 3. New function assert_root_only_dir checks the helper and sudoers directories and every parent up to /. Each one must be owned by root and must not be group or other writable. A rehearsal (the test harness) checks only the directory itself and accepts the running account as owner, because its scratch tree is not root-owned. The staging files now come from mktemp. The non-rehearsal walk to / was checked by hand on this box, where every directory is owned by uid 1000, and it refused as expected. No test covers that walk, because the harness cannot create root-owned directories. The walk also refuses a sticky world-writable parent such as /tmp. That is stricter than needed, but the default helper path is under /usr/local/lib. dash is not installed on this box, so install_test.sh ran under sh only. The PR is not verified on a real host with KVM or sudo.

@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: 4 unresolved threads — minimal-foundry[bot] scripts/install-host-classifier.sh:757, minimal-arch[bot] crates/minimald/src/box_actor/host.rs:16, minimal-arch[bot] scripts/install-host-classifier.sh:1232, minimal-arch[bot] scripts/install-host-classifier.sh:895. No findings.

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

Copy link
Copy Markdown
Member

/revise

Round 4 of #2601, on the epic owner's ruling for thread 4374360491 (fix in this PR). No round is in flight.

scripts/install-host-classifier.sh writes the root-owned QUIC block helper and its passwordless sudoers rule (install_quic443_helper) before it loads the nft table. When the nft -f step then fails, the script dies and leaves both in place.

  1. Install the helper and the sudoers rule only after the table load succeeds. If they must stay before the load, remove both on every die path after they are written.
  2. A failed load leaves neither $QUIC443_HELPER nor $QUIC443_SUDOERS behind, and an existing pair from an earlier install is left as it was.
  3. Add a case to scripts/install_test.sh that fails the nft -f step in rehearsal and asserts neither file exists afterwards.
  4. Change nothing else. Run bash scripts/install_test.sh and cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set.

Generated by Claude Code

Copy link
Copy Markdown
Member

/revise

Round 5 of #2601, on two findings at 2aafba3 that hold. No round is in flight.

  1. Thread 4374906227 (security), render_quic443_helper. The helper runs as root through sudo and execs the nft path that command -v nft found at install time. Nothing checks that path. If the installer runs with a PATH that reaches a user-writable nft, the passwordless helper runs that file as root. Before rendering, resolve the path with readlink -f. Refuse the install unless the binary and every parent directory are root-owned and not group- or other-writable, with the same check and refusal text that install_quic443_helper uses for the helper's directories. Add an install_test.sh case that offers a user-writable nft and expects the refusal.
  2. Thread 4374906219, render_ruleset. The ruleset starts with delete table. A reinstall while the daemon runs empties quic443_blocked_leaves, and a blocked cohort's leaves are then out of the set until the next sync_leaf_set, so the block fails open in that window. Keep the set's elements across the reload: read them with nft -j list set before the load, and add them back in the same nft -f transaction. Add an install_test.sh case that reloads with one element present and checks the element is still there.
  3. Leave thread 4374707223 open: it waits on the epic owner. Change nothing else. Run bash scripts/install_test.sh and cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set.

Generated by Claude Code

The QUIC block helper runs nft as root, so the install now resolves the
nft path with readlink -f and refuses it unless the file and its
directories are root-only. A re-install reads the blocked-leaves set with
nft -j list set and declares the set with those leaves in the same
transaction, so a blocked box does not send UDP 443 again.
@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 nft check conforms. The helper now runs only an nft that root owns, and no other account can write it or its directories. Keeping the blocked leaves on a re-install is correct for §5.3 line 150. But kept_quic443_leaves treats a failed nft -j list set as an empty set. The re-install then empties the set while minimald still records the leaves, so a blocked cohort can send UDP 443. C3 makes that path refuse before the load. The kernel leg and the min finalize-install diagnostic stay open as #2606 and #2607.

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

Comment thread scripts/install-host-classifier.sh Resolved
Comment thread scripts/install-host-classifier.sh Resolved
@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_actor/host.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as 1cf159e8f36c.

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0f67... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-1c60... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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.

toredEnv {
    |        ^^^^^^^^^^^^^
    |
    = 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
warning: build failed, waiting for other jobs to finish...

Review threads the coder judged:

  • 👍 valid — PRRT_kwDOPdfcxM6wK-wS: A re-install now reads quic443_blocked_leaves with nft -j list set and declares the set with the leaves that still exist, in the same nft -f transaction as the delete. install_test.sh reloads with one element present and checks it stays. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6wK-wZ: The install resolves nft with readlink -f and refuses it, before any load, unless the file and its directories are root-only. It uses the same check and text as the helper directory. install_test.sh covers a writable nft file and a writable nft directory. (not posted on the thread: not an open thread on the pull request)

A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, and sends again when it lifts. This round also keeps it blocked across a re-install of the classifier table, and the passwordless helper can no longer run an nft binary that another account can write.

Notes from the agent: Round 5. (1) The install resolves nft with readlink -f in a new resolve_quic443_nft. It refuses the binary unless the file and every directory above it are root-owned and not group- or other-writable. The check runs before anything is laid out or loaded, so a refused nft changes nothing. The per-path check is now assert_root_only_entry, and assert_root_only_dir calls it, so the refusal text is the same for both. A rehearsal still checks only the directory itself and accepts the running account as owner, the same as for the helper directory. (2) Just before the render, kept_quic443_leaves reads the set with nft -j list set and keeps each element that is a plain-named directory directly under the allow subtree and still exists. A missing cgroup makes nft refuse the whole load, so a gone leaf is dropped. render_ruleset then declares the set with elements = { ... } in the same nft -f transaction. --print-ruleset reads nothing and stays as before. The install prints one line that names the kept leaves. The JSON is parsed with grep, not jq, because jq is not a dependency of this script. The elem array shape was not checked against a real nft on this box. Thread 4374707223 was left open as the note asked. The cohort_flip_puts_member_leaves_in_the_blocked_set test lives in net::classifier, so the 52-test run above includes it and it passed. This round did not change Rust code.

A failed nft -j list set while the loaded table declares the set now
stops the install before the marker goes, instead of rendering the
set empty. The installer tests match the physical scratch path that
macOS reports under /private/var, and the awk check carries an SC2016
directive.
Copy link
Copy Markdown
Member

/revise

Round 6 of #2601, on CI failures and arch condition C3 at 1cf159e. No round is in flight.

  1. Shellcheck (tests lane, common::shell_lint all_scripts_pass_shellcheck, and the shellcheck job).

    • scripts/install-host-classifier.sh line 749: SC2015 on quic443_nft="$(readlink -f "$found" 2>/dev/null)" && [ -f "$quic443_nft" ] || ....
    • scripts/install-host-classifier.sh line 970: SC2015 on [ -d "$tree_root/$BOXES_DIR/$ALLOW_DIR/$name" ] && ....
    • scripts/install_test.sh line 3058: SC2016 on the awk '/set quic443_blocked_leaves \{/ ...' program.

    Rewrite the two A && B || C forms as if/then/else. For the awk program, which is meant to stay unexpanded, add a # shellcheck disable=SC2016 directive with a one-line reason above it. Run scripts/lint-shell.sh until it reports nothing.

  2. macOS (test (macos-latest / sh), 887 passed, 2 failed). These two cases fail on macOS and pass on Linux: "the refusal names the writable nft dir and the fix" and "the refusal names the writable nft file and the fix". The refusal itself happens (the exit-1 cases pass), so the printed text differs on BSD tools. Check readlink -f and the stat mode or owner reading on macOS, and make the message name the same path and fix on both. stat_mode already has a BSD fallback, and the nft path check needs the same.

  3. Arch C3, kept_quic443_leaves. When nft -j list set inet "$TABLE_NAME" "$QUIC443_LEAF_SET" fails, return 0 with no output only when the loaded table does not declare the set: either the table is absent, or nft list table inet "$TABLE_NAME" has no set quic443_blocked_leaves line. In every other case, print why and return 1. The caller at line 1306 becomes quic443_kept_leaves="$(kept_quic443_leaves)" || die "cannot read the box leaves that are refused UDP to port 443 from inet $TABLE_NAME: <why>. The loaded table is unchanged." before any delete table. Add an install_test.sh case where the list fails on a table that declares the set: the install exits 1 and the loaded table is unchanged.

  4. Change nothing else. Run bash scripts/install_test.sh, SH=sh sh scripts/install_test.sh, scripts/lint-shell.sh, and cargo nextest run -p common all_scripts_pass_shellcheck.


Generated by Claude Code

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2558. Your note is kept and goes into the next round here, which starts when that coder finishes.

Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet.

@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_WITH_CONDITIONS · 2 conditions · the full ruling

The compare closes the fail-open read that the ruling on 1cf159e named. A re-install that cannot list the declared blocked-leaves set now stops before the marker goes and before the load. It does not render the set empty. Section 5.3 line 150 requires that UDP/443 stays blocked for the cohort while any member blocks, and this head keeps it blocked. Two conditions stay open as filed residuals: C1 (#2606, the kernel leg) and C2 (#2607, the finalize-install diagnostic).

Ruled at d1504d7 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: 3 unresolved threads — minimal-arch[bot] scripts/install-host-classifier.sh, minimal-arch[bot] scripts/install-host-classifier.sh, minimal-foundry[bot] crates/minimald/src/box_actor/host.rs:100. No findings.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 6 of 10 on a failed check suite, pushed as d1504d7e4a3f.

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-bc8e... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6892... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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.

toredEnv {
    |        ^^^^^^^^^^^^^
    |
    = 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
warning: build failed, waiting for other jobs to finish...

Review threads the coder judged:

  • 👍 valid — scripts/install-host-classifier.sh: kept_quic443_leaves now returns 0 only when the loaded table does not declare quic443_blocked_leaves. Otherwise it prints nft's error and returns 1. A new installer test pins this.
  • 👍 valid — scripts/install-host-classifier.sh: The caller now dies with the requested message. The read moved before the marker removal, so the install stops before it touches the marker, the render or the load.
  • 👎 invalid — crates/minimald/src/box_actor/host.rs:100: A sudo that PATH selects runs with minimald's own privileges and gets no root, so it gives no escalation. Anyone who controls minimald's PATH already controls minimald. A fixed /usr/bin/sudo breaks hosts that keep sudo in another place, for example /run/wrappers/bin/sudo on NixOS.

A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, because its own leaf goes into the blocked-leaves set that the output chain rejects. It sends again when the last blocker leaves. A re-install keeps the set's leaves, and a re-install that cannot read the set stops before it touches the table or the marker.

Notes from the agent: Round 6. The shellcheck failure was SC2016 on the awk at install_test.sh:3058. A directive now covers it. The two macOS installer failures were very probably the helper-directory and nft-directory refusals: assert_root_only_dir and readlink -f print the physical path, and the macOS mktemp root is under /var, a symlink to /private/var. The tests now match the physical path. I could not run them on macOS. The log excerpt for the Linux 'tests' job does not name a failure. The full minimald nextest suite passes here (1132 passed), so I made no change for it. kept_quic443_leaves now reads the loaded table from the ruleset that scan_ct_mark_conflicts already listed (new global host_ruleset), so nft list ruleset still runs once. The read now happens before the marker is removed, so a refusal leaves the marker in place.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Revise round 6 ran against ci-linux-native-success, tests and left it red; a coder round cannot clear it, a person should look. No round was started for the failed suite on d1504d7e4a3f.

Older shellcheck releases flag the A && B || C forms as SC2015, so the
nft resolve check and the kept-leaf directory check use if blocks.
@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_WITH_CONDITIONS · 2 conditions · the full ruling

The compare changes no behaviour. Commit 632a5f39 rewrites two shell checks as if blocks, and the checks still refuse the same cases. The merge of T73 does not need a change to the blocked set, because settle_injection changes no cohort membership. BEP-018 conforms to §5.3 line 150 and §7.4 line 275, as on the baseline. C1 (the kernel leg, #2606) and C2 (the min finalize-install item, #2607) stay open as filed residuals.

Ruled at a6472dd 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 — 1 finding, 1 inline.

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

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

Round 7 of 10 on a /revise note, pushed as a6472dd48dcc.

line exit tail
cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-41db... 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
cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-a8c1... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.

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.

toredEnv {
    |        ^^^^^^^^^^^^^
    |
    = 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
warning: build failed, waiting for other jobs to finish...

A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, because its leaf goes into the blocked-leaves set that the reject rule matches. It sends again when the block lifts and the leaf leaves the set. A re-install keeps the leaves in the set, and an install that cannot read the set stops with the loaded table unchanged.

Notes from the agent: Round 7. Items 2 and 3 were already on the branch in d1504d7: the macOS tests compare against the physical path from pwd -P, and kept_quic443_leaves returns 1 with a reason when the listing fails on a table that declares the set, with a test case. The SC2016 directive from item 1 was also there. This round rewrote the two SC2015 forms (the nft resolve check and the kept-leaf directory check) as if blocks. The local shellcheck is 0.11.0 and does not flag SC2015 on these forms, so the local lint passed before and after the change. The CI shellcheck is older and flagged them. Line 805 (... && chmod 0440 ... ) ||) has a similar shape, but CI did not name it, so it is unchanged. macOS was not run on this box.

@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 a6472dd. The installer adds the helper and the sudoers rule only after the table loads. It refuses an nft binary, or a helper directory, that an account other than root can write. A blocked set survives a reinstall, and an unreadable set refuses the install. The fail-closed launch is intended. Arch conditions are residuals #2606 and #2607. The epic owner ruled the security thread on 2026-10-26.


Generated by Claude Code

@norrietaylor norrietaylor changed the title T72 — Reject UDP 443 from a blocked cohort's own leaves, sockets opened before the flip included feat(minimald): reject UDP 443 from a blocked cohort's own leaves (T72) 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 a6472dd: every merge gate passes at this head (arch PASS at a6472dd, zero open threads, every lane aggregator green). Squash title feat(minimald): reject UDP 443 from a blocked cohort's own leaves (T72) 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
Merged via the queue into main with commit 5741f2f Oct 26, 2026
39 checks passed
@norrietaylor
norrietaylor deleted the agent/2558-reject-udp-443-from-a-blocked-cohort-s-own-leave branch October 26, 2026 18:50
norrietaylor added a commit that referenced this pull request Oct 29, 2026
…(0.7.0 backport) (#2742)

* fix(minimal): hold the answerer channel socket owner-only (#2297)

* fix(minimal,minvmd): hold the answerer channel socket owner-only

The box-zone answerer's channel socket was world-connectable: the
launchd plist bound it SockPathMode 0666 and the systemd socket unit
SocketMode=0666, leaving the peer-uid gate the only check on who may
connect. Both units now bind it owner-only -- SockPathOwner the
operator, SockPathMode 0600 on launchd; SocketUser the operator,
SocketMode=0600 on systemd -- with the uid gate kept as defence in
depth.

An install whose socket node carries group or other bits -- one that
predates the mode, or a hand that relaxed it -- now fails custody
through a channel_mode fact, so the advisory re-surfaces and a step
re-run re-binds the socket. The accept loop's foreign-refusal warn is
rate-limited to a few lines a second, so a connect storm cannot flood
the log; the refusals themselves are never limited.

* fix(minimal,minimald,minvmd): name the other account on a refused channel connect

A daemon whose connect the owner-only channel socket refuses now says
the host's DNS service belongs to another account, not a bare
Permission denied. The answerer step needs the operator's account to
resolve, with no stand-in uid, and custody fails a channel socket the
operator does not own.

* fix(minimal,minvmd): leave another operator's DNS service alone

A channel socket owned by another existing, non-root account is that
operator's service: the answerer step is now unavailable with the same
sentence the daemons say on a refused connect, instead of a custody
failure whose step would reinstall the unit and take the service over.
A root-owned socket, or one whose uid names no account, still fails
custody so the step re-binds it.

The minvmd tests that take the process-global foreign-refusal slots
now run one at a time and start and end with every slot free, and the
warn-rate test bounds the burst at two windows' worth of lines, so a
slow runner that spreads the burst does not fail it.

* fix(minimal): offer the step over a socket another account left behind

A channel socket another account owns refuses the answerer step only
when a service is installed; with none, the step's install removes the
leftover socket before it loads the unit, so it is offered.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
(cherry picked from commit 746770d)

* fix(net): refuse out-of-zone names at a guest deny-subtree DNS listener (#2300)

* fix(net): refuse out-of-zone names at a guest deny-subtree DNS listener

A deny-all host-address box on a VM-backed guest resolved out-of-zone
names at the switch gateway: the interim NET-079 carve-out admitted the
gateway's port 53 whole, UDP and TCP, so a box that admits no destination
could still ask a resolver beyond the machine — and DNS over TCP could
retry past the verdict.

The guest's deny subtree now steers its UDP-53 lookups onto a loopback
listener on the answerer's port (NET-141 slice 2): deny_out admits the
listener beside the reply direction, the dstnat match carries no
destination address (the box asks the gateway like every other
host-address box; the steer is the carve-out), and the listener itself
answers REFUSED for every name outside the box zone — apex and a
non-lowercase zone suffix among them — NODATA for an in-zone non-A
question, and forwards the zone's A questions to the switch gateway
alone. DNS over TCP is neither steered nor admitted.

The guest daemon starts the listener after its classifier table loads;
the daemon's own lookups are untouched (cgroup-keyed match, deny path
only). The interim --gateway-resolver installer flag and every test,
digest and pin that spelled the interim carve-out follow the new shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(net): bound and pin the guest DNS listener's forwards

Connect each forward socket to the gateway so a datagram from any other
sender is dropped, cap forwards in flight so a flood cannot exhaust the
daemon's descriptors, and drop the needless to_string calls clippy
rejects in the guest classifier integration test.

* fix(net): refuse questionless and non-QUERY messages at the guest DNS listener

decide() forwarded a message with no question, and an UPDATE or NOTIFY
whose zone section named an in-zone A, because every per-question check
passed vacuously. Answer the first REFUSED and the rest NOTIMP, so the
forward path carries only standard lookups.

Bind the listener directly so its log does not read as the box-zone
answerer, pin the integration test's boot-log check to the exact
example.com REFUSED line, and cite NET-079 (the deny-all case) rather
than NET-141 at the lines this change added.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 2c2f8cc)

* fix(net): spawn the guest DNS tasks with tokio::spawn on release/0.7.0

crate::traced landed on main after the release branch was cut, so the
cherry-pick of #2300 did not compile here.

* build: declare 0.7.3 for the release/0.7.0 backports

v0.7.2 is tagged at the branch head, so the version check requires the
next patch release.

* chore(deps): update pulldown-cmark to 0.13.5 for RUSTSEC-2026-0414

* fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) (#2712)

* fix(minimald): close the classifier's mark, IPv6 and probe gaps

The install's conflict scan now treats a ct-mark write with no mask
as a user of every bit, names each rule with its table and chain,
and --ct-mark-mask refuses the ranges Tailscale and kube-proxy class
with. The table refuses non-loopback IPv6 from the boxes subtree and
counts that refusal in the same named counter as the deny chain.

The per-launch probe reads with a UDP datagram, so a launch no longer
waits the SYN retransmit a refused TCP connect pays. The reading at
daemon start keeps its TCP leg. Each probe logs its leg and duration.

* test(minimald): prove the classifier's mark, IPv6 and probe gaps live

Three root-lane proofs load the real table: the install refuses a
whole-mark ct writer and a mask in a well-known range, and an allow
box's IPv6 connection to a peer is refused and counted. The live probe
proof also checks the per-launch reading waits no retransmit.

* fix(minimald): pass shellcheck and pin the new mask refusals

The rerun hint's empty assignment tripped SC1007 in the shell lint
gate. The installer test pinned the old remedy text and escaped with
0x0000c000, the kube-proxy range the install now refuses: it now
pins the new remedy, escapes with 0x03000000, and pins the
well-known range refusal. The IPv6 refusal comment names the
condition that replaces it.

* fix(minimald): re-apply the recorded ct-mark mask and name min finalize-install

With no --ct-mark-mask, the install step adopts the mask a previous
install recorded under the tree. Mask refusals now name
min finalize-install --ct-mark-mask <mask>. The conflict scan joins the
nft and iptables-save hits with a newline.

* feat(minimal): add min finalize-install --ct-mark-mask and keep it at boot

The mask record lives in cgroupfs, so a reboot empties it. The classifier
step's block now passes the chosen or recorded mask to the one-time run
and to the boot unit's ExecStart, so the operator's bits survive a
reboot. A requested mask that differs from the loaded table's re-runs
the step.

* fix(minimald): refuse a CONNMARK --restore-mark that copies the whole mark

The install's ct-mark scan now treats --restore-mark with no --mask,
--ctmask or --nfmask, or an all-ones one, as a whole-mark writer, as it
does --save-mark. A restore with a narrow mask is still no conflict.

* fix(minimald): allow --restore-mark, bind probe datagrams first

A CONNMARK --restore-mark copies the connection mark into the packet
mark and never writes the connection mark, which is all the classifier
reads. This reverts 16d9160 and pins a restore with no mask as no
conflict.

hold_probe_listeners now binds its datagram endpoints before it spawns
any TCP drain thread, so a failed datagram bind returns before a
listener serves.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
(cherry picked from commit 354548e)

* fix(minimald): adapt the host classifier gaps backport to release/0.7.0

The cherry-pick before this one keeps main's text for every line #2712
changes. Main's neighbouring code comes from the QUIC block work
(#2513, #2553, #2601, #2644), the box-name placement (#2722) and the
crate renames, none of which release/0.7.0 has. The conflicts were
resolved onto release's code without importing that work:

- classifier.rs: the four new unit tests are appended after release's
  last test, without the QUIC cohort tests main has above them. Their
  sandbox::classifier paths are sandbox2::classifier here.
- install-host-classifier.sh: the table gains the refused counter. The
  QUIC leaf set that main declares beside it is not on release, so it
  stays out. The --help range (2,87p) matches release's header too.
- finalize_install.rs: release has one classifier item, built by
  classifier_item_on_this_host, not main's classifier_items_on_this_host
  with the QUIC block helper item. That function takes --ct-mark-mask
  and fills recorded_mask and requested_mask itself. plan_for_this_host
  keeps release's names item placement and only passes the mask on.
- #2712's edits to finalize_install_names_the_quic_block_helper are
  dropped: release has no QUIC block helper item or test.
- install_test.sh: run_hc puts HC_PATH_PREFIX ahead of PATH, the line
  #2601 added on main. Without it the new --restore-mark case never
  reaches its stand-in iptables-save, and shellcheck flags the variable
  as unused.

The user-facing text is main's, unchanged.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
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.

T72 — Reject UDP 443 from a blocked cohort's own leaves, sockets opened before the flip included

1 participant

0