Repository navigation
fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) - #2712
norrietaylor merged 14 commits into
Conversation
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.
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.
…host-classifier-s-mark-ipv6-and-probe
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
|
Approved: no findings at Nothing to do. Verdict APPROVE, posted as a comment: the App cannot review its own pull request; the check and the label carry it. Clobber: none found in the touched code. |
There was a problem hiding this comment.
arch-review: PASS_WITH_CONDITIONS · 4 conditions · the full ruling
The change conforms to design §4.1 line 54 and fails closed at each new check. The live proofs did not run, so C1 needs the root lane and the KVM lane green on this head. The PR body names a residual with no issue: the ct-mark mask is lost at reboot, and §7.4 line 275 asks for a one-time install. C2 files it. The IPv6 refusal is the v1 posture that §4.2 line 69 and §12 item 5 describe. C3 puts its flip condition in the script, and C4 files the missing spec line.
Ruled at 85ae03d against gominimal/arch@109541a48c31 by claude-opus-5-5.
…host-classifier-s-mark-ipv6-and-probe
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.
…host-classifier-s-mark-ipv6-and-probe
There was a problem hiding this comment.
arch-review: PASS_WITH_CONDITIONS · 2 conditions · the full ruling
The IPv6 refusal and the ct-mark refusals conform to §4.1 and §7. They fail closed, and the IPv6 interim now names arch#43 as its flip condition. Two items stay open. The live root-lane proofs did not run on this head. Run the root-lane job with nft present and link the run (C1). A chosen --ct-mark-mask is lost at reboot, because the boot unit passes no mask. File an issue for it and link it from the PR body (C2).
Ruled at f7cbcee against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Round 1 of 10 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
On a native host with the classifier installed, the table refuses an allowed host-address box's IPv6 connection to a non-loopback peer. An install over a firewall rule that writes the whole ct mark is refused, and the error names the table, the chain and the rule. This round makes the CI gates green again. The root-lane proofs that show both effects on a real table did not run on this box, so they are still owed by the root lane. Notes from the agent: Cause of the CI failure: shellcheck SC1007 on |
|
/revise Fix arch-review condition C2 at f7cbcee (arch_rulings 505) in this PR. The owner chose an in-PR fix over a residual issue, so the files outside T105's layers are allowed for this. C1 is closed: the root-lane run on f7cbcee is now in the PR body. Change nothing else.
Generated by Claude Code |
…host-classifier-s-mark-ipv6-and-probe
…ze-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.
… 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.
…host-classifier-s-mark-ipv6-and-probe
There was a problem hiding this comment.
arch-review: PASS · the full ruling
The change conforms to the design at this head. The deny-all verdict and the IPv6 refusal are unchanged since f7cbcee5, and they follow §4.1 line 54 and §7 line 231. The new min finalize-install --ct-mark-mask flag and the mask in the boot unit are on ground that the design does not cover. They close the reboot gap that the earlier PR body reported. Update the PR body: its notes still say that min finalize-install has no --ct-mark-mask flag.
Ruled at ee454d3 against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Round 2 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. An install over a firewall rule that copies whole ct marks is refused, and the refusal names the rule. In this round, an operator can run Notes from the agent: Revise round 2, C2 only. Layer exception: crates/minimal (cli.rs, cmd/finalize_install.rs, tests.rs) and scripts/install_test.sh, as the owner's /revise note allows. Please add a 'Layer exception' line for crates/minimal to the PR body. Design: the ct-mark-mask-* record is a cgroup under the cgroupfs tree, and a reboot empties it. Because of this, the installer's record read alone cannot fix the boot path. Both halves are in place. (1) The classifier step's block passes |
There was a problem hiding this comment.
Babysitter review at ee454d3c. I read the classifier changes against T105's plan block and arch C1–C2 (arch_rulings 505). The install's table now refuses non-loopback IPv6 from the boxes subtree in an ipv6_out chain that counts into the shared refused counter, refuses a whole-mark ct writer and a well-known mark range before the transaction, and the per-launch probe uses UDP with the reject-set rule. C1: the root-lane run on f7cbcee (job 122692831268) passed the three named proofs with the table loaded; its URL is in the Proof section. C2, fixed in-PR as the owner chose: scripts/install-host-classifier.sh re-applies the recorded mask when --ct-mark-mask is absent, the boot unit carries the mask, and min finalize-install --ct-mark-mask <mask> exists so the refusal's remedy runs as written. The out-of-layer files are recorded as a layer exception. No findings.
Generated by Claude Code
|
/revise Fix the HIGH security thread 4405414975 on
Generated by Claude Code |
…host-classifier-s-mark-ipv6-and-probe
… 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.
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This push merges main cleanly and adds one refusal arm to the install's ct-mark scan. A CONNMARK --restore-mark with no mask or an all-ones mask now blocks the install, as --save-mark does. The arm fails closed, and the deny-all verdict stays in the box host (§4.1 line 54). NET-079 still conforms, as it did on ee454d3c. No condition is open.
Ruled at 16d9160 against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Round 3 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. Installing over a firewall rule that copies whole ct marks is refused with the rule named. This now includes a CONNMARK --restore-mark with no mask or an all-ones mask. Notes from the agent: Revise round 3 changes only the restore-mark scan, as the note asks. scripts/install_test.sh is not in the task's layers. The directive asks for its cases explicitly, so I added them there. Wording: the directive says to name the restore-mark hit like the other hits. So it prints '(writes every bit of the connection mark)'. That is not exact: --restore-mark writes the packet mark from every bit of the connection mark, and does not write the connection mark. A reviewer can change the label for this case if they want it exact. The commit for thread 4405414975 is 16d9160. |
|
/revise Two changes the epic owner ruled on at 16d9160. Change nothing else.
Generated by Claude Code |
…host-classifier-s-mark-ipv6-and-probe
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.
|
Round 4 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. An install over a firewall rule that copies whole ct marks into the connection mark is refused with the rule named. An install over a CONNMARK --restore-mark with no mask now succeeds, because that rule writes only the packet mark. Notes from the agent: Revise round 4, commit 62b1ec1. (1) I reverted 16d9160: ct_mark_conflicts_in no longer has the --restore-mark clause, and the --save-mark handling is unchanged. I removed the shell and unit cases that expected a --restore-mark refusal. I added one case in scripts/install_test.sh and one in install_refuses_a_whole_mark_ct_writer: a --restore-mark with no mask installs. Thread 4405414975 was not in this round's unresolved list, so it has no entry here. The owner's ruling for it is the same as for 4407535596: a false positive, because a restore writes only the packet mark. (2) hold_probe_listeners now binds the datagram endpoints before it spawns any TCP drain thread. No unit test can make the datagram bind fail without a fault-injection seam, so I added no new test. The existing guest_effect_probe tests that call the function pass. scripts/install_test.sh is outside the task's layers. The directive asked for the change there, and an earlier round already touched it. Both changes are in one commit. |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This head reverts the CONNMARK --restore-mark refusal and binds the probe datagram endpoints before any drain thread starts. A restore writes only the packet mark. The table reads only ct mark, so the §4.1 line 54 classification stays safe. The scan still refuses a masked restore that touches the classifier bits. The bind order makes a failed bind return before the daemon serves an endpoint. NET-079 conforms, as on the 16d91608 baseline.
Ruled at 62b1ec1 against gominimal/arch@109541a48c31 by claude-opus-5-5.
There was a problem hiding this comment.
Babysitter review at 62b1ec1e. I read T105's mark, IPv6 and probe changes against the plan block and arch's rulings on ee454d3c, 16d91608 and this head (arch_rulings: PASS, no condition or residual open).
- ct-mark scan: whole-mark writers stay refused: an nft
ct mark setof a value other thanct mark, a--set-mark/--set-xmarkwith no mask, and a--save-markwith no mask. A CONNMARK--restore-markis no conflict again, on the owner's ruling that the HIGH was a false positive: a restore writes only the packet mark, and the classifier reads onlyct mark. I checked both in62b1ec1eand inclassifier.rs. - Probe listeners:
hold_probe_listenersbinds the datagram endpoints before any TCP drain thread spawns, so a failed bind returns before an endpoint is served (LOW 4407535601). min finalize-install --ct-mark-mask: the recorded mask is re-applied at boot, and an overlapping mask is refused with the command the user can run. The Layer exception section covers thecrates/minimalfiles.- Threads: zero open. I verified each fix in code before resolving it.
- CI: every lane aggregator is green at this head; the root-lane proof is in the PR body.
No further findings.
Generated by Claude Code
There was a problem hiding this comment.
Babysitter approval at 62b1ec1: every merge gate passes at this head (arch PASS at 62b1ec1, zero open threads, every lane aggregator green). Squash title fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) and the squashed message pass commitlint. Queued for squash merge.
Generated by Claude Code
…(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] <2817389 BD0C 52+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>
T105 — Close the host classifier's mark, IPv6 and probe gaps
slice: S16
kind: task
covers: NET-079
size: L
est: 220 code, 420 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: when the install step refuses a ct-mark writer or a mark range, it names the rule or the range and the component it belongs to, and says to rerun
min finalize-install --ct-mark-mask <mask>with two free bits, pinned by install_refuses_a_whole_mark_ct_writerobservability: the daemon logs one line per per-launch probe with its outcome and how long it took, and the classifier's IPv6 refusal counts in the same drop counter its IPv4 refusals use
layers:
- install:
- scripts/install-host-classifier.sh: the conflict scan treats a ct-mark write without an explicit mask as overlapping every bit; the mask override refuses the ranges well-known components use; the table rejects non-loopback IPv6 egress from the boxes subtree
- crates/minimald/tests/classifier_root_integration.rs: one root-lane proof per gap that loads the real table and observes the effect
test: cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer
test: cargo nextest run -p minimald install_refuses_a_well_known_mark_range
test: cargo nextest run -p minimald host_ip_box_ipv6_egress_refused
- classifier:
- crates/minimald/src/net/classifier.rs: the per-launch probe no longer waits a SYN retransmit on its TCP leg; the TCP probe stays at daemon start
test: cargo nextest run -p minimald per_launch_probe_returns_without_tcp_retransmit_wait
test: cargo nextest run -p minimald host_ip_deny_all_no_outbound
produces: none
consumes: none
proof: On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused, and installing over a firewall rule that copies whole ct marks is refused with the rule named.
depends on:
- assumes OQ3 (the spec's interim holds: a native non-deny-all host-address box resolves through the host's resolver and its allow list is decided by address rules only)
Proof
The table the install loads now has an ipv6_out chain. It refuses every non-loopback IPv6 connection from the boxes subtree, allow boxes included, and counts each refusal in the same named counter
refusedas the deny-all chain. When a host has a rule that copies or sets the whole ct mark (for example wg-quick'sct mark set meta mark), the install refuses. The refusal names the rule with its table and chain, and no --ct-mark-mask gets past it. The unit tests show both over the rendered table and a stand-in nft. The live proof on a native host is the root-lane tests host_ip_box_ipv6_egress_refused and install_refuses_a_whole_mark_ct_writer. They passed in theminimald-root-integrationjob on f7cbcee, with nft present and the table loaded: https://github.com/gominimal/minimal/actions/runs/40651618713/job/122692831268 (install_refuses_a_whole_mark_ct_writer2.24 s,install_refuses_a_well_known_mark_range3.35 s,host_ip_box_ipv6_egress_refused3.56 s,the_installers_table_refuses_the_probe_over_a_scratch_tree3.61 s,classifier_table_loads_on_this_kernel1.98 s).Notes from the agent: Root-lane proofs not run: this box has no nft, no iptables-save, no sudo and no root. So the three new #[ignore]d proofs in classifier_root_integration.rs and the new timing assert in the_installers_table_refuses_the_probe_over_a_scratch_tree are unverified, and the new nft syntax is not checked against a live kernel. That syntax is a
counter refused { packets 0 bytes 0 }object,counter name "refused"statements, and a second filter base chain at the output hook. The just test-root-integration lane owes that evidence.Printed text: the task's diagnostics line says to rerun
min finalize-install --ct-mark-mask <mask>, butmin finalize-installhas no --ct-mark-mask flag (crates/minimal/src/cli.rs). That command cannot run as written, so the refusals name the step's own command with its other arguments kept:sudo <step path> ... --ct-mark-mask <mask>. Note that finalize-install and its boot unit run the step with no mask, so a chosen mask is not kept across a reboot. Adding the flag is outside this task's layers (crates/minimal). A whole-mark writer refusal does not suggest another mask, because no mask avoids it. It says to change the rule to write only its bits, then run the step again.Probe: the per-launch readings (read_filter, read_held_filter, wh…[truncated]
Layer exception
scripts/install_test.shis outside T105's layers. It is the shell harness forscripts/install-host-classifier.sh, which is in the install layer. The layer change rewrote the mask-conflict refusal (mask_rerun_hint) and addedWELL_KNOWN_MARK_RANGES, which refuses the harness's old escape mask0x0000c000(kube-proxy). Round 1 updated the harness to the new refusal text and an escape mask outside every well-known range, socommon::shell_harnesses all_script_test_harnesses_passpasses again. It changes no behaviour of its own.crates/minimal/src/cli.rs,crates/minimal/src/cmd/finalize_install.rsandcrates/minimal/src/tests.rsare also outside the layers. Arch condition C2 (arch_rulings 505) found that a chosen--ct-mark-maskwas lost at reboot, and the owner chose to fix it in this PR rather than file a residual. The boot unitfinalize_install.rswrites now carries the mask,min finalize-install --ct-mark-mask <mask>exists so the refusal's remedy runs as written, andcli.rsandtests.rscarry the flag and its tests.Tests
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writercargo nextest run -p minimald install_refuses_a_well_known_mark_rangecargo nextest run -p minimald host_ip_box_ipv6_egress_refusedcargo nextest run -p minimald per_launch_probe_returns_without_tcp_retransmit_waitcargo nextest run -p minimald host_ip_deny_all_no_outboundVerify
min task run agent-verifypassed ([tasks.verify]).Base
The base branch moved while the coder worked; it was merged in before the tests ran.
Closes #2692
Generated by Claude Code