From ef41f0ad13a486ad9c2d1e45bd4dd2bf16f12fbc Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 09:55:55 +0200 Subject: [PATCH 1/5] bar_map_again: a claim's BAR asked for again after its handle closed, red on a kernel panic A guest job claims QEMU's virtio NIC on tests/testcases, asks for one of its memory BARs, closes the handle it is given and asks for the same BAR again on the same live claim. The kernel owes it a handle or a refusal. At b432ed21c it panics instead, `a handle to a retired SharedMem`, at kernel/src/object/handle.rs's constructor: the claim's cached BAR object was retired when its only handle closed, and the second request installs it again. `cargo test --test toyos-build -- bar_map_again` exits 1 on that panic. The test is red by design and is deleted by a later commit of this branch, with its issue naming the revert that restores it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- .../toyos-rust-tests/src/bin/bar_map_again.rs | 36 +++++++++++++++++++ tests/toyos.rs | 33 +++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/toyos-rust-tests/src/bin/bar_map_again.rs diff --git a/tests/toyos-rust-tests/src/bin/bar_map_again.rs b/tests/toyos-rust-tests/src/bin/bar_map_again.rs new file mode 100644 index 00000000000..3eb2b970e3f --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/bar_map_again.rs @@ -0,0 +1,36 @@ +//! One memory BAR of a claimed function, asked for, its handle closed, and +//! asked for again on the same live claim: the second answer is a handle or a +//! refusal. Its exit is that the kernel answered at all. +//! +//! The function is QEMU's virtio NIC, which no program of `tests/testcases` +//! holds. + +use toyos::endow::Endowments; +use toyos::syscap::SysCap; +use toyos::{AsHandle, PciDev}; +use toyos_abi::syscall::{self, PciId, SYSCAP_LABEL}; + +const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; + +fn main() { + let cap: SysCap = Endowments::get() + .take(SYSCAP_LABEL) + .expect("the test estate is endowed a device-minting capability"); + let nic: PciDev = cap.claim_pci(NIC).expect("bar_map_again: the virtio NIC's claim"); + let info = nic.describe().expect("bar_map_again: the claim's description"); + let bar = info + .bar_bytes + .iter() + .position(|&bytes| bytes != 0) + .expect("bar_map_again: the function has a memory BAR its holder may map") as u32; + let first = syscall::device_bar_map(nic.as_handle(), bar).expect("bar_map_again: the BAR, asked for once"); + syscall::close(first); + println!("bar_map_again: BAR {bar} asked for and its handle closed; asking again"); + match syscall::device_bar_map(nic.as_handle(), bar) { + Ok(second) => { + syscall::close(second); + println!("bar_map_again: answered with a handle"); + } + Err(why) => println!("bar_map_again: refused: {why:?}"), + } +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 996ed86de37..1605a72db5c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -167,6 +167,9 @@ const RUST_SKIP: &[&str] = &[ "acpi_hold", // It powers the machine off: `machine_shutdown_short_stop` runs it. "stop_short", + // It claims QEMU's virtio NIC, which the T14 has none of: `bar_map_again` + // runs it. + "bar_map_again", ]; /// Binaries a metal row or a guest test drives that the shared boot also runs @@ -263,6 +266,11 @@ const MACHINE_TESTS: &[&str] = &[ // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. "machine_shutdown_short_stop", + // A claimable function with a mappable BAR that no program of the boot + // holds: QEMU's virtio NIC on `tests/testcases`. The T14's one such + // function is its I219, and a kernel that dies on this takes the bench's + // machine down with it. + "bar_map_again", ]; /// **The metal profile**: which registrations run on the ThinkPad T14, what @@ -2713,10 +2721,35 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), + "bar_map_again" => bar_map_again(test_config), other => Err(format!("unknown machine test {other}")), } } +/// A claim's memory BAR asked for again once the handle the first answer gave +/// is closed: the kernel answers, with a handle or a refusal, and the job that +/// asked ends on its own. +fn bar_map_again(test_config: &Path) -> Result<(), String> { + const JOB: &str = "bar_map_again"; + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let mut qemu = + QemuInstance::boot_with_options(test_config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + serial::Serial::boot(&qemu).must_be_clean()?; + let result = qemu.run_test("test_rs_bar_map_again", Duration::from_secs(60)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + let answered = ["bar_map_again: answered with a handle", "bar_map_again: refused: "]; + let Some(line) = result.stdout.lines().find(|l| answered.iter().any(|a| l.contains(a))) else { + return Err(format!("the second request was not answered:\n{}", result.stdout)); + }; + eprintln!(" [claims] {}", line.trim()); + Ok(()) +} + /// Every device delivery is still cpu0's, and no CPU took a shootdown IPI the /// issuer did not count. /// From 06bbc236dba1cad8ca2e5e9c7631f9f07c86f0c3 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 09:55:55 +0200 Subject: [PATCH 2/5] netstack_socket_churn: streams whose client sent no close request, red on a count that never returns A guest job on tests/netcase dials a host server four times through netstack. The host ends each connection at once; the job reads the end of stream, drops both pipe ends without a close request, and waits for netstack's own `piped.live` to return to what it was. Then it compares `sockets.tcp` with its reading before the first connection. At b432ed21c that count is 0 before and 4 after: `bridge_piped` lets the bridge go and keeps the socket and its table entry. tests/netcase's runner is given the `netstack` connector so its job can dial and read the counts. `cargo test --test toyos-build -- netstack_socket_churn` exits 1. With the close request added before the drop the same run exits 0, so the red is the missing request alone. The test is red by design and is deleted by a later commit of this branch, with its issue naming the revert that restores it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- tests/netcase/system.toml | 3 + .../src/bin/netstack_socket_churn.rs | 60 +++++++++++++++++++ tests/toyos.rs | 37 ++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs diff --git a/tests/netcase/system.toml b/tests/netcase/system.toml index c8b1e20928e..4aaa4e89953 100644 --- a/tests/netcase/system.toml +++ b/tests/netcase/system.toml @@ -26,7 +26,10 @@ service = true serves = ["netstack"] devices = ["pci:1af4:1041"] +# `netstack` because `netstack_socket_churn` connects through it and reads its +# counts. [programs.test-runner] +receives = ["netstack"] # The block service: the NVMe controller this machine's DATA is on, driven # from userland through its claim. Started again when it ends; the claim goes diff --git a/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs b/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs new file mode 100644 index 00000000000..57fb37be935 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs @@ -0,0 +1,60 @@ +//! Piped TCP connections that end with no close request from their client, as +//! a client that died leaves them: once netstack has let each connection go, +//! its stream count is what it was before the first. +//! +//! argv[1] is the port of the harness's host server, which ends every +//! connection it accepts at once. Each round reads the peer's end of stream, +//! drops both pipe ends, and waits for netstack's own count of live piped +//! connections to return. + +use std::time::{Duration, Instant}; + +use toyos_inspect::{Value, NET}; + +/// The host, as QEMU's user network names it to a guest. +const HOST: [u8; 4] = [10, 0, 2, 2]; + +const ROUNDS: usize = 4; + +/// A hang ceiling on netstack letting one ended connection go: it does so on +/// the pass after the peer acknowledges its FIN. +const LET_GO: Duration = Duration::from_secs(20); + +fn count(key: &str) -> u64 { + let snapshot = inspect::ask(NET).unwrap_or_else(|why| panic!("netstack's inspect answer: {why}")); + match snapshot.get(key) { + Some(Value::U64(n)) => *n, + other => panic!("netstack's snapshot carries {key} as {other:?}"), + } +} + +fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|p| p.parse().ok()) + .expect("usage: netstack_socket_churn "); + let (streams, live) = (count("net.sockets.tcp"), count("net.piped.live")); + for round in 1..=ROUNDS { + let conn = toyos::net::tcp_connect(HOST, port, 0) + .unwrap_or_else(|e| panic!("connection {round} to the host server: {e:?}")); + let mut byte = [0u8; 1]; + assert_eq!(conn.rx.read(&mut byte), Ok(0), "connection {round}: the host ends it without a byte"); + // Both pipe ends, and no close request. + drop(conn); + let asked = Instant::now(); + // Each question is a round trip netstack answers on a pass of its own. + while count("net.piped.live") != live { + assert!( + asked.elapsed() < LET_GO, + "netstack still counts connection {round} live {LET_GO:?} after both its ends closed" + ); + } + } + let left = count("net.sockets.tcp"); + println!( + "netstack_socket_churn: {ROUNDS} connections ended and let go; netstack held {streams} stream(s) before \ + them and holds {left} after" + ); + assert_eq!(left, streams, "netstack keeps a stream for a connection it let go"); + println!("netstack_socket_churn: ok"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 1605a72db5c..494d5c93838 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -132,6 +132,9 @@ const RUST_SKIP: &[&str] = &[ // launches ask DATA's server while this job's share holds all it may: the // `fs_share` metal row runs it on tests/proctreecase. "fs_share", + // It needs a NIC in front of netstack and a host server behind it: + // `netstack_socket_churn` runs it on `tests/netcase`. + "netstack_socket_churn", // It asserts nothing at all: it holds a `tests/lanleasecase` boot open for // twenty seconds. On a shared boot it would be twenty seconds of nothing. "lan_hold", @@ -252,6 +255,10 @@ const MACHINE_TESTS: &[&str] = &[ // behind its emulated VT-d unit and without one: no shipped machine has a // virtio function, so only a QEMU machine can be asked. "iommu_virtio_platform", + // netstack's own state behind a real stack and a peer that ends its + // connections: netstack is one binary that owns its NIC, with no host + // build, and the T14's peer is the bench's network. + "netstack_socket_churn", // The nested-NMI report is a raw write to the 16550, which the T14 does not // have. "nested_nmi_is_loud", @@ -2712,11 +2719,41 @@ fn scanout_wc(console: &str) -> Result<(), String> { Ok(()) } +/// netstack's stream count returns once connections that ended without their +/// client's close request are let go. The host server here ends each +/// connection it accepts at once; the guest's comparison is the verdict. +fn netstack_socket_churn() -> Result<(), String> { + const JOB: &str = "netstack_socket_churn"; + const LEASED: &str = "netstack: DHCP: lease "; + let server = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the host server: {e}"))?; + let port = server.local_addr().map_err(|e| format!("the host server's port: {e}"))?.port(); + // Ends with the process: a guest that never dials leaves it in `accept`. + thread::spawn(move || server.incoming().for_each(drop)); + + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let case = compile::repo_root().join("tests/netcase"); + let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + let mut console = qemu.boot_log().to_string(); + await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + let result = qemu.run_test(&format!("test_rs_netstack_socket_churn {port}"), Duration::from_secs(120)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + if !result.stdout.lines().any(|l| l.trim_end().ends_with("netstack_socket_churn: ok")) { + return Err(format!("the guest never said it was done:\n{}", result.stdout)); + } + Ok(()) +} + /// Run the machine-shape test, which owns its QEMU: the machine shape *is* the /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), + "netstack_socket_churn" => netstack_socket_churn(), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), From 8dd55d06ae8f3d54763710daca8e8ef25d3beb97 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 09:56:44 +0200 Subject: [PATCH 3/5] Delete netstack_socket_churn, red on a stream netstack keeps A red test is deleted with its issue rather than carried: the guest binary, its RUST_SKIP row, its registration, its dispatch arm and body, and the `netstack` connector tests/netcase's runner was given for it. Reverting this commit restores the test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- tests/netcase/system.toml | 3 - .../src/bin/netstack_socket_churn.rs | 60 ------------------- tests/toyos.rs | 37 ------------ 3 files changed, 100 deletions(-) delete mode 100644 tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs diff --git a/tests/netcase/system.toml b/tests/netcase/system.toml index 4aaa4e89953..c8b1e20928e 100644 --- a/tests/netcase/system.toml +++ b/tests/netcase/system.toml @@ -26,10 +26,7 @@ service = true serves = ["netstack"] devices = ["pci:1af4:1041"] -# `netstack` because `netstack_socket_churn` connects through it and reads its -# counts. [programs.test-runner] -receives = ["netstack"] # The block service: the NVMe controller this machine's DATA is on, driven # from userland through its claim. Started again when it ends; the claim goes diff --git a/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs b/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs deleted file mode 100644 index 57fb37be935..00000000000 --- a/tests/toyos-rust-tests/src/bin/netstack_socket_churn.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Piped TCP connections that end with no close request from their client, as -//! a client that died leaves them: once netstack has let each connection go, -//! its stream count is what it was before the first. -//! -//! argv[1] is the port of the harness's host server, which ends every -//! connection it accepts at once. Each round reads the peer's end of stream, -//! drops both pipe ends, and waits for netstack's own count of live piped -//! connections to return. - -use std::time::{Duration, Instant}; - -use toyos_inspect::{Value, NET}; - -/// The host, as QEMU's user network names it to a guest. -const HOST: [u8; 4] = [10, 0, 2, 2]; - -const ROUNDS: usize = 4; - -/// A hang ceiling on netstack letting one ended connection go: it does so on -/// the pass after the peer acknowledges its FIN. -const LET_GO: Duration = Duration::from_secs(20); - -fn count(key: &str) -> u64 { - let snapshot = inspect::ask(NET).unwrap_or_else(|why| panic!("netstack's inspect answer: {why}")); - match snapshot.get(key) { - Some(Value::U64(n)) => *n, - other => panic!("netstack's snapshot carries {key} as {other:?}"), - } -} - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netstack_socket_churn "); - let (streams, live) = (count("net.sockets.tcp"), count("net.piped.live")); - for round in 1..=ROUNDS { - let conn = toyos::net::tcp_connect(HOST, port, 0) - .unwrap_or_else(|e| panic!("connection {round} to the host server: {e:?}")); - let mut byte = [0u8; 1]; - assert_eq!(conn.rx.read(&mut byte), Ok(0), "connection {round}: the host ends it without a byte"); - // Both pipe ends, and no close request. - drop(conn); - let asked = Instant::now(); - // Each question is a round trip netstack answers on a pass of its own. - while count("net.piped.live") != live { - assert!( - asked.elapsed() < LET_GO, - "netstack still counts connection {round} live {LET_GO:?} after both its ends closed" - ); - } - } - let left = count("net.sockets.tcp"); - println!( - "netstack_socket_churn: {ROUNDS} connections ended and let go; netstack held {streams} stream(s) before \ - them and holds {left} after" - ); - assert_eq!(left, streams, "netstack keeps a stream for a connection it let go"); - println!("netstack_socket_churn: ok"); -} diff --git a/tests/toyos.rs b/tests/toyos.rs index 494d5c93838..1605a72db5c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -132,9 +132,6 @@ const RUST_SKIP: &[&str] = &[ // launches ask DATA's server while this job's share holds all it may: the // `fs_share` metal row runs it on tests/proctreecase. "fs_share", - // It needs a NIC in front of netstack and a host server behind it: - // `netstack_socket_churn` runs it on `tests/netcase`. - "netstack_socket_churn", // It asserts nothing at all: it holds a `tests/lanleasecase` boot open for // twenty seconds. On a shared boot it would be twenty seconds of nothing. "lan_hold", @@ -255,10 +252,6 @@ const MACHINE_TESTS: &[&str] = &[ // behind its emulated VT-d unit and without one: no shipped machine has a // virtio function, so only a QEMU machine can be asked. "iommu_virtio_platform", - // netstack's own state behind a real stack and a peer that ends its - // connections: netstack is one binary that owns its NIC, with no host - // build, and the T14's peer is the bench's network. - "netstack_socket_churn", // The nested-NMI report is a raw write to the 16550, which the T14 does not // have. "nested_nmi_is_loud", @@ -2719,41 +2712,11 @@ fn scanout_wc(console: &str) -> Result<(), String> { Ok(()) } -/// netstack's stream count returns once connections that ended without their -/// client's close request are let go. The host server here ends each -/// connection it accepts at once; the guest's comparison is the verdict. -fn netstack_socket_churn() -> Result<(), String> { - const JOB: &str = "netstack_socket_churn"; - const LEASED: &str = "netstack: DHCP: lease "; - let server = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the host server: {e}"))?; - let port = server.local_addr().map_err(|e| format!("the host server's port: {e}"))?.port(); - // Ends with the process: a guest that never dials leaves it in `accept`. - thread::spawn(move || server.incoming().for_each(drop)); - - let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); - let case = compile::repo_root().join("tests/netcase"); - let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], BootOptions::default()); - let mut console = qemu.boot_log().to_string(); - await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; - let result = qemu.run_test(&format!("test_rs_netstack_socket_churn {port}"), Duration::from_secs(120)); - if let Some(why) = &result.error { - return Err(format!("{why}\nthe job said:\n{}", result.stdout)); - } - if result.exit_code != Some(0) { - return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); - } - if !result.stdout.lines().any(|l| l.trim_end().ends_with("netstack_socket_churn: ok")) { - return Err(format!("the guest never said it was done:\n{}", result.stdout)); - } - Ok(()) -} - /// Run the machine-shape test, which owns its QEMU: the machine shape *is* the /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), - "netstack_socket_churn" => netstack_socket_churn(), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), From a86cafa066b35b18128977e4ed956ca518a96f8a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 09:56:44 +0200 Subject: [PATCH 4/5] Delete bar_map_again, red on a kernel panic A red test is deleted with its issue rather than carried: the guest binary, its RUST_SKIP row, its registration, and its dispatch arm and body. Reverting this commit restores the test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- .../toyos-rust-tests/src/bin/bar_map_again.rs | 36 ------------------- tests/toyos.rs | 33 ----------------- 2 files changed, 69 deletions(-) delete mode 100644 tests/toyos-rust-tests/src/bin/bar_map_again.rs diff --git a/tests/toyos-rust-tests/src/bin/bar_map_again.rs b/tests/toyos-rust-tests/src/bin/bar_map_again.rs deleted file mode 100644 index 3eb2b970e3f..00000000000 --- a/tests/toyos-rust-tests/src/bin/bar_map_again.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! One memory BAR of a claimed function, asked for, its handle closed, and -//! asked for again on the same live claim: the second answer is a handle or a -//! refusal. Its exit is that the kernel answered at all. -//! -//! The function is QEMU's virtio NIC, which no program of `tests/testcases` -//! holds. - -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos::{AsHandle, PciDev}; -use toyos_abi::syscall::{self, PciId, SYSCAP_LABEL}; - -const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - let nic: PciDev = cap.claim_pci(NIC).expect("bar_map_again: the virtio NIC's claim"); - let info = nic.describe().expect("bar_map_again: the claim's description"); - let bar = info - .bar_bytes - .iter() - .position(|&bytes| bytes != 0) - .expect("bar_map_again: the function has a memory BAR its holder may map") as u32; - let first = syscall::device_bar_map(nic.as_handle(), bar).expect("bar_map_again: the BAR, asked for once"); - syscall::close(first); - println!("bar_map_again: BAR {bar} asked for and its handle closed; asking again"); - match syscall::device_bar_map(nic.as_handle(), bar) { - Ok(second) => { - syscall::close(second); - println!("bar_map_again: answered with a handle"); - } - Err(why) => println!("bar_map_again: refused: {why:?}"), - } -} diff --git a/tests/toyos.rs b/tests/toyos.rs index 1605a72db5c..996ed86de37 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -167,9 +167,6 @@ const RUST_SKIP: &[&str] = &[ "acpi_hold", // It powers the machine off: `machine_shutdown_short_stop` runs it. "stop_short", - // It claims QEMU's virtio NIC, which the T14 has none of: `bar_map_again` - // runs it. - "bar_map_again", ]; /// Binaries a metal row or a guest test drives that the shared boot also runs @@ -266,11 +263,6 @@ const MACHINE_TESTS: &[&str] = &[ // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. "machine_shutdown_short_stop", - // A claimable function with a mappable BAR that no program of the boot - // holds: QEMU's virtio NIC on `tests/testcases`. The T14's one such - // function is its I219, and a kernel that dies on this takes the bench's - // machine down with it. - "bar_map_again", ]; /// **The metal profile**: which registrations run on the ThinkPad T14, what @@ -2721,35 +2713,10 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), - "bar_map_again" => bar_map_again(test_config), other => Err(format!("unknown machine test {other}")), } } -/// A claim's memory BAR asked for again once the handle the first answer gave -/// is closed: the kernel answers, with a handle or a refusal, and the job that -/// asked ends on its own. -fn bar_map_again(test_config: &Path) -> Result<(), String> { - const JOB: &str = "bar_map_again"; - let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); - let mut qemu = - QemuInstance::boot_with_options(test_config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); - serial::Serial::boot(&qemu).must_be_clean()?; - let result = qemu.run_test("test_rs_bar_map_again", Duration::from_secs(60)); - if let Some(why) = &result.error { - return Err(format!("{why}\nthe job said:\n{}", result.stdout)); - } - if result.exit_code != Some(0) { - return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); - } - let answered = ["bar_map_again: answered with a handle", "bar_map_again: refused: "]; - let Some(line) = result.stdout.lines().find(|l| answered.iter().any(|a| l.contains(a))) else { - return Err(format!("the second request was not answered:\n{}", result.stdout)); - }; - eprintln!(" [claims] {}", line.trim()); - Ok(()) -} - /// Every device delivery is still cpu0's, and no CPU took a shootdown IPI the /// issuer did not count. /// From 23f36bce955a167cd018c310f278e83e9cd17dc7 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 09:58:24 +0200 Subject: [PATCH 5/5] Record three trust-boundary defects an outside audit claimed: two measured, one read An outside static audit of main at b432ed21c claimed three defects in the PCI claim substrate and netstack, each argued from reading and run by nobody. Each was traced in the tree here and, where a test could reach it, run. - A claim's BAR asked for again after its handle closed panics the kernel: measured, `bar_map_again` exits 1 on `a handle to a retired SharedMem`. - netstack keeps the socket of a connection whose client sent no close: measured, `netstack_socket_churn` reads 0 streams before four such connections and 4 after, and exits 0 with the close request sent. - A PCI call in flight names its function by a slot the next claim reuses: traced and not run. No code on the path stops it, and the test needs the kernel to hold a call between two of its own statements, which is an actuator the tree does not have and this change, which fixes nothing, does not add. Each measured defect's test is red, so it was deleted by the commit its issue names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...ter-its-handle-closed-panics-the-kernel.md | 44 +++++++++++++++++++ ...unction-by-a-slot-the-next-claim-reuses.md | 38 ++++++++++++++++ ...a-connection-whose-client-sent-no-close.md | 41 +++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 issues/a-claims-bar-asked-for-again-after-its-handle-closed-panics-the-kernel.md create mode 100644 issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md create mode 100644 issues/netstack-keeps-the-socket-of-a-connection-whose-client-sent-no-close.md diff --git a/issues/a-claims-bar-asked-for-again-after-its-handle-closed-panics-the-kernel.md b/issues/a-claims-bar-asked-for-again-after-its-handle-closed-panics-the-kernel.md new file mode 100644 index 00000000000..15fc1e352a1 --- /dev/null +++ b/issues/a-claims-bar-asked-for-again-after-its-handle-closed-panics-the-kernel.md @@ -0,0 +1,44 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A claim's BAR asked for again after its handle closed panics the kernel + +`pcidev::bar_object` (`kernel/src/pcidev/mod.rs`) makes one object per memory +BAR and keeps it in the claim's binding, to answer every later +`SYS_DEVICE_BAR_MAP` of that BAR with "the same object every time". The object +it keeps is counted by its handles like any other: when the one handle the +first answer installed closes, `HandleEntry`'s drop +(`kernel/src/object/handle.rs`) retires it. The binding still holds it, so the +next request for that BAR hands the retired object to `ops::install`, and +`HandleEntry::new` asserts against exactly that. The holder of a live claim +takes the machine down with three ordinary calls: ask, close, ask. + +The same object is retired without a close when the first install is refused +for a full handle table: the entry built for it is dropped. Read from the +code, not run. + +**Measured** at `06bbc236d`, on `tests/testcases` under QEMU with the virtio +NIC as the claimed function: `cargo test --test toyos-build -- bar_map_again` +exits 1 on + + PANIC: panicked at src/object/handle.rs:108:9: + a handle to a retired SharedMem (koid 197) + +after the job's line `bar_map_again: BAR 4 asked for and its handle closed; +asking again`. + +No metal row was run: the T14's one function a job can claim is its I219, and +a red here is the bench's machine in a panic. + +**Exit condition**: a BAR asked for again on a live claim, after every handle +to the first answer has gone, is answered with a handle that maps or with a +refusal, and `bar_map_again` is green; the arm where the first install is +refused for room is read by a test as well. + +**Owner**: whoever holds `issues/every-driver-is-still-in-the-kernel.md`. + +**Its test is deleted**: `a86cafa06` took `bar_map_again` out, and +`git revert a86cafa06` brings it back. diff --git a/issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md b/issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md new file mode 100644 index 00000000000..5e8445994aa --- /dev/null +++ b/issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md @@ -0,0 +1,38 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A PCI call in flight names its function by a slot the next claim reuses + +Every syscall on a PCI claim — the BAR, the DMA grants and mappings, the +config read — resolves its handle to a `pcidev` slot number +(`pci_slot` and the register target, `kernel/src/syscall/device.rs`) and lets +go of the claim before it acts. What +it then acts on is whatever `BOUND[slot]` holds when `with_bound` +(`kernel/src/pcidev/mod.rs`) takes that slot's lock, which checks that the +slot is bound and not faulted, and not whose binding it is. The two steps are +under different locks with nothing held across them. + +A slot is not a name for one claim: the last handle's close releases it, a +function that was reset leaves it `Free`, and `slot::reserve` +(`kernel/pci/src/slot.rs`) gives the first free slot to the next claim, +whichever function and process that is. A call that resolved its slot before +its own claim was released therefore runs against the next holder's function, +and answers its caller with that function's object. A claim is authority over +one function for one holder; here it reaches another's. + +**Read from the code, not run.** A call made after the close is refused at +the handle, so only a call already past `pci_slot` is exposed, and a thread +in a syscall holding no lock can be preempted there. A guest cannot order a +release and a second claim inside that interval; the test owes the kernel a +hold at that point, which is an actuator this tree does not have. + +**Exit condition**: a call on a claim acts on that claim's binding or is +refused — its identity is held from the handle to the act, or the binding is +checked against it — and a test that holds a call between the two steps, +across the claim's release and the slot's next claim, sees it refused with +the second holder's function untouched. + +**Owner**: whoever holds `issues/every-driver-is-still-in-the-kernel.md`. diff --git a/issues/netstack-keeps-the-socket-of-a-connection-whose-client-sent-no-close.md b/issues/netstack-keeps-the-socket-of-a-connection-whose-client-sent-no-close.md new file mode 100644 index 00000000000..c67131a84d0 --- /dev/null +++ b/issues/netstack-keeps-the-socket-of-a-connection-whose-client-sent-no-close.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# netstack keeps the socket of a connection whose client sent no close + +A piped TCP stream has three things in netstack +(`userland/netstack/src/main.rs`): the bridge in `piped_connections`, the +smoltcp socket in the socket set with its two 64 KiB buffers, and the id's +entry in `sockets`. A client's close request removes all three. When a +connection ends any other way — the peer finishes or resets and the client +drops its pipe ends, or the client dies — `bridge_piped` removes the bridge +and nothing else. The socket and its entry stay for the life of the process. + +`max_piped_connections` counts bridges and pending connects, so each ended +connection gives its slot back while its socket stays: the bound netstack +states for what clients can make it hold does not cover this, and every +client holding the `netstack` connector can grow it one connection at a time. +`inspect` reports the entries in `net.sockets.tcp` and has no count that tells +a kept one from a live one. + +**Measured** at `06bbc236d`, on `tests/netcase` under QEMU against a host +server that ends each connection at once: +`cargo test --test toyos-build -- netstack_socket_churn` exits 1 on + + netstack_socket_churn: 4 connections ended and let go; netstack held 0 stream(s) before them and holds 4 after + +each of the four read after `net.piped.live` had returned to its reading +before the first. The same run with the close request sent before the drop +exits 0. + +**Exit condition**: a connection netstack lets go leaves no socket and no +table entry, whoever ended it, and `netstack_socket_churn` is green. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. + +**Its test is deleted**: `8dd55d06a` took `netstack_socket_churn` out, with +the `netstack` connector `tests/netcase`'s runner held for it, and +`git revert 8dd55d06a` brings both back.