diff --git a/issues/a-file-servers-shares-are-per-instance-and-one-session-launches-instances.md b/issues/a-file-servers-shares-are-per-instance-and-one-session-launches-instances.md deleted file mode 100644 index a2a68cbf184..00000000000 --- a/issues/a-file-servers-shares-are-per-instance-and-one-session-launches-instances.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -status: assigned -kind: defect -opened: 2026-10-04 ---- - -# A file server's shares are per instance, and one session launches instances - -Held by the orchestrator. - -`userland/fileserver/src/main.rs` gives each instance a grant names a quarter -of each machine-wide bound: 32 of `MAX_SERVED`'s 128 served connections, 8 of -`MAX_HANDSHAKES`' 32 waiting on their hello, 16 of `MAX_STREAMS`' 64 streams. -An instance is one start and the children it spawns directly, and the -supervisor mints a fresh one on every start. So four instances at their shares -take each bound whole, and every other program's first connection or `STREAM` -on that server is refused `ResourceExhausted`. - -Four instances are cheap to have: a program whose row lists `starts` (the -compositor, terminal, shell, toybox and sshserver rows of `system.toml`) gets -a new instance on every launch, and a shell launches shells. One session can -therefore take DATA's server from every other session. - -The shares are sized against what was measured, not against a session's need. -With the shares in place, every boot of the guest suite, the shipping -desktop booted under QEMU, and every shipping Rust job run one after another in -one `tests/testcases` boot never had an instance past 3 served connections, 1 -waiting on its hello or 0 streams, nor more than 5 of DATA's connections served -at once; the desktop at rest holds 3. A parallel build (cargo with rustc as its -direct children) is the obvious unmeasured load. - -## Exit condition - -A server's bounds are shared per session, so that one session holding all its -programs may hold — through as many launches as its `starts` allow — leaves -another session's first connection and first stream on that server answered, -shown by a test that does exactly that. diff --git a/issues/four-shares-take-a-file-server.md b/issues/four-shares-take-a-file-server.md new file mode 100644 index 00000000000..4ef7ec523b4 --- /dev/null +++ b/issues/four-shares-take-a-file-server.md @@ -0,0 +1,69 @@ +--- +status: open +kind: defect +opened: 2026-10-07 +--- + +# Four shares take a file server + +`userland/fileserver/src/main.rs` gives each share a grant names a quarter of +each machine-wide bound: 32 of `MAX_SERVED`'s 128 served connections, 8 of +`MAX_HANDSHAKES`' 32 waiting on their hello, 16 of `MAX_STREAMS`' 64 streams. +So four shares at their parts take each bound whole, and a fifth's first +connection or `STREAM` on that server is refused `ResourceExhausted`. + +A share is a service the supervisor started or a login session +(`toyos_manifest::launch`), and neither gets a second by launching: a launch +opens a session only when a `login` row makes it from the machine's session. +What is left is how many shares there are. The shipping boot starts more than +four services that hold DATA's directories, and the compositor's row opens a +session for each program its taskbar starts, so four of them, each at its +parts, refuse the rest. Nothing counts the sessions open, and no session ever +ends (`issues/every-program-sees-only-the-files-it-was-given.md`, "Known +weaknesses"). + +The parts are sized against what was measured, not against a share's need: +every boot of the guest suite, the shipping desktop booted under QEMU, and +every shipping Rust job run one after another in one `tests/testcases` boot +never had one start past 3 served connections, 1 waiting on its hello or 0 +streams, nor more than 5 of DATA's connections served at once. A parallel +build (cargo with rustc as its direct children) is the unmeasured load. + +Owner: the file server. + +## Exit condition + +However many shares are held, each at all a server lets it hold, the next +share's first connection and first stream on that server are answered, or +opening the session that would hold it is refused by name; shown by a test +that does exactly that. + +## Every remote login shares the session of the shell that started sshserver + +No shipped image starts sshserver at boot (`src/build.rs`, +`no_shipped_boot_config_starts_sshserver`), so on a shipped machine it is +detached from a desktop shell and runs in that shell's login session. Its +`login` row opens no session there, so every remote login's programs spend +one share with each other and with the terminal that started the server: one +login at its parts has every other's next connection or stream on that server +refused, and that terminal's. Only where a boot config starts sshserver as a +service (`tests/lantalkcase`, `tests/metalcase`) does each launch it makes +open a session of its own. + +**Ruled** (owner, 2026-10-07): + +- The goal is one session per login, each with its own share and its own view + of the files. +- The right to open a session is a capability the machine hands out: the + supervisor starts sshserver as a machine service. +- Sessions are counted, they end, and their number is capped. +- Nothing inside a login session opens another. +- Until then the shared session stands, recorded here as a weakness. + +Owner: stage 3 of `issues/every-program-sees-only-the-files-it-was-given.md`, +which removes it. + +**Exit**: on a shipped machine sshserver runs as a service the supervisor +started, in the machine's session; two logins through it hold two shares, shown by a test in which one at its parts leaves the +other's first connection and first stream answered; a login past the cap is +refused by name; and a login's end ends its session. diff --git a/issues/the-shell-runs-a-command-whose-redirect-it-could-not-open.md b/issues/the-shell-runs-a-command-whose-redirect-it-could-not-open.md new file mode 100644 index 00000000000..ddde1e8f523 --- /dev/null +++ b/issues/the-shell-runs-a-command-whose-redirect-it-could-not-open.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-07 +--- + +# The shell runs a command whose redirect it could not open + +`apply_redirect` (`userland/shell/src/main.rs`) answers a redirect target it +could not open by printing `cannot open: ` and returning, and its +callers spawn the command all the same, with the shell's own output in place +of the file. `echo secret > /somewhere/refused` therefore writes `secret` to +the terminal, or into whatever the shell's output is, and the line's status is +the command's. The `2>` arms do the same without the line. + +Read from the source, not measured. `tests/toyos-rust-tests/src/bin/fs_share.rs` +launches a shell whose redirect's connection is refused and judges only that +the file was never made, so it does not hold this behaviour in place. + +Owner: the shell. + +## Exit condition + +A command whose redirect could not be opened is not run, the shell says the +error the open answered, and the line's status is a failure; a guest probe +redirects into a path that does not open and reads back neither the command's +output nor a zero status. diff --git a/src/build.rs b/src/build.rs index 9c2b6563646..e9001df3ffa 100644 --- a/src/build.rs +++ b/src/build.rs @@ -171,7 +171,8 @@ struct ProgramConfig { restart: bool, /// The rows it may start through the launcher (`toyos_manifest::Program::starts`). starts: Vec, - /// A launch it makes opens a login session (`toyos_manifest::Program::login`). + /// A launch it makes from the machine's session opens a login session + /// (`toyos_manifest::Program::login`). login: bool, } diff --git a/system.toml b/system.toml index d93d01f23fa..9b1481308f9 100644 --- a/system.toml +++ b/system.toml @@ -77,8 +77,9 @@ service = true serves = ["netstack"] devices = ["pci:1af4:1041"] -# Every launch it makes opens a login session, the one `update` and `swap` -# below start in. +# Every launch it makes is in a login session, the one `update` and `swap` +# below start in: one it opens when it runs in the machine's session, and the +# one it was launched in otherwise, as when a desktop shell starts it. [programs.sshserver] service = true receives = ["netstack"] diff --git a/tests/proctreecase/system.toml b/tests/proctreecase/system.toml index c24edd7bfe8..4c53742d095 100644 --- a/tests/proctreecase/system.toml +++ b/tests/proctreecase/system.toml @@ -13,8 +13,10 @@ # session it was launched in: refused in the machine's, started in the one the # shell opens. # -# And `fs_share`: the shell test-runner's row lists is launched with grants of -# its own, so it is another instance on DATA's server. +# 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 +# shell launched down from it, whose `login` row opens none there. [boot] start = ["logkeeper", "diskserver", "fileserver", "test-runner"] @@ -39,7 +41,7 @@ syscap = ["roster"] starts = ["swap"] [programs.shell] -starts = ["toybox"] +starts = ["toybox", "shell"] login = true [programs.swap] diff --git a/tests/toyos-rust-tests/src/bin/fs_share.rs b/tests/toyos-rust-tests/src/bin/fs_share.rs index d47331c7577..cf74f947b03 100644 --- a/tests/toyos-rust-tests/src/bin/fs_share.rs +++ b/tests/toyos-rust-tests/src/bin/fs_share.rs @@ -1,24 +1,33 @@ -//! One program holding all a file server lets it hold leaves the server -//! answering another. +//! A file server's bounds are shared out by share: one holding all it may +//! leaves the server answering another, and no launch made in a login session +//! gives that session a second. //! -//! This job holds test-runner's grants, so it and every child it spawns -//! directly are one instance. On DATA's server: +//! This job holds test-runner's grants and launcher: test-runner's share, in +//! the machine's session. On DATA's server: //! //! - each of DATA's directories is its grant's root: a file made under //! `/home` is under none of the others; -//! - it takes every stream the server will give it, until one is refused, and -//! every connection the server will serve it, until one is refused; -//! - holding all that, it launches a shell, whose row test-runner's lists, so -//! the shell is an instance of its own: it opens a file under `/home` and -//! streams a child's output into it, and the bytes are read back here; -//! - last, it opens more connections than any one instance may have waiting -//! on their hello: the ones past its share are answered `ResourceExhausted` +//! - it takes every stream the server will give its share, until one is +//! refused; a shell it launches, which opens no session, then has its +//! redirect's stream refused; +//! - it takes every connection the server will serve its share, until one is +//! refused; a shell it launches then has its redirect's connection refused; +//! - holding all that, a shell it launches launches another, which that +//! shell's `login` row opens a login session for. Down a chain of shells +//! each launching the next, every one in that session, each streams a +//! child's output into a file and holds one more stream, its next shell's +//! output. The first's file shows another session's first connection and +//! first stream answered; every one's up to the session's share of streams +//! is written, and the next shell's is refused: a `login` row's launches in +//! a login session opened none; +//! - last, it opens more connections than one share may have waiting on +//! their hello: the ones past it are answered `ResourceExhausted` //! and let go as the server takes them, so the first to end is not the //! first opened, which the server would otherwise let go first, at its //! handshake timeout; a hello on it, which finds the server gone, reads //! why, and every later one keeps none of the windows it could not lend. //! Last, because the server reaps the ones this job drops only when it -//! next reads them, and until then they are this instance's share. +//! next reads them, and until then they are this share's. use std::fs; use std::process::Command; @@ -32,12 +41,15 @@ use toyos_abi::syscall::SyscallError; use toyos_abi::RawHandle; const DIR: &str = "/home/fs_share"; -const OTHER: &str = "/home/fs_share/other"; +const SAME_STREAM: &str = "/home/fs_share/same_stream"; +const SAME_CONNECTION: &str = "/home/fs_share/same_connection"; +const FIRST: &str = "/home/fs_share/first"; +const PAST: &str = "/home/fs_share/past"; const MARK: &str = "/home/fs_share/mark"; const SAID: &str = "answered"; -/// More connections than one instance's share of a server's handshakes: the -/// server's own machine-wide bound on them. +/// More connections than one share of a server's handshakes: the server's own +/// machine-wide bound on them. const UNANSWERED: usize = 32; /// Far past any bound a server keeps, so a server that refuses nothing ends the @@ -53,7 +65,9 @@ fn main() { // std's own connection to /home, made before anything is held, is what // the shell's file is read back through. fs::create_dir_all(DIR).expect("make the test's directory"); - let _ = fs::remove_file(OTHER); + for file in [SAME_STREAM, SAME_CONNECTION, FIRST, PAST] { + let _ = fs::remove_file(file); + } // Every arm runs, so one run says each one that is red. let mut red = Vec::new(); @@ -66,7 +80,7 @@ fn main() { } } - // Every stream the server gives this instance. + // Every stream the server gives this share. let mut dir = Dir::connect(names, "fs:/home").expect("a client of /home"); let file = dir.open("fs_share/streams", O_WRITE | O_CREATE).expect("open a file to stream into"); let mut streams = Vec::new(); @@ -80,11 +94,18 @@ fn main() { } }; match refused { - Some(SyscallError::ResourceExhausted) => println!(" this instance holds {} streams", streams.len()), + Some(SyscallError::ResourceExhausted) => println!(" this share holds {} streams", streams.len()), other => red.push(format!("after {} streams, the next was answered {other:?}", streams.len())), } + // A launch that opens no session: its redirect connects and opens the + // file, and the stream into it is refused, so it stays empty. + let shell = run_shell(&format!("/system/bin/toybox echo {SAID} > {SAME_STREAM}")).output().expect("launch a shell"); + match fs::read_to_string(SAME_STREAM) { + Ok(text) if text.is_empty() => println!(" a shell launched under this share was refused a stream"), + other => red.push(format!("a shell launched under this share streamed {other:?}: {shell:?}")), + } - // Every connection the server serves this instance, all lent one window. + // Every connection the server serves this share, all lent one window. let window = SharedMemory::create(WINDOW_BYTES).expect("a window"); let mut served = Vec::new(); let refused = loop { @@ -99,22 +120,47 @@ fn main() { }; match refused { Some(SyscallError::ResourceExhausted) => { - println!(" this instance is served {} more connections", served.len()) + println!(" this share is served {} more connections", served.len()) } other => red.push(format!("after {} connections, the next hello was answered {other:?}", served.len())), } + // A launch that opens no session: its redirect's connection is refused, + // so the file is never made. + let shell = run_shell(&format!("/system/bin/toybox echo {SAID} > {SAME_CONNECTION}")).output().expect("launch a shell"); + match fs::metadata(SAME_CONNECTION) { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + println!(" a shell launched under this share was refused a connection") + } + other => red.push(format!("a shell launched under this share made {SAME_CONNECTION}: {other:?}, {shell:?}")), + } - // Another instance, while this one holds all that. - let shell = Command::new("/system/bin/shell") - .args(["-c", &format!("/system/bin/toybox echo {SAID} > {OTHER}")]) - .output() - .expect("launch a shell"); - if !shell.status.success() { - red.push(format!("the other instance's shell failed: {shell:?}")); + // A login session, while this share holds all that: the shell this job + // launches opens none, and the one it launches is in one its row opens. + // The first shell in it streams, and every shell holds its next one's + // output, one stream more, until the session holds as many as this share + // does; the last asks for one past that. Each link's line is a variable + // the next shell expands, so no line is quoted inside another. + let links = streams.len() + 1; + let mut chain = run_shell("/system/bin/shell -c $FS_SHARE_1"); + for link in 1..=links { + let next = format!("/system/bin/shell -c $FS_SHARE_{} > {DIR}/next_{link}", link + 1); + let line = match link { + 1 => format!("/system/bin/toybox echo {SAID} > {FIRST} ; {next}"), + _ if link == links => format!("/system/bin/toybox echo {SAID} > {PAST}"), + _ => next, + }; + chain.env(format!("FS_SHARE_{link}"), line); } - match fs::read_to_string(OTHER) { - Ok(text) if text.trim_end() == SAID => println!(" another instance connected and streamed"), - other => red.push(format!("another instance's stream wrote {other:?}")), + let chain = chain.output(); + match fs::read_to_string(FIRST) { + Ok(text) if text.trim_end() == SAID => println!(" a login session connected and streamed"), + other => red.push(format!("a login session's first stream wrote {other:?}: {chain:?}")), + } + match fs::read_to_string(PAST) { + Ok(text) if text.is_empty() => { + println!(" a login session's launches held {} streams and the next was refused", links - 1) + } + other => red.push(format!("past {} streams, a login session's launch streamed {other:?}: {chain:?}", links - 1)), } // Connections that never say hello. @@ -157,3 +203,12 @@ fn main() { assert!(red.is_empty(), "fs_share:\n {}", red.join("\n ")); println!("fs_share: PASS"); } + +/// A shell launched through test-runner's launcher, under this job's share, +/// running `line`, from `/`: no file server's directory, so a redirect is the +/// only file it opens. +fn run_shell(line: &str) -> Command { + let mut shell = Command::new("/system/bin/shell"); + shell.args(["-c", line]).current_dir("/"); + shell +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 781e5bdec42..2c078c18d0c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -127,9 +127,10 @@ const RUST_SKIP: &[&str] = &[ // A kernel primitive with no use for any one boot's devices: the // `port_badge` metal row runs it on tests/proctreecase. "port_badge", - // Needs a launcher whose row lists a program that streams into a file, so - // that another instance asks DATA's server while this one holds its - // shares: the `fs_share` metal row runs it on tests/proctreecase. + // Needs a launcher whose row lists a shell, and a shell whose row opens a + // login session and lists a shell, so that a login session and its + // 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 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. @@ -691,8 +692,8 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: PROCTREECASE, judge: |b| b[0].job_passed("test_rs_port_badge") }, ), ( - // One instance holding all a file server lets it hold leaves the - // server answering another. + // One share holding all a file server lets it hold leaves the server + // answering a login session, whose launches all spend its one share. "fs_share", metal::Metal { arms: PROCTREECASE, judge: |b| b[0].job_passed("test_rs_fs_share") }, ), @@ -921,8 +922,8 @@ const METALCASE: &[metal::Arm] = &[metal::once("metalcase", "tests/metalcase", & /// 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, and the shell `fs_share` -/// asks DATA's server through. +/// rows `launch_authority` is refused and started, and the shells `fs_share` +/// asks DATA's server through, under its share and in a login session. const PROCTREECASE: &[metal::Arm] = &[metal::once( "proctreecase", "tests/proctreecase", @@ -2910,7 +2911,7 @@ fn process_tree(back: &metal::Readback) -> Result<(), String> { /// 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, Session}; + use toyos_manifest::launch::{refused, Refusal, Sessions}; back.job_passed("test_rs_launch_authority")?; let log = back.log(); for (caller, target, why) in [ @@ -2919,7 +2920,7 @@ fn launch_authority(back: &metal::Readback) -> Result<(), String> { ("test-runner", "update", Refusal::OutsideLogin), ("toybox", "swap", Refusal::OutsideLogin), ] { - let line = refused(caller, Session::Machine, target, why); + 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())); } diff --git a/toyos-manifest/src/launch.rs b/toyos-manifest/src/launch.rs index c93347b1cbb..870b372e0e7 100644 --- a/toyos-manifest/src/launch.rs +++ b/toyos-manifest/src/launch.rs @@ -3,9 +3,17 @@ //! **A caller starts only the rows its own row's `starts` lists**: program //! keys, and [`APPS`] for any installed package. **A row that replaces what the //! machine runs starts only in a login session** ([`Program::login_only`]). Both -//! are the owner's ruling. A launch by a row marked `login` opens a login -//! session; every other launch is in its caller's session, and a row the -//! supervisor starts at boot is in the machine's. +//! are the owner's ruling. A launch by a row marked `login`, made in the +//! machine's session, opens a login session of its own ([`Sessions`]); every +//! other launch, a `login` row's in a login session included, is in its +//! caller's session, and a row the supervisor starts itself is in the machine's. +//! +//! **A share is what a file server bounds** ([`Session::share`]): each service +//! the supervisor starts itself has one of its own, each login session has +//! one, and every launch that opens no session spends its caller's. So no +//! number of launches a service or a login session makes, through whatever +//! its rows' `starts` reach, gives it more of a server: only the machine's +//! session opens one. //! //! **The caller is the badge on its connection, not its word** ([`Authority`]): //! the supervisor mints it on the launcher it endows a row, the kernel stamps it @@ -25,24 +33,75 @@ use crate::{package, Program, MAX_PROGRAM_NAME}; /// `/`, so it names no key. pub const APPS: &str = package::DIR; -/// The session a launched program runs in. +/// The session a launched program runs in, and the share it spends. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Session { - /// A row the supervisor started at boot, and every launch made in it. - Machine, - /// One a `login` row opened, and every launch made in it. - Login, + /// A service the supervisor started itself, and every launch made from it + /// that opens no session. + Machine(Share), + /// One a `login` row's launch from the machine's session opened, and every + /// launch made in it. + Login(Share), +} + +/// Which share of each file server's bounds: a number only [`Sessions`] makes +/// and a badge it was encoded into gives back, so it is no other service's or +/// session's, nor the supervisor's. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Share(u64); + +/// The share the supervisor's own files spend, which is no session's. +pub const SUPERVISOR_SHARE: u64 = 0; +const FIRST_SHARE: u64 = 1; + +impl Session { + /// The number a file grant names this session's share of a server by. + pub fn share(self) -> u64 { + match self { + Self::Machine(Share(n)) | Self::Login(Share(n)) => n, + } + } } impl fmt::Display for Session { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Machine => f.write_str("the machine's session"), - Self::Login => f.write_str("a login session"), + Self::Machine(_) => f.write_str("the machine's session"), + Self::Login(_) => f.write_str("a login session"), } } } +/// The shares the supervisor has handed out. +pub struct Sessions { + next: u64, +} + +impl Default for Sessions { + fn default() -> Self { + Self { next: FIRST_SHARE } + } +} + +impl Sessions { + /// A service the supervisor starts itself: in the machine's session, under + /// a share of its own that every start of it spends. + pub fn machine(&mut self) -> Session { + Session::Machine(self.share()) + } + + /// A login session none before it was. + pub fn open(&mut self) -> Session { + Session::Login(self.share()) + } + + fn share(&mut self) -> Share { + let n = self.next; + self.next = n.checked_add(1).expect("2^64 shares handed out in one boot"); + Share(n) + } +} + /// What the supervisor mints on the launcher it endows `row`: the row a /// connection's caller holds, and the session it runs in. #[derive(Clone, Debug, PartialEq, Eq)] @@ -54,16 +113,17 @@ pub struct Authority { const MACHINE: u8 = 0; const LOGIN: u8 = 1; -const _: () = assert!(MAX_PROGRAM_NAME < MAX_BADGE, "an authority, a kind byte and a row, must fit one badge"); +const _: () = assert!(1 + 8 + MAX_PROGRAM_NAME <= MAX_BADGE, "an authority, a kind byte, a share and a row, must fit one badge"); impl Authority { - /// The session's kind, then the row's key. + /// The session's kind, its share, then the row's key. pub fn encode(&self) -> Vec { - let mut out = Vec::with_capacity(1 + self.row.len()); + let mut out = Vec::with_capacity(1 + 8 + self.row.len()); out.push(match self.session { - Session::Machine => MACHINE, - Session::Login => LOGIN, + Session::Machine(_) => MACHINE, + Session::Login(_) => LOGIN, }); + out.extend_from_slice(&self.session.share().to_le_bytes()); out.extend_from_slice(self.row.as_bytes()); out } @@ -71,12 +131,16 @@ impl Authority { /// `None` for bytes [`Self::encode`] cannot have written. pub fn decode(bytes: &[u8]) -> Option { let (&kind, rest) = bytes.split_first()?; + let (share, row) = rest.split_first_chunk::<8>()?; + let share = match u64::from_le_bytes(*share) { + n if n < FIRST_SHARE => return None, + n => Share(n), + }; let session = match kind { - MACHINE => Session::Machine, - LOGIN => Session::Login, + MACHINE => Session::Machine(share), + LOGIN => Session::Login(share), _ => return None, }; - let row = rest; if row.is_empty() || row.len() > MAX_PROGRAM_NAME { return None; } @@ -108,9 +172,28 @@ impl fmt::Display for Refusal { } } -/// The session `target` starts in when `caller`, running in `session`, asks -/// for it, or why it does not start. -pub fn may_start(caller: &Program, session: Session, target: Target<'_>) -> Result { +/// Which session a launch [`may_start`] allowed runs in. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Starts { + /// Its caller's. + In(Session), + /// A login session it opens. + Opening, +} + +impl Starts { + /// The session, opened from `sessions` when the launch opens one. + pub fn session(self, sessions: &mut Sessions) -> Session { + match self { + Self::In(session) => session, + Self::Opening => sessions.open(), + } + } +} + +/// Where `target` starts when `caller`, running in `session`, asks for it, or +/// why it does not start. +pub fn may_start(caller: &Program, session: Session, target: Target<'_>) -> Result { let (listed, program) = match target { Target::Row(row) => (row.name.as_str(), row), Target::Package(row) => (APPS, row), @@ -118,14 +201,16 @@ pub fn may_start(caller: &Program, session: Session, target: Target<'_>) -> Resu if !caller.starts.iter().any(|key| key == listed) { return Err(Refusal::NotListed); } - let session = match caller.login { - true => Session::Login, - false => session, + // Only from the machine's session: a login session reaching a `login` row + // through `starts` would otherwise hold a share per launch of it. + let starts = match (caller.login, session) { + (true, Session::Machine(_)) => Starts::Opening, + _ => Starts::In(session), }; - if program.login_only() && session != Session::Login { + if program.login_only() && matches!(starts, Starts::In(Session::Machine(_))) { return Err(Refusal::OutsideLogin); } - Ok(session) + Ok(starts) } /// The supervisor's line for a refused launch, which the metal judge reads whole. @@ -135,6 +220,8 @@ pub fn refused(caller: &str, session: Session, target: &str, why: Refusal) -> St #[cfg(test)] mod tests { + use std::collections::BTreeSet; + use super::*; fn row(name: &str) -> Program { @@ -162,60 +249,102 @@ mod tests { fn a_caller_starts_what_its_row_lists_and_swap_and_update_only_in_a_login_session() { let (ordinary, unlisted, own, package) = (row("ordinary"), row("unlisted"), caller(false), row("gbae")); let (swap, update) = (swap(), update()); - let (machine, login) = (Session::Machine, Session::Login); + let mut sessions = Sessions::default(); + let (machine, login) = (sessions.machine(), sessions.open()); use Refusal::*; + use Starts::*; #[rustfmt::skip] - let table: [(bool, Session, Target, Result); 24] = [ - (false, machine, Target::Row(&ordinary), Ok(machine)), - (false, login, Target::Row(&ordinary), Ok(login)), + let table: [(bool, Session, Target, Result); 24] = [ + (false, machine, Target::Row(&ordinary), Ok(In(machine))), + (false, login, Target::Row(&ordinary), Ok(In(login))), (false, machine, Target::Row(&unlisted), Err(NotListed)), (false, login, Target::Row(&unlisted), Err(NotListed)), - (false, machine, Target::Row(&own), Ok(machine)), - (false, login, Target::Row(&own), Ok(login)), - (false, machine, Target::Package(&package), Ok(machine)), - (false, login, Target::Package(&package), Ok(login)), + (false, machine, Target::Row(&own), Ok(In(machine))), + (false, login, Target::Row(&own), Ok(In(login))), + (false, machine, Target::Package(&package), Ok(In(machine))), + (false, login, Target::Package(&package), Ok(In(login))), (false, machine, Target::Row(&swap), Err(OutsideLogin)), - (false, login, Target::Row(&swap), Ok(login)), + (false, login, Target::Row(&swap), Ok(In(login))), (false, machine, Target::Row(&update), Err(OutsideLogin)), - (false, login, Target::Row(&update), Ok(login)), - (true, machine, Target::Row(&ordinary), Ok(login)), - (true, login, Target::Row(&ordinary), Ok(login)), + (false, login, Target::Row(&update), Ok(In(login))), + (true, machine, Target::Row(&ordinary), Ok(Opening)), + (true, login, Target::Row(&ordinary), Ok(In(login))), (true, machine, Target::Row(&unlisted), Err(NotListed)), (true, login, Target::Row(&unlisted), Err(NotListed)), - (true, machine, Target::Row(&own), Ok(login)), - (true, login, Target::Row(&own), Ok(login)), - (true, machine, Target::Package(&package), Ok(login)), - (true, login, Target::Package(&package), Ok(login)), - (true, machine, Target::Row(&swap), Ok(login)), - (true, login, Target::Row(&swap), Ok(login)), - (true, machine, Target::Row(&update), Ok(login)), - (true, login, Target::Row(&update), Ok(login)), + (true, machine, Target::Row(&own), Ok(Opening)), + (true, login, Target::Row(&own), Ok(In(login))), + (true, machine, Target::Package(&package), Ok(Opening)), + (true, login, Target::Package(&package), Ok(In(login))), + (true, machine, Target::Row(&swap), Ok(Opening)), + (true, login, Target::Row(&swap), Ok(In(login))), + (true, machine, Target::Row(&update), Ok(Opening)), + (true, login, Target::Row(&update), Ok(In(login))), ]; for (i, (opens, session, target, want)) in table.into_iter().enumerate() { assert_eq!(may_start(&caller(opens), session, target), want, "row {i}"); } } + fn launch(sessions: &mut Sessions, caller: &Program, session: Session, target: &Program) -> Session { + may_start(caller, session, Target::Row(target)).expect("listed").session(sessions) + } + + /// The shipping rows' chains. Each start the supervisor makes itself has a + /// share of its own, which a launch by a row that is not `login` spends. + /// The compositor, a `login` row in the machine's session, opens a session + /// on each launch; every launch in that session spends its share, down a + /// chain of shells and through sshserver, a `login` row it reaches, and + /// the shell sshserver launches. No two of those shares are one, nor any + /// the supervisor's. + #[test] + fn a_sessions_launches_spend_its_one_share() { + let runner = Program { starts: vec!["toybox".into()], ..row("test-runner") }; + let compositor = Program { starts: vec!["terminal".into()], login: true, ..row("compositor") }; + let terminal = Program { starts: vec!["shell".into()], ..row("terminal") }; + let shell = Program { starts: vec!["shell".into(), "sshserver".into()], ..row("shell") }; + let sshserver = Program { starts: vec!["shell".into()], login: true, ..row("sshserver") }; + let mut sessions = Sessions::default(); + let (desktop, tests) = (sessions.machine(), sessions.machine()); + assert_eq!(launch(&mut sessions, &runner, tests, &row("toybox")), tests, "a boot row's launch left its share"); + let mut shares = vec![SUPERVISOR_SHARE, desktop.share(), tests.share()]; + for _ in 0..2 { + let opened = launch(&mut sessions, &compositor, desktop, &terminal); + let mut session = launch(&mut sessions, &terminal, opened, &shell); + for _ in 0..8 { + session = launch(&mut sessions, &shell, session, &shell); + } + session = launch(&mut sessions, &shell, session, &sshserver); + session = launch(&mut sessions, &sshserver, session, &shell); + assert_eq!(session, opened, "a login session's launches left it"); + shares.push(opened.share()); + } + let distinct: BTreeSet = shares.iter().copied().collect(); + assert_eq!(distinct.len(), shares.len(), "two shares are one: {shares:?}"); + } + /// A row that lists nothing starts nothing, a package included, and `/apps` /// lists no row: a key has no `/`. #[test] fn nothing_listed_is_nothing_started_and_apps_names_no_row() { let none = row("none"); let (ordinary, package) = (row("ordinary"), row("gbae")); + let machine = Sessions::default().machine(); for target in [Target::Row(&ordinary), Target::Package(&package)] { - assert_eq!(may_start(&none, Session::Machine, target), Err(Refusal::NotListed)); + assert_eq!(may_start(&none, machine, target), Err(Refusal::NotListed)); } let apps_only = Program { starts: vec![APPS.into()], ..row("apps") }; - assert_eq!(may_start(&apps_only, Session::Machine, Target::Row(&ordinary)), Err(Refusal::NotListed)); - assert_eq!(may_start(&apps_only, Session::Machine, Target::Package(&package)), Ok(Session::Machine)); + assert_eq!(may_start(&apps_only, machine, Target::Row(&ordinary)), Err(Refusal::NotListed)); + assert_eq!(may_start(&apps_only, machine, Target::Package(&package)), Ok(Starts::In(machine))); } #[test] fn an_authority_reads_back_as_written() { + let mut sessions = Sessions::default(); for authority in [ - Authority { row: "terminal".into(), session: Session::Machine }, - Authority { row: "x".repeat(MAX_PROGRAM_NAME), session: Session::Login }, - Authority { row: "s".into(), session: Session::Login }, + Authority { row: "terminal".into(), session: sessions.machine() }, + Authority { row: "x".repeat(MAX_PROGRAM_NAME), session: Session::Login(Share(u64::MAX)) }, + Authority { row: "s".into(), session: sessions.open() }, + Authority { row: "m".into(), session: Session::Machine(Share(u64::MAX)) }, ] { let bytes = authority.encode(); assert!(bytes.len() <= MAX_BADGE); @@ -227,18 +356,24 @@ mod tests { /// authority reads back as it. #[test] fn what_encode_cannot_have_written_is_refused() { - let whole = Authority { row: "shell".into(), session: Session::Login }; + let whole = Authority { row: "shell".into(), session: Sessions::default().open() }; let bytes = whole.encode(); for end in 0..bytes.len() { assert_ne!(Authority::decode(&bytes[..end]).as_ref(), Some(&whole), "a prefix of {end} bytes"); } + let with = |kind: u8, share: u64, row: &[u8]| [&[kind][..], &share.to_le_bytes(), row].concat(); assert_eq!(Authority::decode(&[]), None); - assert_eq!(Authority::decode(&[2, b's']), None); - assert_eq!(Authority::decode(&[MACHINE]), None); - assert_eq!(Authority::decode(&[LOGIN]), None); - assert_eq!(Authority::decode(&[MACHINE, 0xff]), None); - let mut long = vec![MACHINE]; - long.extend(std::iter::repeat_n(b'x', MAX_PROGRAM_NAME + 1)); - assert_eq!(Authority::decode(&long), None); + assert_eq!(Authority::decode(&with(2, FIRST_SHARE, b"s")), None); + for kind in [MACHINE, LOGIN] { + assert_eq!(Authority::decode(&[kind]), None); + // Its share cut short. + assert_eq!(Authority::decode(&[kind, 1, 0, 0, 0, 0, 0, 0]), None); + // No row. + assert_eq!(Authority::decode(&with(kind, FIRST_SHARE, b"")), None); + assert_eq!(Authority::decode(&with(kind, FIRST_SHARE, &[0xff])), None); + // The supervisor's own share, which no session is. + assert_eq!(Authority::decode(&with(kind, SUPERVISOR_SHARE, b"s")), None); + assert_eq!(Authority::decode(&with(kind, FIRST_SHARE, &[b'x'; MAX_PROGRAM_NAME + 1])), None); + } } } diff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs index c9845e57a76..00ef5335115 100644 --- a/toyos-manifest/src/lib.rs +++ b/toyos-manifest/src/lib.rs @@ -28,7 +28,7 @@ //! start the supervisor starts this program at boot //! app-receive a connector every program launched from /apps holds //! starts a row this program may start through the launcher, or `/apps` -//! login a launch this program makes opens a login session +//! login a launch it makes from the machine's session opens a login session //! ``` //! //! [`package`] is the other half: what an installed package says about itself, @@ -209,7 +209,8 @@ pub struct Program { /// and [`launch::APPS`] for any installed package. Non-empty is what /// endows it a launcher at all. pub starts: Vec, - /// A launch it makes opens a login session ([`launch`]). + /// A launch it makes from the machine's session opens a login session + /// ([`launch`]). pub login: bool, } diff --git a/toyos/src/fs.rs b/toyos/src/fs.rs index a21a6d90828..5c6b96cb7fa 100644 --- a/toyos/src/fs.rs +++ b/toyos/src/fs.rs @@ -148,10 +148,10 @@ impl Reply { /// supervisor mints the connector with. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Grant<'a> { - /// The start it was minted for. The supervisor counts its starts, so a - /// process it started and every child that process spawned directly, - /// holding the same connectors, are one instance, and spend one share. - pub instance: u64, + /// The share of the server it spends: the supervisor mints one number for + /// one service it starts itself or one login session, and for every + /// launch made from it that opens no session. + pub share: u64, /// The directory, as a path on the role's volume, every path on the /// connection is resolved beneath: `home`, or the empty path for a volume /// served whole. [`canonical`], and at most [`MAX_GRANT_ROOT`] bytes. @@ -161,21 +161,21 @@ pub struct Grant<'a> { /// The format [`Grant::encode`] writes. Carried because a swap replaces a file /// server and not the supervisor, so one server reads grants another build /// minted, and an older one is refused by name rather than read as this one. -const GRANT_VERSION: u8 = 1; +const GRANT_VERSION: u8 = 2; /// The longest root a grant carries: what one badge holds past the version and -/// the instance. +/// the share. pub const MAX_GRANT_ROOT: usize = MAX_BADGE - 1 - 8; impl<'a> Grant<'a> { - /// The version, the instance, then the root: `None` for a root no grant + /// The version, the share, then the root: `None` for a root no grant /// can carry. pub fn encode<'b>(&self, out: &'b mut [u8; MAX_BADGE]) -> Option<&'b [u8]> { if self.root.len() > MAX_GRANT_ROOT || !canonical(self.root) { return None; } out[0] = GRANT_VERSION; - out[1..9].copy_from_slice(&self.instance.to_le_bytes()); + out[1..9].copy_from_slice(&self.share.to_le_bytes()); let end = 9 + self.root.len(); out[9..end].copy_from_slice(self.root.as_bytes()); Some(&out[..end]) @@ -187,9 +187,9 @@ impl<'a> Grant<'a> { if version != GRANT_VERSION || rest.len() < 8 || rest.len() - 8 > MAX_GRANT_ROOT { return None; } - let instance = u64::from_le_bytes(rest[..8].try_into().expect("eight bytes")); + let share = u64::from_le_bytes(rest[..8].try_into().expect("eight bytes")); let root = core::str::from_utf8(&rest[8..]).ok()?; - canonical(root).then_some(Self { instance, root }) + canonical(root).then_some(Self { share, root }) } } @@ -624,8 +624,8 @@ mod tests { #[test] fn a_grant_round_trips_at_every_bound() { let longest = "r".repeat(MAX_GRANT_ROOT); - for (instance, root) in [(0, ""), (1, "home"), (u64::MAX, "home/toy/Documents"), (7, longest.as_str())] { - let grant = Grant { instance, root }; + for (share, root) in [(0, ""), (1, "home"), (u64::MAX, "home/toy/Documents"), (7, longest.as_str())] { + let grant = Grant { share, root }; let mut out = [0u8; MAX_BADGE]; let bytes = grant.encode(&mut out).expect("a root a grant carries"); assert_eq!(Grant::decode(bytes), Some(grant), "{root:?}"); @@ -637,15 +637,15 @@ mod tests { let mut out = [0u8; MAX_BADGE]; let past = "r".repeat(MAX_GRANT_ROOT + 1); for root in ["/home", "home/", "a//b", ".", "a/../b", past.as_str()] { - assert_eq!(Grant { instance: 1, root }.encode(&mut out), None, "{root:?}"); + assert_eq!(Grant { share: 1, root }.encode(&mut out), None, "{root:?}"); } } #[test] fn bytes_no_grant_was_encoded_as_are_refused() { let mut out = [0u8; MAX_BADGE]; - let good = Grant { instance: 3, root: "home" }.encode(&mut out).unwrap().to_vec(); - // Shorter than a version and an instance. + let good = Grant { share: 3, root: "home" }.encode(&mut out).unwrap().to_vec(); + // Shorter than a version and a share. for n in 0..9 { assert_eq!(Grant::decode(&good[..n]), None, "{n} bytes"); } diff --git a/userland/fileserver/src/main.rs b/userland/fileserver/src/main.rs index 0d92dc5736d..a5032365ea6 100644 --- a/userland/fileserver/src/main.rs +++ b/userland/fileserver/src/main.rs @@ -16,11 +16,14 @@ //! the grant's root (`fileserver::resolve`); a write on a read-only volume is //! refused before the volume sees it. //! -//! **One instance cannot take the server.** Beneath each machine-wide bound — +//! **One share cannot take the server.** Beneath each machine-wide bound — //! connections waiting on their hello, connections served, streams — each -//! instance a grant names has a share of its own, so one holding all it may -//! leaves the rest of each bound to the others. Four at their shares take the -//! server (`issues/a-file-servers-shares-are-per-instance-and-one-session-launches-instances.md`). +//! share a grant names has a part of its own, so one holding all it may leaves +//! the rest of each bound to the others. A share is one service the +//! supervisor started itself or one login session, with every child spawned +//! in it and every launch made from it that opens no session +//! (`toyos_manifest::launch`). Four at their parts take the server +//! (`issues/four-shares-take-a-file-server.md`). //! //! **A server never blocks on a client.** Accept and the first frame are two //! events; a request is buffered until whole; every reply is one `try_send`, @@ -69,11 +72,11 @@ const MAX_FIDS: usize = 1024; /// Streams served at once, machine-wide. const MAX_STREAMS: usize = 64; -/// One instance's shares of [`MAX_SERVED`], [`MAX_HANDSHAKES`] and -/// [`MAX_STREAMS`]. Past its share of served clients a hello is answered -/// `ResourceExhausted`, and past its share of streams a `STREAM` is; past its -/// share of handshakes a connection is, as it is taken, and let go. A quarter -/// of each bound, so an instance at its share leaves the bound to three more. +/// One share's parts of [`MAX_SERVED`], [`MAX_HANDSHAKES`] and +/// [`MAX_STREAMS`]. Past its part of served clients a hello is answered +/// `ResourceExhausted`, and past its part of streams a `STREAM` is; past its +/// part of handshakes a connection is, as it is taken, and let go. A quarter +/// of each bound, so a share at its parts leaves the bound to three more. const SERVED_SHARE: usize = MAX_SERVED / 4; const HANDSHAKE_SHARE: usize = MAX_HANDSHAKES / 4; const STREAM_SHARE: usize = MAX_STREAMS / 4; @@ -133,8 +136,8 @@ struct Client { rx: ipc::FrameRx<{ core::mem::size_of::() }>, /// Its grant's root, beneath which every path it names is resolved. root: String, - /// Its grant's instance, whose shares it spends. - instance: u64, + /// Its grant's share, which it spends. + share: u64, window: Option, fids: BTreeMap, next_fid: u64, @@ -146,8 +149,8 @@ struct Stream { pipe: Pipe, node: Node, offset: u64, - /// The instance of the client that asked for it, whose share it spends. - instance: u64, + /// The share of the client that asked for it, which it spends. + share: u64, } fn main() { @@ -421,7 +424,7 @@ impl Server { /// Take the next connection and read its grant. Only the supervisor mints /// on this port, so a connection without one this server reads is let go - /// by name; one whose instance already has its share of handshakes is + /// by name; one whose share already holds its part of the handshakes is /// answered `ResourceExhausted` and let go. fn accept(&mut self) { let conn = match self.acceptor.accept() { @@ -436,13 +439,13 @@ impl Server { }, Err(why) => return println!("fileserver: letting a connection go: it carries no grant ({why:?})"), }; - let mine = self.clients.values().filter(|c| c.instance == grant.instance && c.window.is_none()).count(); + let mine = self.clients.values().filter(|c| c.share == grant.share && c.window.is_none()).count(); if mine >= HANDSHAKE_SHARE { // Let go whether or not the refusal went: either way it is said. let _ = conn.try_send(REPLY, &Reply::refused(SyscallError::ResourceExhausted)); return println!( - "fileserver: letting a connection go: instance {} has {HANDSHAKE_SHARE} waiting on their hello", - grant.instance + "fileserver: letting a connection go: share {} has {HANDSHAKE_SHARE} waiting on their hello", + grant.share ); } let id = self.next_client; @@ -451,7 +454,7 @@ impl Server { conn, rx: ipc::FrameRx::new(), root: grant.root.to_string(), - instance: grant.instance, + share: grant.share, window: None, fids: BTreeMap::new(), next_fid: 1, @@ -463,7 +466,7 @@ impl Server { fn drop_client(&mut self, id: u64, why: &str) { let Some(client) = self.clients.remove(&id) else { return }; if !why.is_empty() { - println!("fileserver: dropping client {id} of {:?}, instance {}: {why}", client.root, client.instance); + println!("fileserver: dropping client {id} of {:?}, share {}: {why}", client.root, client.share); } for fid in client.fids.values() { // Nobody is left to answer: a refused close is said, and its node @@ -557,9 +560,9 @@ impl Server { fn answer(&mut self, id: u64, op: u32, r: Request) -> Answer { if op == HELLO { - let instance = self.clients[&id].instance; + let share = self.clients[&id].share; let served = self.clients.values().filter(|c| c.window.is_some()); - let (all, mine) = served.fold((0, 0), |(all, mine), c| (all + 1, mine + usize::from(c.instance == instance))); + let (all, mine) = served.fold((0, 0), |(all, mine), c| (all + 1, mine + usize::from(c.share == share))); let client = self.clients.get_mut(&id).expect("pumped"); if client.window.is_some() { return Answer::Drop("it lent a second window"); @@ -569,7 +572,7 @@ impl Server { return Answer::Refuse(SyscallError::ResourceExhausted, why); } if mine >= SERVED_SHARE { - let why = format!("it is refused, since its instance is served {SERVED_SHARE} clients already"); + let why = format!("it is refused, since its share is served {SERVED_SHARE} clients already"); return Answer::Refuse(SyscallError::ResourceExhausted, why); } let Some([lent]) = client.conn.recv_handles_exact::<1>() else { @@ -725,8 +728,8 @@ impl Server { return Err(SyscallError::InvalidArgument); } let node = f.node; - let instance = self.clients[&id].instance; - let mine = self.streams.values().filter(|s| s.instance == instance).count(); + let share = self.clients[&id].share; + let mine = self.streams.values().filter(|s| s.share == share).count(); if self.streams.len() >= MAX_STREAMS || mine >= STREAM_SHARE { return Err(SyscallError::ResourceExhausted); } @@ -734,7 +737,7 @@ impl Server { self.volume.hold(node); let sid = self.next_stream; self.next_stream += 1; - self.streams.insert(sid, Stream { pipe: read, node, offset: r.offset, instance }); + self.streams.insert(sid, Stream { pipe: read, node, offset: r.offset, share }); Ok(Answer::WithHandle(Reply::ok(), write.into())) } STAT | LSTAT => { diff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs index 70040c08b53..60f9e790be5 100644 --- a/userland/supervisor/src/main.rs +++ b/userland/supervisor/src/main.rs @@ -68,7 +68,7 @@ use std::time::{Duration, Instant}; use toyos_swap::{Refusal, Request as SwapRequest, Word}; -use toyos_manifest::launch::{self as authority, Authority, Session, Target}; +use toyos_manifest::launch::{self as authority, Authority, Session, Sessions, Target}; use toyos_manifest::package::{self, Package}; use toyos_manifest::{Manifest, Program}; use toyos::endow::Endowments; @@ -380,20 +380,20 @@ fn main() { // One port per file-server role, made here for the same reason: a // program's first open works whether or not its server runs yet. Its // connector goes at once: every connector a program holds to it is a - // grant minted for that program's start (`Grants`). + // grant minted for that program's share (`Grants`). let mut role_acceptors: BTreeMap<&str, Acceptor> = BTreeMap::new(); for role in system.programs.iter().flat_map(|p| p.roles.iter()) { let (acceptor, _) = port::create().unwrap_or_else(|e| panic!("supervisor: no port for the `{role}` role: {e:?}")); role_acceptors.insert(role, acceptor); } // The supervisor's own files are resolved through grants like every - // program's, under the instance no start is given, and it is the one + // program's, under a share no session is given, and it is the one // process nobody endows a namespace: std resolves through this one, and // the stop's syncs through the second. let files: &'static Namespace = { let own: Vec<(String, Connector)> = role_acceptors .iter() - .flat_map(|(role, acceptor)| mint_grants(role, acceptor, SUPERVISOR_INSTANCE)) + .flat_map(|(role, acceptor)| mint_grants(role, acceptor, authority::SUPERVISOR_SHARE)) .collect(); let build = || { let mut builder = namespace::build(); @@ -419,7 +419,8 @@ fn main() { syscap: &syscap, acceptors, connectors, - grants: Grants { roles: Vec::new(), next: Cell::new(SUPERVISOR_INSTANCE + 1) }, + grants: Grants { roles: Vec::new() }, + sessions: Sessions::default(), files, services: Vec::new(), log, @@ -524,6 +525,8 @@ struct Supervisor<'a> { connectors: BTreeMap<&'a str, Connector>, /// What each program's view is minted from. grants: Grants<'a>, + /// The shares its services and the login sessions launches opened spend. + sessions: Sessions, /// The same directories as a namespace of the supervisor's own, for the stop's syncs. files: &'static Namespace, /// What `[boot] start` named, a file server once per role. @@ -554,6 +557,9 @@ struct Service<'a> { /// started in its place is owed. devices: Vec, kept: Arc>, + /// The machine's session under this service's own share, which every + /// start of it spends: one started again holds no more of a file server. + session: Session, /// When each recent start after an end happened, for [`toyos_manifest::RESTARTS`]. restarts: VecDeque, } @@ -582,6 +588,7 @@ impl<'a> Service<'a> { program: &'a Program, role: Option<&'a str>, acceptors: Vec<(String, Acceptor)>, + session: Session, wake: toyos::RawHandle, ) -> Self { Self { @@ -598,6 +605,7 @@ impl<'a> Service<'a> { ended: false, wake, })), + session, restarts: VecDeque::new(), } } @@ -646,7 +654,7 @@ impl<'a> Service<'a> { grants, &[], storage, - (launcher, Session::Machine), + (launcher, self.session), Output::Boot(log), )?; kept.generation += 1; @@ -797,7 +805,7 @@ impl<'a> Supervisor<'a> { } }; self.make_home(program); - let mut service = Service::new(program, role, kept, wake); + let mut service = Service::new(program, role, kept, self.sessions.machine(), wake); if let Some(role) = role { self.grants.roles.push((role, Arc::clone(&service.kept))); } @@ -1548,10 +1556,10 @@ impl Supervisor<'_> { let (row, session) = (caller.row, caller.session); let found = self.files("a launch's files", move || { resolve(system, &path, |target| -> Result<_, authority::Refusal> { - let session = authority::may_start(row, session, target)?; + let starts = authority::may_start(row, session, target)?; let (Target::Row(program) | Target::Package(program)) = target; let prepared = command.image_from(Path::new(&program.path)).prepare().map(drop); - Ok((session, prepared.map(|()| command))) + Ok((starts, prepared.map(|()| command))) }) }); let resolved = match found { @@ -1585,7 +1593,7 @@ impl Supervisor<'_> { }; // **`MSG_REFUSED`, never `MSG_NOT_DECLARED`**: the latter is std's cue to // spawn the program itself. - let (session, prepared) = match verdict { + let (starts, prepared) = match verdict { Ok(judged) => judged, Err(why) => { say!("{}", authority::refused(&caller.row.name, caller.session, &program.name, why)); @@ -1601,6 +1609,7 @@ impl Supervisor<'_> { return; } }; + let session = starts.session(&mut self.sessions); let caller_slots: Vec<(u32, toyos::RawHandle)> = request.slot_numbers().zip(slots.0.iter().copied()).collect(); @@ -1912,7 +1921,7 @@ fn start<'a>( command.endow(&label, raw.0); held.0.push(raw); } - if let Some(ns) = build_namespace(program, system, connectors, grants, extras)? { + if let Some(ns) = build_namespace(program, system, connectors, grants.view(program, launcher.1), extras)? { let raw = ns.into_raw(); command.endow(SVC_LABEL, raw.0); held.0.push(raw); @@ -2205,13 +2214,12 @@ fn build_namespace( program: &Program, system: &Manifest, connectors: &BTreeMap<&str, Connector>, - grants: &Grants<'_>, + view: Vec<(String, Connector)>, extras: &[(&str, Connector)], ) -> std::io::Result> { // Never the swap port: [`swap_namespace`] says why. let receives: Vec<&String> = program.receives.iter().filter(|name| *name != toyos_swap::PORT).collect(); - let view = grants.view(program); if receives.is_empty() && extras.is_empty() && view.is_empty() { return Ok(None); } @@ -2243,27 +2251,23 @@ fn build_namespace( } } -/// The instance of the supervisor's own grants, which no start is given. -const SUPERVISOR_INSTANCE: u64 = 0; - /// What each program's directory capabilities are minted from: each -/// file-server role's port, and the count of starts. +/// file-server role's port. /// -/// **Each start is minted grants of its own** (`toyos::fs::Grant`), one per -/// directory of every role, all naming one instance, so the servers count the -/// process and every child it spawns directly, which holds the same -/// connectors, against one share. A role whose ports closed for good is -/// minted nothing. +/// **Each start is minted grants naming its session's share** (`toyos::fs::Grant`, +/// [`Session::share`]), one per directory of every role: a service has a +/// share of its own through every start of it, so the servers count it, every +/// child it spawns directly and every launch made from it that opens no +/// session against one share, and a login session's processes against one +/// more. A role whose ports closed for good is minted nothing. struct Grants<'a> { /// Each role's service, whose kept acceptor is the role's port. roles: Vec<(&'a str, Arc>)>, - /// The instance the next start is minted. - next: Cell, } impl Grants<'_> { - /// The directory capabilities `program` is endowed for one start, by - /// namespace name. + /// The directory capabilities `program` is endowed for one start in + /// `session`, by namespace name. /// /// **Every program sees the whole tree the file servers serve**, which is /// the kernel's old view kept whole until each row declares its own @@ -2271,17 +2275,15 @@ impl Grants<'_> { /// with one exception: a storage row sees none, since a file server /// resolving a path of its own through itself waits for ever. Asked before /// anything is locked, since a storage row's start holds its own kept state. - fn view(&self, program: &Program) -> Vec<(String, Connector)> { + fn view(&self, program: &Program, session: Session) -> Vec<(String, Connector)> { if is_storage(program) { return Vec::new(); } - let instance = self.next.get(); - self.next.set(instance + 1); let mut view = Vec::new(); for (role, kept) in &self.roles { let kept = kept.lock().expect("supervisor: a service's state is poisoned"); if let Some((_, acceptor)) = kept.acceptors.first() { - view.extend(mint_grants(role, acceptor, instance)); + view.extend(mint_grants(role, acceptor, session.share())); } } view @@ -2289,18 +2291,18 @@ impl Grants<'_> { } /// A grant on `acceptor`, the `role`'s port, for each of its directories, -/// naming `instance`: each by the namespace name a program opens it under. -fn mint_grants(role: &str, acceptor: &Acceptor, instance: u64) -> Vec<(String, Connector)> { +/// naming `share`: each by the namespace name a program opens it under. +fn mint_grants(role: &str, acceptor: &Acceptor, share: u64) -> Vec<(String, Connector)> { let dirs = toyos_manifest::role_dirs(role).expect("supervisor: the build refuses a role it does not know"); dirs.iter() .map(|dir| { let mut badge = [0u8; MAX_BADGE]; - let badge = Grant { instance, root: dir.root } + let badge = Grant { share, root: dir.root } .encode(&mut badge) .unwrap_or_else(|| panic!("supervisor: {}'s root {:?} is no grant's", dir.dir, dir.root)); let connector = acceptor .mint(badge) - .unwrap_or_else(|e| panic!("supervisor: no grant on {} for instance {instance}: {e:?}", dir.dir)); + .unwrap_or_else(|e| panic!("supervisor: no grant on {} for share {share}: {e:?}", dir.dir)); (format!("{CAPABILITY_PREFIX}{}", dir.dir), connector) }) .collect()