Repository navigation
feat(minimald): bind declared ports before the name and refuse unpublished ports instantly - #1798
Conversation
#1765 added SandboxLauncher.box_addresses and SessionConfig.box_addresses but updated only one of the three initializers, so the package's test targets no longer compiled. Fill the two it missed (the own-ip session request fixture and the sandbox launcher fixture) with None, the value every other initializer carries.
The switch relay's ingress leg dropped every refused TCP frame silently, so a connection to a port nothing published hung until the peer's timeout; and a revoked port (NET-121) had no way to end the connections its forwarder held. - a SYN to a port the box's ingress declaration does not name is now answered with a reset built from the SYN itself (tuple swapped, the box's address as the source, RST|ACK, ack riding the SYN's sequence plus its flag weight, honest IPv4 and TCP checksums), so the peer's connect fails at once with connection refused while the box's kernel never sees the connection (NET-014). Unsolicited UDP stays a drop: a datagram is not a connection. - the gate now tracks the tail of every admitted inbound TCP flow and revokes a port on demand: it admits the port no longer, answers every packet to it with a reset, and terminates each held connection with a reset built from the flow's own last packet, which an established peer reads in-window (NET-121). Revocation is wired to the ingress forwarders in the next commit. - the reset legs write through the same framed switch write half as the egress leg, under a shared mutex, and unbind revocation says 'revoked ingress port' where a mapping refusal says 'no ingress mapping' — both rate-limited with the peer and port, so the daemon log's tail names each refusal. - compile fallout of the gate's new Arc signature: the DNS-gate tests' L4Packet literals gain the sequence pair, the netns harness wraps its gate as the attach now takes it. unpublished_port_connection_refused drives the refusal end to end on the relay harness: the reset's bytes, the silent box end, the still- forwarded declared port, and the log line.
…0s ran out the coder's uncommitted work as it stood when its 7200s ran out The coder was stopped at the end of its time with these changes not committed. The runtime committed its working tree as it stood so the work is kept: this commit is a snapshot, not a finished step, and may not build or pass its tests.
…red-ports-before-the-name-and-refuse-u
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Three of the four NET-121 proofs had fixture bugs that kept them from passing: the registry route is looked up as the full zone name the registry answers (web.min.internal), not the bare two-label box name; the fixture switch now carries the same instance host id the proofs' registry is built with (aaaa1), the pairing the daemon itself makes, so the attach publishes both host ids' dns/add requests the revocation proof counts; and the bound-forwarder proof drives the guard's explicit teardown instead of dropping it, which only aborts the relay and leaves the forwards standing.
The per-forwarder bind line's wording is a contract beside its fields: scripts/session-e2e.sh's published_loopback_host reads the publish record — the host address the forwarder actually bound — out of the daemon log by exactly the phrase 'exposed ingress port on the host loopback', three call sites. Renaming the line to 'bound declared ingress port' left that reader matching nothing, and the native-daemon e2e lane failed in the fresh-install loopback-publish proof at the record read. Restore the wording and note the contract at the line.
|
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. A declared port is reachable by name the moment the name exists, a revoked port's connections end at once, a failed bind is reported and never replaced by another address, and an unpublished port refuses instead of timing out — all four proof tests now run and pass, and the session e2e lane's log record reader matches the daemon's per-forwarder bind line again. Notes from the agent: Revise round 1: the branch head's snapshot commit (4af8354, the previous coder's unverified work) broke the checks in three places, all fixed here. (1) The NET-121 proof declared_ports_bound_before_name_registered looked its registry route up as the bare two-label name 'web', but the registry answers full zone names — now 'web.min.internal' (same fix in failed_forwarder_bind_is_reported_not_substituted, whose Absent assertion was trivially true before and is about the real route now). (2) The same proof counted two /services/dns/add publishes per attach, but its switch carried the default host id 'local' (one publish) while the registry fixture was built with instance id 'aaaa1'; production always pairs them (sessions.rs builds the registry from switch.host_id()), so vm_host_switch() now carries 'aaaa1' and the two-publish count is real. (3) The bound-forwarder proof dropped the guard instead of driving its explicit teardown, so the unbind posts never happened — it now calls NetGuard::teardown, the way the sandbox layer does. (4) The native-daemon-e2e failure: the snapshot renamed the per-forwarder bind log line to 'bound declared ingress port on the host loopback', but scripts/session-e2e.sh reads that record out of the daemon log by the exact phrase 'exposed ingress port on the host loopback' (published_loopback_host, three call sites — the fresh-install loopback-publish and network-posture proofs fail at the record read without it); the wording is restored, with the contract noted at the line. I had no gh access to the CI logs, so the e2e cause was derived from the scrip…[truncated] |
|
/revise Round 2 carries the design review's conditions (deployment-and-egress-gateway.md §7.1 line 248, §4.1 line 56, §5.7 determinism rule, architecture AT10).
Follow-ups this round records in the body, not in code: the host-side gate answers SYN-to-unadmitted-port with RST on the host leg as well; the host-side half of a revoked connection is closed by the host-side component (a gvproxy behaviour pinned by the lock test, or a host-owned forwarder); the gate verifies the request's Tests to run: the four plan lines, plus |
The ingress gate's resets now ride a bounded channel — a send past the bound is dropped, never queued — and are drawn against a per-source windowed budget, so a box flooding SYNs at another's ports degrades to a timeout alone while every other peer is still refused at once; the first refusal a window refuses is one audited warn line per source per window. The two reset shapes follow RFC 793 §3.4, and both are built only from state the gate holds: a bare SYN to an unpublished port (NET-014) is answered RST|ACK from sequence zero acknowledging the SYN, and a connection a revocation ended (NET-121) is answered from the tail the gate tracked, so the self-healing second reset stays in the window the gate watched. A segment of a flow the gate holds nothing for — a spoofed one — is answered with no reset at all, so this box can never be made to emit a reset carrying a spoofed sequence to a victim; every reset is addressed to the frame's source. The refusal stays TCP-only by design (design §7.1): a UDP datagram is not a connection and keeps its silent drop.
A VM-backed box's registration carries the addresses the VM host daemon handed it (T66), and the host loopback half is the address the host-side table's row owns — not a grant this daemon's in-guest book could hand out. The finalize publish now prefers the handed address over the lease, so the attach path's expose requests name exactly it as their local: never the node's shared 127.0.0.1 interim, never an address the daemon chose. A handed box never draws from the loopback pool: the row owns the address, so neither the grant's info line nor the release at destroy runs for it.
The gvproxy stand-in now answers with reason bodies, so the failed-bind proof covers both shapes a bind can fail in: the switch's gate refusing an expose (403 with its reason) is a failed bind exactly like a bind that errors — the attach fails, the refusal's reason reaches the caller beside its port, and every bind that landed is rolled back, so a half-bound box holds nothing. The revocation proof is now end to end against a forwarding stand-in that binds real host listeners: a real client socket connects to the port, the stand-in opens the client's leg through the relay, and the revocation ends the client's connection — EOF or reset within the bound, never a hang — while the next connect is refused, not accepted-then-dropped, because the unexpose dropped the listener before the revocation returned. The reset that ended the connection is the gate's, read off the relay and addressed to the leg. A new proof pins the attach's local as the handed host loopback address: the binds and the teardown both name the row's address and the declared ports, never the node's shared interim.
|
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: |
|
Round 2 of 10 on a /revise note, pushed as The coder ran out of its 7200s before it finished. If it left work uncommitted, its last commit is the runtime's snapshot of that work as it stood — the commit message says so — and not a finished step: it may not build or pass its tests.
Verify Outside the task's
Converted to a draft: something above needs a person before review. A declared port is reachable by name the moment the name exists; a revoked port's connections end at once — proved now from the host-side client's own socket, which sees EOF or a reset within the bound while nothing further reaches the box, and whose next connect is refused rather than accepted-then-dropped; a failed bind — whether the switch's gate refused the expose or the bind errored — is reported with its port and the reason the switch gave, registers neither the name nor a substitute, and rolls back every bind that landed so a half-bound box holds nothing; and an unpublished port answers a SYN with a reset built only from state the gate holds — RFC 793 §3.4's two shapes, budgeted per source over a bounded channel, with the expose's Notes from the agent: Directive items, each addressed: (1) Gate refusal = failed bind: covered. The production path already folds a 403 into post_json's error and apply_ingress already treats any non-2xx expose as a failed bind; the proof now covers it: failed_forwarder_bind_is_reported_not_substituted drives two sequential scenarios — a 403 with a reason body for the third of three declared ports, and the existing 500 bind failure — asserting per shape that the attach error carries the port and the switch's reason, the warn line says port + reason, neither the name nor a substitute is registered (ZoneEntry::Absent), and every bind that landed is rolled back. (2) Full rollback asserted in the same test: the 403-on-the-third-port scenario asserts unexposes for both ports that bound (8080 and 9090) — a half-bound box holds nothing — not just the one-port rollback the bind-failure shape showed. (3) Bounded channel + per-source budget: RESET_CHANNEL_CAPACITY 256 with try_send (excess dropped, never queued); ResetBudget admits RESET_PER_WINDOW 16 per RESET_WINDOW 1s per source, one audited tracing::warn per source per …[truncated] |
|
/revise Round 3 is bounded to the two items round 2 left open, plus the body. Everything else in round 2 is accepted as landed.
Run every test line in the plan block and paste the results. Commit subject and body lines stay at or under 100 columns (commitlint body-max-line-length). |
There was a problem hiding this comment.
Round 2 review at aee331a8 against the round-2 note.
- Items 1, 2, 3, 6 land with proofs: 403-with-reason fails the attach and rolls back to zero forwarders and zero names (
gvproxy_network.rs:1317); boundedtry_sendchannel plus a per-source 16/s budget with one audit line per window (switch.rs:1757,switch.rs:1809); revocation seen by a real host client within 60 s and the next connect refused (gvproxy_network.rs:1770). - Item 4 (MEDIUM): SYN → RST|ACK with seq 0 / ack SYN.seq+1, held flow → tracked pair, unheld flow → nothing, all proved. But
refuse_tcp_segment(switch.rs:1234) still answers an incoming RST on the revoked path (a peer's RST|ACK for a flow still interminated_flows). RFC 793 §3.4: never reset a reset. Add the RST-flag early return before the budget charge. - Item 7 (HIGH, blocking):
gvproxy_network.rs:257keepsmap_or(Ipv4Addr::LOCALHOST, …);published_or_node(dns.rs:1046) still hands the node address when nothing was handed;session.rs:843still leases from the in-guest pool; there is no "no published address handed" failure path; no reporter-absent / reporter-hands-127.0.0.1 proofs. Three of the four plan proofs (declared_ports_bound…,failed_forwarder_bind…,ingress_revocation…) assert127.0.0.1:…, i.e. they rest on the fallback the note removes; onlylocal_is_the_handed_loopback_address(:1984) proves the handed-address half. - Item 5 is in the module doc only, and the PR body is still the round-0 runtime text (draft, exit-100 test table, stale out-of-scope list) with none of the item 5/7 statements or the three follow-ups. Rewrite it; name
net/dns_gate.rsandtests/netns_root_integration.rsas forced signature fallout. - LOW, note-only: revoke_port vs record_inbound race (
switch.rs:2255/:2335) can leave one connection to time out; UDP mapping revoke inserts into the TCP revoked set (policy.rs:209);revoke_ingressstill#[allow(dead_code)](gvproxy_network.rs:72); fixed host port 48080 in a loopback-binding test.
Round 3 note follows as a /revise comment.
The pseudo-header's fourth word is big-endian [zero][protocol], so the protocol contributes itself (6) unshifted; the << 8 it was built with parked it in the zero byte's place (0x0600), giving a checksum no TCP stack verifies, so every reset this module built was dropped at the peer that was meant to receive it. RFC 793 §3.1 is the reference. The test helpers that judged resets now verify the checksum by the wire's own definition — assemble the pseudo-header from the frame and fold the sum to 0xffff — instead of recomputing it with the same function under test, and a known-answer unit test pins 0x23f2 for a fixed segment, checked against an independent implementation off-tree.
revoke_ingress found its forwarder by external port but revoked the gate by internal port, so with two mappings publishing the same in-box port, revoking one ended the sibling's connections too. The gate cannot tell forwarded connections apart — a box's inbound frames carry only the internal port — so the revocation now withholds the gate's termination while another live forwarder still serves that port, and the port's connections end when the last forwarder to it is revoked: the first moment they can be told apart at all. The forwarder records a successful unbind on an Arc<AtomicBool>, so teardown skips the unexpose for a port already revoked and a revocation that failed at the switch stays unrevoked, with both retry and teardown still having work to do. The end-to-end test drives two external ports into one internal port, revokes the first, and shows the sibling's established connection surviving and new ones still completing its handshake; the last revocation ends all three.
…red-ports-before-the-name-and-refuse-u
|
Round 3 of 10 on the runtime's review findings, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
A declared port is reachable by name the moment the name exists, a revoked port's connections end at once, a failed bind is reported and never replaced by another address, and an unpublished port refuses instead of timing out — and the resets that carry the refusal and the terminations now carry a checksum a real TCP stack verifies, so they arrive instead of being dropped. A revocation no longer ends a sibling forwarder's connections when two mappings publish the same in-box port: only the last forwarder to a port ends its connections. Notes from the agent: Round 3 addressed all three findings; two fixes plus their tests, two commits (fix(minimald): carry the TCP protocol in its own pseudo-header byte; fix(minimald): scope a revoked port's termination to its own forwarder). Both fixes were mutation-verified: reintroducing the << 8 fails the new known-answer test and the wire-fold check in refused_resets_carry_the_two_shapes_and_only_held_flows; making revoke ignore keep_admitted fails the new test's quiet-window assertion. The HIGH finding's premise is stale at this head — see the thread verdict — and session_host.rs was left unchanged on purpose: the task's layer note there ('holds the forwarders until stop') is satisfied structurally by Host::net_guard (session_host.rs:1670), which hands the session the Arc holding |
…red-ports-before-the-name-and-refuse-u
|
Round 9 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:
A handed box's name now publishes once at its final address: while the range verdict is pending its finalize waits for it — bounded by the 5 s session-start deadline — so the name appears at the box's hand the moment the verdict lands Present, or at the 127.0.0.1 interim if the deadline expires, with the present landing's sweep upgrading that interim to the hand; every move (hand to interim, interim to grant or hand, interim back under an absent landing) logs one warn naming the box and both addresses, a box destroyed inside the landing's window has nothing resurrected and its drawn grant released, and with it the task's proof stands: a declared port is reachable by name the moment the name exists, a revoked port's connections end at once, a failed bind is reported and never replaced by another address, and an unpublished port refuses instead of timing out. Notes from the agent: Item 3's |
…handed box A handed box whose hand the range verdict does not vouch for now publishes at the 127.0.0.1 interim and never asks the pool for a grant. Under the registry write lock, a registration re-reads the verdict and promotes the interim to the hand when a present landing has raced it. The verdict wait is one deadline for the daemon, the book's open plus HAND_VERDICT_WAIT, and every waiter races that same instant. Only a box's first finalize waits. Session::run (resume, create, destroy-hook bring-up) and rename never wait: they publish the interim and the present landing moves them. Under a pending verdict the grant answers every namespace with the interim, a recorded one too, and keeps the record's line, so no reserved-range address publishes before a landed present. The present landing restores the recorded address. The present landing's discard releases a drawn grant only when the box's publish is gone, and its draw no longer grants for a handed box whose hand is unvouched. Tests poll a verdict-waiter count before landing, prove the discard's own release, and cover no-wait resume and destroy, the shared deadline, the resumed recorded address, and a re-registered box keeping its grant.
The present landing runs on the deferred walk's spawned task, off every mailbox, so its apply's write-lock re-check is the runtime guard; say so at the spawn site and on apply_interim_upgrade. Add a test that destroys a box between the landing's enumeration and its grant: nothing is re-published and the grant is released exactly once, by the discard. Add a test that queues a destroy behind a finalize waiting at the verdict deadline: the publish at the hand precedes the removal, and no name is left.
Extract the decision register_hostname takes under the registry write lock into promote_interim_to_hand and test it directly with no timers: an interim publish with a hand moves to the hand under a present verdict, stays on the interim under an absent one, and never moves over a name another session holds (HostnameRegistry::name_held_by_another).
There was a problem hiding this comment.
Reviewed at 4831e39: round-9 defects D1–D5 and Q2 are fixed, the landing re-check, the mailbox ordering and the promotion decision are each pinned by a test that fails under its mutation, and the architecture conditions are confirmed. Known gap (promotion call site) is recorded in the body.
|
/revise Round 10, the last round. Add one test, and change nothing else. Scope:
Add a test that drives
Assert that after In the round report, show that the new test fails when the Leave the PR body as it is; it is edited by hand. |
…red-ports-before-the-name-and-refuse-u
The helper's unit test pins the decision promote_interim_to_hand makes; this one pins that register_hostname makes the call: drive the registration itself, with the ask answered "not vouched" and the verdict landed present before the write lock — the exact window the re-read under the lock exists for. The drive is the box's first finalize parked on the pending verdict, the one await between its ask and its publish; the test holds the registry's write lock across that park, wakes the ask with an absent landing, waits for its waiter to drain (the proof the ask consumed the absent answer), stores the present verdict through the raw book store — never the landing's sweep, which would take the very lock being held — and then releases the lock. Nothing decides by a deadline. Removing the call leaves the box standing at 127.0.0.1 and fails the test; restoring it publishes and answers at the hand.
…red-ports-before-the-name-and-refuse-u
|
Round 10 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. The promote_interim_to_hand call inside register_hostname is now pinned by a test that drives the registration itself: a finalize whose ask was answered with the 127.0.0.1 interim publishes at its own hand — publish, A answer and route — when the verdict lands present before the write lock, and removing the call turns that test red (the box stands at the interim). Notes from the agent: Round 10, one test added, nothing else changed: crates/minimald/src/session/tests.rs only, as the directive scoped. Why the drive is not the sketch's literal shape: with the range book already landed Present and wait_for_verdict = false, hand_or_interim answers the hand directly, the promotion never fires, and the test would pass with the call removed. The call only fires in the window between the ask and the write lock, so the test drives exactly that window, deterministically and with no timer deciding anything: the box's first finalize is parked on the pending verdict (the one await between ask and publish, proven by verdict_waiters), the test then holds the registry's write lock, an absent landing wakes the ask to the interim (its loopback-hand-to-interim line is asserted as the drive's own account), the waiters count draining to zero proves the absent answer was consumed before the present verdict is stored, and the present verdict goes in through LoopbackLeaseBook::set_range_verdict — the raw store, the half of a real landing that runs before its sweep takes the registry. The sweep is deliberately not run: it would take the very lock being held, and it moves interim boxes itself (that move is an_interim_handed_publish_takes_its_hand_when_the_verdict_lands_late's to pin), which would make this test vacuous. Releasing the lock lets the registration publish, and it takes its hand. The deadline is held at DEADLINE_HELD_OPEN_MS and never fires. One constraint forced a shape worth knowing: await-holding-lock is a denied workspace lint (CI clippy runs -D warnings), so th…[truncated] |
There was a problem hiding this comment.
Re-approved at 61e17ad: round 10 adds register_hostname_promotes_an_interim_to_its_vouched_hand, which drives register_hostname through the write-lock window with no timer and closes the call-site gap recorded in the body. minimal-review APPROVE on this head.
T21 — Bind declared ports before the name and refuse unpublished ports instantly
slice: S5
kind: task
covers: NET-014, NET-121
size: L
est: 350 code, 240 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle's daemon log tail names each forwarder bind, unbind, failed bind and revocation with the port and the reason
observability: one info log lin 8000 e per forwarder bind and unbind, and one warn line per failed bind, with the port and the reason
layers:
- daemon:
- crates/minimald/src/session.rs: registers the name only after the declared forwarders bind; a failed bind registers neither the name nor a substitute
- crates/minimald/src/net/gvproxy_network.rs: makes bind-then-register explicit
- crates/minimald/src/net/policy.rs: revocation unbinds the forwarder and ends its connections, through a daemon-owned forwarder where the switch's unexpose leaves them open
- crates/minimald/src/net/switch.rs: the ingress gate answers a SYN to an unpublished port with a reset instead of dropping it
- crates/minimald/src/session_host.rs: holds the forwarders until stop
- crates/minimald/src/net/dns.rs: never registers a substitute address
test: cargo nextest run -p minimald declared_ports_bound_before_name_registered
test: cargo nextest run -p minimald ingress_revocation_unbinds_forwarder_and_terminates_connections
test: cargo nextest run -p minimald failed_forwarder_bind_is_reported_not_substituted
test: cargo nextest run -p minimald unpublished_port_connection_refused
produces: none
consumes:
- crates/sessions/src/core/loopback.rs
proof: A declared port is reachable by name the moment the name exists, a revoked port's connections end at once, a failed bind is reported and never replaced by another address, and an unpublished port refuses instead of timing out.
depends on:
- blocked by #1645
Proof
A declared port is reachable by name the moment the name exists, a revoked port's connections end at once, a failed bind is reported and never replaced by another address, and an unpublished port refuses instead of timing out.
Task tests (all
cargo nextest run -p minimald <name>, Linux):declared_ports_bound_before_name_registeredingress_revocation_unbinds_forwarder_and_terminates_connectionsfailed_forwarder_bind_is_reported_not_substitutedunpublished_port_connection_refusedPublish address under the host's range verdict
The host's publish surface carries a range verdict (NET-123):
Pendingwhile the daemon-start walk runs (VM host), thenPresentorAbsentwhen it lands.One daemon-wide deadline. The book opens one verdict deadline (
HAND_VERDICT_WAITfrom book open). Only finalize waits on it. Every waiter races the same instant, so N finalizes under a slow walk cost one wait, not N. Session resume, rename and the destroy-hooks bring-up never wait: they publish the interim and let the landing move them.At finalize (
session.rs):vouches_forholds (outside the reserved range, or under aPresentverdict). Otherwise it publishes the127.0.0.1interim. A handed box never takes a grant from the answerer's pool.LoopbackGrant::publishable_address):Grantedgives its own address;RangeAbsentandRangePendinggive the interim; faults (PoolSpent,RecordUnavailable) give no address, and the attach fails with "no published address handed".Pending, every ask answersRangePending, including a namespace with a recorded address. A resumed box therefore stands on the interim until the verdict lands, never on a range address the host may not hold.When the verdict lands (
sessions.rsland_range_verdict), the verdict is set before either arm runs:Absentmoves every own-address publish inside the reserved range, name and route included, onto127.0.0.1(one warn per box). That covers a hand a resumed box published (NET-013), which otherwise failed its attach withEADDRNOTAVAILuntil destroy.Presentre-asks for every publish standing at the interim. A box with a hand is promoted to its hand under the registry's write lock; a box without one re-publishes at its grant. The landing runs on its own task, not through the Manager's mailbox, so it re-checks under the write lock that the session still holds its name and stands at the interim: a box destroyed in between is not re-published, and a grant taken for it is released exactly once.On Linux native the book opens on the synchronous probe's verdict,
Present, so neither fallback fires there.Root fix, out of scope here: minvmd should measure the host loopback before it hands a reserved-range slice address (#1818). Until then, the verdict gate and the landing sweep hold the interim.
Verdict tests (all
cargo nextest run -p minimald <name>, Linux):every_hand_waiter_races_the_one_daemon_deadline(net/dns.rs)a_pending_verdict_withholds_every_grant_and_keeps_the_recorded_line(net/dns.rs)a_landed_absent_verdict_withdraws_the_vouching_a_handed_range_address_needs(net/dns.rs)a_surface_withheld_grant_publishes_the_interim_and_a_fault_publishes_nothing(net/dns.rs)handed_finalizes_share_one_verdict_deadlinea_handed_box_pending_at_finalize_moves_to_the_interim_when_the_verdict_lands_absentan_interim_publish_takes_its_grant_when_the_verdict_lands_presenta_resumed_handed_box_publishes_the_interim_without_waiting_and_takes_its_hand_on_presenta_resumed_box_publishes_the_interim_until_present_restores_its_recorded_addressa_destroy_under_a_pending_verdict_does_not_wait_and_publishes_the_interima_box_re_registered_inside_the_landing_window_keeps_its_granta_box_destroyed_between_enumeration_and_grant_is_released_oncea_destroy_queued_behind_a_waiting_finalize_runs_after_the_promotionthe_write_lock_promotion_moves_an_interim_to_a_vouched_hand_onlyThe call site is pinned too:
register_hostname_promotes_an_interim_to_its_vouched_handdrivesregister_hostnamethrough the write-lock window, with no timer deciding, and fails when thepromote_interim_to_handcall is removed.the_write_lock_promotion_moves_an_interim_to_a_vouched_hand_onlypins the decision itself.Files outside the task's layers
crates/minimald/src/net/dns.rs,crates/minimald/src/sessions.rs: the range verdict, the shared deadline and the landing sweep the publish path depends on.crates/minimald/src/net/dns_gate.rs: the resolver side of the published name.crates/minimald/src/session/tests.rs,crates/minimald/src/session_host/tests.rs: tests for the above; both also changed on main and were merged.crates/minimald/tests/netns_root_integration.rs: root-lane proof of the bind-before-name order.Closes #1657