Repository navigation
The shipped netstack runs ToyOS's own network stack and smoltcp leaves the tree; libc reads a stream's end as std does - #801
Conversation
… the requests, and smoltcp is gone `userland/netstack` is now the card, the clock, the kernel's random source, the kernel's pipes and the clients' connections around `toyos-net-node`: every frame goes to the node as the card handed it over, a frame leaves only into room the card said it has, and one request is one call of the node's. - `main.rs` is the loop. `serve.rs` is the requests onto node calls, the id table and `inspect`. `pipes.rs` is the kernel's pipe ends as the node's `ToClient`, `FromClient` and `Wake`. - `dhcp.rs`, `listen.rs`, `mdns.rs`, `resolve.rs` and the two smoltcp test harnesses are deleted; `smoltcp` and `managed` leave `Cargo.lock`. - The node's places come from a memory figure in which a place costs what a listener can be made to hold. - A listener's wake pipe and a datagram socket's receive pipe are watched for their owner's leaving; the 1 ms pass while a request was pending is gone. - `netstack_socket_churn` reads the node's counts. `netstack_streams` and `netstack_lookup` are new, on virtio and on QEMU's e1000e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…he streams job reads its stream end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…d against the node, and the track says what stage 5 left Closed, each by its own exit: - a-handshake-nobody-finishes-holds-a-listeners-port-shut: the node's `a_handshake_nobody_finishes_leaves_the_port_open_and_is_given_up`. - a-handshake-reset-before-it-ends-hands-its-option-to-the-next-connection: the one-socket listener left with smoltcp; the node's `a_handshake_reset_before_it_ends_leaves_the_next_connection_its_listeners_option`. - netstack-passes-every-millisecond-while-a-request-is-pending: the loop's timeout is the node's next deadline and no 1 ms constant remains. - netstack-keeps-the-datagram-socket-of-a-client-that-sent-no-close: the wake pipe of a listener and the receive pipe of a datagram socket are watched for their owner's leaving; `netstack_socket_churn` reads both counts back. - netstack-removes-a-closed-stream-before-its-fin-leaves: a host peer of `netstack_streams` reads its stream's end after the guest's close. - a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal: `toyos-dhcp` verifies a kept lease by INIT-REBOOT when the link returns, and netstack reports the change (`Node::link`). - a-shutdown-of-the-sending-half-drops-what-the-send-pipe-still-holds: the node's `a_shutdown_sends_what_the_pipe_held_and_then_the_fin`, and `netstack_streams` shuts down at once and reads every byte back. - netstack-cuts-a-departed-clients-unsent-tail-at-the-ceiling: the node's `a_departed_clients_tail_arrives_whole_while_its_peer_takes_it`. netstack-datagram-sockets-and-listeners-have-no-bound is renamed to what is still true of it: the places are one number for every client. Filed: a DHCP message the client refuses is a log line each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
QEMU's user network queues one connection on a forwarded port and resets a dial that arrives while one is queued. Under load the harness's two dials, back to back, met that: 3 of 8 runs of the whole suite red at a host load of about 30, the second dial ending `Connection reset by peer (os error 54)` and the frames recorded on the guest's card (`-object filter-dump`) holding one SYN for the listener's port. The harness now reads QEMU's table of connections after each dial and dials the next once it shows the last one carried; 5 of 5 runs of the suite are green at the same load. A failed dial is the test's first word, since it is why the job's wakes do not come. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
The shell that #793 edited is gone: its mdns.rs told the responder its link, drew the delay of each probing and said what became of the name. On the node the first two are `name`'s, and netstack hands `Node::answer_as` its draw and writes the two lines for `Event::Name` as that file wrote them, which `boot_netcase` now waits on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
The seven mutation patches of the pull request's body, each against its head. Each was applied with m1-credit-past-room diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index d78413b34..422b9a48c 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -388,7 +388,7 @@ fn main() {
0 => card.wake_on_room(),
room => room,
};
- if room == 0 || node.transmit(now, room, |frame| card.tx(frame.len(), |slot| slot.copy_from_slice(frame)), draw) < room {
+ if room == 0 || node.transmit(now, usize::MAX, |frame| card.tx(frame.len(), |slot| slot.copy_from_slice(frame)), draw) < room {
break;
}
}m2-no-sweep diff --git a/userland/netstack/src/serve.rs b/userland/netstack/src/serve.rs
index 28a1f280b..6a9157afc 100644
--- a/userland/netstack/src/serve.rs
+++ b/userland/netstack/src/serve.rs
@@ -692,7 +692,7 @@ impl Sockets {
// nothing from then.
let (ids, by_stream) = (&mut self.ids, &mut self.by_stream);
self.ends.retain(|socket_id, ends| {
- let held = ends.to_client.held().is_some() || ends.from_client.held().is_some();
+ let held = true;
if !held {
by_stream.remove(&ends.stream);
if matches!(ids.get(socket_id), Some(Socket::Stream(_))) {m3-refused-watch-ignored diff --git a/userland/netstack/src/serve.rs b/userland/netstack/src/serve.rs
index 28a1f280b..4d81239af 100644
--- a/userland/netstack/src/serve.rs
+++ b/userland/netstack/src/serve.rs
@@ -756,7 +756,6 @@ impl Sockets {
_ if !held => {}
Err(why) => {
say!("netstack: resetting a connection — the kernel refused the watch of its {end:?} pipe: {why:?}");
- node.pipe_broken(now, ends.stream, end);
}
Ok(met) if met & OTHER_END_GONE != 0 => node.pipe_gone(now, ends.stream, end),
Ok(_) => node.bridge(now),m4-owners-not-watched diff --git a/userland/netstack/src/serve.rs b/userland/netstack/src/serve.rs
index 28a1f280b..96a1a38fa 100644
--- a/userland/netstack/src/serve.rs
+++ b/userland/netstack/src/serve.rs
@@ -726,10 +726,10 @@ impl Sockets {
match socket {
Socket::Listener { wakes, .. } => {
if let Some(pipe) = wakes.held() {
- poller.watch(&*pipe, OTHER_END_GONE, TOKEN_LISTENER | u64::from(*socket_id));
+ let _ = (pipe, socket_id, TOKEN_LISTENER);
}
}
- Socket::Datagram(socket) => poller.watch(&socket.to_client, OTHER_END_GONE, TOKEN_DATAGRAM | u64::from(*socket_id)),
+ Socket::Datagram(_) => {}
Socket::Stream(_) => {}
}
}m5-receive-not-retried diff --git a/userland/netstack/src/serve.rs b/userland/netstack/src/serve.rs
index 28a1f280b..e295cad5c 100644
--- a/userland/netstack/src/serve.rs
+++ b/userland/netstack/src/serve.rs
@@ -609,11 +609,7 @@ impl Sockets {
/// the clients that waited for it, and the table entries of what the node
/// let go.
pub fn settle(&mut self, node: &mut Node, now: Instant) {
- for waiting in std::mem::take(&mut self.receiving) {
- if let Some(waiting) = self.deliver(node, now, waiting) {
- self.receiving.push(waiting);
- }
- }
+ let _ = now;
let events: Vec<StreamEvent> = node.drain_stream_events().collect();
for event in events {m6-places-doubled diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index d78413b34..0310e9acf 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -339,7 +339,7 @@ fn main() {
let total_mem = total_memory();
let places = places_for(total_mem);
- node.set_places(clock(), places);
+ node.set_places(clock(), places * 2);
let mut sockets = serve::Sockets::new(draw(), places);
let mut leases = Leases { began, said: None, settled: false };
m7-deadlines-not-fired diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index d78413b34..b33dad75e 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -379,7 +379,7 @@ fn main() {
while card.rx(|frame| node.receive(clock(), frame, draw)) {}
let now = clock();
if node.next_deadline().is_some_and(|at| at <= now) {
- node.fire(now, draw);
+ let _ = now;
}
loop {
// A card with no room is asked to say when it has some, and aThe negative control's second patch, applied over the shell's revert so that the job reaches its listener on the smoltcp base: diff --git a/tests/toyos-rust-tests/src/bin/netstack_streams.rs b/tests/toyos-rust-tests/src/bin/netstack_streams.rs
index 0f39ae589..7c9a53c76 100644
--- a/tests/toyos-rust-tests/src/bin/netstack_streams.rs
+++ b/tests/toyos-rust-tests/src/bin/netstack_streams.rs
@@ -53,8 +53,7 @@ fn bulk(port: u16) {
writing.write_all(&chunk[..len]).unwrap_or_else(|e| panic!("writing byte {sent} of the bulk: {e}"));
sent += len;
}
- // At once: what the pipe still holds is the stack's to send first.
- writing.shutdown(Shutdown::Write).expect("shutting the sending half");
+ let _ = Shutdown::Write;
});
let mut read = 0;
let mut chunk = [0u8; 8192]; |
Review, round 1, head
|
…dary (#796) and the NVMe install (#797), into the move Conflicts, every hunk accounted for: - Cargo.lock: netstack's dependencies take both sides, toyos-dhcp and the stack's crates from the move, toyos-device-memory and toyos-virtio from main; smoltcp stays gone. - tests/common/qemu.rs: both new profiles kept, HeadlessE1000e beside HeadlessNoUsb, in the enum, the architecture match and the shape table. - userland/netstack/src/virtio_net.rs: main's file, which moved the queue and its tests to toyos-virtio and deleted the test comment the move had reworded; the move's one remaining hunk, "smoltcp" to "the stack" in the transmit queue's comment, is applied again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…a rule in any 10 s, with a count of the rest Any host on the link could send replies the client refuses, one a frame, and each became a line of a log logkeeper keeps on the stick: the node handed every refusal `toyos-dhcp` queued straight to netstack as `Event::Dhcp`. The shard already bounds its crates' refusals with each crate's `RefusalLog` (toyos-net-wire's `counters!`), and `toyos-dhcp` declares its counters with the same macro, so it has one: the node now holds it and admits each of the client's refusals through it where it drains the stack's (`Node::log`), and `Event::Dhcp` carries the count of the rule's refusals since its last line, which netstack writes as the stack's lines are written. `a_hundred_refused_replies_are_one_line_and_a_count` (node, lease) is the issue's exit test: a hundred BOOTP replies give one line and a counter of 100, and one more after 10 s gives a line carrying 99. Closes issues/a-dhcp-message-the-client-refuses-is-a-log-line-each.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… is named and passed over, and a wake bridges the streams once A client could end netstack with requests alone: every UdpRecvFrom on an idle socket kept its connection's handle in `receiving`, which nothing bounded, and once netstack's handle table was full the next `acceptor.accept().expect(..)` panicked it, and every program lost the network. - `receiving` is a map from socket id to its one waiting receive. A second receive while the first waits is refused `ERR_RESOURCE_EXHAUSTED`; a waiter that hung up is replaced, so a client that left does not shut its socket's receives out. A client's requests now hold no more of netstack's handles than its sockets do. - An accept the kernel refuses has already taken the connection off the port's queue and told its client (`sys_accept`), so netstack names the first refusal of a run, says how many followed when an accept succeeds again, and goes on. - `netstack_socket_churn` asks a second receive on the socket whose first waits and reads `ResourceExhausted`. Swept once more for anything a client or the wire reaches in `serve.rs`, `pipes.rs`, `main.rs` and `client.rs`: every index is bounded by its producer (`FrameRx`'s kept length, [udp]'s cut to the buffer), every cast fits, and each `unreachable!` is the node's contract, not a client's choice. Two NOTEs of the review: - A ready stream watch no longer calls `node.bridge`: it marks the wake, and `Sockets::bridge`, after the poller's answers, passes the streams once however many answers the wake carried. - `shutdown(Read)` is answered from the id table, which already says the id names a stream; it no longer asks `node.nodelay` as an existence check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…eam line is a guest count, and the harness backs off between its QMP queries - `issues/the-pipe-abi-has-no-word-for-an-unreachable-host-or-a-lookup-to-try-again.md`: an ICMP-ended connect, a lookup ending `Unreachable` and one ending `LeaseChanged`, each with the word owed, owner the pipe ABI, exit an ABI change after the move. `serve.rs` cites it where it answers them. - The track's line on a pass over every stream exited on a T14 measurement at 1,000 idle streams, which cannot exist: the places end at 103 and the bench holds no streams. Its exit is now a guest count of netstack's pipe reads per frame at 1 and at 100 idle streams. - `netstack_streams`'s dial asked QEMU's `info usernet` back to back until the forward carried its peer; it now waits 1 ms after each answer, doubling to 64 ms, under the same 30 s ceiling. QEMU raises no event for a carried connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
BLOCKER 4: the guest's frames for a red
|
| time | direction | ports | flags | payload |
|---|---|---|---|---|
| 0.000 ms | to guest | 61088 → 7010 | SYN | 0 |
| 2.314 ms | from guest | 7010 → 61088 | SYN, ACK | 0 |
| 2.324 ms | to guest | 61088 → 7010 | ACK | 0 |
| 2.342 ms | to guest | 61088 → 7010 | PSH, ACK | 1 |
| 45.548 ms | from guest | 7010 → 61088 | ACK | 0 |
| 59934.070 ms | from guest | 7010 → 61088 | FIN, ACK | 0 |
| 59934.106 ms | to guest | 61088 → 7010 | ACK | 0 |
| 59967.223 ms | to guest | 61088 → 7010 | FIN, ACK | 0 |
The second peer's port, 61089, is in no frame of the capture (0 segments), and no frame from the guest carries RST. The FIN at 59.9 s is the job's own end after its 60 s wait.
The same capture on the three green runs (suite 1 virtio, suite 1 e1000e, suite 2 e1000e): two SYNs 0.067 to 0.089 ms apart, two SYN-ACKs, both peers' bytes, each answered and closed by the guest. For example suite 1 virtio:
| time | direction | ports | flags | payload |
|---|---|---|---|---|
| 0.000 ms | to guest | 60878 → 7010 | SYN | 0 |
| 0.067 ms | to guest | 60879 → 7010 | SYN | 0 |
| 1.795 ms | from guest | 7010 → 60878 | SYN, ACK | 0 |
| 1.861 ms | from guest | 7010 → 60879 | SYN, ACK | 0 |
| 11.173 ms | from guest | 7010 → 60878 | PSH, ACK | 1 |
| 22.063 ms | from guest | 7010 → 60879 | PSH, ACK | 1 |
So the listener queues a second connect that arrives while the first waits for its accept, whenever QEMU delivers one, and the red is QEMU's forward dropping the second host connection before it reached the guest. The harness's wait for QEMU's table to show the first carried, which round 2 now paces with a back-off, keeps the second dial from meeting that.
The frames were read with a 40-line reader of the pcap format that prints flags, ports, payload length and relative time per TCP segment to or from one port, and nothing else.
The temporary patch, applied with git apply at 14a1f5441 and reversed with git apply -R
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index d0643a124..cbb5359f1 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -2179,6 +2179,14 @@ fn qemu_command(
qemu.arg("-netdev").arg("user,id=net0").arg("-device").arg("e1000e,netdev=net0");
}
}
+ if !matches!(shape.nic, Nic::Absent) {
+ if let Ok(dir) = std::env::var("TOYOS_NETDUMP") {
+ static DUMPS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
+ let n = DUMPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ let nic = if matches!(shape.nic, Nic::E1000e) { "e1000e" } else { "virtio" };
+ qemu.arg("-object").arg(format!("filter-dump,id=dump0,netdev=net0,file={dir}/{}-{n}-{nic}.pcap", std::process::id()));
+ }
+ }
if shape.virtio.present() {
if shape.virtio.sound() {
// No guest test plays audio: the device is here as a DMA master and
diff --git a/tests/toyos.rs b/tests/toyos.rs
index bfd17d612..b14cf43e3 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -3012,52 +3012,32 @@ fn netstack_streams(profile: qemu::Profile) -> Result<(), String> {
let options = BootOptions { qmp: true, profile, ..Default::default() };
let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)], options)?;
let [to_listener] = forwards_into(&qemu, [LISTENER])?;
- let mut monitor = qemu::QmpMonitor::open(qemu.qmp_socket());
- // The connections QEMU's user network has taken off the forwarded port
- // and is carrying to the guest's listener: every row of its table that
- // names the guest's port, but the forward's own.
- let mut carried = move || {
- let table = monitor.human("info usernet");
- table
- .lines()
- .map(|row| row.split_whitespace().collect::<Vec<_>>())
- .filter(|row| row.first().is_some_and(|kind| kind.starts_with("TCP[") && *kind != "TCP[HOST_FORWARD]"))
- .filter(|row| [3, 5].iter().any(|&at| row.get(at) == Some(&LISTENER.to_string().as_str())))
- .count()
- };
- // Each peer says its own byte as soon as it has dialled, and the next
- // dials once QEMU carries this one: its forward queues one connection,
- // and resets a dial that arrives while one is queued.
- let mut dial = |said: u8| -> Result<std::net::TcpStream, String> {
- let before = carried();
- let mut peer = std::net::TcpStream::connect(("127.0.0.1", to_listener)).map_err(|e| e.to_string())?;
- peer.write_all(&[said]).map_err(|e| e.to_string())?;
- peer.set_read_timeout(Some(ANSWERED)).map_err(|e| e.to_string())?;
- let dialled = Instant::now();
- // QEMU says nothing when it carries a connection, so its table is
- // asked again after a wait that doubles to 64 ms.
- let mut pause = Duration::from_millis(1);
- while carried() == before {
- if dialled.elapsed() > ANSWERED {
- return Err(format!("QEMU's user network took no connection off its forward in {ANSWERED:?}"));
- }
- thread::sleep(pause);
- pause = (pause * 2).min(Duration::from_millis(64));
- }
- Ok(peer)
- };
+ let t0 = Instant::now();
+ let mut log = String::new();
let mut peers = Vec::new();
let result =
qemu.run_test_paced(&format!("test_rs_{JOB} {port} {LISTENER}"), Duration::from_secs(240), |_, line| {
if line.trim_end().ends_with(WAITS) {
- peers.extend(b"12".iter().map(|&said| dial(said).map(|peer| (said, peer))));
+ for &said in b"12" {
+ let at = t0.elapsed();
+ let dialled = std::net::TcpStream::connect(("127.0.0.1", to_listener));
+ let after = t0.elapsed();
+ match dialled {
+ Err(e) => { log += &format!("[dial {} connect at {at:?}..{after:?}: Err {e}] ", said as char); peers.push(Err(format!("connect: {e}"))); }
+ Ok(mut peer) => {
+ let wrote = peer.write_all(&[said]);
+ log += &format!("[dial {} connect Ok at {at:?}..{after:?} local port {:?}; write_all at {:?}: {wrote:?}] ", said as char, peer.local_addr().map(|a| a.port()), t0.elapsed());
+ match wrote { Err(e) => peers.push(Err(format!("write_all: {e}"))), Ok(()) => { peer.set_read_timeout(Some(ANSWERED)).unwrap(); peers.push(Ok((said, peer))) } }
+ }
+ }
+ }
}
});
- // A dial that failed first: it is why the job's wakes did not come.
+ eprintln!("NETDUMP-DIALS {}: {log}", matches!(profile, qemu::Profile::HeadlessE1000e));
let peers: Vec<_> = peers.into_iter().collect::<Result<_, _>>().map_err(|e| {
- format!("the host could not dial the guest's listener: {e}\nthe job said:\n{}", result.stdout)
+ format!("DIALS {log}\nthe host could not dial the guest's listener: {e}\nthe job said:\n{}", result.stdout)
})?;
- job_ok(JOB, &result)?;
+ job_ok(JOB, &result).map_err(|e| format!("DIALS {log}\n{e}"))?;
if peers.len() != 2 {
return Err(format!("the job never said its listener waits:\n{}", result.stdout));
}|
The round-2 mutation patches, each against m8-dhcp-refusals-unbounded diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 8f673c3aa..430348842 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -238,7 +238,7 @@ impl Node {
let events = &mut self.events;
self.stack.refusals(|refusal, suppressed| events.push(Event::Stack { refusal, suppressed }));
for refusal in self.client.drain_refusals() {
- if let Some(suppressed) = self.dhcp_log.admit(now, refusal.rule) {
+ if let Some(suppressed) = Some(0) {
events.push(Event::Dhcp { refusal, suppressed });
}
}m9-second-receive-not-refused diff --git a/userland/netstack/src/serve.rs b/userland/netstack/src/serve.rs
index 007ff58ad..7afef99a9 100644
--- a/userland/netstack/src/serve.rs
+++ b/userland/netstack/src/serve.rs
@@ -524,13 +524,6 @@ impl Sockets {
return;
};
// A waiting client that hung up holds the socket's one wait for nobody.
- if let Some(waiting) = self.receiving.remove(&req.socket_id) {
- if !waiting.client.gone() {
- self.receiving.insert(req.socket_id, waiting);
- msg.client.error(ERR_RESOURCE_EXHAUSTED);
- return;
- }
- }
let waiting = Receiving { client: msg.client, max_len: req.max_len };
if let Some(waiting) = self.deliver(node, now, req.socket_id, waiting) {
self.receiving.insert(req.socket_id, waiting);m1-credit-past-room diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 619aa93f2..cac9f4d0f 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -391,7 +391,7 @@ fn main() {
0 => card.wake_on_room(),
room => room,
};
- if room == 0 || node.transmit(now, room, |frame| card.tx(frame.len(), |slot| slot.copy_from_slice(frame)), draw) < room {
+ if room == 0 || node.transmit(now, usize::MAX, |frame| card.tx(frame.len(), |slot| slot.copy_from_slice(frame)), draw) < room {
break;
}
} |
Round 2, head
|
| check | exit |
|---|---|
cargo run -- --build-only |
0 |
cargo run -- --ci host |
0: Host: 78 step(s), all green |
cargo run -- --clippy |
0: clippy: 24 invocations clean |
cargo test -p toyos-net-node -p toyos-dhcp |
0 |
cargo test, the whole guest suite, load 26.24 |
0: 42 passed, 42 total |
| the eight netstack tests by name | 0: 8 passed, 8 total |
m8, node --test lease |
red, 101 |
m9, netstack_socket_churn |
red, 1 (stall at the second receive) |
m1, netstack_streams |
red on virtio, 1: a frame was offered to a transmit queue that had said it has no room; the e1000e arm green, as in round 1 |
The T14
Two boots of boot:outbound are staged, not run. They are built from wt/toyos-move-t14-r2 (ae8adbca7: this head, the rows merged, and one measurement-only line printing the ring counts). Its netstack, node, dhcp, i219, virtio and stack crates are byte-identical to 14a1f5441.
Two things the request says outright:
- No line of the rows' job carried
transmit.fulland the other ring counts, so the measurement branch adds one. - Nothing in that boot queues more than 15 frames in one pass, so
transmit.fullis expected to read 0. By its issue's exit, that is unread rather than red, and it needs a boot that fills the ring.
The link pull is on the orchestrator's attended list.
|
The T14 on ToyOS's own network stack, end to end (the orchestrator's reading). Two boots from power-off of the two images staged from the measurement branch
So the T14 reached the internet with only ToyOS's own code from the Intel driver up, and took its lease about 5 s sooner than on the old stack. This reading stands for nothing the next rounds change (BLOCKER 1's merge changes the node), and is taken again at the head that lands. |
… move The node now keeps a send pipe whose FIN is queued until its writer leaves and lets a failed stream's send pipe go first, so std reads a peer's FIN after the client's shutdown as the end. The code merges clean. Issues: `a-shutdown-of-the-sending-half-drops-what-the-send-pipe-still-holds` stays deleted, its exit met by `netstack_streams` (every byte of 4 MiB read back after a shutdown at once); #803's hunk to it was its measured evidence on smoltcp's shell, which leaves with the shell. `a-netstack-client-cannot-tell- a-reset-from-the-peers-fin` takes #803's text; the track keeps the move's idle-stream line and takes #803's pipe-order line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… IORT decode with the SMMUv3 and ITS encodings (#798) and the shipping members' rows (#799), into the move No conflict; netstack draws from `toyos_abi::syscall::random`, whose source #802 changes beneath the call and not the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…s the pipe order With #803's order merged, a client that shut its sending half down keeps the send pipe's reader until it lets the pipe go, so the read after the bulk's last byte meets the server's FIN as the end, which the job asserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…reads every end in C and in std against a host peer #803's libc half, as posted on that pull request (comment 6082081208), applied unchanged: `recv`'s read of 0 asks the send pipe with a zero-byte write and answers `ECONNRESET` where its reader is gone; `send` after `SHUT_WR` answers `EPIPE` before reaching the pipe; `recv` after `SHUT_RD` answers 0; a refused write is `ECONNRESET` or `EAGAIN`. Its rule is `streamend.rs`, host-tested by `toyos-libc-copies` (48 tests). `libc_sockets` gains a host peer whose first byte names how it ends a stream, and two jobs on it: `tests/netcase/stream_ends.c` reads recv 0 at the peer's FIN after SHUT_WR, EPIPE for a send after it, ECONNRESET on a reset mid-stream and after SHUT_WR, and recv 0 after SHUT_RD; `stream_ends_std` reports six ends in std, and the harness first runs the same source on its host's TCP and requires that report. The rows are #803's measurement's, less `half_close`, which `netstack_streams` holds on both cards. The C job closes no socket: libc's close of one ends the program, already filed. `a-netstack-client-cannot-tell-a-reset-from-the-peers-fin` is closed by its exit: netstack runs on the node, the std job reads each end as the host does, and the C case reads all five rows. Its two citing issues and the track's line now point at the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… stream-end order (#803), into the move #803 is the branch merged earlier here, now on main; nothing of it conflicts. #800 changed `userland/netstack/src/mdns.rs`, which the move deletes with the shell on smoltcp. Its two hunks are carried where the move writes the name's events, `userland/netstack/src/serve.rs`: the loss line now says the name is asked for again every `toyos_mdns::RETRY_MS / 1000` s, byte for byte the text #800 wrote. The track's mDNS line takes #800's record (the owner's ruling and what was built) under the move's wording of where the responder runs and where `HOSTNAME` lives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…y's collapsible_match asks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Round 3: why round 2's T14 boots carry no mDNS claim lineA correct absence: each boot powered off before the name could be claimed. No defect. What the readbacks show. The lines are from the two readbacks at The rows' job waits for the lease, reads its anchors and ends. The image's list then runs Why the claim needs longer. A claim cannot exist before 750 ms after a lease on a link that is up:
Why it is not a silent failure. The responder did start:
What round 3 stages. The measurement branch
|
Round 3: the libc half's three named mutations and the node order's negative controlAll four were run at The method was the same for each patch:
They ran one at a time, at a host load of 48 to 101 (each log's first line is The review on #803 named three mutations. Two of them, as written there, do not build: libc builds with
m10-recv-end-unprobed diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 0992bf218..8cd10bc27 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -397,7 +397,7 @@ pub unsafe extern "C" fn recv(fd: i32, buf: *mut u8, len: usize, _flags: i32) ->
}
let data = core::slice::from_raw_parts_mut(buf, len);
let read = syscall::read(RawHandle(entry.rx_fd as u32), data).map_err(|_| Refusal::Other).and_then(|n| match n {
- 0 => streamend::read_end(syscall::write_nonblock(RawHandle(entry.tx_fd as u32), &[])).map(|()| 0),
+ 0 => { let _ = streamend::read_end; Ok(0) }
n => Ok(n),
});
match read {m11-write-shut-unmarked diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 0992bf218..d5230690c 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -534,7 +534,7 @@ pub unsafe extern "C" fn shutdown(fd: i32, how: i32) -> i32 {
return -1;
}
entry.read_shut |= matches!(how, SHUT_RD | SHUT_RDWR);
- entry.write_shut |= matches!(how, SHUT_WR | SHUT_RDWR);
+ let _ = SHUT_WR;
}
0
}m12-read-shut-unanswered diff --git a/userland/libc/src/socket.rs b/userland/libc/src/socket.rs
index 0992bf218..9b60a6d4c 100644
--- a/userland/libc/src/socket.rs
+++ b/userland/libc/src/socket.rs
@@ -392,9 +392,7 @@ pub unsafe extern "C" fn recv(fd: i32, buf: *mut u8, len: usize, _flags: i32) ->
match entry.kind {
SocketKind::Tcp => {
- if entry.read_shut {
- return 0;
- }
+ let _ = entry.read_shut;
let data = core::slice::from_raw_parts_mut(buf, len);
let read = syscall::read(RawHandle(entry.rx_fd as u32), data).map_err(|_| Refusal::Other).and_then(|n| match n {
0 => streamend::read_end(syscall::write_nonblock(RawHandle(entry.tx_fd as u32), &[])).map(|()| 0),The control: the reverse of #803's node change, as merged here at
|
|
T14 round 3, run by the orchestrator: two
The claim line ( |
Review, round 3, head
|
… guests go The move made the node shipped code, and three of its getters had no caller but its own tests: `Node::nodelay` (its last production caller left with serve.rs's `shutdown(Read)`), `Node::listener_nodelay` and `Node::dhcp`. Each is deleted. The listener tests read the option from the accept's answer (`Accepted::nodelay`) and the stream tests on the wire, as `nodelay_reaches_the_stack` did already; the deadline test tells the client's wait from the stack's by `Node::next_deadline` against `Node::shard`'s, and the refusal test counts the client's refusals by the `Event::Dhcp` lines the node drains, the second line's 99 suppressed. `netstack_lookup` and `netstack_lookup_e1000e` are cut. The e1000e arm reached nothing that `netstack_streams_e1000e` (the Intel driver in a guest) and `netstack_lookup` did not. The virtio arm passed on any word a server gave, so a T14 row asking the same `.invalid` name is as stable: the outbound rows' `outbound_internet` reads `lookup=addresses` through the same serve.rs lookup path on the I219, and the resolver's decisions are the node's host tests'. Issues: the pipe ABI's owed words are the network track's; the track's linear-pass line carries `Node::pipe_gone`'s pass of its own; two false sentences, the node's source unchanged by the move and no T14 lease on `toyos-dhcp`, are corrected, the second with the four leases taken since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Round 4, m7 moved to Result: EXIT=1, m7-deadlines-not-fired diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -383,5 +383,5 @@ fn main() {
let now = clock();
if node.next_deadline().is_some_and(|at| at <= now) {
- node.fire(now, draw);
+ let _ = now;
}
loop { |
Review, round 4, head
|
…ardware, and cites no deleted getter The owner's ruling defers the T14 cable pull on the condition that link-down and INIT-REBOOT are recorded as unread on hardware, with that pull as the exit. Round 4's review found no such record: the deleted late-lease issue was closed by host tests, the mDNS paragraph's attended session names when and not what a log must show, and the Intel link issue is about the driver's ring. The track now carries the line, with the exit the review spelled out: one attended boot, cable pulled and returned, whose log shows the link-down line, the held lease verified or taken again with no no-address line, then the name's claim again. The reset-stream line cited `Node::nodelay`, which round 3 deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Review, round 5, head
|
|
CI at |
The shipped
netstackruns ToyOS's own stack, and smoltcp is deleted:cargo tree -p netstacknames neithersmoltcpnormanaged,Cargo.lockloses both, andgit grep -i smoltcpis empty outsideissues/. With it, libc reads a stream's end as std does (#803's libc half), and one boot oftests/netcasereads every end in C and in std against a host peer.Found on the way, not fixed here
Each is below netstack's shell or outside this change's fence (
toyos/,sdk/,rust/, the stack's crates).connection reset). Closed. The node's half is The node keeps the pipe order std reads a stream's end by: a FIN after a shutdown reads as a FIN, a failure as a reset #803, now onmainand merged here: the node keeps a send pipe whose FIN is queued until its writer leaves, and lets a failed stream's send pipe go first. Its libc half, which The node keeps the pipe order std reads a stream's end by: a FIN after a shutdown reads as a FIN, a failure as a reset #803 moved to this branch, is carried here unchanged (Decisions, and the guest tests'libc_sockets).netstack_streamsreads the stream's end again, andissues/a-netstack-client-cannot-tell-a-reset-from-the-peers-fin.mdcloses by its exit.-object filter-dumpon the guest NIC, the round-0 back-to-back dials restored, a temporary patch reversed after): at a load of 44 the second suite run reddened on virtio; the guest's capture holds one SYN on the listener's port and none of the second peer's port, and no RST from the guest; both hostconnects returnedOkand the second peer'swrite_allreturnedBroken pipe (os error 32). Green runs captured the same way hold two SYNs 0.07 to 0.09 ms apart, both answered. The reading is on the pull request (comment "BLOCKER 4: the guest's frames"). The harness dials its second peer once QEMU's table shows the first carried, asking the table again after a back-off of 1 ms doubling to 64 ms.issues/a-connect-between-two-accepts-is-reset.mdnames the forward as a third candidate for its reading.closeof a socket ends the program.close_socket(userland/libc/src/socket.rs) has no caller. Socloseon a socket's descriptor reachesSYS_CLOSEwith the socket table's index, and the kernel ends the program for naming a handle it does not hold (handle fault: ... syscall=10 no such handle, exit 139, seen on this round's first run of the C case). Already filed:issues/libc-close-of-a-socket-ends-the-process.md. The C case closes no socket; its exit lets each one go.toyos_dns::Lookup::on_datagram's two parameters (the change istoyos-dns's); a waiting datagram whose link returns before a transmit opportunity (the change istoyos-net-udp's); and the cost of a pass at 1,000 idle streams, which cannot exist: the places end at 103. That last line now also carriesNode::pipe_gone's pass of its own: each end-gone answer passes every stream, beside the one passSockets::bridgeruns for a wake's readiness answers, so k clients leaving in one wake cost k + 1 passes. The fix is small (the answer marks the wake, as a readiness answer does), but it changes netstack's pass order under the T14's reading, so it is filed on that line and not made here.What replaced what
userland/netstackis the card, the clock, the kernel's random source, the kernel's pipes and the clients' connections aroundtoyos-net-node. It holds no protocol and reads no frame.main.rs(1,699 lines to 541) is the loop. One pass: the card's link and frames to the node, every deadline that is due, the card's transmit room offered to the node until it has no frame or the card no room, the node's answers to the log and the waiting clients, then one wait on the kernel bounded by the node's next deadline.Card::rxhands a frame over and gives its buffer back; the smoltcpDeviceand its tokens are gone.serve.rs(new, 854 lines) is the requests onto node calls, the id table, the clients that wait for an answer, the watches andinspect. One request is one node call; the table below is every word it writes.pipes.rs(new, 72 lines) is the kernel's pipe ends as the node'sToClient,FromClientandWake. The node owns an end and netstack keeps a weak half to watch it by, so the handle closes when the node lets go.dhcp.rs(217),listen.rs(116),mdns.rs(157, with A lost .local name is probed for again a minute after each loss: claimed when no host answers, at most one probing a minute whatever a peer sends, and one log line a loss #800's two lines),resolve.rs(323),listen/tests.rs(523),resolve/tests.rs(490), 1,826 lines of whole files, and 1,494 ofmain.rs.netstack: DHCP: lease …, the no-lease line, the twonetstack: mDNS:lines (toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome, RFC 6762 §8.1's bound on what a peer's messages cost, and the link's return in both callers #793's claim line, whichboot_netcasewaits on, and A lost .local name is probed for again a minute after each loss: claimed when no host answers, at most one probing a minute whatever a peer sends, and one log line a loss #800's loss line, which says the name is asked for again everytoyos_mdns::RETRY_MS / 1000s), and theinspectkeyslease.held,.address,.server,.router,.dns.inspect:piped.ownerless,piped.maxandsockets.untabledare gone;places.heldandplaces.maxare the node's own counts;neighbour.routeris the router's entry in [ip]'s table, which the outbound rows read.Net (
git diff --shortstat origin/main...01bb4c8c3,mainatd6298c83e): 66 files, +2,401 / −4,002.userland/netstack/src: +1,282 / −3,322. libc with its host copy: +114 / −7. Tests (tests/, the node's tests,tests/libc-arch): +822 / −127.issues/: +181 / −484.Decisions
draw()callstoyos_abi::syscall::randomand panics on a refusal; the shard's seven keys are built from it, no draw part of two, and every node call that takes a draw takes it,Node::transmitand toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome, RFC 6762 §8.1's bound on what a peer's messages cost, and the link's return in both callers #793'sNode::answer_asamong them. Held by reading.LISTEN_READY= 128 finished connections with a receive buffer of 65,535 bytes each, and its 2 MiB wake pipe: 128 × 65,535 + 2,097,152 = 10,485,632 bytes. A stream (two pipes, two buffers: 4,325,374 bytes) and a datagram socket (two pipes, two queues of 16 datagrams bounded at 65,536 bytes: 4,325,376 bytes) are less. The node gets an eighth of physical memory over that price, at least 1 and at most 103, which is what one poller watches at two watches a place beside the acceptor, the card, 32 unspoken connections and 16 lookups' clients. A 4 GiB guest reads 50 (net.places.max, measured). Outside the places and not priced: at mosttoyos_dns::MAX_LOOKUPS= 16 lookups, each with one datagram socket for every query of its rounds.ERR_RESOURCE_EXHAUSTED, and a waiter whose client hung up is replaced, so a client that left does not shut its socket's receives out. A client's requests then hold no more of netstack's handles than its sockets do. Pending connections are bounded at 32, lookups at 16, connects by the places.expecton a client-driven resource remains.toyos-dhcp's ownRefusalLog(the shard's mechanism, fromtoyos-net-wire'scounters!): one line a rule in any 10 s, carrying how many it did not log. Without it any host on the link wrote one line of the stick's log a frame.Sockets::bridgepasses the streams once after the poller's answers, however many it carried. An end-gone answer still runs a pass of its own (Node::pipe_gone): Found on the way, 4.Node::nodelaylost its last production caller with that request, andNode::listener_nodelayandNode::dhcpnever had one: all three are deleted. The listener tests read the option from the accept's answer (Accepted::nodelay) and the stream tests on the wire, asnodelay_reaches_the_stackalready did. The deadline test tells the client's wait from the stack's byNode::next_deadlineagainstNode::shard's, which netstack'sinspectreads. The refusal test counts the client's refusals by theEvent::Dhcplines the node drains, the second carrying 99 suppressed.ECONNRESET. That is the order the node keeps.sendafterSHUT_WRanswersEPIPEbefore it reaches the pipe, which the node keeps and reads no more.recvafterSHUT_RDanswers 0: that is BSD's reading ofSHUT_RDwith bytes in flight, not every host's; Linux returns the bytes already queued first, which is what m12 turns into a red. A refused write isECONNRESETorEAGAIN. Its rule isuserland/libc/src/streamend.rs, host-tested bytoyos-libc-copies(48 tests). NoSIGPIPEis raised, asissues/libc-answers-epipe-and-raises-no-sigpipe.mdrecords.Event::Name, and the responder's link is whatNode::linkwas told.tests/netcase/system.tomlnames the 82574L beside the virtio card, so one config boots on either; the harness hasProfile::HeadlessE1000e.Every word netstack writes for the node's
Refused::{AddrInUse, NotConnected, InvalidInput, PermissionDenied, ResourceExhausted}ConnectRefused::FullERR_RESOURCE_EXHAUSTEDConnectRefused::Stack(Route(_)): no address or no routeERR_NOT_CONNECTEDConnectRefused::Stack(NotUnicast),Tcp(InvalidRemote), a port of 0ERR_INVALID_INPUTConnectRefused::Stack(Tcp(AddrInUse))ERR_ADDR_IN_USEStreamEvent::Failed:Refused,Reset,TimedOutERR_CONNECTION_REFUSED,ERR_CONNECTION_RESET,ERR_TIMED_OUTStreamEvent::Failed:Unreachable(_),ProhibitedERR_OTHER, named in the log: the pipe ABI has no word for an ICMP error (filed)StreamEvent::TimedOutERR_TIMED_OUTStreamEvent::ClosedERR_CONNECTION_REFUSED, as on smoltcpListenRefused::{Full, NotLocal, InUse}ERR_RESOURCE_EXHAUSTED,ERR_INVALID_INPUT(the word a datagram bind to such an address gets),ERR_ADDR_IN_USEAcceptRefused::{NoListener, Nothing},NoPipes,FullERR_NOT_CONNECTED,ERR_INVALID_INPUT,ERR_RESOURCE_EXHAUSTEDNotStarted::{NotConnected, ResourceExhausted}Ended::Failed(NoSuchName | NoAddress)Ended::Failed(TimedOut)ERR_TIMED_OUTEnded::Failed(Unreachable),Ended::LeaseChangedERR_NOT_CONNECTED: neither has a word of its own (filed); both are a machine on no network that answers the nameEnded::Failed(Truncated | ServerFailed | TooManyAliases),Ended::NoPortERR_OTHERandERR_RESOURCE_EXHAUSTED, named in the log, as on smoltcpERR_NOT_CONNECTEDThe rows in bold are choices of an existing word where the ABI has none:
issues/the-pipe-abi-has-no-word-for-an-unreachable-host-or-a-lookup-to-try-again.mdrecords the words owed (an ICMP-ended connect: network or host unreachable, port unreachableECONNREFUSED,ProhibitedEHOSTUNREACH; a lookup endingUnreachable: network unreachable; one endingLeaseChanged: a try-again word, getaddrinfo'sEAI_AGAIN), owner the network track, exit an ABI change after the move. No caller in the tree branches on any of the three today.The old shell's tests
24 host tests left with the two smoltcp harnesses and
mdns.rs; netstack's own arevirtio_net.rs's 5. Read by name and header against the node's tests, not run pairwise.a_finished_handshake_is_owed_one_wakea_listener_answers_a_syn_and_wakes_its_owner_when_the_handshake_endsa_peer_that_closes_with_its_last_ack_is_a_connectiona_peer_that_resets_before_it_is_taken_frees_the_port,a_wake_spent_on_a_reset_connection_announces_the_nexta_wake_left_by_a_connection_its_peer_reset_stands_for_the_nextan_accept_refused_for_its_pipes_is_woken_againan_accept_spends_a_wake_whatever_it_answersan_accept_refused_for_room_is_woken_again_when_room_returnsmore_places_wake_the_owner_of_a_connection_that_waits,a_wake_is_owed_only_for_a_connection_there_is_a_place_foran_aborted_connection_is_spent_once_its_reset_has_left,a_connection_closed_by_both_ends_is_spentspent, the shell's reading of a smoltcp socket:a_reset_ends_both_pipes_and_the_stream,a_client_that_left_is_finished_by_the_stack…next_hop…and…neighbour_entry…ceiling testsRESET_LIFE, the shell's wait on smoltcp's ARP for a reset; a reset's next hop is [ip]'sa_peer_that_acknowledges_nothing_of_a_full_send_buffer_is_reset_at_the_ceiling,a_peer_gone_silent_is_reset_at_the_ceilinga_departed_clients_unsent_bytes_have_100_seconds,a_client_done_writing_after_the_peers_fin_has_100_seconds_without_progressa_peer_that_answers_and_never_closes_is_reset_at_the_ceilinga_departed_clients_tail_arrives_whole_while_its_peer_takes_it,a_peer_keeps_sixteen_departed_clients_connections_alive_and_no_more; a client gone with nothing in its pipe is [tcp]'s 60 s, which the track recordsa_server_that_answers_no_arp_holds_up_no_other_servera_query_for_a_resolver_ip_has_given_up_ends_its_lookup_in_the_opportunity_that_would_have_carried_it, and [ip]'srfc_4861_7_2_2_a_silent_next_hop_holds_up_no_othera_server_with_no_route_holds_up_no_other_servera_query_udp_refuses_is_counted_holds_no_port_and_the_next_resolver_is_asked_at_oncean_alias_answered_while_queries_are_stuck_restarts_the_lookupan_alias_answered_late_restarts_the_lookup_and_lets_the_old_names_queries_gothe_lookup_past_the_cap_is_refused_as_exhaustedthe_lookup_past_the_cap_is_refused_until_an_answer_is_takena_lookup_whose_client_left_is_let_go_at_oncea_lookup_let_go_frees_its_ports_and_its_place_at_oncea_lookup_holds_a_socket_per_query_that_left_and_none_once_endeda_query_holds_its_port_while_its_answer_is_read_and_an_ended_lookup_holds_nonea_query_leaves_from_no_port_a_client_holdsa_query_is_rfc_1035s_octets_from_a_port_and_with_an_id_of_its_own_draws; the port is [udp]'s to picka_server_that_never_answers_is_asked_at_each_waits_end,a_lookup_is_not_carried_by_a_later_ones_schedulewake_in_asks_for_what_the_name_owes_and_nothing_elsenext_deadline, inuserland/netstack/node/tests/name.rsGuest tests, and why a guest
netstack has no host build: it owns a card. The node's host tests answer it from the tests' own script. What only a guest shows is the kernel's pipes and watches under the real shell, and a network stack nobody here wrote on the far end.
netstack_streams,netstack_streams_e1000e(new): 4 MiB written to the host kernel's TCP through QEMU's user network, the sending half shut at once, every byte read back and compared, and then the stream's end read as an end; then two host peers dial the guest's listener before it accepts either, both are accepted in turn, answered and closed, and each reads its stream's end. The T14's bench may open no peer that echoes megabytes or dials in.netstack_lookupandnetstack_lookup_e1000eare cut. The e1000e arm reached nothing thatnetstack_streams_e1000e(the Intel driver in a guest) and the virtio arm did not. The virtio arm passed on any word a server gave, so it held only that a query leaves through serve.rs's lookup path and an answer comes back. A T14 row reads exactly that: The T14's outbound rows: its router and the internet, judged from the stick (lands behind the move; do not merge before it) #784'soutbound_internetreadslookup=addressesthrough the same path on the I219, green on both of round 3's boots. The resolver's decisions are the node's host tests'. Until The T14's outbound rows: its router and the internet, judged from the stick (lands behind the move; do not merge before it) #784 lands behind this change, no test in the tree reaches serve.rs's lookup arm, as none did onmainbefore the move.netstack_socket_churn(changed): reads the node's counts. New parts: a listener and a datagram socket whose owner left with no close are let go; a receive asked before its datagram exists is answered by it, and a second receive on that socket while the first waits is refusedResourceExhausted(a guest because the bound is on handles netstack holds for connections the kernel moved, which no host test of the node holds); the connect pastnet.places.maxis refusedResourceExhaustedwith every place held. Gone: the count of an ownerless connection after a write into a pipe netstack had let go, which the node refuses at the write; the node'sa_departed_clients_…tests hold those rules on a host.libc_sockets(changed): a host peer whose first byte names how it ends a stream, and two jobs on it on the same boot.tests/netcase/stream_ends.creadsrecv0 at the peer's FIN afterSHUT_WR,EPIPEfor a send afterSHUT_WR,ECONNRESETon a reset mid-stream and afterSHUT_WR, andrecv0 afterSHUT_RD. The C rows have no oracle: they are libc's rule as written, andshut_rd's 0 is BSD's choice, where Linux would answer the bytes in flight first.stream_ends_stdreports six ends in std. The harness first runs the same source on its host's TCP and requires that report, line for line: the oracle.half_close, whichnetstack_streamsholds on both cards.Checks, at head
01bb4c8c3The head is
409a4fa9b, which differs from01bb4c8c3byissues/toyos-has-its-own-network-stack.mdalone (the link-down record above, and a line that cited the deletedNode::nodelay). At409a4fa9b,cargo run -- --ci hostexits 0:Host: 78 step(s), all green. The table below is01bb4c8c3's.origin/mainmerged atd6298c83e(#800, #803, #802). The machine was at a load of 77 to 86; each log's first line isuptimeand its second the head.cargo run -- --build-onlycargo run -- --ci hostHost: 78 step(s), all greencargo run -- --clippyclippy: 24 invocations cleancargo test -p toyos-net-node -p toyos-dhcp -p toyos-mdns -p toyos-libc-copiescargo test, the whole guest suitetest result: ok. 43 passed, 43 totalnetstack_socket_churn,libc_sockets,iommu_virtio_platform,bar_map_again,netstack_streams,netstack_streams_e1000eby nametest result: ok. 6 passed, 6 total01bb4c8c3differs from04a46fb74, the head the T14 read, by the three getters and their test reads, the two lookup guests andissues/:git diff --stat 04a46fb74 01bb4c8c3is 11 files, +30 / −94, none underuserland/netstack/src, and the node's three deletions are of functions nothing in netstack called.Negative control (round 1, at
803dc0d41, the whole shell and the lockfile reverted toorigin/mainunder this branch's tests, a checked patch reversed after):netstack_streamsandnetstack_streams_e1000e: red, exit 1, on both cards, where the head is green. As landed the job reds at its first part,reading byte 2202108 of the bulk: connection reset(virtio: byte 2231324): the shell on smoltcp drops what the send pipe held at a shutdown, the defect of the closeda-shutdown-of-the-sending-half-…issue. With the job's shutdown removed so that it reaches the listener, also a checked patch:the listener was woken for 1 of two peers in 60s, and two accepts then answered [Ok(52199), Err(NotConnected)], exit 1, on both cards.cargo tree -p netstacklines namingsmoltcpormanaged: 2 on the base, 0 at the head.Mutations, each a checked patch, built, run, reversed, tree clean after; the patches are in comments on this pull request (round 1's seven, round 2's three, round 3's three and its control: #801 (comment)).
usize::MAXframes, not the card's roomnetstack_streams, both cards, at14a1f5441a frame was offered to a transmit queue that had said it has no room. Green on the e1000e: QEMU's model finishes a transmit inside the register write, so its ring never fills in a guest;toyos-i219'sa_full_transmit_ring_answers_room_0,a_written_back_descriptor_returns_roomanda_burst_past_the_ring_leaves_whole_on_the_room_it_is_toldhold the Intel driver's room, and the T14 reads the live pathnetstack_socket_churnnetstack keeps a stream for a connection it let gonetstack_socket_churnnet.piped.live is 1 and not 0 20s after the kernel refused the watchnetstack_socket_churnnet.sockets.listeners is 1 and not 0 20s after a listener's owner dropped its wake pipenetstack_socket_churnnetstack_socket_churnnetstack holds more connections than the 50 places it said it hasnetstack_streams, both cards, at01bb4c8c3STALLED: waiting for netstack's lease — it went quiet,0 passed, 2 failed; tree restored,git statusempty (patch: #801 (comment))RefusalLogtoyos-net-node --test lease, at14a1f5441a_hundred_refused_replies_are_one_line_and_a_countnetstack_socket_churn, at14a1f5441STALLED: 794s of guard expiredrecvanswers a read of 0 as 0, without asking the send pipelibc_sockets, at2d0b0aa44reset_mid_stream: then: 0, errno 0 <-- WRONG, and the same forreset_after_half_closeshutdowndoes not mark the sending halflibc_sockets, at2d0b0aa44shut_first: a send after it: 1, errno 0 <-- WRONGrecvdoes not answer 0 afterSHUT_RDlibc_sockets, at2d0b0aa44shut_rd: then recv: 1 <-- WRONGlibc_sockets,netstack_streams,netstack_streams_e1000e, at2d0b0aa44shut_first: then the peer's FIN: -1 <-- WRONG; both cardsthe read after the bulk's last byte answered Err(... ConnectionReset ...) and not the stream's endm10 and m12 as #803's review wrote them do not build under libc's
-D warnings: the item each disables would be dead. Each patch touches that item with alet _and keeps the behaviour named.2d0b0aa44's tree matches the head in libc, the node and netstack.The accept refusal's arm is reached by no test: with receives bounded, netstack's handles are bounded by its places, 32 unspoken connections and 16 lookups, far under the kernel's 4,096. Held by reading.
Independent oracles: QEMU's user network (its DHCP server, ARP and resolver), the host kernel's TCP behind it (and, for std's report of each end, that kernel's TCP read by the same source), QEMU's e1000e model; on metal, the T14's router and the internet (round 3's boots, below). Not yet: a link pull on the I219.
The own stack's first real guests
tests/netcase:netstack_socket_churn,libc_sockets,iommu_virtio_platformandnetstack_streamson each card. No other guest config starts it, and no guest test boots the shippedsystem.toml, which--build-onlybuilds. On its first boot there it leased from QEMU's DHCP server and passednetstack_socket_churn; with toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome, RFC 6762 §8.1's bound on what a peer's messages cost, and the link's return in both callers #793 merged it claims its name on every boot. No defect of the node, the shard, [tcp], [udp] or [ip] reddened a test.The tracker
Closed this round:
a-netstack-client-cannot-tell-a-reset-from-the-peers-fin, by its exit. netstack runs on the node;stream_ends_stdreads each end as the host kernel's TCP does; the C case reads all five rows, and each of the three mutations its exit names turns it red. Its two citing issues (a-streams-failure-reaches-its-client-as-a-reset-whatever-it-was,libc-answers-epipe-and-raises-no-sigpipe) and the track's line now point at the code.The run counts #803's review named:
Ok(0)in two runs" and "in each of four runs".a-shutdown-of-the-sending-half-drops-what-the-send-pipe-still-holds: "65,536 bytes ... in two runs".One run of each count was round 1's at the base, whose log was lost, so three runs are logged. Both files leave the tree with this branch: the first closes here, and the second closed in round 1, its exit met by
netstack_streams. The merge resolutions keep neither count, and #803's measurements stand in its comments.Closed in earlier rounds, each by its exit:
a-dhcp-message-the-client-refuses-is-a-log-line-each(bya_hundred_refused_replies_are_one_line_and_a_count),a-handshake-nobody-finishes-holds-a-listeners-port-shut,a-handshake-reset-before-it-ends-hands-its-option-to-the-next-connection,netstack-passes-every-millisecond-while-a-request-is-pending,netstack-keeps-the-datagram-socket-of-a-client-that-sent-no-close,netstack-removes-a-closed-stream-before-its-fin-leaves,a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal,a-shutdown-of-the-sending-half-drops-what-the-send-pipe-still-holds,netstack-cuts-a-departed-clients-unsent-tail-at-the-ceiling.netstack-datagram-sockets-and-listeners-have-no-boundis renamednetstacks-places-are-one-number-for-every-client, which is what is left of it. Fifteen more are restated against the code that is there now. New:the-pipe-abi-has-no-word-for-an-unreachable-host-or-a-lookup-to-try-again. The track loses the lines the move closes, keeps the three it carries (the idle-stream line now exits on a guest count of netstack's pipe reads per frame at 1 and 100 idle streams, since 1,000 cannot exist under 103 places and the T14 holds no streams), and its written exit,rg smoltcpempty outsideissues/, is met; it now reads that every stage is built and every line closed.Present weaknesses, each recorded on the track or in its issue
issues/netstack-holds-a-pending-request-for-a-client-that-left.md); a receive's is replaced when the next receive on its socket arrives.issues/a-streams-failure-reaches-its-client-as-a-reset-whatever-it-was.md).closeof a socket ends the program (issues/libc-close-of-a-socket-ends-the-process.md).The T14
Round 2's reading is the orchestrator's comment on this pull request: two boots from power-off of
ae8adbca7. In both,boot:outboundexited 0 with2 passed, 0 failed. The lease came 8,172 and 8,397 ms after netstack came up, against 13,337 ms on smoltcp. Both anchors connected, the gateway neighbour readreachable, anddescriptors.sent/wire.sent/strandedread 20 / 20 / 0. These boots stand for nothing the node's #803 change touched.The missing mDNS claim line is a correct absence (#801 (comment)):
reboot.toyos-mdns's own test).Round 3's reading is the orchestrator's comment on this pull request (#801 (comment)): two boots of
boot:outboundfrom power-off,wt/toyos-move-t14-r3at1e2cc9c05, which is04a46fb74with #784's rows and two measurement-only steps (thering:line; afteroutbound: done, a wait of at most 15 s for netstack's mDNS line). Each image's sha256 matched the request; the judge exited 0 on both,2 passed, 0 failed.name: claimedon both: inside RFC 6762 §8.1's 750 ms floor and the 1 s the request named.lookup=addresses connect=connected; gateway neighbourreachable;ring:sent equal to wire sent (20/20, 18/18), stranded 0.The link pull and return is not automated. The I219 is netstack's claim, and the running driver touches no PHY register after bring-up, so a flap needs a writer outside it:
toyos-i219/src/power.rsreads it), so theMDICwrite that would clear it has nothing to carry it.LANPHYPC(CTRLbits 16 and 17) takes the PHY's power away, and only the driver's open sequence (toyos-i219/src/wake.rs, then the MAC and PHY reset together) brings a PHY back withinMDIC's reach and configured.MDICwrite under §4.5.2's arbitration. Done from netstack ortoyos-i219, it is code shipped for a test. Done from atest-actuatorskernel, it is a second implementation oftoyos-i219'sMDICand arbitration, writing a device the kernel granted to netstack alone. AndMDICon this part has left the T14 needing a power-off by hand (ARBITRATION_PACE_NANOS,toyos-i219/src/phy.rs).So the pull stays an attended step, the orchestrator's. Until it runs, the track records the node's link-down handling and its INIT-REBOOT as never run on a real card or against a real DHCP server (
issues/toyos-has-its-own-network-stack.md), with that boot as the exit: the link-down line, then on return the held lease verified by a REQUEST and its ACK or taken again with no no-address line, then the name's claim line again.Not measured
boot:outboundqueues more than 15 frames in one pass, sotransmit.fullreads 0 there: unread, not red); a link pull and return, which needs a person at the machine (The T14, above).udp.tx-queue-fullmet by a program: no guest job sends sixteen datagrams past a hop that does not answer.🤖 Generated with Claude Code
https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C