Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

69 changes: 69 additions & 0 deletions issues/four-shares-take-a-file-server.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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: <path>` 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.
3 changes: 2 additions & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ struct ProgramConfig {
restart: bool,
/// The rows it may start through the launcher (`toyos_manifest::Program::starts`).
starts: Vec<String>,
/// 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,
}

Expand Down
5 changes: 3 additions & 2 deletions system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
8 changes: 5 additions & 3 deletions tests/proctreecase/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -39,7 +41,7 @@ syscap = ["roster"]
starts = ["swap"]

[programs.shell]
starts = ["toybox"]
starts = ["toybox", "shell"]
login = true

[programs.swap]
Expand Down
115 changes: 85 additions & 30 deletions tests/toyos-rust-tests/src/bin/fs_share.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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();

Expand All @@ -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();
Expand All @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Loading
Loading