Skip to content

netstack keeps each program's DNS answers for their TTL, in one cache for the machine that empties with the lease - #883

Merged
Japabu merged 9 commits into
mainfrom
wt/toyos-dnscache
Oct 12, 2026
Merged

Japabu merged 9 commits into
mainfrom
wt/toyos-dnscache

Conversation

@Japabu

@Japabu Japabu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Build round 3 at d6e5a33f0. Round 2 (75cf86188) with its build review's two NOTEs made (review) and origin/main 97f07c381 (#888) merged. Round 2 was round 1's build (804ccd7fc) of the design the orchestrator accepted (acceptance), which is round 2's design with that review's named changes (review), with the build review's NOTEs closed (review) and origin/main ab655a115 (#871, #861, #881) merged. The supervisor's two-pass boot, which departs from the accepted design, was accepted on the pull request (acceptance). The T14 has run the metal measurement: K2 and K4 hold (below).

The owner ruled that ToyOS gets a DNS cache in netstack, with answers per program: "Per-program answers" (ruling). The measurement behind it is #861's T14 connection-setup scout (analysis). Of the 8.65 ms by which ToyOS's TCP setup trailed Ubuntu's, 6.31 ms was the lookup's WAN round trip, which netstack paid on every lookup.

What this does

One cache for the machine; each answer belongs to one program

  • netstack keys a lookup by the badge on its connection (system/netstack/src/serve.rs). It reads the badge once per lookup with Acceptor::badge and treats it as opaque bytes. A lookup on an unbadged connection is refused ERR_PERMISSION_DENIED. Any other refusal of the badge read is a kernel fault and panics.

  • The supervisor mints each program its own netstack connector (system/supervisor/src/main.rs, Grants::netstack). The badge is the program's Authority: its row and its session. The connector goes into svc. The shared connector is dropped as soon as main creates the port, as launcher's is. build_namespace never adds the port's own connector.

  • A badge names a grant, not a process. A direct child shares its spawner's view, as it shares its file share. A program started through launcher gets a view of its own.

  • Once netstack has ended for good, its kept acceptors are cleared and a program started after that is minted no netstack. NetstackConn::connect already answers both NotEndowed and ServerGone as NetstackNotFound. Only inspect's message changes: "holds no netstack connector" instead of "its port is closed".

  • Boot hands every service its acceptors before it starts any (Supervisor::boot, in two passes), so what a start mints on another service's port (a role's directories, a partition, netstack) does not depend on the order of [boot] start. The order of spawns, homes and sessions is unchanged. The accepted design minted a program started before netstack from Supervisor::acceptors; this departure was accepted on the pull request (above).

  • The build refuses the three manifests the mint cannot serve (build/build.rs, receives_reach_a_running_server, new in round 2):

    • a row receiving netstack where no row [boot] start names serves it, which the mint would endow nothing;
    • a row providing netstack (round 3): the supervisor never takes netstack from a provider (build_namespace), and a provider let the first shape through;
    • netstack's own row receiving it, whose start would lock the Kept its own Service::spawn holds.

    The gate is checked on every manifest rendered, a metal arm's derived config included. The supervisor panics, naming the row, if a manifest reaches it anyway.

What is kept, and for how long (toyos-dns)

  • One clock per lookup (round 2's change 1). Every TTL a lookup reads, including the SOA's, counts from its first query's send. A retried query or an alias asked again cannot stretch what an earlier reply said.
  • A positive answer is kept for the least TTL among the aliases followed and the address RRset. An RRset whose TTLs differ is read at its lowest (RFC 2181 §5.2).
  • A negative answer (RFC 2308 §2.1, §2.2) is kept for the least of its aliases' TTLs and the SOA's TTL and MINIMUM (§5). The SOA counts only if it sits in the authority section and its owner is the denied name or one of its ancestors. For NXDOMAIN the denied name is the end of the reply's own alias chain.
    • A negative answer with no SOA keeps nothing.
    • A malformed SOA drops the whole reply, as a malformed CNAME does: two names, then exactly five 32-bit numbers.
  • TTLs are read as RFC 8767 §4 has them (change 4): unsigned, and capped at KEEP_CAP_S = 3600 s. A TTL of zero anywhere in what was read keeps nothing.
  • Nothing is kept under any name but the one asked. That covers alias targets, additional records and authority records.
  • Failures are never kept, each of the five named in Cache::keep's match, so a variant added later fails to compile until someone decides whether it is kept (round 2).
  • Bounds:
    • A view holds at most VIEW_ENTRIES = 128 names, and the cache at most MAX_VIEWS = 32 views.
    • A full view drops its expired entries first, then its least recently used entry. No program's lookup takes another program's entry.
    • With MAX_VIEWS held, a new view replaces a view whose entries have all expired, and otherwise the least recently used view, live entries included. That last case is the recorded channel (issues/netstack-replaces-one-programs-cached-answers-to-keep-anothers.md, which cites the live-view test, change 5).
  • An emptying is a new generation. An answer whose lookup started under an older generation is handed to its client and not kept.
  • Lookup::keep_until says until when an ended lookup's answer may be given. Step::Done is unchanged.

The cache lives and dies with the held lease (toyos-net-node)

  • lease.rs keeps the lease state and the cache in one type, network::Network. Its fields are private to that module, and the state is written only by Network::set, which empties the cache as it writes (change 6, made true by type). Neither lease nor lease::tcp can write the state without emptying the cache.
  • Every write empties it: hold, renew (Extended included), release, probe and taken.
  • Stack::link empties it when the link goes down. The held lease stays across the link-down.
  • Node::resolve takes the view and answers a kept name in the call (Resolving::Kept). It holds no lookup slot and sends nothing. Otherwise it starts a lookup carrying the generation, and an ended lookup's answer is kept before it is handed over.

The log lines

serve.rs's two lines for a failed lookup no longer name the name (round 1's NOTE), and Sockets::lookups no longer holds it. Nothing netstack logs or answers inspect names a lookup or anything kept.

Round 3: build round 2's review

Round 2: the build review's findings

  • BLOCKER, the two-pass boot built without acceptance: closed by the orchestrator's acceptance on the pull request (6129668081).
  • NOTE, a netstack row that receives netstack locks its own Kept: refused by name when the manifest is rendered: "netstack receives netstack, which it serves itself".
  • NOTE, a row receiving netstack in an image that serves none is endowed nothing: refused when the manifest is rendered: "<row> receives netstack, which no row in [boot] start serves and no row provides". The test-only receives_have_providers and apps_receive_a_served_name became that one render gate, tightened two ways: a served name counts only where a row [boot] start names serves it, since a launch starts no row that serves (starts_name_what_a_launch_can_start), and a row never receives a port it serves. [apps] receives gets the same rule, without provides names. Every committed config passes it (every_config_renders_its_manifest).
  • NOTE, Err(_) => return in Cache::keep: the five failures are named.
  • NOTE, the isolation issue's exit covers only a second row: issues/nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program.md now says the T14 read only the row half, and its exit asks a second session of the same row too.
  • NOTE, the body and the judges' exits: the T14 reading is below, each judge beside its exit code.
  • NOTE, the bimodal accepted→read delay: filed as kind: finding, issues/the-step-from-netstacks-accept-to-a-whole-request-is-bimodal.md, with the step as the stamps bound it, its exit and an owner.

Gates at d6e5a33f0

Every log is whole, with the home directory masked as <home>, at evidence 6c137e49.

  • cargo run -- --ci host: EXIT=0 (command and exit), "Host: 76 step(s), all green" (ci-host.log L8901). It holds a_received_name_reaches_a_server_that_runs_and_never_its_own and every_config_renders_its_manifest (L35, L44), toyos-dns's 78 tests (L2101) and the node's tests/resolve.rs, 42 tests (L3738). Its FAILED lines are the loom suite's negative controls, as before.
  • Round 2's, at 75cf86188, not run again: since then the tree changed only by the merge's .claude/agents/orchestrator.md, the gate's new refusal, which every committed config passes (every_config_renders_its_manifest, above), and the finding's text; git diff 75cf86188 d6e5a33f0 -- system kernel sdk is empty. Their logs are at evidence 5136bed6.
    • cargo test --test toyos-build, the whole QEMU suite on both architectures: EXIT=0, "63 passed, 63 total" (guest-suite.log L609). Every one of its boots renders its manifest through the gate and runs the supervisor's two-pass boot. netstack_streams (L203), netstack_socket_churn (L205), netstack_streams_e1000e (L212) and libc_sockets (L232), which holds nodelay_accepted, PASS; their test-runner's netstack is a minted one.
    • cargo run -- --build-only: EXIT=0 on x86_64 (log) and on aarch64 (--arch aarch64, log).

Checks

