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
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ members = [
"lib/random",
"lib/rootimage",
"lib/scsi",
"lib/session",
"lib/ssh",
"lib/swap",
"lib/symbols",
Expand Down Expand Up @@ -99,6 +100,7 @@ members = [
"system/host",
"system/inspect",
"system/logkeeper",
"system/logout",
"system/netstack",
"system/netstack/dhcp",
"system/netstack/dns",
Expand All @@ -117,6 +119,7 @@ members = [
"system/ps2server/ps2",
"system/hda",
"system/hda/driver",
"system/session",
"system/shell",
"system/soundserver",
"system/soundserver/mixer",
Expand All @@ -138,6 +141,7 @@ members = [
"system/usbd/xhci",
"system/vdagent",
"system/vdagent/bridge",
"tests/guest/hangupholder",
"tests/guest/kernelprobe",
"tests/guest/metalprobe",
"tests/guest/osbench",
Expand Down Expand Up @@ -243,6 +247,7 @@ toyos-osrelease = { path = "sdk/osrelease" }
# The swap port's name and the one program the image may give it to, so the
# gate on every manifest reads what the supervisor reads.
toyos-swap = { path = "lib/swap" }
toyos-session = { path = "lib/session" }
# QEMU's clipboard channel's port name, id and port count, so the launch reads
# the declaration the guest's agent drives.
toyos-vdagent = { path = "lib/vdagent" }
Expand Down
57 changes: 50 additions & 7 deletions build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ struct ProgramConfig {
restart: bool,
/// The rows it may start through the launcher (`toyos_manifest::Program::starts`).
starts: Vec<String>,
/// A launch it makes from the machine's session opens a login session
/// A login authority, for which the supervisor opens login sessions
/// (`toyos_manifest::Program::login`).
login: bool,
}
Expand Down Expand Up @@ -642,24 +642,26 @@ impl Userland {
/// rather than a constant here and a string in the supervisor.
///
/// Not `launcher`: a row holds it by its `starts`, badged with its row, and no
/// row receives it.
const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest::grants::PORT];
/// row receives it. Nor `login`: a `login` row holds it, badged with its key.
const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest::grants::PORT, toyos_session::ADMIN_PORT];

/// The ports that named rows alone may receive, each with those rows: the
/// swap port, whose holder replaces any service's binary, the grants port,
/// whose holder writes which folder of the home a package sees, the prompt
/// port, whose holder draws above every window and has every key while it
/// does, the input port, whose holders type into the focused window, the
/// clipboard port, whose holder is sent every copy made in the session, and
/// the display port, whose holder sets the mode and the scale every window is
/// drawn at.
/// clipboard port, whose holder is sent every copy made in the session, the
/// display port, whose holder sets the mode and the scale every window is
/// drawn at, and the session-admin port, whose holder signs its user's
/// sessions out.
const HELD_ALONE: &[(&str, &[&str])] = &[
(toyos_swap::PORT, &[toyos_swap::HOLDER]),
(toyos_manifest::grants::PORT, &[toyos_manifest::grants::HOLDER]),
(toyos_manifest::consent::PROMPT, &[toyos_manifest::consent::SERVER]),
(toyos_input::wire::PORT, toyos_input::wire::HOLDERS),
(toyos_manifest::clipboard::PORT, &[toyos_manifest::clipboard::HOLDER]),
(toyos_manifest::display::PORT, &[toyos_manifest::display::HOLDER]),
(toyos_session::ADMIN_PORT, &[toyos_session::ADMIN_HOLDER]),
];

/// Rows, as a refusal names them.
Expand Down Expand Up @@ -771,13 +773,37 @@ fn receives_reach_a_running_server(config: &SystemConfig) -> Result<(), String>
}
}

/// An image with a `login` row declares the session owner's row, which the
/// supervisor starts for each session it opens, and starts it nowhere else:
/// not at boot, and not through a launch.
fn logins_have_their_owner(config: &SystemConfig) -> Result<(), String> {
let owner = toyos_session::OWNER;
if let Some((name, _)) = config.programs.iter().find(|(_, row)| row.login) {
if !config.programs.contains_key(owner) {
return Err(format!("`{name}` is a `login` row, and no `{owner}` row owns its sessions"));
}
}
if config.boot.start.iter().any(|key| key == owner) {
return Err(format!("`{owner}` is in `[boot] start`, and the supervisor starts it for a session alone"));
}
match config.programs.iter().find(|(_, row)| row.starts.iter().any(|key| key == owner)) {
Some((name, _)) => Err(format!("`{name}` starts `{owner}`, which the supervisor starts for a session alone")),
None => Ok(()),
}
}

/// The resolved config as the records the supervisor reads.
///
/// The format, the renderer and the parser are `lib/manifest/`, whose
/// round-trip test is what makes "what the build writes is what the supervisor reads" a
/// fact rather than two hand-matched implementations.
fn render_manifest(config: &SystemConfig) -> Vec<u8> {
for gate in [held_by_their_holders_alone, starts_name_what_a_launch_can_start, receives_reach_a_running_server] {
for gate in [
held_by_their_holders_alone,
starts_name_what_a_launch_can_start,
receives_reach_a_running_server,
logins_have_their_owner,
] {
if let Err(why) = gate(config) {
panic!("the image config cannot be rendered as a manifest: {why}");
}
Expand Down Expand Up @@ -3158,6 +3184,7 @@ mod tests {
"tests/images/latencycase.toml",
"tests/images/launchcase.toml",
"tests/images/libccase.toml",
"tests/images/sessioncase.toml",
"tests/images/metalcase.toml",
"tests/images/metaldevicecase.toml",
"tests/images/netcase.toml",
Expand Down Expand Up @@ -3213,6 +3240,22 @@ mod tests {
}
}

/// Every committed config with a `login` row declares the session owner,
/// and none starts it but the supervisor, for a session.
#[test]
fn a_login_row_has_a_session_owner_only_the_supervisor_starts() {
for cfg in ALL_CONFIGS {
logins_have_their_owner(&load(cfg)).unwrap_or_else(|e| panic!("{cfg}: {e}"));
}
let gate = |text: &str| logins_have_their_owner(&toml::from_str(text).unwrap());
assert!(gate("[programs.compositor]\nlogin = true\n[programs.session]\n").is_ok());
assert!(gate("[programs.compositor]\nlogin = true\n").unwrap_err().contains("no `session` row"));
assert!(gate("[boot]\nstart = [\"session\"]\n[programs.session]\n").is_err());
assert!(gate("[programs.shell]\nstarts = [\"session\"]\n[programs.session]\n").is_err());
let admin: SystemConfig = toml::from_str("[programs.toybox]\nreceives = [\"session-admin\"]\n").unwrap();
assert!(held_by_their_holders_alone(&admin).unwrap_err().contains("only `logout`"));
}

/// The swap port reaches `swap` and nothing else, and the idle slot
/// `update` and nothing else, in every committed config and in a config
/// that tries any other door.
Expand Down
28 changes: 19 additions & 9 deletions images/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ devices = ["acpi"]
receives = ["power"]

# `starts` is what the desktop's taskbar and launcher start. `login = true`
# makes the local desktop a login session, so `swap` and `update` run from its
# shell. `prompt` is the layer above every window, which `filepicker` alone
# reaches: serving it makes the sessions its launches open the screen's, whose
# packages may ask the person at it for a folder. `clipboard` is sent every copy
# makes it the desktop's login authority: the supervisor opens the desktop
# session as it starts it, and its launches are that session's, so `swap` and
# `update` run from its shell. `prompt` is the layer above every window, which
# `filepicker` alone reaches: serving it makes the session opened for it the
# screen's, whose packages may ask the person at it for a folder. `clipboard` is sent every copy
# made in the session, and `vdagent` alone receives it. `input` is where a source
# of keys and pointer reports reaches the owner of input, which it is.
# `display` sets the mode and the scale, and `toybox` alone receives it, for
Expand Down Expand Up @@ -135,15 +136,24 @@ devices = ["pci:02:00:00/1af4:1041"]
drives = ["pci:07:80:00/1af4:1043"]
receives = ["clipboard"]

# 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.
# A login authority: each authenticated connection opens a login session, and
# its shell, and a command as `shell -c`, is launched in it, the one `update`
# and `swap` below start in.
[programs.sshserver]
service = true
receives = ["netstack"]
starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"]
starts = ["shell"]
login = true

# A login session's owner: the supervisor starts one for each session it
# opens, and it parents that session's detached apps and signs it out.
[programs.session]

# Its user's login sessions, listed or signed out: the one row the build lets
# receive `session-admin`. `launch logout 3 --force` ends session 3.
[programs.logout]
receives = ["session-admin"]

# `swap` is the authority to replace a running service's binary; the build
# refuses it to any other program (`build/build.rs`). `ssh <machine> swap netstack
# <sha256> <length> < netstack` runs it for a login.
Expand Down Expand Up @@ -201,7 +211,7 @@ starts = ["shell"]

[programs.shell]
receives = ["surface"]
starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "/apps"]
starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "logout", "/apps"]

[programs.doom]
receives = ["compositor", "soundserver"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ kill reads it. What names a child's image and working directory is
cleanup without cooperation, run on the causing path in bounded steps. init
never dies and stays tiny.

Since stage 5, init is the supervisor and, for each login session, its
owner: "keep running after I close this" in a login session places the app
under that session's owner, and only in the machine's session under the
supervisor.

**Ruled** (owner, 2026-10-21): libc's signals are the focused set this track
plans, over native process events: a child's end (stage 4) and an interrupt,
hang-up or terminate request (stage 6), and no POSIX signal subsystem up
Expand Down Expand Up @@ -172,23 +177,57 @@ an explicit force issues it, and never a timer's.
control pipe, so it watches at most 254 children: the 255th is refused
`EAGAIN`, a `poll` takes 255 descriptors beside its wake pipe, and `SIG_IGN`
is shown over 255 children.
5. **A login is a session under its owner**. Per login the supervisor starts
one session owner, which provides the session's launcher and parents what
its user starts: the desktop's at boot, one per authenticated SSH
connection. `launch <app>` asks it for an independent app, which outlives
the shell that asked; a bare command is the shell's helper, and the shell's
`detach`, which starts a program under the supervisor today, goes. Sign-out
asks the session's tree to quit, shows what stays with cancel and force, and
ends the subtree only on force. Its namespace is
5. **A login is a session under its owner**. **In review** (#1035, the P1b pull
request). Per login the supervisor starts one session owner,
`/system/bin/session`, which accepts on the session's launcher and hands each
connection to the supervisor with its badge; the supervisor serves the
launch in the session of the owner's link, never the request's. It parents
the session's detached apps, which the supervisor spawns under the place
the owner gives it, and the supervisor tells it of each app's start and
end. The desktop's session opens with the screen's row, endowed to it, and
again when it ends; any other `login` row opens one on the supervisor's
`login` port, sshserver one per connection. A `login` row holds no
launcher of the machine's session: its launches are its sessions' alone,
and with none open it launches nothing and says so. `launch <app>` asks the shell's
session for an independent app; a bare command is the shell's helper.
Sign-out asks every app to quit with a hang-up, reports what stays to the
session's controls, keeps the session on a cancel and ends what stays only
on a force; `logout`, the one row holding `session-admin`, lists the
sessions open and signs one out. The machine has one user, so a login
session's `logout` acts on every session. An owner's end other than its
sign-out's is a failure, and a login authority whose sessions failed more
than the restart budget's three times in ten seconds is refused its next
open; no other open is counted. Its namespace is
`issues/a-user-is-a-home-tree-and-a-login-row.md`'s login row; the SSH
session's shell holds no host key and no SSH service authority. *Exit*:
ending the desktop's session owner ends every program of that session and
leaves the compositor and every other session running; closing a shell
leaves an app it launched running under the session owner; a dropped SSH
connection hangs up its session's tree, says what stays and ends it only on
a force; the SSH session's shell can name no host key. Negative control: the
stage reverted whole, where a launched app dies with its shell. Oracle:
session's shell holds no host key and no SSH service authority.
*Exit*: ending the desktop's session owner ends every program of that
session and leaves the compositor and every other session running; closing
a shell leaves an app it launched running under the session owner; a
dropped SSH connection hangs up its session's tree, says what stays and ends
it only on a force; the SSH session's shell can name no host key. Oracle:
systemd-logind, whose session scope ends every process of a login.
- Met in `session_lifecycle`'s QEMU x86-64 boot (`tests/images/sessioncase.toml`):
a detached app outlives its shell; the desktop's sign-out, by its keys,
waits for an app that ignores a hang-up, Escape keeps it and Enter kills
it, and the compositor opens the desktop again; a job's session, opened
on `login`, signs out the same way, `logout` lists it active and ending,
and ten sessions open and sign out in a row. Jobs of a `login`
test-runner run in a session it opens for them.
- The owner's unasked end ending its apps has no guest arm, since nothing
may kill a process by its number: the kernel's parent rule is
`process_tree`'s, and the supervisor's half, placing detached apps under
the owner and reading its unasked end as the session's failure, is
host-tested (`a_detached_app_is_placed_under_its_sessions_owner`,
`an_owner_that_ends_unasked_ends_its_session_as_a_failure`).
- The SSH arms wait for a T14 row of sshserver, which this stage owes:
until then a connection's session and its sign-out are only built.
- Negative control: the stage reverted builds neither the job nor its
image, so the controls are mutations, each red by name: a graceful
sign-out that kills (`session_lifecycle` stalls at Escape's cancel, and
the owner's host laws red); a handed-over launch in its badge's session;
a sibling's sign-out forgetting a failure; one failure refusing a login;
a detached app placed under the supervisor; a `login` row holding the
machine's launcher; any row receiving `session-admin`.
6. **A program is asked to quit**.
- 6.1 **Done** (#995, the kernel's half): `SYS_PROCESS_QUIT` (135: the 124 this
stage named went to `SYS_COUNTERS`), `(process, reason)`, needs `MANAGE`
Expand Down
Loading