From 05dff83e80f0a9860e66f4649608dc4b54162564 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:37:38 +0200 Subject: [PATCH 1/3] Delete the net and spin applets, proctest, and the launch_authority metal row The owner asked for `net` and `proctest` to go, and for any test program that earns nothing to go with them; his ruling on #889 adds the `launch_authority` metal row, as an integration test that touches too much. - toybox's `net` applet (`system/toybox/src/net.rs`, its `mod`, its `commands!` arm and `bin/net`): nothing ran or named it, and it could only print `net: connection failed`, its row holding no `netstack`. `issues/toybox-net-cannot-reach-netstack.md` goes with it: its exit is "or is removed". - toybox's `spin` applet (`system/toybox/src/spin.rs`, its `mod`, its arm and `bin/spin`): an infinite loop nothing ran. - `tests/guest/proctest/`, its workspace member, its lock entry, its `proctest = {}` row and the three `starts` that listed it in `images/system.toml`: nothing ran `/system/bin/proctest` but itself and the `launch_authority` row's unlisted arm. With `spin` gone this meets the exit of `issues/the-shipped-image-carries-two-test-programs.md`, which goes too. - The `launch_authority` metal row: its guest binary, its `RUST_SKIP` entry, its judge, its job in `PROCTREECASE`, and the rows of `tests/images/proctreecase.toml` it alone used: test-runner's `swap` and `update` starts, toybox's `starts = ["swap"]`, and the `swap`, `update` and `proctest` rows. Its property, that a launch starts only what the caller's row lists and `swap` and `update` only in a login session, is the supervisor's `toyos_manifest::launch::may_start`, which `lib/manifest/src/launch.rs`'s host tests hold refusal by refusal: `NotListed` for an unlisted row from either session and either kind of caller, and for a row that lists nothing. `toyos_manifest::launch::refused`'s doc no longer names a metal judge that reads its line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 4 - Cargo.toml | 1 - images/system.toml | 9 +- ...shipped-image-carries-two-test-programs.md | 23 --- issues/toybox-net-cannot-reach-netstack.md | 17 --- lib/manifest/src/launch.rs | 2 +- system/toybox/src/main.rs | 4 +- system/toybox/src/net.rs | 57 -------- system/toybox/src/spin.rs | 4 - tests/guest/proctest/Cargo.toml | 10 -- tests/guest/proctest/README.md | 3 - tests/guest/proctest/src/main.rs | 135 ------------------ .../rust-tests/src/bin/launch_authority.rs | 90 ------------ tests/images/proctreecase.toml | 17 +-- tests/toyos.rs | 38 +---- 15 files changed, 8 insertions(+), 406 deletions(-) delete mode 100644 issues/the-shipped-image-carries-two-test-programs.md delete mode 100644 issues/toybox-net-cannot-reach-netstack.md delete mode 100644 system/toybox/src/net.rs delete mode 100644 system/toybox/src/spin.rs delete mode 100644 tests/guest/proctest/Cargo.toml delete mode 100644 tests/guest/proctest/README.md delete mode 100644 tests/guest/proctest/src/main.rs delete mode 100644 tests/guest/rust-tests/src/bin/launch_authority.rs diff --git a/Cargo.lock b/Cargo.lock index 8e7c4dc1663..5ae142cfc8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3041,10 +3041,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proctest" -version = "0.1.0" - [[package]] name = "quick-xml" version = "0.41.0" diff --git a/Cargo.toml b/Cargo.toml index 8e5f0d3242f..347b34b2c04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,6 @@ members = [ "system/usbd", "tests/guest/kernelprobe", "tests/guest/metalprobe", - "tests/guest/proctest", "tests/guest/test-runner", "tests/metal", ] diff --git a/images/system.toml b/images/system.toml index c8ee4a37757..bb677b274be 100644 --- a/images/system.toml +++ b/images/system.toml @@ -41,7 +41,6 @@ folder = "read-write" # its own `/state/` and makes that directory first. Every other row runs # with the session user's home. [programs] -proctest = {} # The installer. It holds nothing: it writes under `/apps` because `/apps` is # writable to it, and a row that gave it more would make that untrue. pkg = {} @@ -100,7 +99,7 @@ devices = ["pci:1af4:1041"] [programs.sshserver] service = true receives = ["netstack"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"] login = true # `swap` is the authority to replace a running service's binary; the build @@ -160,7 +159,7 @@ starts = ["shell"] [programs.shell] receives = ["surface"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"] [programs.doom] receives = ["compositor", "soundserver"] @@ -199,7 +198,7 @@ receives = ["compositor", "filepicker"] [programs.toybox] receives = ["compositor", "soundserver", "surface", "power"] syscap = ["roster"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "proctest", "sshserver", "swap", "toyfetch", "update", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "/apps"] # Symlinks created on ROOT at build time. # Keys are ROOT-relative; targets are the guest paths they resolve to. @@ -214,14 +213,12 @@ starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "pai "bin/ls" = "/system/bin/toybox" "bin/mkdir" = "/system/bin/toybox" "bin/mv" = "/system/bin/toybox" -"bin/net" = "/system/bin/toybox" "bin/ps" = "/system/bin/toybox" "bin/pwd" = "/system/bin/toybox" "bin/reboot" = "/system/bin/toybox" "bin/rm" = "/system/bin/toybox" "bin/screen" = "/system/bin/toybox" "bin/shutdown" = "/system/bin/toybox" -"bin/spin" = "/system/bin/toybox" "bin/stats" = "/system/bin/toybox" "bin/tone" = "/system/bin/toybox" diff --git a/issues/the-shipped-image-carries-two-test-programs.md b/issues/the-shipped-image-carries-two-test-programs.md deleted file mode 100644 index 77dd0b228d2..00000000000 --- a/issues/the-shipped-image-carries-two-test-programs.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# The shipped image carries two test programs - -"Nothing ships for tests alone" (`.claude/agents/reviewer.md`, Fit) is untrue -of the shipped `images/system.toml` in two rows: - -- `proctest = {}`: `tests/guest/proctest/README.md` calls it a test harness, and - nothing in the tree runs `/system/bin/proctest` but `proctest` itself. -- `"bin/spin" = "/system/bin/toybox"`: `system/toybox/src/spin.rs` loops - forever, and nothing in the tree runs it; its one named use is a test, the - metal `sshd_exec` row - `issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` owes. - -Found by `git grep -n -w -e proctest` and -`git grep -n -w -e spin -- system.toml tests userland issues/build`. - -**Exit condition**: the shipped `images/system.toml` names neither, and a test that -needs one carries it in its own image alone. diff --git a/issues/toybox-net-cannot-reach-netstack.md b/issues/toybox-net-cannot-reach-netstack.md deleted file mode 100644 index 7fbb638d60b..00000000000 --- a/issues/toybox-net-cannot-reach-netstack.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-26 ---- - -# toybox's `net` applet cannot reach netd - -`/bin/net` (`system/toybox/src/net.rs`) fetches a URL with -`TcpStream::connect`, but `[programs.toybox]` in `system.toml` receives -`compositor`, `soundd` and `surface` and not `netd`. Every connect it makes is -answered `NetdNotFound` by the SDK, so the applet can only ever print -`net: connection failed`. Endowing the row with `netd` would hand every other -toybox applet the network too, the same granularity `system.toml`'s own comment -on the row names. - -Exit condition: `net` fetches from a host, or is removed. diff --git a/lib/manifest/src/launch.rs b/lib/manifest/src/launch.rs index 09f8c55375a..4d7347cb992 100644 --- a/lib/manifest/src/launch.rs +++ b/lib/manifest/src/launch.rs @@ -252,7 +252,7 @@ pub fn may_start(caller: &Program, session: Session, target: Target<'_>) -> Resu Ok(starts) } -/// The supervisor's line for a refused launch, which the metal judge reads whole. +/// The supervisor's line for a refused launch. pub fn refused(caller: &str, session: Session, target: &str, why: Refusal) -> String { format!("supervisor: launcher: {caller} in {session} may not start {target}: {why}") } diff --git a/system/toybox/src/main.rs b/system/toybox/src/main.rs index 9e3df687707..ad92fea301b 100644 --- a/system/toybox/src/main.rs +++ b/system/toybox/src/main.rs @@ -8,14 +8,12 @@ mod locale; mod ls; mod mkdir; mod mv; -mod net; mod ps; mod pwd; mod reboot; mod rm; mod screen; mod shutdown; -mod spin; mod stats; mod tone; @@ -30,7 +28,7 @@ macro_rules! commands { }; } -commands!(cat, cp, echo, free, grep, hexdump, locale, ls, mkdir, mv, net, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone); +commands!(cat, cp, echo, free, grep, hexdump, locale, ls, mkdir, mv, ps, pwd, reboot, rm, screen, shutdown, stats, tone); fn main() { let args: Vec = std::env::args().collect(); diff --git a/system/toybox/src/net.rs b/system/toybox/src/net.rs deleted file mode 100644 index ccc5c234b7e..00000000000 --- a/system/toybox/src/net.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::io::{Read, Write}; -use std::net::TcpStream; -use std::time::Duration; - -pub fn main(args: Vec) { - let (host, port, path) = if args.len() > 1 { - parse_url(&args[1]) - } else { - ("example.com".into(), 80, "/".into()) - }; - - println!("Connecting to {host}:{port}..."); - - let mut stream = match TcpStream::connect((&*host, port)) { - Ok(s) => s, - Err(e) => { - eprintln!("net: connection failed: {e}"); - return; - } - }; - stream.set_read_timeout(Some(Duration::from_secs(10))).ok(); - - println!("Connected! Sending HTTP request..."); - - let request = format!("GET {path} HTTP/1.0\r\nHost: {host}\r\n\r\n"); - if let Err(e) = stream.write_all(request.as_bytes()) { - eprintln!("net: write failed: {e}"); - return; - } - - let mut response = Vec::new(); - if let Err(e) = stream.read_to_end(&mut response) { - eprintln!("net: read failed: {e}"); - return; - } - - let text = String::from_utf8_lossy(&response); - let preview = if text.len() > 1000 { &text[..1000] } else { &text }; - println!("{preview}"); - if text.len() > 1000 { - println!("... ({} bytes total)", text.len()); - } -} - -fn parse_url(url: &str) -> (String, u16, String) { - // Simple URL parser: "http://host:port/path" or just "host" - let url = url.strip_prefix("http://").unwrap_or(url); - let (host_port, path) = match url.find('/') { - Some(i) => (&url[..i], &url[i..]), - None => (url, "/"), - }; - let (host, port) = match host_port.rfind(':') { - Some(i) => (&host_port[..i], host_port[i+1..].parse().unwrap_or(80)), - None => (host_port, 80), - }; - (host.to_string(), port, path.to_string()) -} diff --git a/system/toybox/src/spin.rs b/system/toybox/src/spin.rs deleted file mode 100644 index 1ce01bbd268..00000000000 --- a/system/toybox/src/spin.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub fn main(_args: Vec) { - println!("spinning forever..."); - loop {} -} diff --git a/tests/guest/proctest/Cargo.toml b/tests/guest/proctest/Cargo.toml deleted file mode 100644 index 794beee24d0..00000000000 --- a/tests/guest/proctest/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "proctest" -description = "Test-only: subprocess creation, piped I/O and an interactive child, checked from inside a ToyOS guest." -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[package.metadata.toyos.host] -fails = ["linux", "macos", "windows"] -issue = "issues/apps-that-build-or-run-on-toyos-alone.md" diff --git a/tests/guest/proctest/README.md b/tests/guest/proctest/README.md deleted file mode 100644 index 6fbc02b9350..00000000000 --- a/tests/guest/proctest/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# proctest - -Test harness that validates subprocess creation, piped I/O, and interactive communication with child processes. diff --git a/tests/guest/proctest/src/main.rs b/tests/guest/proctest/src/main.rs deleted file mode 100644 index 8c076700359..00000000000 --- a/tests/guest/proctest/src/main.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::io::{BufRead, BufReader, Read, Write}; -use std::process::{Command, Stdio}; - -fn main() { - let args: Vec = std::env::args().collect(); - match args.get(1).map(|s| s.as_str()) { - Some("echo") => subcmd_echo(), - Some("repl") => subcmd_repl(), - _ => run_tests(), - } -} - -fn run_tests() { - test_status(); - test_output(); - test_piped_stdout(); - test_piped_stdin_stdout(); - test_interactive(); - - println!("all tests passed"); -} - -fn subcmd_echo() { - std::os::toyos::io::set_stdin_raw(true); - let mut buf = Vec::new(); - std::io::stdin().read_to_end(&mut buf).unwrap(); - std::io::stdout().write_all(&buf).unwrap(); -} - -fn subcmd_repl() { - std::os::toyos::io::set_stdin_raw(true); - let stdin = std::io::stdin(); - for line in stdin.lock().lines() { - let line = line.unwrap(); - let (cmd, arg) = line.split_once(' ').unwrap_or((&line, "")); - match cmd { - "echo" => println!("you said {}", arg), - "exit" => { - println!("exiting"); - return; - } - _ => println!("unknown command: {}", cmd), - } - } -} - -fn test_status() { - print!("test Command::status()... "); - let status = Command::new("/system/bin/echo") - .arg("hi") - .status() - .expect("failed to run echo"); - assert!(status.success(), "echo exited with failure"); - println!("ok"); -} - -fn test_output() { - print!("test Command::output()... "); - let output = Command::new("/system/bin/echo") - .args(&["hello", "world"]) - .output() - .expect("failed to run echo"); - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("hello world"), "unexpected output: {stdout:?}"); - println!("ok (got: {})", stdout.trim()); -} - -fn test_piped_stdout() { - print!("test spawn() with piped stdout... "); - let mut child = Command::new("/system/bin/echo") - .args(&["piped", "test"]) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn echo"); - let mut output = String::new(); - child.stdout.take().unwrap().read_to_string(&mut output).unwrap(); - let status = child.wait().expect("failed to wait"); - assert!(status.success()); - assert!(output.contains("piped test"), "unexpected output: {output:?}"); - println!("ok (got: {})", output.trim()); -} - -fn test_piped_stdin_stdout() { - print!("test spawn() with piped stdin+stdout... "); - let mut child = Command::new("/system/bin/proctest") - .arg("echo") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn proctest echo"); - - let msg = "hello from parent\n"; - child.stdin.take().unwrap().write_all(msg.as_bytes()).unwrap(); - - let mut output = String::new(); - child.stdout.take().unwrap().read_to_string(&mut output).unwrap(); - let status = child.wait().expect("failed to wait"); - assert!(status.success()); - assert_eq!(output, msg, "echo mismatch: {output:?}"); - println!("ok (echoed: {})", output.trim()); -} - -fn test_interactive() { - print!("test interactive subprocess... "); - let mut child = Command::new("/system/bin/proctest") - .arg("repl") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to spawn proctest repl"); - - let mut stdin = child.stdin.take().unwrap(); - let mut stdout = BufReader::new(child.stdout.take().unwrap()); - let mut line = String::new(); - - stdin.write_all(b"echo hello\n").unwrap(); - line.clear(); - stdout.read_line(&mut line).unwrap(); - assert_eq!(line, "you said hello\n", "unexpected: {line:?}"); - - stdin.write_all(b"echo world\n").unwrap(); - line.clear(); - stdout.read_line(&mut line).unwrap(); - assert_eq!(line, "you said world\n", "unexpected: {line:?}"); - - stdin.write_all(b"exit\n").unwrap(); - line.clear(); - stdout.read_line(&mut line).unwrap(); - assert_eq!(line, "exiting\n", "unexpected: {line:?}"); - - let status = child.wait().expect("failed to wait"); - assert!(status.success()); - println!("ok"); -} diff --git a/tests/guest/rust-tests/src/bin/launch_authority.rs b/tests/guest/rust-tests/src/bin/launch_authority.rs deleted file mode 100644 index 3e10361eb16..00000000000 --- a/tests/guest/rust-tests/src/bin/launch_authority.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! A launch starts only what the caller's row lists, and `swap` and `update` -//! only in a login session. -//! -//! This job holds test-runner's launcher, in the machine's session, and asks -//! the supervisor for: -//! -//! - `toybox` as `ps`, which test-runner's row lists: it runs under its row, -//! which alone gives it the roster `ps` exits 0 with; -//! - `proctest`, which the row does not list, and `swap` and `update`, which -//! it lists and which start only in a login session: each is refused, and -//! nothing is spawned in its place; -//! - `toybox stats swap`: toybox's row lists `swap` and opens no session, so -//! it asks in the session it was launched in, the machine's, and is refused; -//! - a shell, whose row opens a login session, running `toybox stats swap`: -//! toybox asks in that session, and `swap` starts. -//! -//! `stats` exits 1 when its command does not start, and 0 once it has run. -//! -//! And a child it spawns directly holds no launcher, under its label or in -//! the namespace it inherits: the chain where any program a shell spawned -//! could ask for `swap` is gone. - -use std::process::{Command, Stdio}; - -use toyos::endow::{self, EndowError, Endowments}; -use toyos::launch::LAUNCHER; - -const SELF_PATH: &str = "/system/bin/test_rs_launch_authority"; -const STATS_SWAP: &str = "/system/bin/toybox stats /system/bin/swap"; -const CHILD: &str = "child"; -/// What the child exits with holding no launcher, and holding one. -const HOLDS_NONE: i32 = 0; -const HOLDS_ONE: i32 = 1; - -fn main() { - match std::env::args().nth(1).as_deref() { - Some(CHILD) => child(), - _ => test(), - } -} - -fn test() { - // Every arm runs, so one run says each one that is red. - let mut red = Vec::new(); - - let ps = Command::new("/system/bin/toybox").arg("ps").output().expect("launch toybox as ps"); - if ps.status.code() != Some(0) { - red.push(format!("a listed row did not run under its row: {ps:?}")); - } - - for program in ["/system/bin/proctest", "/system/bin/swap", "/system/bin/update"] { - match Command::new(program).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).spawn() { - Err(_) => println!(" {program}: refused"), - Ok(mut started) => { - let _ = started.kill(); - let _ = started.wait(); - red.push(format!("{program} started for a caller in the machine's session")); - } - } - } - - let from_machine = Command::new("/system/bin/toybox") - .args(["stats", "/system/bin/swap"]) - .output() - .expect("launch toybox as stats"); - match from_machine.status.code() { - Some(1) => println!(" toybox in the machine's session: swap refused"), - _ => red.push(format!("toybox launched in the machine's session started swap: {from_machine:?}")), - } - - let from_login = Command::new("/system/bin/shell").args(["-c", STATS_SWAP]).output().expect("launch a shell"); - match from_login.status.code() { - Some(0) => println!(" toybox in a login session: swap started"), - _ => red.push(format!("toybox launched in a login session did not start swap: {from_login:?}")), - } - - let child = Command::new(SELF_PATH).arg(CHILD).status().expect("spawn this binary as a child"); - if child.code() != Some(HOLDS_NONE) { - red.push(format!("a child spawned directly holds a launcher ({child:?})")); - } - - assert!(red.is_empty(), "launch_authority:\n {}", red.join("\n ")); - println!("launch_authority: PASS"); -} - -fn child() -> ! { - let labelled = Endowments::get().labels().any(|label| label == LAUNCHER); - let inherited = !matches!(endow::service(LAUNCHER), Err(EndowError::NotEndowed)); - std::process::exit(if labelled || inherited { HOLDS_ONE } else { HOLDS_NONE }) -} diff --git a/tests/images/proctreecase.toml b/tests/images/proctreecase.toml index c55ff8182cc..92fb92a96a1 100644 --- a/tests/images/proctreecase.toml +++ b/tests/images/proctreecase.toml @@ -7,12 +7,6 @@ # # `launch_toctou` rides it too: a launch of `toybox` through a link it re-points. # -# And `launch_authority`: test-runner's row lists `swap` and `update`, which a -# job is refused outside a login session, and not `proctest`, which it is -# refused outright; `toybox`, which opens no session, starts `swap` in the -# session it was launched in: refused in the machine's, started in the one the -# shell opens. -# # And `fs_share`: a shell test-runner launches opens no session and spends # test-runner's share of DATA's server; a shell that shell launches is in a # login session its row opens, which has a share of its own, and so is every @@ -42,13 +36,12 @@ syscap = ["logread"] # because test-runner hands each binary a duplicate of its capability. [programs.test-runner] syscap = ["dup", "roster"] -starts = ["toybox", "shell", "swap", "update", "grants", "/apps"] +starts = ["toybox", "shell", "grants", "/apps"] # `roster`, which a child the job spawns itself never holds: a child holding # it ran under this row, and `launch_toctou` asks which bytes that child was. [programs.toybox] syscap = ["roster"] -starts = ["swap"] [programs.shell] starts = ["toybox", "shell", "grants"] @@ -57,14 +50,6 @@ login = true [programs.grants] receives = ["grants"] -[programs.swap] -receives = ["swap"] - -[programs.update] -slots = true - -[programs.proctest] - [symlinks] "bin/cat" = "/system/bin/toybox" diff --git a/tests/toyos.rs b/tests/toyos.rs index da5cbc2ec3c..adcef8b15ef 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -124,10 +124,6 @@ const RUST_SKIP: &[&str] = &[ // `tests/images/testcases.toml` does not give: the `launch_toctou` metal row runs it on // tests/images/proctreecase.toml. "launch_toctou", - // Needs a launcher whose row lists `swap` and `update` and not `proctest`, - // and a shell that opens a login session: the `launch_authority` metal row - // runs it on tests/images/proctreecase.toml. - "launch_authority", // A kernel primitive with no use for any one boot's devices: the // `port_badge` metal row runs it on tests/images/proctreecase.toml. "port_badge", @@ -875,12 +871,6 @@ const METAL: &[(&str, metal::Metal)] = &[ "launch_toctou", metal::Metal { arms: PROCTREECASE, judge: |b| b[0].job_passed("test_rs_launch_toctou") }, ), - ( - // A launch starts only what the caller's row lists, and `swap` and - // `update` only in a login session. - "launch_authority", - metal::Metal { arms: PROCTREECASE, judge: |b| launch_authority(b[0]) }, - ), ( // A connection's badge is the minted bytes, read back by its own // port's acceptor alone. @@ -1173,9 +1163,8 @@ const METALCASE: &[metal::Arm] = &[metal::once("metalcase", "tests/images/metalc /// A launcher and a declared `cat` and shell, which `process_tree`'s subtree /// launches, a `toybox` row holding `roster`, which `launch_toctou` races, the -/// rows `launch_authority` is refused and started, the shells `fs_share` -/// asks DATA's server through, under its share and in a login session, and -/// the `/apps` `app_view` launches its package from. +/// shells `fs_share` asks DATA's server through, under its share and in a +/// login session, and the `/apps` `app_view` launches its package from. const PROCTREECASE: &[metal::Arm] = &[metal::once( "proctreecase", "tests/images/proctreecase.toml", @@ -1183,7 +1172,6 @@ const PROCTREECASE: &[metal::Arm] = &[metal::once( &[ "test_rs_process_tree", "test_rs_launch_toctou", - "test_rs_launch_authority", "test_rs_port_badge", "test_rs_fs_share", "test_rs_app_view", @@ -4982,28 +4970,6 @@ fn process_tree(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// `launch_authority` passed, and the supervisor refused each launch for its -/// own reason: `proctest` as unlisted, `swap` and `update` as outside a login -/// session, for test-runner and for the toybox it launched. The guest sees -/// only that each was refused. -fn launch_authority(back: &metal::Readback) -> Result<(), String> { - use toyos_manifest::launch::{refused, Refusal, Sessions}; - back.job_passed("test_rs_launch_authority")?; - let log = back.log(); - for (caller, target, why) in [ - ("test-runner", "proctest", Refusal::NotListed), - ("test-runner", "swap", Refusal::OutsideLogin), - ("test-runner", "update", Refusal::OutsideLogin), - ("toybox", "swap", Refusal::OutsideLogin), - ] { - let line = refused(caller, Sessions::default().machine(), target, why); - if !log.text().lines().any(|l| l.contains(&line)) { - return Err(format!("the supervisor never said `{line}`\n{}", log.text())); - } - } - Ok(()) -} - /// `app_view` passed, every arm its package asks held, and the supervisor /// refused each of the two launches for its own reason: a package named after /// the shell's row, and one whose folder is a file. The guest sees only that From b36458c004fa9a6d1a5c5adbd87a464fe9f1e5e8 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 20:36:05 +0200 Subject: [PATCH 2/3] issues: what the launch_authority row alone held, and consent_prompt's red at host load 47 The launch_authority row's last arm asserted that a child spawned directly holds no launcher; nothing else asserts it at any tier, so its deletion leaves it to an issue the orchestrator owns. The whole guest suite on this branch's c24bee1db went red on consent_prompt alone, 62 of 63, at host load 34.22 38.33 47.31 on 14 cores: at `skip` a click on the open launcher's row launched nothing and the job ran out its 600 s ceiling. The branch changes nothing that test boots or runs but a doc comment, so it is a defect of main's tree, recorded with the host's load as root CLAUDE.md asks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...on-an-open-launchers-row-made-no-launch.md | 35 +++++++++++++++++++ ...hild-spawned-directly-holds-no-launcher.md | 30 ++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 issues/consent-prompts-click-on-an-open-launchers-row-made-no-launch.md create mode 100644 issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md diff --git a/issues/consent-prompts-click-on-an-open-launchers-row-made-no-launch.md b/issues/consent-prompts-click-on-an-open-launchers-row-made-no-launch.md new file mode 100644 index 00000000000..fdc879d7f13 --- /dev/null +++ b/issues/consent-prompts-click-on-an-open-launchers-row-made-no-launch.md @@ -0,0 +1,35 @@ +--- +status: open +kind: defect +opened: 2026-10-12 +--- + +# `consent_prompt`'s click on an open launcher's row made no launch + +A whole guest suite (`cargo test`) on the development host, on pull request +#889's tree at `c24bee1db`, went red on `consent_prompt` alone, 62 of 63 +green, while the host's load average read 34.22, 38.33 and 47.31 a minute +after it ended, on 14 cores. The suite had scaled its liveness ceilings by +1.00x. Of what that test boots or runs, the tree differs from `main`'s at +`ab655a115` only in a doc comment of `toyos_manifest::launch::refused`. + +The steps `always`, `again`, `deny` and `denied` passed. At `skip` the host +clicked `+`, saw the launcher's row change on the panel within its 20 s, and +clicked the row. Nothing launched: the supervisor said nothing more of +`consentee`, no window opened, and the compositor logged only idle frames +until the job's 600 s ceiling ended it. With no launch there was no prompt, +so `Consent::prompt_up`, which only the prompt's line starts, never ran. The +log: +https://github.com/ToyOSOrg/evidence/blob/c7a625ddef468380faaca0e94f0be7ffb52dfed3/build/guest.log#L1259 + +The host waits for the launcher before its second click because two clicks +in one read are one click +(`issues/two-clicks-the-compositor-reads-at-once-are-one-click.md`). That +wait ends when any pixel of the row's rectangle changes (`Consent::begin`, +`tests/toyos.rs`), which is not the row being drawn. + +Owner: the desktop track, `issues/toyos-has-a-desktop.md`. + +**Exit**: why a click on the open launcher's row made no launch is named and +fixed, and the host's step reds by name, within a bound of its own, when its +click launches nothing. diff --git a/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md b/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md new file mode 100644 index 00000000000..ddb4cbd0dd5 --- /dev/null +++ b/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md @@ -0,0 +1,30 @@ +--- +status: open +kind: tooling +opened: 2026-10-12 +--- + +# Nothing holds that a child spawned directly holds no launcher + +The `launch_authority` metal row, deleted by the owner's ruling on pull +request #889, ended by spawning its own binary, which no row declares, and +asserting the child held no launcher: none under the `launcher` label, and +none in the `svc` namespace it inherits. No test at any tier asserts that +now. + +Two decisions make it true, and neither has a test of its own: + +- the supervisor endows a row's launcher in a namespace of its own under + `launcher`, never as an entry of `svc` (`launcher_namespace`, + `system/supervisor/src/main.rs`); +- std's direct spawn endows the child a copy of the caller's `svc` and + only what the caller endowed, never the caller's launcher + (`sdk/std/sys/process.rs`). + +**Owner**: the orchestrator, which holds +`issues/the-supervisor-is-host-tested-and-owns-the-stop.md`, whose stage 2 +moves the supervisor's namespace selection into a host-tested crate. + +**Exit**: each decision is held by a test on the cheapest tier that reaches +it, and a mutation named in its pull request reds each: the launcher +added to `svc`, and a direct spawn endowing the child its caller's launcher. From 43a843c81478ab57cb2f586c10c447934d642c8b Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 21:09:35 +0200 Subject: [PATCH 3/3] Inline the launcher's refusal line into its one caller; stage 2 owes the svc mutation `toyos_manifest::launch::refused` had two callers, the supervisor's `say!` and the launch_authority metal judge, which read the line whole. With the judge deleted it had one, so its format string moves into that `say!` and the function goes. The line the supervisor writes is the same bytes. The launch_authority row's last arm held two decisions. The supervisor's, a row's launcher endowed under `launcher` and never in `svc`, is namespace selection, which stage 2 of the supervisor track already owes a host test and a mutation; that stage's exit now names the mutation, and the issue the branch opened keeps std's direct spawn alone, so one record owes each test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...hild-spawned-directly-holds-no-launcher.md | 23 ++++++++----------- ...rvisor-is-host-tested-and-owns-the-stop.md | 8 +++++-- lib/manifest/src/launch.rs | 5 ---- system/supervisor/src/main.rs | 2 +- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md b/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md index ddb4cbd0dd5..f37b5d8d250 100644 --- a/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md +++ b/issues/nothing-holds-that-a-child-spawned-directly-holds-no-launcher.md @@ -12,19 +12,14 @@ asserting the child held no launcher: none under the `launcher` label, and none in the `svc` namespace it inherits. No test at any tier asserts that now. -Two decisions make it true, and neither has a test of its own: +This file owes std's half of it: a direct spawn endows the child a copy of +the caller's `svc` and only what the caller endowed, never the caller's +launcher (`sdk/std/sys/process.rs`). The supervisor's half, a row's launcher +endowed under `launcher` and never in `svc`, is owed by stage 2 of +`issues/the-supervisor-is-host-tested-and-owns-the-stop.md`. -- the supervisor endows a row's launcher in a namespace of its own under - `launcher`, never as an entry of `svc` (`launcher_namespace`, - `system/supervisor/src/main.rs`); -- std's direct spawn endows the child a copy of the caller's `svc` and - only what the caller endowed, never the caller's launcher - (`sdk/std/sys/process.rs`). +**Owner**: the orchestrator. -**Owner**: the orchestrator, which holds -`issues/the-supervisor-is-host-tested-and-owns-the-stop.md`, whose stage 2 -moves the supervisor's namespace selection into a host-tested crate. - -**Exit**: each decision is held by a test on the cheapest tier that reaches -it, and a mutation named in its pull request reds each: the launcher -added to `svc`, and a direct spawn endowing the child its caller's launcher. +**Exit**: a test on the cheapest tier that reaches std's direct spawn holds +that it endows the child no launcher, and a mutation named in its pull +request reds it: a direct spawn endowing the child its caller's launcher. diff --git a/issues/the-supervisor-is-host-tested-and-owns-the-stop.md b/issues/the-supervisor-is-host-tested-and-owns-the-stop.md index b235919cbe1..8e57885ed84 100644 --- a/issues/the-supervisor-is-host-tested-and-owns-the-stop.md +++ b/issues/the-supervisor-is-host-tested-and-owns-the-stop.md @@ -59,8 +59,12 @@ nothing. **Exit**: the crate's tests pass in `cargo test --workspace --exclude toyos-build`; a host test refuses a manifest whose dependencies form an undeclared cycle; each refusal and - each decision above has a mutation that reds a host test, named in the PR; - and the stage deletes each decision from `system/supervisor`, which calls + each decision above has a mutation that reds a host test, named in the PR, + namespace selection's among them: a row's launcher endowed as an entry of + `svc` (`launcher_namespace`), which a child its holder spawns directly + would inherit, and no such child holding one is what the `launch_authority` + metal row, deleted in #889, last asserted; and the stage deletes each + decision from `system/supervisor`, which calls the crate for it, named per decision in the PR; `toyos_manifest::launch`, who may start what and the launcher's badge codec, moves into the crate and out of `toyos-manifest`. No decision exists in two places. diff --git a/lib/manifest/src/launch.rs b/lib/manifest/src/launch.rs index 4d7347cb992..03645af006c 100644 --- a/lib/manifest/src/launch.rs +++ b/lib/manifest/src/launch.rs @@ -252,11 +252,6 @@ pub fn may_start(caller: &Program, session: Session, target: Target<'_>) -> Resu Ok(starts) } -/// The supervisor's line for a refused launch. -pub fn refused(caller: &str, session: Session, target: &str, why: Refusal) -> String { - format!("supervisor: launcher: {caller} in {session} may not start {target}: {why}") -} - #[cfg(test)] mod tests { use std::collections::BTreeSet; diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 3bcec5fa71b..642c7e58b1d 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -1903,7 +1903,7 @@ impl Supervisor<'_> { let (starts, binary, prepared) = match verdict { Ok(judged) => judged, Err(why) => { - say!("{}", authority::refused(&caller.row.name, caller.session, &program.name, why)); + say!("supervisor: launcher: {} in {} may not start {}: {why}", caller.row.name, caller.session, program.name); let _ = conn.try_signal(launch::MSG_REFUSED); return; }