Host tests are the cheapest tier for every decision.

  • toyos-dns, 16 new tests:
    • the least TTL of aliases and differing addresses;
    • every TTL counted from the first query's send, across an alias asked again and across a query sent again (both of round 2's cases);
    • a zero TTL anywhere keeps nothing;
    • RFC 8767's unsigned reading and the hour cap, 0x8000_0000 and the real .invalid reply's 86 400 s included;
    • NXDOMAIN and NODATA kept for the least of the SOA's TTL and MINIMUM and the aliases;
    • no SOA, another zone's SOA or a deeper name's SOA keeps nothing;
    • an SOA's RDATA checked to the byte;
    • a deadline answered before it and not at it;
    • no view answered from another's entry;
    • each of the five failures never kept, and both absences kept;
    • a lookup started before an emptying keeps nothing;
    • a full view drops its own LRU entry and no other view's, and its expired entries before a live one;
    • with MAX_VIEWS held, an all-expired view replaced before a live one, and with none expired the LRU live view replaced (split, change 5).
  • toyos-net-node tests/resolve.rs, 9 new tests. Every frame is read by etherparse, and every reply and SOA is spelled by hand from RFC 1035 §4.1:
    • a kept name is answered in the call, an hour less a second after it was asked, and sends no frame;
    • a kept answer is given while every lookup place is held;
    • the same name for another view sends its own query;
    • a link down and up with every INIT-REBOOT unanswered, the client keeping the lease in use, empties the cache;
    • a lease lost and a new one naming the same resolver empties it;
    • a renewal with the same terms (Extended) empties it;
    • a lookup in flight across a link-down is answered and keeps nothing;
    • an absence is kept where an SOA says how long, and asked again where none does;
    • nothing is kept under the alias target or under another name in the reply.
  • toyos-build, a_received_name_reaches_a_server_that_runs_and_never_its_own (round 2; its provides case, round 3): one config holding every accepted kind of receive renders, and each refusal has a config that takes it and nothing else, its message asserted.

Mutations, round 1. Eighteen checked patches against 804ccd7fc: the design's nine, and nine more where a defect would otherwise land unseen. Each was shown to build, its named test was run (the run checks the test ran and ran alone), and the tree was restored. All 22 test runs are red, each EXIT=101 (mutations.log L927–L949; the patches, plan.json and the scripts are beside it). Since 804ccd7fc the code they patch changed only in Cache::keep's failure arm (git diff 804ccd7fc d6e5a33f0 -- system/netstack/dns system/netstack/node/src), and a_failure_is_never_kept_and_an_absence_is asserts each of the five failures.

mutation red
cache keyed by name alone, view ignored a_view_is_never_answered_from_another_views_entry, the_same_name_for_another_view_sends_its_own_query
least TTL taken as the largest rfc2181_5_2_…differing_addresses
every TTL counted from the last query's send both every_ttl_counts_from_the_first_querys_send_…
emptying out of the lease setter a_lease_lost_…, a_renewal_with_the_same_terms_…
emptying out of Stack::link a_link_that_goes_down_…
generation check out of the keep the_answer_of_a_lookup_started_before_an_emptying_is_not_kept, a_lookup_in_flight_as_the_link_goes_down_…
expired view no longer preferred a_new_view_replaces_one_whose_entries_have_all_expired_before_a_live_one
alias target kept under its own name nothing_is_kept_under_a_name_the_view_did_not_ask
SOA accepted from any zone rfc2308_5_an_absence_with_no_soa_or_with_one_outside_…
a kept answer asked only once a place is free a_kept_answer_is_given_while_every_lookup_place_is_held
expired entries no longer dropped first a_full_view_drops_its_expired_entries_before_a_live_one
zero TTL given a deadline rfc1035_4_1_3_a_ttl_of_zero_…
high-order bit read as zero (RFC 2181 §8) rfc8767_4_…
no hour cap rfc8767_4_…
SOA's MINIMUM not read rfc2308_5_an_absence_is_kept_for_the_least_…
a failure kept a_failure_is_never_kept_and_an_absence_is
a full view drops another view's entry a_full_view_drops_its_own_least_recently_used_entry_and_no_other_views
an answer given at its deadline an_answer_is_given_before_its_deadline_and_not_at_it

Mutations, round 2. Six checked patches of the render gate against 75cf86188, each applied, built, a_received_name_reaches_a_server_that_runs_and_never_its_own run alone and the tree restored by the same script. The unmutated run is EXIT=0, and all six are red, EXIT=101 (mutations.log; the patches, the script and each run's log beside it). Mutations 1 and 2 are the two shapes the review named passing as they did before.

mutation red on
a row may receive what it serves "netstack receives netstack, which it serves itself"
any declared row's serves counts, started or not "host receives netstack, which no row in [boot] start serves"
[apps] receives unchecked "[apps] receives names ghost"
a row's provides no longer counts the accepted config refused
[apps] takes a provides name "[apps] receives names surface"
the supervisor's ports no longer count the accepted config refused

Mutation, round 3. One checked patch against 0fb70e840, the gate's commit, by the same script: the provides refusal deleted. The unmutated run is EXIT=0, and mutation 7 is red, EXIT=101: the review's shape, "relay provides netstack" with host receiving it, answers Ok(()) (mutations.log, m7's log; the patch and the script beside them). The script left the tree clean.

Negative control, round 1. The whole change's code was reverted onto its base 867ec23ed. The node's tests were kept at the head, adapted to the base's API by a test-side shim alone, and run, and then the tree was restored (negative.log, EXIT=101 at L266).

  • 8 of the 9 new tests red, including K2's (a_name_its_view_keeps_is_answered_in_the_call_and_sends_nothing) and K4's (the_same_name_for_another_view_sends_its_own_query, through its premise that the view keeps the answer).
  • The 33 earlier tests are green.
  • The ninth, a_lookup_in_flight_…keeps_nothing, holds trivially on a base that keeps nothing. It guards the generation, and mutation 6 reds it.

Independent oracles:

  • the RFCs, quoted from rfc-editor.org's copies: RFC 1035 §4.1.3 and §3.3.13, RFC 2181 §5.2 and §5.4.1, RFC 2308 §5, RFC 8767 §4 and RFC 6761 §6.4;
  • the four real Cloudflare replies toyos-dns already carried, whose TTLs read by hand (289/9, 719, 3587/887/7, and SOA 86 400/86 400) are now asserted through read;
  • the T14, below.

No new guest test. The one thing no host test reaches is the wiring: the supervisor minting a distinct badge per row and session, and netstack keying the view by it. A guest test of it needs a resolver the harness answers. Under slirp the lease's resolver forwards to the host's, which reaches the internet. The -netdev socket that would let the harness answer is issues/there-is-no-network-gate.md's, and issues/nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program.md records the gap. The render gate is a host test: it decides from the config alone.

No new dependency.

The T14 measurement

The orchestrator booted the three images of request.txt in one T14 session (the run): BASE 89a2dd00f, the negative control without the cache; CACHE 703f474c6, the head 804ccd7fc with the scout's and the cache's measurement stamps; and TESTCASES, the head's shared testcases boot. Every boot returned rc 0, and line 2 of each flash log hashes the image as staged. Its readbacks, flash logs and the orchestrator's judge logs are whole and masked at evidence a370c0a9.

The judges, each beside its exit code. Those logs record no command or exit, so each judge was run again over the same readbacks, from the tree that staged its image, touching no machine (commands and exits):

judge tree command exit log
BASE 89a2dd00f cargo test --test toyos-build -- --metal --metal-readback <dir>/base internet_download EXIT=0 judge-base.log
CACHE 703f474c6 cargo test --test toyos-build -- --metal --metal-readback <dir>/cache internet_download EXIT=0 judge-cache.log
TESTCASES 804ccd7fc cargo test --test toyos-build -- --metal --metal-readback <dir>/testcases endowment_denied pipe_flag_forgery netstack_gone_mid_bind EXIT=0 judge-testcases.log

Each re-judged log differs from the orchestrator's only in cargo's build lines and in the record's list of numbers the run did not measure, which gained six rows since (diff-base, diff-cache, diff-testcases). The line numbers below are the re-judged logs'.

The rows, as the orchestrator's ruling has them: K2 and K4 are gates; G, K1 and K3 are reported, never gated. M is a figure a judge printed, D one derived from M figures; times in ms.

row BASE CACHE verdict
K2, gate: at most 2 of the 6 fetches' lookups send a query 6 of 6 M (L1214) 0 of 6, 0 with no lookup stamp M (L1239) holds
K4, gate: host exits 0; between the brackets one row=host lookup that sent a query; each bracket one row=test-runner lookup that sent none not read: BASE has no badge, so no row, and its judge prints "K4 does not hold" as request.txt foresaw (L1272–L1277) host exit=0; k4_a test-runner query=no; k4_host host query=yes keep_s=38; k4_b test-runner query=no; "K4 holds" M (L1285–L1290) holds
G, reported: the uncached span's median at most BASE's + 0.017 0.064 M (L1271) 0.068 M (L1284) +0.004 D, inside the bound of 0.081 D
K1, reported: the median lookup (J2−J1) over the six fetches 7.149 M (L1217) 0.473, range 0.365–0.617 M (L1242) under the design's 1.0
K3, reported: the hits' median beside BASE's B−(R+W+A) 0.381 M (L1215) 0.473 M (L1242) +0.092 D, none of it the cache's (below)

The tripwires stand:

  • E1: both arms PASS internet_download (BASE L1278, CACHE L1291), six whole downloads of the same hash each.
  • E2: in BASE all 228 lookup stamps read clean=1 and queries_between=1; in CACHE all six fetches are stamped and every miss read clean=1 (lookups-base.txt, lookups-cache.txt).
  • E3: both arms read "uncached lookups: 10, of which 10 sent a query" (BASE L1267, CACHE L1280).
  • The timings bug guard: each judge reads "0 past its record" (BASE L1282, CACHE L1295, TESTCASES L26).
  • TESTCASES: endowment_denied, netstack_gone_mid_bind and pipe_flag_forgery each end exit=0 (L428, L442, L450), and the judge reads "3 passed, 0 failed" (L28).

K2: the answers come from the cache. CACHE's first lookup of the file's host, in phase probes, missed, sent one clean query and read keep_s=54 off the T14's own reply. The entry lasted until that query's send plus 54 s, 63.25 s on the stamp clock D, and the six fetches looked the host up between 13.67 s and 23.39 s M: all six were hits. Over the boot, 12 of 253 lookups sent a query: that first one, the ten uncached names by design, and host's. The other 241 were row=test-runner hits. In BASE all 228 sent one.

K4: the isolation held. host's lookup, row=host, sent a query at 25.108 s between bracket A's hit at 25.105 s and bracket B's at 25.117 s, both row=test-runner, and was the only stamp between them M (L371–L377). The job's entry was live on both sides: it ran to 63.25 s D, bracket B hit after host's query, and host's reply read keep_s=38, which is 54 less the 15.856 s between the two reads D. So the view host was minted held none of test-runner's answers. host ran in test-runner's session, so this reads the row half of the badge; the session half is the isolation issue's exit.

A repeated name now takes 0.473 ms from the job's request to its answer, against the 6.313 ms wire lookup #861's analysis measured and this session's BASE 7.149 ms, of which 6.591 was the wire. The median tcp (J4−J0) went from 23.532 to 17.364 M (BASE L1219, CACHE L1244).

G and K3, reported. The badge read, the miss and the keep together cost the uncached span +0.004 D. The 0.092 D by which the hits' median exceeds BASE's reference lies in accepted→read, the step before netstack holds the request: netstack's own work in a hit, from the request read whole to the answer, has a median of 5.3 µs over the 241 hits D. That step is bimodal in both arms (0.0285–0.2220 ms on 474 of 481 stamps, 0.3586–0.4056 ms on seven: accread-base.txt, accread-cache.txt), now issues/the-step-from-netstacks-accept-to-a-whole-request-is-bimodal.md.

What the merge changed of what the T14 measured. The arms already ran #861's netstack: the scout d26de3a19 merged into both carries #861's head 443fb5d10, and git diff 443fb5d10 ab655a115 -- system/netstack is empty. The cache arm's netstack and supervisor differ from this head only by the measurement stamps and round 2's two changes, neither of which an image the build renders can tell apart (git diff 703f474c6 d6e5a33f0 -- system/netstack system/supervisor). What the T14 ran and this head does not is the kernel before #871, which saves user FP/vector state only at a switch instead of at every system call. K2 and K4 read the cache's keying and the supervisor's mint, which #871 does not touch. K1, K3, G and the finding's split are times through system calls #871 changed, so they are figures of the T14's tree, not of this head.

Size

git diff --shortstat origin/main...d6e5a33f0: 15 files, +1308 −246.

  • Production: +568 −132, net +436. Round 3's part is the gate's provides refusal (+4 net in build/build.rs). Round 2's part is the render gate (+32 −1), the supervisor's panic (+9 −1) and the named failures (+1 −1). The rest is round 1's: most of it toyos-dns (+291 −45), the SOA reader, the chain helper, one clock, and Cache with its bounds and generation.
  • Tests: +598 −114. Round 3's provides case is +4. Round 2's gate test replaced two test-only checks and their tests (+29 −60).
  • Issues: +142.

Roast

  1. It narrows "shared by the whole machine". The owner accepted this (ruling). Sharing among one row and session's processes is the only round trip netstack's cache saves that a per-process cache would not.
  2. It reaches past netstack. The supervisor boots in two passes so every service holds its acceptors before any start, and the build now refuses a received name no started row serves, for every port and not for netstack alone, and any row that provides netstack. No committed config had either refused shape.
  3. It adds the MAX_VIEWS channel, recorded in the issue above.
  4. The reader grows at the trust boundary by the TTL, the SOA and an NXDOMAIN reply's chain, while issues/toyos-dns-decodes-what-a-published-crate-decodes.md asks whether this reader should be hickory-proto's.
  5. A forged reply that passes every check lasts. It lasts until the earliest of its TTL, the hour and the next emptying, in one view.
  6. A changed record is served stale for up to its TTL.
  7. Every lease write and every link-down empties every view, an extended renewal from the same server included.
  8. Negative caching hides a new name for up to an hour. The T14's resolver answered .invalid with SOAs that keep it 7 to 562 s.
  9. Failures are not kept.
  10. 128 names a view, 32 views and one hour are judgements.
  11. Nothing guards the wiring after the T14 read it, until the network gate exists, and nothing has read the session half of the badge.
  12. The T14's timings predate User FP/vector state is saved only at a switch, the preempt count drops lock, and the build refuses stray vector instructions #871's kernel, so K1, K3 and G are not re-read at this head; they were reported, never gated.

Unsure

🤖 Generated with Claude Code

https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Owner's ruling, recorded by the orchestrator. He was shown this shape: one cache in netstack, each answer served only to the program whose lookup fetched it, each program's first lookup of a name still paying the round trip, and the supervisor minting netstack's connector per program in this same PR. He accepted it: "Per-program answers". The design review running now decides the rest. The build starts once that review's named changes are made and accepted.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 1, at b67eac552. Net lines: 0 (git diff --shortstat origin/main...b67eac552 is empty). The body estimates about 250 production lines and 400 test lines.

Rulings on the brief's questions

  • Isolation. Keeping each program's answers to itself is what "isolation is non-negotiable" requires. §1's table holds: the time A saves on an answer B fetched is exactly what A learns about B. The padded variant is rightly refused, because it is a second path that still leaks. But it narrows a ruling the owner gave after he had read option (a) of LAN stage D and B: netstack wakes only on a change, and the I219 takes a 1,000-descriptor ring at a fixed 131 µs throttle #861's scout comment: "the isolation principle decides whether that is refused, partitioned per client, or accepted". So roast 1 is his call. Once answers are per program, netstack's only remaining edge over the scout's option (b), a per-process cache with no shared state, is that processes of one row and session share. He should rule with that in view.

  • Identity. The badge is stamped by the kernel. Only a holder of READ on the acceptor can mint one (kernel/src/syscall/ipc.rs:316), and it is the same Authority that launcher and grants already read. netstack treats it as opaque, so toyos-manifest stays out of netstack. The badge names a grant, not a process (sdk/toyos/src/port.rs:44-46). svc gives it to every direct spawn (system/supervisor/src/main.rs:2871-2873), so a direct child shares its spawner's view, as it shares its spawner's file share. That is also why the scout job's view is test-runner's. The body says none of this.

  • The remaining leaks.

    • The upstream resolver's cache can be recorded but not fixed by a stub.
    • The lookup slots are already recorded (issues/netstack-lookup-slots-have-no-per-client-share.md).
    • The MAX_VIEWS channel is new and unrecorded (BLOCKER 2).
    • The log lines can be fixed in two lines (NOTE).
  • RFCs. These hold:

    • TTL: the smallest along the alias chain; a TTL of 0 is not kept; a TTL with its most significant bit set counts as zero.
    • Negative answers: RFC 2308 §5's smaller of the SOA's TTL and MINIMUM, also capped by the aliases' TTLs. Nothing is kept without an SOA, and the SOA's owner must be the denied name or one of its ancestors.
    • Nothing is kept under any name but the one asked.
    • RFC 9520 addresses the resolver that iterates to authoritative servers, which is the lease's resolver, so roast 7 stands.

    BLOCKER 6 and a NOTE cover the two gaps.

  • Bounds. Counting entries rather than bytes is right. An entry's size is bounded by its name (255 bytes) and by MAX_ANSWERED, so the count bounds the bytes. I checked the four real replies' TTLs against the committed bytes in dns/src/tests.rs: 289/9, 719, 3587/887/7, and 86 400 for both the SOA's TTL and its MINIMUM. All match the body.

  • Kill conditions. K1 is a valid kill line, and K2 is valid as "until explained". K3 and K4 are below.

  • The supervisor change belongs in this PR. Without it, every lookup is refused (the connection carries no badge) or the cache is keyed on nothing. Landed apart from the cache, it would mint a badge that nothing reads. It lands with the work that needs it, as an ABI change does.

BLOCKER

  1. PR body §1, roast 1. The design narrows the owner's ruling, and only he can accept that. The build starts only once his word on roast 1 is recorded on this PR.

  2. PR body §3 ("No program's lookup ever evicts another program's entry") and §1 ("Each was there before it"). When MAX_VIEWS are held, a new program's first lookup replaces another program's view, which may still hold live entries. That program then sees its next lookup miss and learns that others have looked names up. The design adds this channel, and §3's next bullet contradicts the one quoted. No bounded cache avoids it when its tenants can be multiplied: every ssh login opens a session. So it must be recorded in an issue with evidence, an owner and an exit. Correct both claims in the body, and replace a view whose entries have all expired before a view with live entries, since dropping an expired view reveals nothing.

  3. PR body §5. Coalescing saves no time: two concurrent misses for one name wait the same round trip whether or not they share one. It only spares upstream queries, and no measurement asks for that. In exchange it brings a waiter list, a last-waiter rule, two node tests and a mutation. Delete §5. Two concurrent misses in one view each send a query, as today, and the later answer replaces the earlier.

  4. PR body K4, roast 10. K4 cannot run as written. At d26de3a19, tests/images/downloadcase.toml gives test-runner no starts, and the image has no host row. Nor can K4 tell isolation from expiry: the 60 s address TTL counts down from an unknown point, so a host miss after the job's own entry expired would pass K4. Required:

    • Put a host row and starts = ["host"] on test-runner in the scout image. test-runner already hands a job its launcher (tests/guest/test-runner/src/main.rs:261-265).
    • Bracket host's lookup between two of the job's own lookups of the same name, both with no n3.
    • Require that host's stamp is present and carries n3.

    "The wiring rests on reading alone" is not an acceptable outcome for the design's central claim.

  5. PR body §4 ("A held lease that is re-verified after the link drops keeps its resolvers, so it keeps its cache"). The lease outlives a move to another network in two ways:

    • When the INIT-REBOOT REQUEST goes unanswered, the client keeps the lease in use (system/netstack/dhcp/src/lib.rs:547-549). RFC 2131 §4.3.2 has a server with no record of the client stay silent, so a move to another network on the same subnet keeps the lease without verifying it.
    • If the lease goes and the new one names the same resolvers, with no lookup in between (L1 → none → L1), the list looks unchanged.

    Either way, one network's answers are served on the next whenever both name the same private resolver, and a negative answer hides a name there for up to an hour. Without a cache, those lookups would ask the new network. The node test listed ("after the lease names other resolvers, or none, the next resolve sends a query") cannot observe the "none" case: with no lease, the resolve is refused NotConnected and sends nothing. Required:

    • Empty the cache when the link goes down and on every change of the held lease, at the event.
    • Add node tests for: a link that goes down and up with the INIT-REBOOT unanswered; and a lost lease followed by a new one naming the same resolver. In each, the next resolve sends a query.
  6. PR body §2 ("The time counts from when the answered query was sent"). When the chain ends in an alias the reply holds no address for, that name is asked again, so the answer spans two queries. If the first reply's alias TTLs are counted from the second query's send, the entry outlives what the first reply said by the time between the two queries. Required: count each reply's TTLs from its own query's send, or every TTL from the first query's send. Add a host test where an alias in the first reply holds the least TTL.

NOTE

  • PR body "Checks": after K4, nothing guards the wiring. If a later change passed a constant view in serve.rs, or minted one netstack badge for every program, no test would turn red. No guest test resolves a name; https_get dials an address. File it, with the exit: "a netcase guest test against a resolver the harness answers (issues/there-is-no-network-gate.md's -netdev socket) shows that a second row's lookup of a name the first row holds sends a query".

  • system/supervisor/src/main.rs:2824-2837, Unsure 1. This can be answered by reading the code. A boot-started netstack's original acceptor stays in its Kept, minting needs READ on it, and Grants::view already mints from kept acceptors the same way. The design must name the three cases where the per-program mint changes behaviour:

    • A program started before netstack: the acceptor is still in self.acceptors.
    • netstack ended without a swap: its acceptors are cleared at :778, so a later program is minted no netstack at all and gets NotFound where today it gets ServerGone.
    • netstack started by a launch (Served::Move): the supervisor keeps no acceptor to mint from.

    The unbadged connector created at :393-402 must be dropped at once, as launcher's is at :439.

  • PR body §2, RFC 2181 §5.2. That section says an RRset with differing TTLs is ignored, as if empty, when its source is not authoritative, and is used at its lowest TTL when it is. "Answer, keep nothing" is neither. Either drop the branch, since the chain's smallest TTL already bounds such a set, or follow the section.

  • PR body §4, "Several interfaces". Under the second branch, one program's lookup through another interface empties every program's views, which is a cross-program channel. Drop the paragraph: it designs for a track this PR is not part of.

  • PR body K3. The text says K3 fires when the badge read and the cache cost more than the 0.143 ms of local steps a hit skips. But its bound (base lookup_local + 0.1, about 0.674 ms) lets them cost 0.243 ms, against a predicted hit of about 0.43 ms. Set the bound at base lookup_local − (read→query + wake + reply→answer) + 0.1, all taken from the same session's base arm.

  • PR body, the prediction ("gives the two aliases 300 s … [M]"). dig-ttl.txt shows the aliases at 245→219 and never 300. It also asked the development host's resolver, not the one the T14's lease names, which on the bench is the provider's public resolver (issues/toyos-records-carry-the-sticks-serial-the-cards-mac-and-the-resolvers-in-full.md:17). If K2 needs explaining, the explanation reads the T14's own replies.

  • system/netstack/src/serve.rs:678 and :682. Both lines name the looked-up name, not just the one the body quotes. Fixing it means removing two {name}s in a file this branch edits. It is inside the fence if the orchestrator puts it there; otherwise it is filed, as the body says.

  • PR body §1, the upstream cache. A stub cannot fix it: any program can send its own RD=0 query to the lease's resolver. Its issue needs an exit a reader can check. That is a question for the owner, because only a per-view iterative resolver removes the channel.

  • The build's evidence. build_namespace builds every program's namespace, so the supervisor change reaches every boot. The build's body must show the whole guest suite green alongside cargo run -- --ci host, and name the netstack guest tests among them: netstack_socket_churn, netstack_streams, nodelay_accepted, netstack_gone_mid_bind, pipe_flag_forgery, endowment_denied.

DESIGN NEEDS CHANGES: the owner's word on roast 1, recorded on this PR; the MAX_VIEWS channel recorded, the body's two claims about it corrected, and an expired view replaced before a live one; §5 coalescing deleted; K4 made runnable and bracketed by the job's own hits; the cache emptied on link-down and on every change of the held lease, with both node tests; the two-reply deadline specified, with its host test; and the NOTEs fixed before the build lands.

Japabu added a commit that referenced this pull request Oct 12, 2026
…sign that leaves it

Three issues the design of #883 owes, written as the tree stands once its
build lands, which is the only way they reach main:

- netstack-replaces-one-programs-cached-answers-to-keep-anothers: when
  MAX_VIEWS views hold live answers, a new program's first lookup replaces the
  least recently used view whole, and that program learns a count of others.
  The cache adds this channel; no bound on a pool whose tenants anyone can
  multiply avoids it (the design review's BLOCKER 2).
- the-lease-resolvers-cache-tells-a-program-what-this-machine-asked: a stub
  cannot partition the upstream resolver's cache, and only an iterative
  resolver per view removes the channel, which is the owner's to decide
  (the review's NOTE on §1).
- nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program: after
  the T14's K4 reading, no test guards the badge wiring; the exit is the one
  the review named (its NOTE on Checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu Japabu changed the title netstack keeps each program's DNS answers for their TTL, in one cache for the machine netstack keeps each program's DNS answers for their TTL, in one cache for the machine that empties with the lease Oct 12, 2026
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 2 at 9c2aec980: each finding of the round-1 review and its answer. The body is rewritten as the revised design and roast. The one commit with content adds three issue files. The build lands them with the cache, and no code changed.

BLOCKER

  1. The owner's word on roast 1. It is recorded at the ruling and cited at the body's top and in roast 1. Roast 1 now carries the review's point. With answers per program, sharing among the processes of one row and session is the only round trip netstack's cache saves that LAN stage D and B: netstack wakes only on a change, and the I219 takes a 1,000-descriptor ring at a fixed 131 µs throttle #861's option (b) would not. Beyond that it buys one bound and an emptying at the link and lease events only netstack sees.
  2. MAX_VIEWS.
    • Recorded as issues/netstack-replaces-one-programs-cached-answers-to-keep-anothers.md, an open defect:
      • evidence: the toyos-dns host test the build adds under the name the issue gives;
      • owner: whoever holds the network-stack track;
      • exit: a view charged to the program it serves, and a node test in which MAX_VIEWS + 1 programs keep answers and the first program's next lookup sends no query.
    • Both claims corrected. §3 now says that across views one program's lookup does replace another's answers. §1's "What remains open" now says two channels were there before the cache and one is the cache's own.
    • The replacement order is a view whose entries have all expired first, then the least recently used view. It has a host test and a mutation.
  3. §5 coalescing is deleted. Two concurrent misses in one view each send a query, as today, and the later answer replaces the earlier. Its two node tests and its mutation are gone.
  4. K4 is runnable and bracketed.
    • The scout image gets a host row (receives = ["netstack"]) and starts = ["host"] on test-runner. The job runs /system/bin/host through std's Command, which the launcher starts as the host row.
    • The steps are a warm-up lookup, bracket A, host, then bracket B.
    • K4 requires that host exits 0, that brackets A and B name test-runner's row with no n3, and that exactly one stamp between them names host and carries n3. Every lookup stamp now names its view's row.
    • Round 1's roast 10 ("the wiring rests on reading alone") is gone.
  5. §4 is rewritten. The cache lives and dies with the held lease.
    • It sits in lease.rs's Stack. Every write of the lease state goes through one setter that empties it: hold, renew (Extended included), release, probe and taken.
    • Stack::link empties it when the link goes down.
    • A lookup carries the cache's generation, and an answer from before an emptying is handed over, not kept.
    • Extended is included because an INIT-REBOOT that a new network's server answers with the same parameters is Config::Extended (renewed, system/netstack/dhcp/src/lib.rs).
    • Node tests:
      • link down and up with the INIT-REBOOT unanswered, then a query;
      • lease lost, then a new one naming the same resolver, then a query;
      • an Extended renewal, then a query;
      • a lookup in flight across a link-down keeps nothing.
    • The "or none" test is dropped, for the review's reason.
  6. The reply clock. Each reply's deadline is its own query's send plus the least TTL among the records read from it. The entry's deadline is the earliest over the replies. A chain can span up to MAX_ALIASES + 1 replies, not only two. The host test is a two-reply chain whose first reply's alias holds the least TTL: it ends at the first query's send plus that TTL. The mutation counts every reply from the last query's send.

NOTE

  • The wiring after K4. It is filed as issues/nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program.md, a tooling issue with the review's exit verbatim, owned through issues/there-is-no-network-gate.md.

  • The supervisor and round 1's Unsure 1. This is answered by reading, and the three cases are named under "Where it lives":

    • (1) a program started before netstack is minted from Supervisor::acceptors, and nothing changes;
    • (2) after netstack has ended for good, a program is minted none. NetstackConn::connect maps NotEndowed and ServerGone alike to NetstackNotFound, so std, libc and toyos::net see no change. inspect's message does change, and roast 2 names it;
    • (3) cannot happen: starts_name_what_a_launch_can_start refuses a starts naming a row that serves a port, and a package's row serves nothing.

    The unbadged connector is dropped at once, as launcher's is.

  • RFC 2181 §5.2. The branch is dropped. The section has a client that sends every query to particular servers treat them as authoritative "for this purpose", and an authoritative RRset with differing TTLs read at its lowest. The least-TTL rule already takes that.

  • "Several interfaces" is dropped.

  • K3's bound is now B − (R + W + A) + 0.1 ms, all from the same session's base arm: 0.531 ms at d26de3a's values.

  • The prediction. The "300 s" claim is gone. 245→219 s and 5→60 s are stated as the development host's resolver's, which is not the T14's. K2's explanation reads the TTLs the cache arm's stamps record from the T14's own replies.

  • serve.rs:678 and :682 are in this PR's fence. The build removes {name} from both lines, and the name from Sockets::lookups. They are no longer listed as a remaining channel.

  • The upstream cache. It is filed as issues/the-lease-resolvers-cache-tells-a-program-what-this-machine-asked.md, a question with status owner. Its exit is the owner's answer. That answer makes the file a track whose exit is a node test with no query to the lease's resolver, or a defect that stays open.

  • The build's evidence. The body commits to the whole guest suite beside cargo run -- --ci host, and names netstack_socket_churn, netstack_streams, nodelay_accepted, netstack_gone_mid_bind, pipe_flag_forgery and endowment_denied.

The review's rulings, carried into the body. §1 now says the badge names a grant and not a process, so a direct child shares its spawner's view and test-runner's jobs share test-runner's. §2 and roast 9 take the review's reading of RFC 9520.

Evidence. This round adds no measurement, so nothing new went to ToyOSOrg/evidence pr-883. The new content is read from the tree at 528b9a6c2, at the paths cited, and RFC text is quoted from rfc-editor.org's copies.

Gates. cargo run -- --ci host at 9c2aec980: EXIT=0, "Host: 73 step(s), all green" (ci-host.log L10000–L10001, the log whole with the home directory masked; its FAILED lines are the suite's negative controls, each "1 verdict(s) reached"). No guest test ran, because nothing a guest runs changed.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 2, at 9c2aec980. Net lines (git diff --shortstat origin/main...9c2aec980): 3 files, +94 −0, all under issues/; production 0, tests 0. During the round origin/main moved to 867ec23ed (#867, #878). The merge-base is still 528b9a6c2, and neither PR touches a file the design cites. Evidence: cargo run -- --ci host at 9c2aec980, EXIT=0, "Host: 73 step(s), all green" (ci-host.log L10000–L10001; line 1 names the head). Three issue files reach no guest test.

Round 1's BLOCKERs

  1. The owner's word on roast 1: CLOSED. "Per-program answers" is recorded on this PR (ruling) and cited at the body's top and in roast 1. Roast 1 now carries round 1's point about option (b).

  2. MAX_VIEWS: CLOSED.

    • issues/netstack-replaces-one-programs-cached-answers-to-keep-anothers.md is at the head: an open defect with evidence, an owner and an exit a node test can read.
    • §1 and §3 no longer say that no program evicts another's answers.
    • §3 replaces a view whose entries have all expired before a live one, with a host test and a mutation.
  3. Coalescing: CLOSED. §5 sends two queries for two concurrent misses. The waiter list, its node tests and its mutation are gone from Checks.

  4. K4: CLOSED. The step gives the scout image a host row and starts = ["host"], brackets host's lookup between two of the job's own hits, and requires host's stamp with n3. It runs as written:

    • std routes a Command that endows nothing through the launcher it holds (sdk/std/sys/process.rs:349-363);
    • test-runner hands its job that launcher (tests/guest/test-runner/src/main.rs:259-271);
    • /system/bin/host resolves through std (system/host/src/main.rs).

    Without the row, the launch answers NotDeclared and std spawns host directly (sdk/std/sys/process.rs:613). host then shares test-runner's view, no stamp names host, and K4 fails loudly.

  5. Lease and link: CLOSED. §4 empties the cache at every lease write (hold, renew including Extended, release, probe, taken) and at link-down, and each lookup carries a generation. Each node test isolates one emptying. After an unanswered INIT-REBOOT, run_lease before T1 returns Output::default() (system/netstack/dhcp/src/lib.rs:478-493, :547-549). Nothing writes the lease there, so only Stack::link's emptying can make that test send a query.

  6. The two-reply deadline: CLOSED as asked. Each reply counts from its own query's send, with the two-reply host test and the last-send mutation. A gap in the same rule is the BLOCKER below.

Round 1's NOTEs: all taken.

  • The wiring issue is filed with round 1's exit.
  • The supervisor's three cases are named and check against sdk/toyos/src/net.rs:321-326, system/inspect/src/lib.rs:27-31 and build/build.rs:678.
  • RFC 2181 §5.2's branch is gone, and the body's reading matches the RFC's text.
  • "Several interfaces" is gone.
  • K3's bound and the prediction are fixed.
  • The build removes the name from both log lines.
  • The upstream cache is filed as a question with status owner.
  • The build's evidence names the guest suite.

BLOCKER

  • PR body §2 and Checks; system/netstack/dns/src/lib.rs:653-657 — the reply clock's retried query has no test.
    • The design says "each reply's deadline is the send of the query it answered". But on_datagram reads a reply to any query still in sent, so the first query's reply can be read after a retry or the next resolver's query has left. Queries go out one per WAIT_MS, up to ROUNDS × 3 resolvers.
    • No listed test sends a second query for one name before the first query's reply arrives. So the patch "take the deadline from self.sent.last()'s send instead of self.sent[which]'s" passes every listed test, while the entry outlives its reply by up to 16 s.
    • Required, one of:
      • count every TTL from the lookup's first query's send: one time per lookup instead of one per query, which closes this case and the two-reply case by construction;
      • or add the host test "a reply to the first query, read after the second was sent, ends at the first query's send plus its TTL", with that mutation.
    • The negative answer's SOA deadline follows the same rule.

NOTE

  • PR body, K1 — owner's ruling of today: a repeated lookup's median under 1.0 ms becomes a reported figure. "Dead" and "the branch closes and nothing lands" go.

  • PR body, K3 — owner's ruling: a hit's own cost becomes a reported figure. Its bound and "does not land until explained" go, and Unsure's port_badge line moves to the check below.

  • PR body, kill conditions — owner's ruling: a no-regression gate for the badge read on uncached lookups, written before the run. It names:

    • the uncached lookups: the same in both arms, and at least as many as the base arm's six. As planned, the cache arm has two or three misses (the job's first lookup, host's, an expiry), too few for a median;
    • the span that holds the badge read and the keep: read->query and reply->answer, or lookup_local whole;
    • the base-arm figure from the same session that it is held to;
    • a bound derived before the run and never read off the measurement (tests/CLAUDE.md).

    K2 and K4 stay gates as written.

  • PR body §2 and Checks ("a zero or MSB-set TTL keeps nothing") — RFC 8767 §4 is Standards Track and "Updates: 1034, 1035, 2181". It reads a TTL with the high-order bit set as positive and clamps it, where RFC 2181 §8 read it as zero. Under today's ruling the design follows RFC 8767: read the TTL unsigned and clamp it at KEEP_CAP_S. That deletes the MSB branch and its test arm.

  • PR body Checks; issues/netstack-replaces-one-programs-cached-answers-to-keep-anothers.md:27-28 — a_new_view_replaces_an_expired_view_first_and_else_the_least_recently_used makes two claims: a view whose entries have all expired goes before a live one, and with none expired the least recently used goes. Make it two tests, and have the issue cite the one that shows a live view replaced.

  • PR body §4 ("so a new writer cannot skip it") — a private setter does not stop self.state = … anywhere in lease.rs, or in its child lease::tcp, which sees Stack's private fields. Either make it true by type (the state and the cache in one type whose field only the setter writes) or drop the claim.

DESIGN NEEDS CHANGES: count every TTL from the lookup's first query's send, or add a host test and mutation for a reply read after a later query of the same name; make K1 and K3 reported figures; add a no-regression gate on uncached lookups for the badge read, with its lookups, span, base figure and derived bound written before the run; read TTLs as RFC 8767 §4 has them; split the replacement-order test in two; make the lease setter's "cannot skip" true by type or drop it.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design acceptance, by the orchestrator, with round 2's named changes (#883 (comment)). Under the owner's rulings (per-program answers; follow the RFCs and the published state of the art; a gain is reported, never gated), the build is the body's design with these changes:

  1. One clock per lookup. Every TTL, the negative answer's SOA deadline included, counts from the lookup's first query's send. That closes both the retried-query case and the two-reply case by construction.
  2. K1 and K3 become reported figures. Their kill and hold wording goes.
  3. A no-regression gate on uncached lookups for the badge read, written before the run with its lookups, its span, the same-session base figure and a bound derived beforehand. K2 and K4 stay gates.
  4. TTLs are read as RFC 8767 §4 has them: unsigned, clamped at KEEP_CAP_S. The MSB branch and its test arm go.
  5. The replacement-order test splits in two, and the issue cites the live-view one.
  6. The lease setter's "cannot skip" claim is made true by type (the state and the cache in one type whose field only the setter writes) or dropped.

Japabu and others added 2 commits October 12, 2026 15:26
…sign that leaves it

Three issues the design of #883 owes, written as the tree stands once its
build lands, which is the only way they reach main:

- netstack-replaces-one-programs-cached-answers-to-keep-anothers: when
  MAX_VIEWS views hold live answers, a new program's first lookup replaces the
  least recently used view whole, and that program learns a count of others.
  The cache adds this channel; no bound on a pool whose tenants anyone can
  multiply avoids it (the design review's BLOCKER 2).
- the-lease-resolvers-cache-tells-a-program-what-this-machine-asked: a stub
  cannot partition the upstream resolver's cache, and only an iterative
  resolver per view removes the channel, which is the owner's to decide
  (the review's NOTE on §1).
- nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program: after
  the T14's K4 reading, no test guards the badge wiring; the exit is the one
  the review named (its NOTE on Checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
… for the machine that empties with the lease

The design accepted on #883, with round 2's named changes. Every lookup
asked the lease's resolver across the WAN; on the T14 that round trip was
6.31 ms of the 8.65 ms by which ToyOS's TCP setup trailed Ubuntu's (#861's
scout).

toyos-dns: the reader reads each record's TTL unsigned and caps it at an
hour (RFC 8767 §4), and for a negative answer the alias chain of an NXDOMAIN
reply and the authority section's SOA, which counts only where it is owned
by the denied name or an ancestor (RFC 2308 §5). A lookup keeps one clock:
every TTL it reads counts from its first query's send, so neither a retried
query nor an alias asked again stretches what an earlier reply said.
`Cache` keeps an answer per view, under the name asked only, bounded at 128
names a view and 32 views; a full view drops its expired entries and then
its least recently used, and a new view replaces one whose entries have all
expired before the least recently used live one. An emptying is a new
generation, and an answer whose lookup started under an older one is not
kept.

toyos-net-node: the cache sits beside the lease state in one type whose
state only its setter writes, and the setter empties it; a link that goes
down empties it too. `Node::resolve` takes the view, answers a kept name in
the call, holding no lookup place, and keeps an ended lookup's answer.

netstack: a lookup's view is the badge on its connection, read per lookup;
an unbadged connection's lookup is refused ERR_PERMISSION_DENIED. The two
log lines that named a failed lookup's name no longer do.

supervisor: every program whose row receives netstack is minted its own
connector with its row and session; the shared connector is dropped at
once. Boot hands every service its acceptors before it starts any, so a
mint on another service's port does not depend on [boot] start's order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu
Japabu force-pushed the wt/toyos-dnscache branch from 9c2aec9 to 804ccd7 Compare October 12, 2026 14:35
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The orchestrator, on the metal run's gates under the owner's refined ruling (#885, #885 (comment)).

DNS caching with TTLs follows the published state of the art: RFC 1035, 2308 and 8767, and every OS's stub cache. Its bench numbers are therefore reported and never block. The no-regression gate G (base median + 0.017 ms on the uncached span) was written to this round's brief before the owner removed "regresses nothing", so G is reported, not gated.

K2 (answers come from the cache) and K4 (no program reads another's answers) check that the design works, so they stay gates. The metal timings ceiling (2× a machine's record) stays a bug guard, as he ruled.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 run of #883's metal measurement: K2 and K4 hold. G, K1 and K3 are reported.

The orchestrator booted the three images of request.txt in one T14 session:

  • base 89a2dd00f;
  • cache 703f474c6;
  • the testcases boot of the head 804ccd7fc.

Every boot returned rc 0, and every judge exited 0. Line 2 of each flash log hashes the image as it was staged (base, cache, testcases).

Every log is whole and masked, at evidence a370c0a9. It holds the judges' logs, the readbacks' kernel.log, loader.log, boot.txt and verdict.txt, and the flash logs. derived/ holds the script that reads every lookup stamp out of both kernel.logs, with its output.

The rows are applied as the orchestrator's ruling has them: K2 and K4 are gates, and G, K1 and K3 are reported. M marks a figure a boot or its judge printed. D marks a figure derived from M figures. All times are in ms.

row base cache verdict
K2, gate: at most 2 of the 6 fetches' lookups send a query 6 of 6 M (L1210) 0 of 6, 0 with no lookup stamp M (L1235) holds
K4, gate: host exits 0; between the brackets, one row=host lookup that sent a query; each bracket one row=test-runner lookup that sent none not read: base has no badge, so no row (L1268–L1273) host exit=0; k4_a test-runner query=no; k4_host host query=yes keep_s=38; k4_b test-runner query=no. "K4 holds" M (L1281–L1286) holds
G, reported: the median of the uncached span is at most base's + 0.017 0.064 M (L1267) 0.068 M (L1280) +0.004 D, within the bound of 0.081 D
K1, reported: the median lookup (J2−J1) over the six fetches 7.149 M (L1217) 0.473, range 0.365–0.617 M (L1242) under the design's 1.0
K3, reported: the hits' median beside base's B−(R+W+A) 0.381 D, the judge's (L1211) 0.473 M (L1238) +0.092 D. None of it is the cache's work (below)

The tripwires stand, so nothing is voided.

  • E1: both arms PASS internet_download (base L1274, cache L1287). Each has six whole bytes=170439044 sha256=1a9ee8ca…7e7f lines and six setup run= lines, counted in both kernel.logs.
  • E2, base: all 228 lookup stamps read clean=1 and queries_between=1 (lookups-base.txt).
  • E2, cache: all six fetches are stamped and none missed. Every miss in the boot read clean=1.
  • E3: both arms read "uncached lookups: 10, of which 10 sent a query".
  • The timings bug guard: each of the three judges reads "0 past its record".
  • testcases: endowment_denied, netstack_gone_mid_bind and pipe_flag_forgery each end exit=0 (L428, L442, L450). The judge reads "3 passed, 0 failed" (L23).

K2: the answers come from the cache

The cache arm first looked up the file's host in phase probes. That lookup missed, sent one clean query, and read keep_s=54 off the T14's own reply M (L14).

  • The entry lasts until its first query's send plus 54 s. On the stamp clock that is 9.252 + 54 = 63.25 s D.
  • The six fetches looked the host up between 13.67 s and 23.39 s M. All six were hits, with no n3.

Over the whole boot, 12 of 253 lookups sent a query M (lookups-cache.txt L1–L2):

  • that first lookup;
  • the ten uncached names, by design;
  • host's lookup.

The other 241 were hits, all row=test-runner. In base, all 228 lookups sent a query.

K4: the isolation held

host's lookup sent a query between two of the job's own hits. The stamps, in order M (L367–L373):

  • bracket A, row=test-runner, is a hit at 25.105 s;
  • row=host sends a query at 25.108 s;
  • bracket B, row=test-runner, is a hit at 25.117 s.

That is the only stamp between the brackets, and host exited 0.

The query is isolation, not expiry. The job's entry was live on both sides of it:

  • The entry ran to 63.25 s D, and bracket B hit after host's query.
  • host's reply read keep_s=38. The job read 54 at 9.252 s, and 15.856 s passed before host's query, which leaves 38.14 D. The resolver counted the same record down between the two reads, and both readings agree with it.

So the view host was minted held none of test-runner's answers, and the same name asked from another view went to the wire.

A repeated name's lookup time

A repeated name now takes 0.473 ms, from the job's request to its answer. That is the median J2−J1 over the six fetches, range 0.365–0.617 M. It compares as follows:

The job also looks the host up every 50 ms during the downloads. Per run, the medians of those lookups are:

  • cache: 0.28–0.41 M (run 1);
  • base: 6.27–7.78 M (run 1).

The median tcp (J4−J0) went from 23.532 to 17.364 M, which is −6.168 D. Most of the difference from the 6.676 saved is the SYN's wire, which rose by 0.543 D (15.419 → 15.962 M) between the two boots, eight minutes apart.

G and K3, reported

G. On the uncached span's median, the badge read, the miss and the keep together cost +0.004 D:

  • read→query went from 0.041 to 0.044 M;
  • reply→answer went from 0.023 to 0.024 M.

The bound of 0.081 D would hold. Roast 12's TCG figure, a cache span 0.2 longer, did not show on the T14.

K3. The 0.092 D by which the hits' median exceeds base's reference is not the cache's work.

  • netstack's own work in a hit is about 5 µs. That is n2→n6: from the request read whole to the answer about to be written, which takes in the badge read and the view's lookup. Its median is 0.0053 over all 241 hits D, and its range is 0.0056–0.0204 over the six fetches' hits D (L19–L24, L51).
  • The gap is in accepted→read (n1→n2), the step before netstack holds the request. Per fetch, M via the stamps:
    • cache: fetches 4–6 took 0.359, 0.392 and 0.387, and fetches 1–3 took 0.155–0.192;
    • base: fetches 5–6 took 0.395 and 0.406, fetch 4 took 0.075, and fetches 1–3 took 0.152–0.165 (L235–L240).
  • The reference, taken per fetch as client→accepted + accepted→read + answer→client, has a median of 0.466 in cache and 0.382 in base D. That accounts for the whole gap, and none of it lies in what the change touches.

What calls for investigation

Nothing in the cache. Both gates hold, the tripwires stand, and the cache's own figures are what the design predicts. A hit is about 5 µs of netstack's work, and a miss costs 0.004 D more than base.

The request path is now nearly all of a lookup. In each fetch's hit, client→accepted + accepted→read + answer→client is 0.358–0.609 of the 0.365–0.617 total D.

  • accepted→read is bimodal in both arms. It is 0.03–0.22 usually and 0.36–0.41 on a few stamps D: 4 of 253 in cache (L54) and 3 of 228 in base (L246).
  • The slow ones are always a fetch's own lookup or host's first, which is 0.362 in both arms.
  • Nothing in these stamps says what that step waits on. If the lookup is to get faster, this is the step to read. No file in issues/ names it.

Three things that are not ToyOS's and need no investigation:

  • The cache arm's first query took 27.803 on the wire, against base's 6.657 M. That is the resolver's own time: the lookup's local parts match base's first lookup (cache L5, base L5).
  • The ten .invalid absences read keep_s from 7 to 562 s, in clusters at 561–562, 420–421, 12 and 7 M (L67–L76). The provider's resolver answered each NXDOMAIN with an SOA the cache times it by, which answers Unsure 3. The TTLs are the resolver's.
  • A boot's first query waits 0.906 (cache) and 1.040 (base) from read to send M, against about 0.04 afterwards (cache L5, base L5). It shows in both arms, so it is not the change's.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #883, build round 1, at 804ccd7fc, against .claude/agents/reviewer.md on origin/main ecd6d1cc5. Since the merge-base 867ec23ed, main has touched no file this branch changes.

Design round 2's BLOCKER: CLOSED. The finding was that a reply read after a retried query had no test.

  • Acceptance change 1 counts every TTL from Lookup::began.
  • every_ttl_counts_from_the_first_querys_send_across_a_query_sent_again is green (ci-host.log L2013).
  • m03, which sets self.began = now on every send, turns it red with EXIT=101 (mutations.log summary).

Net lines (git diff --shortstat origin/main...804ccd7fc): 13 files, +1184 −185.

  • Production: +524 −131, net +393.
  • Tests: +565 −54.
  • Issues: +95.

The growth is the TTL and SOA reading, the clock, Cache, the lease's Network and the supervisor's mint. I found nothing to delete.

Judged, and no finding:

  • The RFCs. The reader and Cache follow each of these:
    • RFC 1035 §4.1.3: a TTL of 0 keeps nothing.
    • RFC 1035 §7.4: a whole RRset or nothing; a TC reply is never kept; nothing is kept from the additional section.
    • RFC 2181 §5.2: differing TTLs are read at their lowest, the configured servers counting as authoritative. §5.4.1: only the asked name is kept.
    • RFC 2308 §2.1: NXDOMAIN applies to the end of the chain. §2.2 and §5: the least of the SOA's TTL and MINIMUM; no SOA keeps nothing. §7: failures are not kept.
    • RFC 8767 §4: TTLs are read unsigned and capped at an hour; serving stale answers is a MAY, not taken.
    • One clock per lookup is never later than any record's own reply.
  • Mutations. All 18 patches build and each is the defect it names. Every one of the 22 runs is red with EXIT=101 at 804ccd7fc.
  • Negative control. The whole change's code, the supervisor included, was reverted onto 867ec23ed, with a shim on the test side only. 8 of the 9 new tests are red and the 33 earlier ones are green, EXIT=101.
  • Host and guest gates at the head.
    • cargo run -- --ci host: EXIT=0 (ci-host.log L8840–L8841).
    • Guest suite: 62/62, EXIT=0, virt_* on aarch64 included.
    • --build-only is green on both architectures.
  • The T14.
    • The testcases boot is the head's: the image's sha256 00fce6fd… in the flash log at L2 matches the hash staged in request.txt. endowment_denied, netstack_gone_mid_bind and pipe_flag_forgery each ended with exit=0, and the judge says "3 passed, 0 failed".
    • K2 (0 of 6) and K4 ("K4 holds", row=host query=yes keep_s=38 between two row=test-runner hits) were read on 703f474c6. Its cache, resolver, lease and supervisor code differs from the head only by measurement stamps (git diff 804ccd7fc 703f474c6 -- system/supervisor system/netstack/dns system/netstack/node/src).

BLOCKER

  • system/supervisor/src/main.rs:838 — boot now moves every service's acceptors into its Kept before it spawns any service. As a consequence, an image whose netstack row is outside [boot] start gets no netstack minted.
    • Not accepted. The accepted design (6125214538, round 2's case (1)) minted a program started before netstack from Supervisor::acceptors, and said nothing changes for other services. This was built without acceptance on the PR.
    • On its merits, I find it right:
      • every mint has one source instead of a fallback path;
      • the order of spawns, homes and sessions is unchanged;
      • restart_ended, run from pass 2's files calls, skips services not yet started, since their ended is false;
      • in that image shape, the accepted design would have handed every receiver a connector to a port nothing serves.
    • Closed when the orchestrator accepts the departure in a comment on the PR, or when the accepted design is built instead.

NOTE

  • system/supervisor/src/main.rs:2856 — Grants::netstack locks netstack's Kept while Service::spawn (:703) holds the starting service's own.
    • A netstack row that also receives netstack would lock its own mutex, and the supervisor would hang at boot without a line.
    • No image has that row, and nothing refuses it. Grants::view (:2870) returns before locking in the matching storage case.
    • Fix: refuse that row by name, or return before the lock.
  • system/supervisor/src/main.rs:2776 — build_namespace now filters netstack out of receives, so its assert no longer fires for a row that receives netstack in an image that serves none. Such a row is now endowed nothing, silently.
    • Only every_receives_names_a_provider refuses that shape, and it runs over committed configs. A rendered config, such as a metal arm's, is never checked.
    • The block port already behaves this way. Refuse the shape when the manifest is rendered, or record it.
  • system/netstack/dns/src/lib.rs:917 — Err(_) => return is a wildcard over Failure, so a variant added later would silently not be kept. Name the five failures (root CLAUDE.md, "exhaustive matches").
  • issues/nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program.md:18 — the exit covers only "a second row". Nothing reads the session half of the badge: K4's host ran in test-runner's machine-session share.
    • A mint that dropped the session would let two logins of one row share answers, and no test or exit would see it.
    • The exit should also cover a second session of the same row.
  • PR body — it still says "The metal measurement is staged and not yet run" and ends "T14 RUN REQUESTED". A claim stands only on a measurement in the body, so the T14 reading in 6127249123 (K2, K4 and the testcases boot) belongs in the body before the body becomes main's record.
  • Evidence a370c0a9, run/*/judge-*.log — none of the three judge logs records its command or its exit code, so "every judge exited 0" stands on the comment alone. When the body takes the reading, it should give each judge's exit code beside its log.
  • The bimodal accepted→read delay (6127249123; lookups-cache.txt L54, lookups-base.txt L246). Yes, it belongs in an issue, filed on this branch:
    • This branch's measurement found it, off the task, and the evidence is this run's.
    • It is in both arms, and nothing the change touches lies in that step, so it sends nothing back.
    • File it as kind: finding, status: open, with an owner and an exit a T14 run can read.
    • Name the step as the stamps bound it: from netstack's accept to its read of the whole request.
    • That span also holds the client's write after its connect returns and netstack's next pass of its loop, so it is not yet known to be netstack's. answer→client (0.13–0.19 ms) is the same order.
    • Exit: a run whose stamps on both sides of the IPC name what the slow mode waits on, after which it is removed or recorded as a defect.

SEND BACK

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The orchestrator accepts the supervisor departure (review #883 (comment), BLOCKER): boot moves every service's acceptors into its Kept before it spawns any service. The review finds it right on merits: every mint has one source instead of a fallback, the spawn order is unchanged, and the accepted design's fallback would have handed receivers a connector to a port nothing serves. The NOTEs go to the fix round.

Japabu and others added 4 commits October 12, 2026 19:57
serve.rs's one conflict is its imports: #861 took `Poller` out for
`watches::Watches`, and this branch added `toyos::port::Acceptor` for the
badge read. Both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
`Cache::keep` matched every failure but the two absences with a wildcard,
so a variant added to `Failure` later would be dropped from the cache
without anyone deciding it. The five are named, and a sixth fails to
compile until somebody says whether it is kept (review of #883, NOTE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…ow serves itself

The supervisor mints every program's `netstack` from the running
server's kept acceptor (`Grants::netstack`), which made two shapes of
manifest go wrong without a word (review of #883, two NOTEs):

- a row receiving `netstack` in an image where no row `[boot] start`
  names serves it was endowed nothing, since `build_namespace` filters
  the name out and no assert sees it;
- netstack's own row receiving `netstack` would have locked its own
  `Kept`, which `Service::spawn` holds, and the boot would hang silent.

`receives_reach_a_running_server` refuses both when the manifest is
rendered, so a metal arm's derived config is checked as a committed one
is. It is the test-only `receives_have_providers` and
`apps_receive_a_served_name` made one render gate and tightened: a
served name counts only when a row `[boot] start` names serves it, since
a launch starts no row that serves (`starts_name_what_a_launch_can_start`),
and a row never receives a port it serves. Every committed config passes
it (`every_config_renders_its_manifest`).

The supervisor's `?` on a missing netstack service becomes a panic that
names the row: no manifest the build renders reaches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…odal accepted-to-read step is filed

- nothing-shows-netstack-keys-a-lookups-cached-answers-by-its-program:
  the badge is a row and a session, and the T14's K4 read only the row
  half (`host` ran in test-runner's session). The exit now asks a second
  session of the same row as well (review of #883, NOTE).
- the-step-from-netstacks-accept-to-a-whole-request-is-bimodal: #883's
  T14 run found accepted->read 0.0285-0.2220 ms on 474 of 481 lookup
  stamps and 0.3586-0.4056 ms on seven, in both arms, which no file
  named. Filed as the review asked, with its exit and an owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #883, build round 2, at 75cf86188, against .claude/agents/reviewer.md on origin/main 97f07c381. Since the merge-base ab655a115, main has changed only .claude/agents/orchestrator.md, which this branch does not touch.

Round 1's BLOCKER: CLOSED. The two-pass boot was built without acceptance. The orchestrator has since accepted the departure on the pull request (6129668081), which is the closure round 1 named.

Round 1's NOTEs, all CLOSED:

  • A netstack row receiving netstack: refused by name at render. m1 turns a_received_name_reaches_a_server_that_runs_and_never_its_own red with EXIT=101.
  • A receiver of netstack where no [boot] start row serves it: refused at render, and m2 turns the same test red. The supervisor's ? is now a panic that names the row.
  • The wildcard over Failure: the five failures are named.
  • The isolation exit: it now also asks for a second session of the same row.
  • The body and the judges: the T14 reading is in the body. exits.txt gives each judge's command with EXIT=0. The three diffs differ only in cargo's build lines and in the record's list of unmeasured numbers.
  • The bimodal step: filed as kind: finding, with an owner and an exit a T14 run can read.

Gates at the head (5136bed6):

  • cargo run -- --ci host: EXIT=0, "Host: 76 step(s), all green" (L9167). The new test is at L45.
  • Guest suite: 63/63, EXIT=0. vector_state_survives_a_switch, netstack_streams, netstack_streams_e1000e and libc_sockets pass.
  • --build-only: EXIT=0 on x86_64 and aarch64.
  • Round 2's mutations: all six are red with EXIT=101. The baseline is EXIT=0, and the script left the tree clean.

Net lines (git diff --shortstat origin/main...75cf86188): 15 files, +1298 −246.

  • Production: +564 −132, net +432.
  • Tests: +594 −114.
  • Issues: +140.
  • Round 2's own share:
    • production +42 −3: the render gate +32 −1, the panic +9 −1, the failure arm +1 −1;
    • tests +29 −60, which replace two test-only gates with one render gate.
  • I accept the growth and found nothing further to delete.

K2 and K4 stand at this head. K1, K3 and G owe no re-read.

BLOCKER

None.

NOTE

LAND AFTER NAMED CHANGES

Japabu and others added 3 commits October 12, 2026 20:52
`receives_reach_a_running_server` counted any row's `provides` as a server
for a received name, `netstack` included. But the supervisor mints every
`netstack` connector from its started server's kept acceptor
(`Grants::netstack`) and `build_namespace` never takes `netstack` from a
provider, so a manifest in which a row provides `netstack` and no started
row serves it rendered, and the first receiver's start panicked the
supervisor. The gate now refuses `netstack` in any `provides` by name, and
its test has a config that takes that refusal alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…s no tree

The finding said both T14 arms predate #861 and asked for a run on a tree
with it. Both arms, `89a2dd00f` and `703f474c6`, descend from #861's head
`443fb5d10` and carry `system/netstack/src/watches.rs`, so that exit was
already met. What they predate is #871's kernel; the exit now asks only for
the stamps that name what the slow mode waits on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu
Japabu marked this pull request as ready for review October 12, 2026 19:07
@Japabu
Japabu added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 8b922f5 Oct 12, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-dnscache branch October 12, 2026 19:28
Japabu added a commit that referenced this pull request Oct 12, 2026
…wt/toyos-pge

No conflict. Of the files both sides changed, only tests/toyos.rs: main
drops proctest and the launch_authority row; this branch adds three
RUST_SKIP entries, three metal rows and PGE in the control_regs judge,
each in a hunk main does not touch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 12, 2026
…wt/toyos-smallkernel-track

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 12, 2026
… into wt/toyos-busywake

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 12, 2026
…os-microcode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 13, 2026
…wt/toyos-aml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 13, 2026
None of the three touches a file this branch changes. #890 changes
.claude/agents/reviewer.md's Tests and Guest tests, #876 and #883 file
Wi-Fi and DNS issues and change netstack, supervisor and build/build.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
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.

1 participant