Repository navigation
An installed app sees its own package read-only and its own folder as HOME, and nothing else of /apps or /home - #807
Conversation
… HOME, and nothing else of /apps or /home A file grant carries its access: `toyos::fs::Grant` gains `Access` (read-only or read-write), and the badge format goes to version 3, with the access byte between the share and the root. fileserver refuses `PermissionDenied`, before the volume sees it, every request that would change what a read-only connection holds: an open to write, append, truncate, create or create anew, WRITE, TRUNCATE, MKDIR, RMDIR, UNLINK, RENAME, SYMLINK and STREAM. The list is `fileserver::rights::changes`, closed by default, so a request the wire gains is refused on a read-only connection until it is named as a read. HELLO's RIGHT_WRITE answers the grant's access and the volume's together. A row's view is `toyos_manifest::Program::view`. A package launched from /apps is minted two grants on DATA: `apps/<name>` read-only, and `home/toy/Apps/<name>` read-write; nothing of /config, /state, /log or /boot, no other package and no other part of the home. Its HOME is that folder (`Program::home`), which the supervisor makes with Config, Data, Cache and State before every launch. `[apps]` keeps no directory of its own: it is connectors only. Every row the image declares, pkg and the shell among them, keeps the whole tree read-write, as the package track rules the installer holds nothing a shell does not. A grant the package's name puts past the badge is a refused launch, never a supervisor panic. A running swap of fileserver across this change is refused in effect: a server of one side reads the other's grants as no grant and lets every connection go by name. The change lands by image. The `app_view` machine test boots tests/proctreecase, whose test-runner row now lists /apps, installs itself as a package and launches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Mutation and negative-control patches, each applied with H1-rename-readsdiff --git a/userland/fileserver/src/rights.rs b/userland/fileserver/src/rights.rs
index 036a43ac8..468d0ca55 100644
--- a/userland/fileserver/src/rights.rs
+++ b/userland/fileserver/src/rights.rs
@@ -13,7 +13,7 @@ use toyos::fs::*;
pub fn changes(op: u32, flags: u64) -> bool {
match op {
OPEN => flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0,
- HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => false,
+ RENAME | HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => false,
_ => true,
}
}H2-any-access-byte-reads-writediff --git a/toyos/src/fs.rs b/toyos/src/fs.rs
index 303fc2e93..3eccf459f 100644
--- a/toyos/src/fs.rs
+++ b/toyos/src/fs.rs
@@ -206,7 +206,7 @@ impl<'a> Grant<'a> {
let access = match rest[8] {
0 => Access::ReadOnly,
1 => Access::ReadWrite,
- _ => return None,
+ _ => Access::ReadWrite,
};
let root = core::str::from_utf8(&rest[9..]).ok()?;
canonical(root).then_some(Self { share, access, root })H3-package-dir-writablediff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs
index 136603f47..1b3146277 100644
--- a/toyos-manifest/src/lib.rs
+++ b/toyos-manifest/src/lib.rs
@@ -291,7 +291,7 @@ impl Program {
/// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2).
pub fn view(&self) -> Vec<View> {
match self.package() {
- Some(name) => vec![data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)],
+ Some(name) => vec![data_dir(package::Package::dir(name), true), data_dir(app_home(name), true)],
None => whole_tree(),
}
}G1-fileserver-ignores-accessdiff --git a/userland/fileserver/src/main.rs b/userland/fileserver/src/main.rs
index 528372ac7..2a61d9a8b 100644
--- a/userland/fileserver/src/main.rs
+++ b/userland/fileserver/src/main.rs
@@ -459,7 +459,7 @@ impl Server {
rx: ipc::FrameRx::new(),
root: grant.root.to_string(),
share: grant.share,
- writes: grant.access == Access::ReadWrite && self.volume.writable(),
+ writes: self.volume.writable(),
window: None,
fids: BTreeMap::new(),
next_fid: 1,G2-supervisor-mints-every-dir-writablediff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs
index c5e50e840..2b7af7744 100644
--- a/userland/supervisor/src/main.rs
+++ b/userland/supervisor/src/main.rs
@@ -2298,7 +2298,7 @@ impl Grants<'_> {
/// namespace name a program opens it under. A package's name is part of its
/// directories', so one no grant carries is refused, by name.
fn mint(acceptor: &Acceptor, share: u64, dir: &View) -> Result<(String, Connector), String> {
- let access = if dir.write { Access::ReadWrite } else { Access::ReadOnly };
+ let access = Access::ReadWrite;
let mut badge = [0u8; MAX_BADGE];
let badge = Grant { share, access, root: &dir.root }
.encode(&mut badge)NC-whole-change-reverteddiff --git b/system.toml a/system.toml
index fdc2ef3c6..b5430d2c9 100644
--- b/system.toml
+++ a/system.toml
@@ -26,8 +26,7 @@ start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "
# a row read out of one would be a directory deciding what the machine hands
# out; this is the image's answer, one for all of them. Connectors only —
# `devices` and `syscap` have no spelling here, so nothing installed claims
-# hardware or enters the RT band — and no directory: a package sees its own
-# `/apps/<name>` read-only and its own `/home/toy/Apps/<name>`, its `HOME`.
+# hardware or enters the RT band.
[apps]
receives = ["compositor", "soundserver", "filepicker"]
diff --git b/toyos-manifest/src/lib.rs a/toyos-manifest/src/lib.rs
index 136603f47..00ef53351 100644
--- b/toyos-manifest/src/lib.rs
+++ a/toyos-manifest/src/lib.rs
@@ -64,16 +64,6 @@ pub fn session_home() -> String {
format!("/home/{USER}")
}
-/// An installed package's own folder in the session user's home: its `HOME`,
-/// and the one directory of the home its view holds.
-pub fn app_home(name: &str) -> String {
- format!("{}/Apps/{name}", session_home())
-}
-
-/// What an app's own folder holds, made with it: where it keeps its config,
-/// data, cache and state. English on disk, as every home folder is.
-pub const APP_FOLDERS: [&str; 4] = ["Config", "Data", "Cache", "State"];
-
/// Where each system service keeps its own persistent data, one directory per
/// program key.
pub const STATE: &str = "/state";
@@ -106,42 +96,6 @@ pub fn role_dirs(role: &str) -> Option<&'static [RoleDir]> {
ROLES.iter().find(|(name, _)| *name == role).map(|(_, dirs)| *dirs)
}
-/// One directory capability in a program's view: the grant the supervisor
-/// mints on `role`'s port (`toyos::fs::Grant`).
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct View {
- pub role: &'static str,
- /// What the program's namespace calls it, after `fs:`.
- pub dir: String,
- /// Where it is on the role's volume.
- pub root: String,
- /// Whether a request that changes what it holds is served.
- pub write: bool,
-}
-
-/// Every directory every role serves, each read-write: the view of a program
-/// no narrower one is declared for.
-pub fn whole_tree() -> Vec<View> {
- ROLES
- .iter()
- .flat_map(|(role, dirs)| {
- dirs.iter().map(|d| View { role, dir: d.dir.to_string(), root: d.root.to_string(), write: true })
- })
- .collect()
-}
-
-/// The DATA directory `dir`, beneath one DATA serves.
-fn data_dir(dir: String, write: bool) -> View {
- let role = "data";
- let parent = role_dirs(role)
- .expect("DATA is a role")
- .iter()
- .find(|d| dir.strip_prefix(d.dir).is_some_and(|rest| rest.starts_with('/')))
- .unwrap_or_else(|| panic!("manifest: {dir} is beneath no directory DATA serves"));
- let root = format!("{}{}", parent.root, &dir[parent.dir.len()..]);
- View { role, dir, root, write }
-}
-
/// How often a `restart` row is started again before the supervisor gives up on it: at
/// most this many ends inside [`RESTART_WINDOW_SECS`]. Past it the row's ports
/// close, and a client's next connection is answered `Gone`.
@@ -267,32 +221,12 @@ impl Program {
self.slots || self.receives.iter().any(|r| r == SWAP_PORT)
}
- /// The installed package this row launches: a row [`Manifest::app_row`]
- /// made.
- pub fn package(&self) -> Option<&str> {
- package::package_of(&self.path)
- }
-
/// The `HOME` the supervisor starts this row with. A location grants nothing: what
/// the program can reach is its view's business, never this string's.
pub fn home(&self) -> String {
- match (self.service, self.package()) {
- (true, _) => format!("{STATE}/{}", self.name),
- (false, Some(name)) => app_home(name),
- (false, None) => session_home(),
- }
- }
-
- /// The directories this row's program is endowed. **An installed package
- /// sees its own directory read-only and its own folder of the home
- /// read-write, and nothing else any role serves**: no other package, no
- /// other part of the home, no `/config`, `/state`, `/log` or `/boot`.
- /// Every other row sees the whole tree, until each declares its own
- /// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2).
- pub fn view(&self) -> Vec<View> {
- match self.package() {
- Some(name) => vec![data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)],
- None => whole_tree(),
+ match self.service {
+ true => format!("{STATE}/{}", self.name),
+ false => session_home(),
}
}
}
@@ -305,11 +239,10 @@ pub struct Manifest {
/// Names the supervisor serves itself. The supervisor is in every image and is no `[programs]`
/// key, so these have no declaration to come from.
pub supervisor_serves: Vec<String>,
- /// The connectors every program launched from `/apps` is given beside its
- /// view ([`Program::view`]), and nothing else. `/apps` is writable to
- /// the installer, so this row is the image's rather than the package's —
- /// which is why a device class and a `syscap` right have no spelling on
- /// the package side at all.
+ /// The namespace every program launched from `/apps` is given: connectors,
+ /// and nothing else. A package directory is writable, so this row is the
+ /// image's rather than the package's — which is why a device class and a
+ /// `syscap` right have no spelling on the package side at all.
pub apps: Vec<String>,
/// Program names, in the order `[boot] start` gave them — which orders
/// nothing, because every port exists before any server runs.
@@ -617,67 +550,10 @@ mod tests {
let m = sample();
assert_eq!(m.program("soundserver").unwrap().home(), "/state/soundserver");
assert_eq!(m.program("terminal").unwrap().home(), "/home/toy");
+ assert_eq!(m.app_row("gbae", "/apps/gbae/gbae").home(), "/home/toy");
let m = parse("program sshserver /system/bin/sshserver\nservice\nprogram shell /system/bin/shell\n");
assert!(m.program("sshserver").unwrap().service);
assert!(!m.program("shell").unwrap().service);
- // The shell keeps its history under the session's home, in its own
- // `Apps/shell` (`OWN_FOLDER` in `userland/shell`).
- assert_eq!(m.program("shell").unwrap().home(), "/home/toy");
- }
-
- /// **An installed package's `HOME` is its own folder** of the session
- /// user's home, the layout's `/home/<user>/Apps/<name>`.
- #[test]
- fn a_package_s_home_is_its_own_folder() {
- let row = sample().app_row("gbae", "/apps/gbae/gbae");
- assert_eq!(row.package(), Some("gbae"));
- assert_eq!(row.home(), "/home/toy/Apps/gbae");
- assert_eq!(APP_FOLDERS, ["Config", "Data", "Cache", "State"]);
- assert_eq!(sample().program("compositor").unwrap().package(), None);
- }
-
- fn views(row: &Program) -> Vec<(&'static str, String, String, bool)> {
- row.view().into_iter().map(|v| (v.role, v.dir, v.root, v.write)).collect()
- }
-
- /// **An installed package sees its own directory read-only and its own
- /// folder read-write, and nothing else any role serves.** Spelled out
- /// whole, so a third directory, a wider root or a writable package is red.
- #[test]
- fn a_package_s_view_is_its_own_directory_read_only_and_its_own_folder() {
- let row = sample().app_row("gbae", "/apps/gbae/gbae");
- assert_eq!(
- views(&row),
- [
- ("data", "/apps/gbae".into(), "apps/gbae".into(), false),
- ("data", "/home/toy/Apps/gbae".into(), "home/toy/Apps/gbae".into(), true),
- ]
- );
- // The longest name a package has is still beneath its own directories.
- let longest = "n".repeat(MAX_PROGRAM_NAME);
- let row = sample().app_row(&longest, &format!("/apps/{longest}/{longest}"));
- assert_eq!(
- views(&row).into_iter().map(|(_, _, root, write)| (root, write)).collect::<Vec<_>>(),
- [(format!("apps/{longest}"), false), (format!("home/toy/Apps/{longest}"), true)]
- );
- }
-
- /// Every row the image declares sees the whole tree read-write, the
- /// installer and the shell included: neither has authority over `/apps`
- /// the other lacks.
- #[test]
- fn every_declared_row_sees_the_whole_tree_read_write() {
- let m = parse("program pkg /system/bin/pkg\nprogram shell /system/bin/shell\n");
- let whole: Vec<_> = ["/apps", "/config", "/home", "/state", "/log", "/boot"]
- .iter()
- .zip(["apps", "config", "home", "state", "", ""])
- .zip(["data", "data", "data", "data", "log", "boot"])
- .map(|((dir, root), role)| (role, dir.to_string(), root.to_string(), true))
- .collect();
- let s = sample();
- for row in [m.program("pkg").unwrap(), m.program("shell").unwrap(), s.program("compositor").unwrap()] {
- assert_eq!(views(row), whole, "{}", row.name);
- }
}
#[test]
diff --git b/toyos-manifest/src/package.rs a/toyos-manifest/src/package.rs
index b616fc62b..08e3182d5 100644
--- b/toyos-manifest/src/package.rs
+++ a/toyos-manifest/src/package.rs
@@ -4,8 +4,8 @@
//! to resolve a launch, so the format lives beside [`crate::Manifest`] for the
//! same reason: one renderer, one parser, one round-trip test.
//!
-//! **Nothing here is a grant.** `/apps` is writable to every row the image
-//! declares, so a manifest is a peer's claim about itself: it says which binary
+//! **Nothing here is a grant.** `/apps` is writable to every program that can
+//! name it, so a manifest is a peer's claim about itself: it says which binary
//! *of its own directory* a launch starts. A device, a right and another
//! package's binary have no spelling in this file at all.
//!
diff --git b/toyos/src/fs.rs a/toyos/src/fs.rs
index 303fc2e93..5c6b96cb7 100644
--- b/toyos/src/fs.rs
+++ a/toyos/src/fs.rs
@@ -4,8 +4,8 @@
//! **A directory capability is a connector in the program's namespace**, named
//! [`CAPABILITY_PREFIX`] and the absolute directory it serves (`fs:/home`).
//! Each is a connector to its role's one port, which the supervisor minted with
-//! a [`Grant`]: the directory, whether it may be changed, and whose share of
-//! the server it spends. The kernel stamps that on every connection made through it and answers it to the
+//! a [`Grant`]: the directory, and whose share of the server it spends. The
+//! kernel stamps that on every connection made through it and answers it to the
//! port's acceptor alone, so the server reads what was granted off the
//! connection and nothing the client says. A program names a file only under a
//! directory it holds, and the kernel's part is who holds which connector.
@@ -152,64 +152,44 @@ pub struct Grant<'a> {
/// one service it starts itself or one login session, and for every
/// launch made from it that opens no session.
pub share: u64,
- /// Whether a request that changes what the directory holds is served.
- pub access: Access,
/// 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.
pub root: &'a str,
}
-/// What a [`Grant`] lets its holder do to the directory.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Access {
- /// Read, list and stat; every request that would change what the
- /// directory holds is refused `PermissionDenied`.
- ReadOnly,
- ReadWrite,
-}
-
/// 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 = 3;
+const GRANT_VERSION: u8 = 2;
-/// The longest root a grant carries: what one badge holds past the version,
-/// the share and the access.
-pub const MAX_GRANT_ROOT: usize = MAX_BADGE - 1 - 8 - 1;
+/// The longest root a grant carries: what one badge holds past the version and
+/// the share.
+pub const MAX_GRANT_ROOT: usize = MAX_BADGE - 1 - 8;
impl<'a> Grant<'a> {
- /// The version, the share, the access, then the root: `None` for a root
- /// no grant can carry.
+ /// 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.share.to_le_bytes());
- out[9] = match self.access {
- Access::ReadOnly => 0,
- Access::ReadWrite => 1,
- };
- let end = 10 + self.root.len();
- out[10..end].copy_from_slice(self.root.as_bytes());
+ let end = 9 + self.root.len();
+ out[9..end].copy_from_slice(self.root.as_bytes());
Some(&out[..end])
}
/// `None` for bytes [`Self::encode`] cannot have written.
pub fn decode(bytes: &'a [u8]) -> Option<Self> {
let (&version, rest) = bytes.split_first()?;
- if version != GRANT_VERSION || rest.len() < 9 || rest.len() - 9 > MAX_GRANT_ROOT {
+ if version != GRANT_VERSION || rest.len() < 8 || rest.len() - 8 > MAX_GRANT_ROOT {
return None;
}
let share = u64::from_le_bytes(rest[..8].try_into().expect("eight bytes"));
- let access = match rest[8] {
- 0 => Access::ReadOnly,
- 1 => Access::ReadWrite,
- _ => return None,
- };
- let root = core::str::from_utf8(&rest[9..]).ok()?;
- canonical(root).then_some(Self { share, access, root })
+ let root = core::str::from_utf8(&rest[8..]).ok()?;
+ canonical(root).then_some(Self { share, root })
}
}
@@ -645,12 +625,10 @@ mod tests {
fn a_grant_round_trips_at_every_bound() {
let longest = "r".repeat(MAX_GRANT_ROOT);
for (share, root) in [(0, ""), (1, "home"), (u64::MAX, "home/toy/Documents"), (7, longest.as_str())] {
- for access in [Access::ReadOnly, Access::ReadWrite] {
- let grant = Grant { share, access, 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:?} {access:?}");
- }
+ 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:?}");
}
}
@@ -659,38 +637,30 @@ 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 { share: 1, access: Access::ReadWrite, 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 { share: 3, access: Access::ReadOnly, root: "home" }.encode(&mut out).unwrap().to_vec();
- // Shorter than a version, a share and an access.
- for n in 0..10 {
+ 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");
}
- // Another version, and the one before this.
- for version in [GRANT_VERSION - 1, GRANT_VERSION + 1] {
- let mut other = good.clone();
- other[0] = version;
- assert_eq!(Grant::decode(&other), None, "version {version}");
- }
- // An access byte that is neither, which is never read as either.
- for access in [2, 0x80, 0xff] {
- let mut other = good.clone();
- other[9] = access;
- assert_eq!(Grant::decode(&other), None, "access {access}");
- }
+ // Another version.
+ let mut other = good.clone();
+ other[0] = GRANT_VERSION + 1;
+ assert_eq!(Grant::decode(&other), None);
// A root the wire refuses, or not UTF-8.
for root in [&b"/home"[..], b"a/../b", b"a//b", b"\xff"] {
- let mut bad = good[..10].to_vec();
+ let mut bad = good[..9].to_vec();
bad.extend_from_slice(root);
assert_eq!(Grant::decode(&bad), None, "{root:?}");
}
// One byte past the longest root.
- let mut long = good[..10].to_vec();
+ let mut long = good[..9].to_vec();
long.extend(core::iter::repeat_n(b'r', MAX_GRANT_ROOT + 1));
assert_eq!(Grant::decode(&long), None);
}
diff --git b/userland/fileserver/src/lib.rs a/userland/fileserver/src/lib.rs
index 0c29712db..1dc8bb7b9 100644
--- b/userland/fileserver/src/lib.rs
+++ a/userland/fileserver/src/lib.rs
@@ -2,8 +2,7 @@
//! cache every byte of its volume passes through ([`cache`]), the volumes it
//! can serve ([`data`] for the bcachefs DATA role, [`fat`] for FAT32's LOG and
//! BOOT, [`absent`] for a role with no volume this boot), and the resolver that
-//! keeps every path inside the directory a connection was given ([`resolve`]),
-//! and which requests a read-only one is refused ([`rights`]).
+//! keeps every path inside the directory a connection was given ([`resolve`]).
//!
//! **This is the page cache.** A block of the volume — a btree node, a FAT
//! sector, a file's data — is read into [`cache::Cache`] once and served from
@@ -19,6 +18,5 @@ pub mod data;
pub mod disk;
pub mod fat;
pub mod resolve;
-pub mod rights;
pub mod volume;
pub mod writeback;
diff --git b/userland/fileserver/src/main.rs a/userland/fileserver/src/main.rs
index 528372ac7..a5032365e 100644
--- b/userland/fileserver/src/main.rs
+++ a/userland/fileserver/src/main.rs
@@ -13,9 +13,8 @@
//! **A connection is what its grant says** (`toyos::fs::Grant`): the badge
//! the supervisor minted its connector with, which the kernel stamped on it and
//! answers this port's acceptor alone. Every path on it is resolved beneath
-//! the grant's root (`fileserver::resolve`); a request that would change what
-//! it holds, on a read-only grant or a read-only volume, is refused before the
-//! volume sees it (`fileserver::rights`).
+//! the grant's root (`fileserver::resolve`); a write on a read-only volume is
+//! refused before the volume sees it.
//!
//! **One share cannot take the server.** Beneath each machine-wide bound —
//! connections waiting on their hello, connections served, streams — each
@@ -45,7 +44,6 @@ use fileserver::data::{DataVolume, Located, Probed};
use fileserver::disk::{Claimed, Disk, Ram, Served};
use fileserver::fat::FatVolume;
use fileserver::resolve::{self, Found, Refusal as Escape, Resolved};
-use fileserver::rights;
use fileserver::volume::{Kind, Meta, Node, OpenHow, Out, Volume};
use fileserver::writeback::WriteBack;
use toyos::endow::{self, Endowments};
@@ -140,8 +138,6 @@ struct Client {
root: String,
/// Its grant's share, which it spends.
share: u64,
- /// Its grant is read-write and the volume is: what it may change.
- writes: bool,
window: Option<SharedMemory>,
fids: BTreeMap<u64, Fid>,
next_fid: u64,
@@ -459,7 +455,6 @@ impl Server {
rx: ipc::FrameRx::new(),
root: grant.root.to_string(),
share: grant.share,
- writes: grant.access == Access::ReadWrite && self.volume.writable(),
window: None,
fids: BTreeMap::new(),
next_fid: 1,
@@ -587,8 +582,8 @@ impl Server {
Ok(window) => client.window = Some(window),
Err(_) => return Answer::Drop("its window would not map"),
}
- let granted = if client.writes { RIGHT_WRITE } else { 0 };
- return Answer::Reply(Reply { value: granted, ..Reply::ok() });
+ let rights = if self.volume.writable() { RIGHT_WRITE } else { 0 };
+ return Answer::Reply(Reply { value: rights, ..Reply::ok() });
}
if self.clients[&id].window.is_none() {
return Answer::Drop("it asked before it lent a window");
@@ -618,7 +613,9 @@ impl Server {
}
fn serve_one(&mut self, id: u64, op: u32, r: Request) -> Result<Answer, SyscallError> {
- if rights::changes(op, r.flags) && !self.clients[&id].writes {
+ let changes = matches!(op, WRITE | TRUNCATE | MKDIR | RMDIR | UNLINK | RENAME | SYMLINK | STREAM)
+ || (op == OPEN && r.flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0);
+ if changes && !self.volume.writable() {
return Err(SyscallError::PermissionDenied);
}
match op {
diff --git b/userland/fileserver/src/rights.rs a/userland/fileserver/src/rights.rs
deleted file mode 100644
index 036a43ac8..000000000
--- b/userland/fileserver/src/rights.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-//! Which requests change what a connection's directory holds: each is refused
-//! `PermissionDenied` before the volume sees it, on a connection whose grant is
-//! read-only (`toyos::fs::Access`) or whose volume is.
-//!
-//! **Closed by default**: an operation this list does not name as one that
-//! only reads is one that changes, so a request added to the wire is refused on
-//! a read-only connection until it is named here.
-
-use toyos::fs::*;
-
-/// Whether request `op`, with an open's `flags`, may change what the directory
-/// holds.
-pub fn changes(op: u32, flags: u64) -> bool {
- match op {
- OPEN => flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0,
- HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => false,
- _ => true,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- /// Every request the wire has, by what it does to the directory: the
- /// independent spelling `changes` is held to.
- const READS: [u32; 10] = [HELLO, CLOSE, READ, STAT, LSTAT, FSTAT, FSYNC, SYNC, READDIR, READLINK];
- const WRITES: [u32; 8] = [WRITE, TRUNCATE, MKDIR, RMDIR, UNLINK, RENAME, SYMLINK, STREAM];
-
- #[test]
- fn every_request_that_writes_changes_the_directory_and_none_that_reads_does() {
- for op in WRITES {
- assert!(changes(op, 0), "request {op} writes");
- }
- for op in READS {
- assert!(!changes(op, 0), "request {op} only reads");
- }
- // Every request number the wire has is one of the two, or `OPEN`.
- let mut named: Vec<u32> = READS.iter().chain(&WRITES).copied().chain([OPEN]).collect();
- named.sort_unstable();
- assert_eq!(named, (HELLO..=SYNC).collect::<Vec<_>>());
- }
-
- /// An open changes the directory by any one flag that writes, creates or
- /// truncates, alone or with a read.
- #[test]
- fn an_open_changes_the_directory_by_every_flag_but_read() {
- assert!(!changes(OPEN, O_READ));
- assert!(!changes(OPEN, 0));
- for flag in [O_WRITE, O_APPEND, O_CREATE, O_TRUNCATE, O_CREATE_NEW] {
- assert!(changes(OPEN, flag), "flag {flag}");
- assert!(changes(OPEN, flag | O_READ), "flag {flag} with a read");
- }
- }
-
- /// A request number the wire does not have is refused on a read-only
- /// connection, never served as a read.
- #[test]
- fn a_request_the_wire_does_not_have_changes_the_directory() {
- for op in [0, SYNC + 1, REPLY, LINK, u32::MAX] {
- assert!(changes(op, 0), "request {op}");
- }
- }
-}
diff --git b/userland/supervisor/src/main.rs a/userland/supervisor/src/main.rs
index c5e50e840..888a98181 100644
--- b/userland/supervisor/src/main.rs
+++ a/userland/supervisor/src/main.rs
@@ -42,15 +42,8 @@
//! ([`FILES_BOUND`]).
//!
//! **Every program it starts gets `HOME` from its row** (`Program::home`), over
-//! anything a launching caller carried: a service its own `/state/<name>` and
-//! an installed package its own `Apps/<name>` folder of the session user's
-//! home, each made before it runs, and everything else the session user's
-//! home, made at boot.
-//!
-//! **Every program it starts holds the directories its row's view names**
-//! (`Program::view`), each a grant minted for that start ([`Grants`]): an
-//! installed package its own directory read-only and its own folder
-//! read-write, and every other row the whole tree.
+//! anything a launching caller carried: a service its own `/state/<name>`, made
+//! before it runs, and everything else the session user's home, made at boot.
//! A launch of a program no row names is answered with the session's, which
//! the caller's direct spawn carries in place of its own.
//!
@@ -77,9 +70,9 @@ use toyos_swap::{Refusal, Request as SwapRequest, Word};
use toyos_manifest::launch::{self as authority, Authority, Session, Sessions, Target};
use toyos_manifest::package::{self, Package};
-use toyos_manifest::{Manifest, Program, View};
+use toyos_manifest::{Manifest, Program};
use toyos::endow::Endowments;
-use toyos::fs::{Access, Grant, CAPABILITY_PREFIX};
+use toyos::fs::{Grant, CAPABILITY_PREFIX};
use toyos::ipc::{self, Connection, RxStep};
use toyos::launch::{self, Parent, Request, LAUNCHER};
use toyos::namespace::{self, Namespace};
@@ -390,14 +383,9 @@ fn main() {
// process nobody endows a namespace: std resolves through this one, and
// the stop's syncs through the second.
let files: &'static Namespace = {
- let whole = toyos_manifest::whole_tree();
- let own: Vec<(String, Connector)> = whole
+ let own: Vec<(String, Connector)> = role_acceptors
.iter()
- .filter_map(|view| Some((role_acceptors.get(view.role)?, view)))
- .map(|(acceptor, view)| {
- mint(acceptor, authority::SUPERVISOR_SHARE, view)
- .unwrap_or_else(|why| panic!("supervisor: no grant of its own: {why}"))
- })
+ .flat_map(|(role, acceptor)| mint_grants(role, acceptor, authority::SUPERVISOR_SHARE))
.collect();
let build = || {
let mut builder = namespace::build();
@@ -837,22 +825,14 @@ impl<'a> Supervisor<'a> {
}
}
- /// A service's own `HOME`, or an installed package's and its
- /// [`toyos_manifest::APP_FOLDERS`], made before it runs.
+ /// A service's own `HOME`, made before it first runs.
fn make_home(&mut self, program: &Program) {
- let folders: &'static [&str] = match (program.service, program.package()) {
- (true, _) if is_storage(program) => return,
- (true, _) => &[],
- (false, Some(_)) => &toyos_manifest::APP_FOLDERS,
- (false, None) => return,
- };
+ if !program.service || is_storage(program) {
+ return;
+ }
let home = program.home();
let asked = home.clone();
- let made = self.files("a program's home", move || {
- make_dir(&asked)?;
- folders.iter().try_for_each(|folder| make_dir(&format!("{asked}/{folder}")))
- });
- match made {
+ match self.files("a service's home", move || make_dir(&asked)) {
Ok(Ok(())) => {}
Ok(Err(e)) => say!("supervisor: {}: {home} could not be made: {e}", program.name),
Err(why) => say!("supervisor: {}: {home} was not made: {why}", program.name),
@@ -1927,7 +1907,7 @@ fn start<'a>(
command.endow(&label, raw.0);
held.0.push(raw);
}
- if let Some(ns) = build_namespace(program, system, connectors, grants.view(program, launcher.1)?, 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);
@@ -2261,7 +2241,7 @@ fn build_namespace(
/// file-server role's port.
///
/// **Each start is minted grants naming its session's share** (`toyos::fs::Grant`,
-/// [`Session::share`]), one per directory of its row's view: a service has a
+/// [`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
@@ -2273,40 +2253,45 @@ struct Grants<'a> {
impl Grants<'_> {
/// The directory capabilities `program` is endowed for one start in
- /// `session`, by namespace name: its row's view (`Program::view`), but
- /// that 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, session: Session) -> std::io::Result<Vec<(String, Connector)>> {
+ /// `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
+ /// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2),
+ /// 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, session: Session) -> Vec<(String, Connector)> {
if is_storage(program) {
- return Ok(Vec::new());
+ return Vec::new();
}
- let wanted = program.view();
let mut view = Vec::new();
for (role, kept) in &self.roles {
let kept = kept.lock().expect("supervisor: a service's state is poisoned");
- let Some((_, acceptor)) = kept.acceptors.first() else { continue };
- for dir in wanted.iter().filter(|dir| dir.role == *role) {
- view.push(mint(acceptor, session.share(), dir).map_err(std::io::Error::other)?);
+ if let Some((_, acceptor)) = kept.acceptors.first() {
+ view.extend(mint_grants(role, acceptor, session.share()));
}
}
- Ok(view)
+ view
}
}
-/// A grant on `acceptor`, `dir`'s role's port, naming `share`, by the
-/// namespace name a program opens it under. A package's name is part of its
-/// directories', so one no grant carries is refused, by name.
-fn mint(acceptor: &Acceptor, share: u64, dir: &View) -> Result<(String, Connector), String> {
- let access = if dir.write { Access::ReadWrite } else { Access::ReadOnly };
- let mut badge = [0u8; MAX_BADGE];
- let badge = Grant { share, access, root: &dir.root }
- .encode(&mut badge)
- .ok_or_else(|| format!("{}'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 share {share}: {e:?}", dir.dir));
- Ok((format!("{CAPABILITY_PREFIX}{}", dir.dir), connector))
+/// A grant on `acceptor`, the `role`'s port, for each of its directories,
+/// 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 { 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 share {share}: {e:?}", dir.dir));
+ (format!("{CAPABILITY_PREFIX}{}", dir.dir), connector)
+ })
+ .collect()
}
/// [`toyos_swap::PORT`] in a namespace of its own, for a program whose row |
|
Mutation runs on head mut-H1-rename-reads.logmut-H2-any-access-byte-reads-write.logmut-H3-package-dir-writable.logmut-G1-fileserver-ignores-access.logmut-G2-supervisor-mints-every-dir-writable.logmut-NC-whole-change-reverted.log |
|
appview1.logmetal-stage.log (
|
|
Whole guest suite, |
|
|
|
Review of #807 at Net lines: BLOCKER
NOTE
SEND BACK |
|
T14 |
…m end's order (#803) and the .local name's re-probe (#800), into the app grants Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… refused, and app_view is a proctreecase metal row Review of #807, round 1: - A package's folder is `/home/toy/Apps/<name>`, and the shell keeps its history in `Apps/shell`. `Manifest::app_row` now refuses a package named after any row the image declares, by `row_named`, and the supervisor says it as the launch's refusal. - A package's launch is refused (`MSG_REFUSED`) when its folder or one of `APP_FOLDERS` is not a directory after the supervisor made it, the file worker's being busy included. A service's home keeps its old, logged path. - `app_view` moves from a QEMU machine test to a metal row on `tests/proctreecase`, whose judge reads the job's pass, `every arm held`, its closing line, and the supervisor's two refusals; the job gains both refusal arms. - fileserver recognises a request before it judges the grant: a number the wire does not have drops the client on every connection. - `Grants::view` and `mint` cannot fail: a compile-time assertion holds the longest package folder within `MAX_GRANT_ROOT`. - The tautological `APP_FOLDERS` assertion goes; the isolation track says what a package still reaches (`/tmp`, `/system`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…as a defect gbae bfe8dabf8, started with no ROM, opens its file menu on its cwd. Run under a package's view in a QEMU guest (its `list_directory` verbatim, a temporary arm of app_view, posted on #807), the menu reaches no ROM from `/` or `/home/toy`; a ROM given by path loads only from its own folder; its config is kept in its own folder. The exit is the file picker of the package track's stage 6, and the track's running line now points at the defect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…n panicking With the whole change reverted, the launch of the row-named package goes ahead and leaves the app's folder behind, so the plant failed and the job panicked before it named any hole. The negative control is now red by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
gbae measured under a package's view (BLOCKER 4 of round 1), at head gbae-measure.patchdiff --git a/tests/toyos-rust-tests/src/bin/app_view.rs b/tests/toyos-rust-tests/src/bin/app_view.rs
index e299fbfb9..fe17e9d28 100644
--- a/tests/toyos-rust-tests/src/bin/app_view.rs
+++ b/tests/toyos-rust-tests/src/bin/app_view.rs
@@ -51,6 +51,7 @@ const APP: &str = "app";
fn main() {
match std::env::args().nth(1).as_deref() {
Some(APP) => app(),
+ Some("gbae") => gbae(),
_ => job(),
}
}
@@ -86,6 +87,17 @@ fn job() {
}
fs::remove_file(HOME).expect("take the planted file away");
+ // MEASURE gbae: ROMs where a user keeps them, and in the app's own folder.
+ for rom in ["/home/toy/Downloads/measure.gba", "/home/toy/Apps/appview/Data/measure.gba"] {
+ let dir = rom.rsplit_once('/').unwrap().0;
+ fs::create_dir_all(dir).unwrap_or_else(|e| panic!("make {dir}: {e}"));
+ fs::write(rom, b"rom").unwrap_or_else(|e| panic!("write {rom}: {e}"));
+ }
+ for cwd in ["/", "/home/toy"] {
+ let ran = Command::new(PROGRAM).arg("gbae").current_dir(cwd).output().expect("launch for gbae");
+ print!("{}", String::from_utf8_lossy(&ran.stdout));
+ print!("{}", String::from_utf8_lossy(&ran.stderr));
+ }
let ran = Command::new(PROGRAM).arg(APP).output().expect("launch the package through the launcher");
print!("{}", String::from_utf8_lossy(&ran.stdout));
print!("{}", String::from_utf8_lossy(&ran.stderr));
@@ -226,3 +238,70 @@ fn app() {
}
println!(" every arm held");
}
+
+/// gbae bfe8dabf8 `src/menu.rs` `list_directory`, verbatim but its type.
+struct FileEntry {
+ name: String,
+ path: std::path::PathBuf,
+ is_directory: bool,
+}
+fn list_directory(directory: &std::path::Path) -> Vec<FileEntry> {
+ let mut directories = Vec::new();
+ let mut roms = Vec::new();
+ for entry in std::fs::read_dir(directory).into_iter().flatten().flatten() {
+ let path = entry.path();
+ let name = entry.file_name().to_string_lossy().to_string();
+ if name.starts_with('.') {
+ continue;
+ }
+ if path.is_dir() {
+ directories.push(FileEntry { name: format!("{}/", name), path, is_directory: true });
+ } else if path.extension().is_some_and(|extension| extension.eq_ignore_ascii_case("gba")) {
+ roms.push(FileEntry { name, path, is_directory: false });
+ }
+ }
+ let by_name = |a: &FileEntry, b: &FileEntry| a.name.to_lowercase().cmp(&b.name.to_lowercase());
+ directories.sort_by(by_name);
+ roms.sort_by(by_name);
+ let parent = directory.parent().map(|parent| FileEntry {
+ name: "../".to_string(),
+ path: parent.to_path_buf(),
+ is_directory: true,
+ });
+ parent.into_iter().chain(directories).chain(roms).collect()
+}
+
+/// What gbae, launched with no ROM, can browse to from its cwd, load, and
+/// keep its config in.
+fn gbae() {
+ let cwd = std::env::current_dir().unwrap_or_default();
+ println!("GBAE cwd {}", cwd.display());
+ let mut seen = std::collections::BTreeSet::new();
+ let mut queue = vec![(cwd.clone(), 0)];
+ let mut roms = Vec::new();
+ while let Some((dir, depth)) = queue.pop() {
+ if !seen.insert(dir.clone()) || depth > 6 {
+ continue;
+ }
+ let listed = list_directory(&dir);
+ let names: Vec<&str> = listed.iter().map(|e| e.name.as_str()).collect();
+ println!("GBAE menu {} -> {:?}", dir.display(), names);
+ for e in listed {
+ if e.is_directory {
+ queue.push((e.path, depth + 1));
+ } else {
+ roms.push(e.path);
+ }
+ }
+ }
+ println!("GBAE browsable roms {:?}", roms);
+ for rom in ["/home/toy/Downloads/measure.gba", "/home/toy/Apps/appview/Data/measure.gba", "Data/measure.gba"] {
+ println!("GBAE load_rom({rom}) -> {:?}", std::fs::read(rom).map(|b| b.len()));
+ }
+ let base = std::env::var_os("XDG_CONFIG_HOME")
+ .map(std::path::PathBuf::from)
+ .or_else(|| std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config")));
+ let path = base.map(|base| base.join("gbae").join("config")).unwrap();
+ let written = path.parent().map_or(Ok(()), std::fs::create_dir_all).and_then(|()| std::fs::write(&path, "volume = 5\n"));
+ println!("GBAE config {} save -> {:?}, load -> {:?}", path.display(), written, std::fs::read_to_string(&path));
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 008c522ec..92be51b79 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -334,6 +334,7 @@ const MACHINE_TESTS: &[&str] = &[
// reads it have no host build, and the T14 boots from a stick beside an
// NVMe disk that is another system's.
"nvme_disk_keeps_log_and_home",
+ "app_view_qemu",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -3200,6 +3201,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
"bar_map_again" => bar_map_again(test_config),
"console_image_boots" => console_image_boots(),
"nvme_disk_keeps_log_and_home" => nvme_disk_keeps_log_and_home(test_config),
+ "app_view_qemu" => app_view_qemu(),
other => Err(format!("unknown machine test {other}")),
}
}
@@ -3239,6 +3241,18 @@ fn served_by_diskserver(qemu: &mut QemuInstance, console: &mut String) -> Result
Ok(())
}
+fn app_view_qemu() -> Result<(), String> {
+ let case = compile::repo_root().join("tests/proctreecase");
+ let (_, job) = suite_bin(qemu::SUITE_ARCH, "app_view");
+ let mut qemu =
+ QemuInstance::boot_with_options(&case, &[], &[("app_view".to_string(), job)], BootOptions::default());
+ let said = job_said(&mut qemu, "test_rs_app_view")?;
+ eprintln!("{said}");
+ let rest = qemu.drain_serial(Duration::from_secs(5));
+ eprintln!("{rest}");
+ Ok(())
+}
+
/// Run `command` as a job of the boot, to exit 0: what it said.
fn job_said(qemu: &mut QemuInstance, command: &str) -> Result<String, String> {
let result = qemu.run_test(command, Duration::from_secs(60));gbae-measure.log |
|
Round 2 mutation patches and runner, at head H1-rename-readsdiff --git a/userland/fileserver/src/rights.rs b/userland/fileserver/src/rights.rs
index 474f8e356..35f0bdfcc 100644
--- a/userland/fileserver/src/rights.rs
+++ b/userland/fileserver/src/rights.rs
@@ -13,7 +13,7 @@ use toyos::fs::*;
pub fn changes(op: u32, flags: u64) -> Option<bool> {
match op {
OPEN => Some(flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0),
- HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => Some(false),
+ RENAME | HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => Some(false),
WRITE | TRUNCATE | MKDIR | RMDIR | UNLINK | RENAME | SYMLINK | STREAM => Some(true),
_ => None,
}H2-any-access-byte-reads-writediff --git a/toyos/src/fs.rs b/toyos/src/fs.rs
index 303fc2e93..3eccf459f 100644
--- a/toyos/src/fs.rs
+++ b/toyos/src/fs.rs
@@ -206,7 +206,7 @@ impl<'a> Grant<'a> {
let access = match rest[8] {
0 => Access::ReadOnly,
1 => Access::ReadWrite,
- _ => return None,
+ _ => Access::ReadWrite,
};
let root = core::str::from_utf8(&rest[9..]).ok()?;
canonical(root).then_some(Self { share, access, root })H3-package-dir-writablediff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs
index 13ffce908..62b5a75ef 100644
--- a/toyos-manifest/src/lib.rs
+++ b/toyos-manifest/src/lib.rs
@@ -297,7 +297,7 @@ impl Program {
/// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2).
pub fn view(&self) -> Vec<View> {
match self.package() {
- Some(name) => vec![data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)],
+ Some(name) => vec![data_dir(package::Package::dir(name), true), data_dir(app_home(name), true)],
None => whole_tree(),
}
}H4-row-named-package-alloweddiff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs
index 13ffce908..9d224f4c9 100644
--- a/toyos-manifest/src/lib.rs
+++ b/toyos-manifest/src/lib.rs
@@ -332,9 +332,6 @@ impl Manifest {
/// named after a row the image declares is refused** ([`row_named`]): its
/// folder of the home would be that row's.
pub fn app_row(&self, name: &str, program: &str) -> Result<Program, String> {
- if self.program(name).is_some() {
- return Err(row_named(name));
- }
Ok(Program {
name: name.to_string(),
path: program.to_string(),H5-unknown-request-is-a-writediff --git a/userland/fileserver/src/rights.rs b/userland/fileserver/src/rights.rs
index 474f8e356..b72d97903 100644
--- a/userland/fileserver/src/rights.rs
+++ b/userland/fileserver/src/rights.rs
@@ -15,7 +15,7 @@ pub fn changes(op: u32, flags: u64) -> Option<bool> {
OPEN => Some(flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0),
HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => Some(false),
WRITE | TRUNCATE | MKDIR | RMDIR | UNLINK | RENAME | SYMLINK | STREAM => Some(true),
- _ => None,
+ _ => Some(true),
}
}
harnessdiff --git a/tests/toyos.rs b/tests/toyos.rs
index 008c522ec..a10bbb6d4 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -334,6 +334,7 @@ const MACHINE_TESTS: &[&str] = &[
// reads it have no host build, and the T14 boots from a stick beside an
// NVMe disk that is another system's.
"nvme_disk_keeps_log_and_home",
+ "app_view_qemu",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -3200,6 +3201,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
"bar_map_again" => bar_map_again(test_config),
"console_image_boots" => console_image_boots(),
"nvme_disk_keeps_log_and_home" => nvme_disk_keeps_log_and_home(test_config),
+ "app_view_qemu" => app_view_qemu(),
other => Err(format!("unknown machine test {other}")),
}
}
@@ -3239,6 +3241,18 @@ fn served_by_diskserver(qemu: &mut QemuInstance, console: &mut String) -> Result
Ok(())
}
+/// `app_view` under QEMU, for mutations and the negative control only: the
+/// verdict is the `app_view` metal row's.
+fn app_view_qemu() -> Result<(), String> {
+ let case = compile::repo_root().join("tests/proctreecase");
+ let (_, job) = suite_bin(qemu::SUITE_ARCH, "app_view");
+ let mut qemu =
+ QemuInstance::boot_with_options(&case, &[], &[("app_view".to_string(), job)], BootOptions::default());
+ let said = job_said(&mut qemu, "test_rs_app_view")?;
+ eprintln!("{said}");
+ Ok(())
+}
+
/// Run `command` as a job of the boot, to exit 0: what it said.
fn job_said(qemu: &mut QemuInstance, command: &str) -> Result<String, String> {
let result = qemu.run_test(command, Duration::from_secs(60));G1-fileserver-ignores-accessdiff --git a/userland/fileserver/src/main.rs b/userland/fileserver/src/main.rs
index 662115a16..5c4e470d5 100644
--- a/userland/fileserver/src/main.rs
+++ b/userland/fileserver/src/main.rs
@@ -463,7 +463,7 @@ impl Server {
rx: ipc::FrameRx::new(),
root: grant.root.to_string(),
share: grant.share,
- writes: grant.access == Access::ReadWrite && self.volume.writable(),
+ writes: self.volume.writable(),
window: None,
fids: BTreeMap::new(),
next_fid: 1,G2-supervisor-mints-every-dir-writablediff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs
index 62fcba2c7..d6fe18176 100644
--- a/userland/supervisor/src/main.rs
+++ b/userland/supervisor/src/main.rs
@@ -2327,7 +2327,7 @@ const _: () = assert!(
/// A grant on `acceptor`, `dir`'s role's port, naming `share`, by the
/// namespace name a program opens it under.
fn mint(acceptor: &Acceptor, share: u64, dir: &View) -> (String, Connector) {
- let access = if dir.write { Access::ReadWrite } else { Access::ReadOnly };
+ let access = Access::ReadWrite;
let mut badge = [0u8; MAX_BADGE];
let badge = Grant { share, access, root: &dir.root }
.encode(&mut badge)G3-launch-without-its-folderdiff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs
index 62fcba2c7..0ffbc7dc2 100644
--- a/userland/supervisor/src/main.rs
+++ b/userland/supervisor/src/main.rs
@@ -1638,8 +1638,6 @@ impl Supervisor<'_> {
if program.package().is_some() {
if let Err(why) = self.make_app_home(program) {
say!("supervisor: launcher: {} was not started: {why}", program.name);
- let _ = conn.try_signal(launch::MSG_REFUSED);
- return;
}
}
let started = start(G4-row-named-package-alloweddiff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs
index 13ffce908..9d224f4c9 100644
--- a/toyos-manifest/src/lib.rs
+++ b/toyos-manifest/src/lib.rs
@@ -332,9 +332,6 @@ impl Manifest {
/// named after a row the image declares is refused** ([`row_named`]): its
/// folder of the home would be that row's.
pub fn app_row(&self, name: &str, program: &str) -> Result<Program, String> {
- if self.program(name).is_some() {
- return Err(row_named(name));
- }
Ok(Program {
name: name.to_string(),
path: program.to_string(),run.sh#!/bin/zsh
# Each patch: checked, applied, run, restored, in one pass.
W=<worktree>
M=<scratch>/r2/mut
cd $W
run() { # name cmd patches...
local name=$1 cmd=$2; shift 2
local log=$M/mut-$name.log
: > $log
for p in "$@"; do
git apply --check $M/$p.patch >> $log 2>&1 || { echo "APPLY-CHECK FAILED $p" >> $log; echo "$name EXIT=apply-failed" >> $M/summary.txt; return; }
git apply $M/$p.patch
done
eval "$cmd" >> $log 2>&1
local e=$?
echo "EXIT=$e" >> $log
echo "$name EXIT=$e" >> $M/summary.txt
git checkout -- .
git status --porcelain --ignore-submodules=none >> $M/summary.txt
}
: > $M/summary.txt
uptime >> $M/summary.txt
run H1-rename-reads "cargo test -p fileserver --lib" H1-rename-reads
run H5-unknown-request-is-a-write "cargo test -p fileserver --lib" H5-unknown-request-is-a-write
run H2-any-access-byte-reads-write "cargo test -p toyos --lib" H2-any-access-byte-reads-write
run H3-package-dir-writable "cargo test -p toyos-manifest" H3-package-dir-writable
run H4-row-named-package-allowed "cargo test -p toyos-manifest" H4-row-named-package-allowed
run G0-harness-alone "cargo test --test toyos-build -- app_view_qemu" harness
run G1-fileserver-ignores-access "cargo test --test toyos-build -- app_view_qemu" harness G1-fileserver-ignores-access
run G2-supervisor-mints-every-dir-writable "cargo test --test toyos-build -- app_view_qemu" harness G2-supervisor-mints-every-dir-writable
run G3-launch-without-its-folder "cargo test --test toyos-build -- app_view_qemu" harness G3-launch-without-its-folder
run G4-row-named-package-allowed "cargo test --test toyos-build -- app_view_qemu" harness G4-row-named-package-allowed
run NC-whole-change-reverted "cargo test --test toyos-build -- app_view_qemu" NC-whole-change-reverted
uptime >> $M/summary.txt
echo DONE >> $M/summary.txtsummary.txt |
|
Round 2 negative control at head diff --git a/system.toml b/system.toml
index fdc2ef3c6..b5430d2c9 100644
--- a/system.toml
+++ b/system.toml
@@ -26,8 +26,7 @@ start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "
# a row read out of one would be a directory deciding what the machine hands
# out; this is the image's answer, one for all of them. Connectors only —
# `devices` and `syscap` have no spelling here, so nothing installed claims
-# hardware or enters the RT band — and no directory: a package sees its own
-# `/apps/<name>` read-only and its own `/home/toy/Apps/<name>`, its `HOME`.
+# hardware or enters the RT band.
[apps]
receives = ["compositor", "soundserver", "filepicker"]
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 008c522ec..d066a6155 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -334,6 +334,7 @@ const MACHINE_TESTS: &[&str] = &[
// reads it have no host build, and the T14 boots from a stick beside an
// NVMe disk that is another system's.
"nvme_disk_keeps_log_and_home",
+ "app_view_qemu",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -3200,6 +3201,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
"bar_map_again" => bar_map_again(test_config),
"console_image_boots" => console_image_boots(),
"nvme_disk_keeps_log_and_home" => nvme_disk_keeps_log_and_home(test_config),
+ "app_view_qemu" => app_view_qemu(),
other => Err(format!("unknown machine test {other}")),
}
}
@@ -3239,6 +3241,18 @@ fn served_by_diskserver(qemu: &mut QemuInstance, console: &mut String) -> Result
Ok(())
}
+/// `app_view` under QEMU, for mutations and the negative control only: the
+/// verdict is the `app_view` metal row's.
+fn app_view_qemu() -> Result<(), String> {
+ let case = compile::repo_root().join("tests/proctreecase");
+ let (_, job) = suite_bin(qemu::SUITE_ARCH, "app_view");
+ let mut qemu =
+ QemuInstance::boot_with_options(&case, &[], &[("app_view".to_string(), job)], BootOptions::default());
+ let said = job_said(&mut qemu, "test_rs_app_view")?;
+ eprintln!("{said}");
+ Ok(())
+}
+
/// Run `command` as a job of the boot, to exit 0: what it said.
fn job_said(qemu: &mut QemuInstance, command: &str) -> Result<String, String> {
let result = qemu.run_test(command, Duration::from_secs(60));
@@ -3575,7 +3589,7 @@ fn launch_authority(back: &metal::Readback) -> Result<(), String> {
fn app_view(back: &metal::Readback) -> Result<(), String> {
back.job_passed("test_rs_app_view")?;
let log = back.log();
- let named = format!("supervisor: launcher: {}", toyos_manifest::row_named("shell"));
+ let named = format!("supervisor: launcher: {}", "the package shell is named after a row the image declares, and /home/toy/Apps/shell is that row's folder");
for said in [
" every arm held",
"app_view: the app saw its own package read-only and its own folder as HOME, and nothing else",
diff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs
index 13ffce908..00ef53351 100644
--- a/toyos-manifest/src/lib.rs
+++ b/toyos-manifest/src/lib.rs
@@ -64,22 +64,6 @@ pub fn session_home() -> String {
format!("/home/{USER}")
}
-/// An installed package's own folder in the session user's home: its `HOME`,
-/// and the one directory of the home its view holds.
-pub fn app_home(name: &str) -> String {
- format!("{}/Apps/{name}", session_home())
-}
-
-/// Why a launch of the package `name` is refused when a row the image declares
-/// has that name: [`app_home`] is a folder that row keeps its own in.
-pub fn row_named(name: &str) -> String {
- format!("the package {name} is named after a row the image declares, and {} is that row's folder", app_home(name))
-}
-
-/// What an app's own folder holds, made with it: where it keeps its config,
-/// data, cache and state. English on disk, as every home folder is.
-pub const APP_FOLDERS: [&str; 4] = ["Config", "Data", "Cache", "State"];
-
/// Where each system service keeps its own persistent data, one directory per
/// program key.
pub const STATE: &str = "/state";
@@ -112,42 +96,6 @@ pub fn role_dirs(role: &str) -> Option<&'static [RoleDir]> {
ROLES.iter().find(|(name, _)| *name == role).map(|(_, dirs)| *dirs)
}
-/// One directory capability in a program's view: the grant the supervisor
-/// mints on `role`'s port (`toyos::fs::Grant`).
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct View {
- pub role: &'static str,
- /// What the program's namespace calls it, after `fs:`.
- pub dir: String,
- /// Where it is on the role's volume.
- pub root: String,
- /// Whether a request that changes what it holds is served.
- pub write: bool,
-}
-
-/// Every directory every role serves, each read-write: the view of a program
-/// no narrower one is declared for.
-pub fn whole_tree() -> Vec<View> {
- ROLES
- .iter()
- .flat_map(|(role, dirs)| {
- dirs.iter().map(|d| View { role, dir: d.dir.to_string(), root: d.root.to_string(), write: true })
- })
- .collect()
-}
-
-/// The DATA directory `dir`, beneath one DATA serves.
-fn data_dir(dir: String, write: bool) -> View {
- let role = "data";
- let parent = role_dirs(role)
- .expect("DATA is a role")
- .iter()
- .find(|d| dir.strip_prefix(d.dir).is_some_and(|rest| rest.starts_with('/')))
- .unwrap_or_else(|| panic!("manifest: {dir} is beneath no directory DATA serves"));
- let root = format!("{}{}", parent.root, &dir[parent.dir.len()..]);
- View { role, dir, root, write }
-}
-
/// How often a `restart` row is started again before the supervisor gives up on it: at
/// most this many ends inside [`RESTART_WINDOW_SECS`]. Past it the row's ports
/// close, and a client's next connection is answered `Gone`.
@@ -273,32 +221,12 @@ impl Program {
self.slots || self.receives.iter().any(|r| r == SWAP_PORT)
}
- /// The installed package this row launches: a row [`Manifest::app_row`]
- /// made.
- pub fn package(&self) -> Option<&str> {
- package::package_of(&self.path)
- }
-
/// The `HOME` the supervisor starts this row with. A location grants nothing: what
/// the program can reach is its view's business, never this string's.
pub fn home(&self) -> String {
- match (self.service, self.package()) {
- (true, _) => format!("{STATE}/{}", self.name),
- (false, Some(name)) => app_home(name),
- (false, None) => session_home(),
- }
- }
-
- /// The directories this row's program is endowed. **An installed package
- /// sees its own directory read-only and its own folder of the home
- /// read-write, and nothing else any role serves**: no other package, no
- /// other part of the home, no `/config`, `/state`, `/log` or `/boot`.
- /// Every other row sees the whole tree, until each declares its own
- /// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2).
- pub fn view(&self) -> Vec<View> {
- match self.package() {
- Some(name) => vec![data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)],
- None => whole_tree(),
+ match self.service {
+ true => format!("{STATE}/{}", self.name),
+ false => session_home(),
}
}
}
@@ -311,11 +239,10 @@ pub struct Manifest {
/// Names the supervisor serves itself. The supervisor is in every image and is no `[programs]`
/// key, so these have no declaration to come from.
pub supervisor_serves: Vec<String>,
- /// The connectors every program launched from `/apps` is given beside its
- /// view ([`Program::view`]), and nothing else. `/apps` is writable to
- /// the installer, so this row is the image's rather than the package's —
- /// which is why a device class and a `syscap` right have no spelling on
- /// the package side at all.
+ /// The namespace every program launched from `/apps` is given: connectors,
+ /// and nothing else. A package directory is writable, so this row is the
+ /// image's rather than the package's — which is why a device class and a
+ /// `syscap` right have no spelling on the package side at all.
pub apps: Vec<String>,
/// Program names, in the order `[boot] start` gave them — which orders
/// nothing, because every port exists before any server runs.
@@ -328,19 +255,14 @@ impl Manifest {
}
/// The row a launch of an installed package is built from: synthesized,
- /// because a package has no `[programs]` key to hold one. **A package
- /// named after a row the image declares is refused** ([`row_named`]): its
- /// folder of the home would be that row's.
- pub fn app_row(&self, name: &str, program: &str) -> Result<Program, String> {
- if self.program(name).is_some() {
- return Err(row_named(name));
- }
- Ok(Program {
+ /// because a package has no `[programs]` key to hold one.
+ pub fn app_row(&self, name: &str, program: &str) -> Program {
+ Program {
name: name.to_string(),
path: program.to_string(),
receives: self.apps.clone(),
..Program::default()
- })
+ }
}
/// Every `serves` name in the whole manifest, not only the ones [`start`]
@@ -592,7 +514,7 @@ mod tests {
/// row's construction rather than by a check.
#[test]
fn a_package_row_is_connectors_and_nothing_else() {
- let row = sample().app_row("gbae", "/apps/gbae/gbae").unwrap();
+ let row = sample().app_row("gbae", "/apps/gbae/gbae");
assert_eq!(row.receives, ["compositor", "soundserver"]);
assert!(row.devices.is_empty());
assert!(row.syscap.is_empty());
@@ -628,81 +550,10 @@ mod tests {
let m = sample();
assert_eq!(m.program("soundserver").unwrap().home(), "/state/soundserver");
assert_eq!(m.program("terminal").unwrap().home(), "/home/toy");
+ assert_eq!(m.app_row("gbae", "/apps/gbae/gbae").home(), "/home/toy");
let m = parse("program sshserver /system/bin/sshserver\nservice\nprogram shell /system/bin/shell\n");
assert!(m.program("sshserver").unwrap().service);
assert!(!m.program("shell").unwrap().service);
- // The shell keeps its history under the session's home, in its own
- // `Apps/shell` (`OWN_FOLDER` in `userland/shell`).
- assert_eq!(m.program("shell").unwrap().home(), "/home/toy");
- }
-
- /// **An installed package's `HOME` is its own folder** of the session
- /// user's home, the layout's `/home/<user>/Apps/<name>`.
- #[test]
- fn a_package_s_home_is_its_own_folder() {
- let row = sample().app_row("gbae", "/apps/gbae/gbae").unwrap();
- assert_eq!(row.package(), Some("gbae"));
- assert_eq!(row.home(), "/home/toy/Apps/gbae");
- assert_eq!(sample().program("compositor").unwrap().package(), None);
- }
-
- /// **A package named after a declared row is refused**, by name: the
- /// shell keeps its history in `Apps/shell`, which would be the package's
- /// folder.
- #[test]
- fn a_package_named_after_a_declared_row_is_refused() {
- let m = parse("program shell /system/bin/shell\n");
- assert_eq!(m.app_row("shell", "/apps/shell/shell"), Err(row_named("shell")));
- assert!(row_named("shell").contains("/home/toy/Apps/shell"));
- let s = sample();
- for row in &s.programs {
- assert_eq!(s.app_row(&row.name, &format!("/apps/{0}/{0}", row.name)), Err(row_named(&row.name)));
- }
- assert!(m.app_row("gbae", "/apps/gbae/gbae").is_ok());
- }
-
- fn views(row: &Program) -> Vec<(&'static str, String, String, bool)> {
- row.view().into_iter().map(|v| (v.role, v.dir, v.root, v.write)).collect()
- }
-
- /// **An installed package sees its own directory read-only and its own
- /// folder read-write, and nothing else any role serves.** Spelled out
- /// whole, so a third directory, a wider root or a writable package is red.
- #[test]
- fn a_package_s_view_is_its_own_directory_read_only_and_its_own_folder() {
- let row = sample().app_row("gbae", "/apps/gbae/gbae").unwrap();
- assert_eq!(
- views(&row),
- [
- ("data", "/apps/gbae".into(), "apps/gbae".into(), false),
- ("data", "/home/toy/Apps/gbae".into(), "home/toy/Apps/gbae".into(), true),
- ]
- );
- // The longest name a package has is still beneath its own directories.
- let longest = "n".repeat(MAX_PROGRAM_NAME);
- let row = sample().app_row(&longest, &format!("/apps/{longest}/{longest}")).unwrap();
- assert_eq!(
- views(&row).into_iter().map(|(_, _, root, write)| (root, write)).collect::<Vec<_>>(),
- [(format!("apps/{longest}"), false), (format!("home/toy/Apps/{longest}"), true)]
- );
- }
-
- /// Every row the image declares sees the whole tree read-write, the
- /// installer and the shell included: neither has authority over `/apps`
- /// the other lacks.
- #[test]
- fn every_declared_row_sees_the_whole_tree_read_write() {
- let m = parse("program pkg /system/bin/pkg\nprogram shell /system/bin/shell\n");
- let whole: Vec<_> = ["/apps", "/config", "/home", "/state", "/log", "/boot"]
- .iter()
- .zip(["apps", "config", "home", "state", "", ""])
- .zip(["data", "data", "data", "data", "log", "boot"])
- .map(|((dir, root), role)| (role, dir.to_string(), root.to_string(), true))
- .collect();
- let s = sample();
- for row in [m.program("pkg").unwrap(), m.program("shell").unwrap(), s.program("compositor").unwrap()] {
- assert_eq!(views(row), whole, "{}", row.name);
- }
}
#[test]
diff --git a/toyos-manifest/src/package.rs b/toyos-manifest/src/package.rs
index b616fc62b..08e3182d5 100644
--- a/toyos-manifest/src/package.rs
+++ b/toyos-manifest/src/package.rs
@@ -4,8 +4,8 @@
//! to resolve a launch, so the format lives beside [`crate::Manifest`] for the
//! same reason: one renderer, one parser, one round-trip test.
//!
-//! **Nothing here is a grant.** `/apps` is writable to every row the image
-//! declares, so a manifest is a peer's claim about itself: it says which binary
+//! **Nothing here is a grant.** `/apps` is writable to every program that can
+//! name it, so a manifest is a peer's claim about itself: it says which binary
//! *of its own directory* a launch starts. A device, a right and another
//! package's binary have no spelling in this file at all.
//!
diff --git a/toyos/src/fs.rs b/toyos/src/fs.rs
index 303fc2e93..5c6b96cb7 100644
--- a/toyos/src/fs.rs
+++ b/toyos/src/fs.rs
@@ -4,8 +4,8 @@
//! **A directory capability is a connector in the program's namespace**, named
//! [`CAPABILITY_PREFIX`] and the absolute directory it serves (`fs:/home`).
//! Each is a connector to its role's one port, which the supervisor minted with
-//! a [`Grant`]: the directory, whether it may be changed, and whose share of
-//! the server it spends. The kernel stamps that on every connection made through it and answers it to the
+//! a [`Grant`]: the directory, and whose share of the server it spends. The
+//! kernel stamps that on every connection made through it and answers it to the
//! port's acceptor alone, so the server reads what was granted off the
//! connection and nothing the client says. A program names a file only under a
//! directory it holds, and the kernel's part is who holds which connector.
@@ -152,64 +152,44 @@ pub struct Grant<'a> {
/// one service it starts itself or one login session, and for every
/// launch made from it that opens no session.
pub share: u64,
- /// Whether a request that changes what the directory holds is served.
- pub access: Access,
/// 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.
pub root: &'a str,
}
-/// What a [`Grant`] lets its holder do to the directory.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Access {
- /// Read, list and stat; every request that would change what the
- /// directory holds is refused `PermissionDenied`.
- ReadOnly,
- ReadWrite,
-}
-
/// 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 = 3;
+const GRANT_VERSION: u8 = 2;
-/// The longest root a grant carries: what one badge holds past the version,
-/// the share and the access.
-pub const MAX_GRANT_ROOT: usize = MAX_BADGE - 1 - 8 - 1;
+/// The longest root a grant carries: what one badge holds past the version and
+/// the share.
+pub const MAX_GRANT_ROOT: usize = MAX_BADGE - 1 - 8;
impl<'a> Grant<'a> {
- /// The version, the share, the access, then the root: `None` for a root
- /// no grant can carry.
+ /// 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.share.to_le_bytes());
- out[9] = match self.access {
- Access::ReadOnly => 0,
- Access::ReadWrite => 1,
- };
- let end = 10 + self.root.len();
- out[10..end].copy_from_slice(self.root.as_bytes());
+ let end = 9 + self.root.len();
+ out[9..end].copy_from_slice(self.root.as_bytes());
Some(&out[..end])
}
/// `None` for bytes [`Self::encode`] cannot have written.
pub fn decode(bytes: &'a [u8]) -> Option<Self> {
let (&version, rest) = bytes.split_first()?;
- if version != GRANT_VERSION || rest.len() < 9 || rest.len() - 9 > MAX_GRANT_ROOT {
+ if version != GRANT_VERSION || rest.len() < 8 || rest.len() - 8 > MAX_GRANT_ROOT {
return None;
}
let share = u64::from_le_bytes(rest[..8].try_into().expect("eight bytes"));
- let access = match rest[8] {
- 0 => Access::ReadOnly,
- 1 => Access::ReadWrite,
- _ => return None,
- };
- let root = core::str::from_utf8(&rest[9..]).ok()?;
- canonical(root).then_some(Self { share, access, root })
+ let root = core::str::from_utf8(&rest[8..]).ok()?;
+ canonical(root).then_some(Self { share, root })
}
}
@@ -645,12 +625,10 @@ mod tests {
fn a_grant_round_trips_at_every_bound() {
let longest = "r".repeat(MAX_GRANT_ROOT);
for (share, root) in [(0, ""), (1, "home"), (u64::MAX, "home/toy/Documents"), (7, longest.as_str())] {
- for access in [Access::ReadOnly, Access::ReadWrite] {
- let grant = Grant { share, access, 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:?} {access:?}");
- }
+ 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:?}");
}
}
@@ -659,38 +637,30 @@ 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 { share: 1, access: Access::ReadWrite, 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 { share: 3, access: Access::ReadOnly, root: "home" }.encode(&mut out).unwrap().to_vec();
- // Shorter than a version, a share and an access.
- for n in 0..10 {
+ 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");
}
- // Another version, and the one before this.
- for version in [GRANT_VERSION - 1, GRANT_VERSION + 1] {
- let mut other = good.clone();
- other[0] = version;
- assert_eq!(Grant::decode(&other), None, "version {version}");
- }
- // An access byte that is neither, which is never read as either.
- for access in [2, 0x80, 0xff] {
- let mut other = good.clone();
- other[9] = access;
- assert_eq!(Grant::decode(&other), None, "access {access}");
- }
+ // Another version.
+ let mut other = good.clone();
+ other[0] = GRANT_VERSION + 1;
+ assert_eq!(Grant::decode(&other), None);
// A root the wire refuses, or not UTF-8.
for root in [&b"/home"[..], b"a/../b", b"a//b", b"\xff"] {
- let mut bad = good[..10].to_vec();
+ let mut bad = good[..9].to_vec();
bad.extend_from_slice(root);
assert_eq!(Grant::decode(&bad), None, "{root:?}");
}
// One byte past the longest root.
- let mut long = good[..10].to_vec();
+ let mut long = good[..9].to_vec();
long.extend(core::iter::repeat_n(b'r', MAX_GRANT_ROOT + 1));
assert_eq!(Grant::decode(&long), None);
}
diff --git a/userland/fileserver/src/lib.rs b/userland/fileserver/src/lib.rs
index 0c29712db..1dc8bb7b9 100644
--- a/userland/fileserver/src/lib.rs
+++ b/userland/fileserver/src/lib.rs
@@ -2,8 +2,7 @@
//! cache every byte of its volume passes through ([`cache`]), the volumes it
//! can serve ([`data`] for the bcachefs DATA role, [`fat`] for FAT32's LOG and
//! BOOT, [`absent`] for a role with no volume this boot), and the resolver that
-//! keeps every path inside the directory a connection was given ([`resolve`]),
-//! and which requests a read-only one is refused ([`rights`]).
+//! keeps every path inside the directory a connection was given ([`resolve`]).
//!
//! **This is the page cache.** A block of the volume — a btree node, a FAT
//! sector, a file's data — is read into [`cache::Cache`] once and served from
@@ -19,6 +18,5 @@ pub mod data;
pub mod disk;
pub mod fat;
pub mod resolve;
-pub mod rights;
pub mod volume;
pub mod writeback;
diff --git a/userland/fileserver/src/main.rs b/userland/fileserver/src/main.rs
index 662115a16..a5032365e 100644
--- a/userland/fileserver/src/main.rs
+++ b/userland/fileserver/src/main.rs
@@ -13,9 +13,8 @@
//! **A connection is what its grant says** (`toyos::fs::Grant`): the badge
//! the supervisor minted its connector with, which the kernel stamped on it and
//! answers this port's acceptor alone. Every path on it is resolved beneath
-//! the grant's root (`fileserver::resolve`); a request that would change what
-//! it holds, on a read-only grant or a read-only volume, is refused before the
-//! volume sees it (`fileserver::rights`).
+//! the grant's root (`fileserver::resolve`); a write on a read-only volume is
+//! refused before the volume sees it.
//!
//! **One share cannot take the server.** Beneath each machine-wide bound —
//! connections waiting on their hello, connections served, streams — each
@@ -45,7 +44,6 @@ use fileserver::data::{DataVolume, Located, Probed};
use fileserver::disk::{Claimed, Disk, Ram, Served};
use fileserver::fat::FatVolume;
use fileserver::resolve::{self, Found, Refusal as Escape, Resolved};
-use fileserver::rights;
use fileserver::volume::{Kind, Meta, Node, OpenHow, Out, Volume};
use fileserver::writeback::WriteBack;
use toyos::endow::{self, Endowments};
@@ -92,10 +90,6 @@ const _: () = assert!(1 + MAX_SERVED + MAX_HANDSHAKES + MAX_STREAMS <= Poller::M
/// How long an accepted connection may take to lend its window.
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2);
-/// Why a client asking for a request number the wire does not have is let go,
-/// whatever its grant.
-const NO_SUCH_OPERATION: &str = "it asked for an operation this protocol does not have";
-
/// The volume memory stands in for when DATA has no partition: 1 GiB of
/// blocks, of which only what is written costs anything.
const RAM_BLOCKS: u64 = 1 << 18;
@@ -144,8 +138,6 @@ struct Client {
root: String,
/// Its grant's share, which it spends.
share: u64,
- /// Its grant is read-write and the volume is: what it may change.
- writes: bool,
window: Option<SharedMemory>,
fids: BTreeMap<u64, Fid>,
next_fid: u64,
@@ -463,7 +455,6 @@ impl Server {
rx: ipc::FrameRx::new(),
root: grant.root.to_string(),
share: grant.share,
- writes: grant.access == Access::ReadWrite && self.volume.writable(),
window: None,
fids: BTreeMap::new(),
next_fid: 1,
@@ -591,8 +582,8 @@ impl Server {
Ok(window) => client.window = Some(window),
Err(_) => return Answer::Drop("its window would not map"),
}
- let granted = if client.writes { RIGHT_WRITE } else { 0 };
- return Answer::Reply(Reply { value: granted, ..Reply::ok() });
+ let rights = if self.volume.writable() { RIGHT_WRITE } else { 0 };
+ return Answer::Reply(Reply { value: rights, ..Reply::ok() });
}
if self.clients[&id].window.is_none() {
return Answer::Drop("it asked before it lent a window");
@@ -622,10 +613,10 @@ impl Server {
}
fn serve_one(&mut self, id: u64, op: u32, r: Request) -> Result<Answer, SyscallError> {
- match rights::changes(op, r.flags) {
- None => return Ok(Answer::Drop(NO_SUCH_OPERATION)),
- Some(true) if !self.clients[&id].writes => return Err(SyscallError::PermissionDenied),
- Some(_) => {}
+ let changes = matches!(op, WRITE | TRUNCATE | MKDIR | RMDIR | UNLINK | RENAME | SYMLINK | STREAM)
+ || (op == OPEN && r.flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0);
+ if changes && !self.volume.writable() {
+ return Err(SyscallError::PermissionDenied);
}
match op {
OPEN => {
@@ -835,7 +826,7 @@ impl Server {
self.dirtied();
Ok(Answer::Reply(Reply::ok()))
}
- _ => Ok(Answer::Drop(NO_SUCH_OPERATION)),
+ _ => Ok(Answer::Drop("it asked for an operation this protocol does not have")),
}
}
diff --git a/userland/fileserver/src/rights.rs b/userland/fileserver/src/rights.rs
deleted file mode 100644
index 474f8e356..000000000
--- a/userland/fileserver/src/rights.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-//! Which requests change what a connection's directory holds: each is refused
-//! `PermissionDenied` before the volume sees it, on a connection whose grant is
-//! read-only (`toyos::fs::Access`) or whose volume is.
-//!
-//! **Closed by default**: an operation this list does not name is one the
-//! wire does not have, and its client is let go on every connection, so a
-//! request added to the wire is served nowhere until it is named here.
-
-use toyos::fs::*;
-
-/// Whether request `op`, with an open's `flags`, may change what the directory
-/// holds: `None` for an operation the wire does not have.
-pub fn changes(op: u32, flags: u64) -> Option<bool> {
- match op {
- OPEN => Some(flags & (O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW) != 0),
- HELLO | CLOSE | READ | STAT | LSTAT | FSTAT | FSYNC | SYNC | READDIR | READLINK => Some(false),
- WRITE | TRUNCATE | MKDIR | RMDIR | UNLINK | RENAME | SYMLINK | STREAM => Some(true),
- _ => None,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- /// Every request the wire has, by what it does to the directory: the
- /// independent spelling `changes` is held to.
- const READS: [u32; 10] = [HELLO, CLOSE, READ, STAT, LSTAT, FSTAT, FSYNC, SYNC, READDIR, READLINK];
- const WRITES: [u32; 8] = [WRITE, TRUNCATE, MKDIR, RMDIR, UNLINK, RENAME, SYMLINK, STREAM];
-
- #[test]
- fn every_request_that_writes_changes_the_directory_and_none_that_reads_does() {
- for op in WRITES {
- assert_eq!(changes(op, 0), Some(true), "request {op} writes");
- }
- for op in READS {
- assert_eq!(changes(op, 0), Some(false), "request {op} only reads");
- }
- // Every request number the wire has is one of the two, or `OPEN`.
- let mut named: Vec<u32> = READS.iter().chain(&WRITES).copied().chain([OPEN]).collect();
- named.sort_unstable();
- assert_eq!(named, (HELLO..=SYNC).collect::<Vec<_>>());
- }
-
- /// An open changes the directory by any one flag that writes, creates or
- /// truncates, alone or with a read.
- #[test]
- fn an_open_changes_the_directory_by_every_flag_but_read() {
- assert_eq!(changes(OPEN, O_READ), Some(false));
- assert_eq!(changes(OPEN, 0), Some(false));
- for flag in [O_WRITE, O_APPEND, O_CREATE, O_TRUNCATE, O_CREATE_NEW] {
- assert_eq!(changes(OPEN, flag), Some(true), "flag {flag}");
- assert_eq!(changes(OPEN, flag | O_READ), Some(true), "flag {flag} with a read");
- }
- }
-
- /// A request number the wire does not have is named neither, so its
- /// client is let go whatever its grant.
- #[test]
- fn a_request_the_wire_does_not_have_is_neither() {
- for op in [0, SYNC + 1, REPLY, LINK, u32::MAX] {
- assert_eq!(changes(op, 0), None, "request {op}");
- }
- }
-}
diff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs
index 62fcba2c7..888a98181 100644
--- a/userland/supervisor/src/main.rs
+++ b/userland/supervisor/src/main.rs
@@ -42,15 +42,8 @@
//! ([`FILES_BOUND`]).
//!
//! **Every program it starts gets `HOME` from its row** (`Program::home`), over
-//! anything a launching caller carried: a service its own `/state/<name>` and
-//! an installed package its own `Apps/<name>` folder of the session user's
-//! home, each made before it runs, and everything else the session user's
-//! home, made at boot.
-//!
-//! **Every program it starts holds the directories its row's view names**
-//! (`Program::view`), each a grant minted for that start ([`Grants`]): an
-//! installed package its own directory read-only and its own folder
-//! read-write, and every other row the whole tree.
+//! anything a launching caller carried: a service its own `/state/<name>`, made
+//! before it runs, and everything else the session user's home, made at boot.
//! A launch of a program no row names is answered with the session's, which
//! the caller's direct spawn carries in place of its own.
//!
@@ -77,9 +70,9 @@ use toyos_swap::{Refusal, Request as SwapRequest, Word};
use toyos_manifest::launch::{self as authority, Authority, Session, Sessions, Target};
use toyos_manifest::package::{self, Package};
-use toyos_manifest::{Manifest, Program, View};
+use toyos_manifest::{Manifest, Program};
use toyos::endow::Endowments;
-use toyos::fs::{Access, Grant, CAPABILITY_PREFIX};
+use toyos::fs::{Grant, CAPABILITY_PREFIX};
use toyos::ipc::{self, Connection, RxStep};
use toyos::launch::{self, Parent, Request, LAUNCHER};
use toyos::namespace::{self, Namespace};
@@ -390,11 +383,9 @@ fn main() {
// process nobody endows a namespace: std resolves through this one, and
// the stop's syncs through the second.
let files: &'static Namespace = {
- let whole = toyos_manifest::whole_tree();
- let own: Vec<(String, Connector)> = whole
+ let own: Vec<(String, Connector)> = role_acceptors
.iter()
- .filter_map(|view| Some((role_acceptors.get(view.role)?, view)))
- .map(|(acceptor, view)| mint(acceptor, authority::SUPERVISOR_SHARE, view))
+ .flat_map(|(role, acceptor)| mint_grants(role, acceptor, authority::SUPERVISOR_SHARE))
.collect();
let build = || {
let mut builder = namespace::build();
@@ -848,30 +839,6 @@ impl<'a> Supervisor<'a> {
}
}
- /// An installed package's `HOME`, its folder of the session's home, and
- /// its [`toyos_manifest::APP_FOLDERS`], made before it runs. `Err` is why
- /// one is not a directory, and the launch is refused: its grant would
- /// name nothing, or something else than a folder.
- fn make_app_home(&mut self, program: &Program) -> Result<(), String> {
- let home = program.home();
- let asked = home.clone();
- let made = self.files("an app's home", move || {
- let folders = toyos_manifest::APP_FOLDERS.iter().map(|folder| format!("{asked}/{folder}"));
- std::iter::once(asked.clone()).chain(folders).try_for_each(|dir| {
- make_dir(&dir)?;
- match std::fs::symlink_metadata(&dir)?.is_dir() {
- true => Ok(()),
- false => Err(std::io::Error::other(format!("{dir} is no directory"))),
- }
- })
- });
- match made {
- Ok(Ok(())) => Ok(()),
- Ok(Err(e)) => Err(format!("{home} could not be made: {e}")),
- Err(why) => Err(format!("{home} was not made: {why}")),
- }
- }
-
/// `work`, a call into the file servers, made on [`Worker`], and its
/// answer — with every server that ends meanwhile started again, so a call
/// its end left waiting in the port's queue goes on to the new process.
@@ -1635,13 +1602,6 @@ impl Supervisor<'_> {
// `inherit_handle` duplicates into the child, so the supervisor's own copies go with
// `slots` when this returns.
self.make_home(program);
- if program.package().is_some() {
- if let Err(why) = self.make_app_home(program) {
- say!("supervisor: launcher: {} was not started: {why}", program.name);
- let _ = conn.try_signal(launch::MSG_REFUSED);
- return;
- }
- }
let started = start(
command,
program,
@@ -1777,10 +1737,7 @@ fn resolve<'a, V>(system: &'a Manifest, path: &str, judge: impl FnOnce(Target<'_
installed.program
));
}
- let row = match system.app_row(name, path) {
- Ok(row) => row,
- Err(why) => return Resolved::Refused(why),
- };
+ let row = system.app_row(name, path);
let verdict = judge(Target::Package(&row));
Resolved::Package(row, verdict)
}
@@ -2284,7 +2241,7 @@ fn build_namespace(
/// file-server role's port.
///
/// **Each start is minted grants naming its session's share** (`toyos::fs::Grant`,
-/// [`Session::share`]), one per directory of its row's view: a service has a
+/// [`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
@@ -2296,46 +2253,45 @@ struct Grants<'a> {
impl Grants<'_> {
/// The directory capabilities `program` is endowed for one start in
- /// `session`, by namespace name: its row's view (`Program::view`), but
- /// that 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.
+ /// `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
+ /// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2),
+ /// 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, session: Session) -> Vec<(String, Connector)> {
if is_storage(program) {
return Vec::new();
}
- let wanted = program.view();
let mut view = Vec::new();
for (role, kept) in &self.roles {
let kept = kept.lock().expect("supervisor: a service's state is poisoned");
- let Some((_, acceptor)) = kept.acceptors.first() else { continue };
- for dir in wanted.iter().filter(|dir| dir.role == *role) {
- view.push(mint(acceptor, session.share(), dir));
+ if let Some((_, acceptor)) = kept.acceptors.first() {
+ view.extend(mint_grants(role, acceptor, session.share()));
}
}
view
}
}
-/// The longest root a view names, a package's folder of the home at the
-/// longest name a package has, is one a grant carries.
-const _: () = assert!(
- "home/".len() + toyos_manifest::USER.len() + "/Apps/".len() + toyos_manifest::MAX_PROGRAM_NAME
- <= toyos::fs::MAX_GRANT_ROOT
-);
-
-/// A grant on `acceptor`, `dir`'s role's port, naming `share`, by the
-/// namespace name a program opens it under.
-fn mint(acceptor: &Acceptor, share: u64, dir: &View) -> (String, Connector) {
- let access = if dir.write { Access::ReadWrite } else { Access::ReadOnly };
- let mut badge = [0u8; MAX_BADGE];
- let badge = Grant { share, access, root: &dir.root }
- .encode(&mut badge)
- .unwrap_or_else(|| panic!("supervisor: {}'s root {:?} is no grant's, past the bound asserted above", dir.dir, dir.root));
- let connector = acceptor
- .mint(badge)
- .unwrap_or_else(|e| panic!("supervisor: no grant on {} for share {share}: {e:?}", dir.dir));
- (format!("{CAPABILITY_PREFIX}{}", dir.dir), connector)
+/// A grant on `acceptor`, the `role`'s port, for each of its directories,
+/// 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 { 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 share {share}: {e:?}", dir.dir));
+ (format!("{CAPABILITY_PREFIX}{}", dir.dir), connector)
+ })
+ .collect()
}
/// [`toyos_swap::PORT`] in a namespace of its own, for a program whose row |
|
Round 2 logs at head mut-H1-rename-reads.logmut-H2-any-access-byte-reads-write.logmut-H3-package-dir-writable.logmut-H4-row-named-package-allowed.logmut-H5-unknown-request-is-a-write.logmut-G0-harness-alone.logmut-G1-fileserver-ignores-access.logmut-G2-supervisor-mints-every-dir-writable.logmut-G3-launch-without-its-folder.logmut-G4-row-named-package-allowed.logmut-NC-whole-change-reverted.log |
|
T14 at
|
|
Review of #807 at Net lines: Round 1 BLOCKERs
The round-1 NOTEs are answered too:
Measured at this head:
Read for this round, with no defect found:
BLOCKERNone open. NOTE
LAND |
|
CI at |
Stage I1: stage 2 of the layout track, plus the
/appsslice of stage 2 of the isolation track. An installed app now sees its own package read-only and its own folder asHOME. Of what the file servers serve, it sees nothing else of/appsor/home, and nothing of/config,/state,/logor/boot. It still reaches/tmpand/system, which the kernel serves to every program.Before this change, every
/appslaunch was minted every directory of every role read-write, withHOME=/home/toy. That meant an installed app could write all of/apps,/home,/config,/stateand/log.This lands a regression for gbae's ROM browsing. Under this view gbae, started from the desktop, can browse to no ROM. A ROM given on its command line loads only from its own folder. This was measured, not guessed: see gbae below. It is recorded as
issues/an-installed-gbae-browses-to-no-rom.md, and its exit is the file picker of the package track's stage 6 (issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md). The grant was not widened for it.Head:
0ad87a593, ond6298c83e(main with #800, #802 and #803 merged in).What changed, per decision
toyos/src/fs.rs).GrantgainsAccess::{ReadOnly, ReadWrite}.GRANT_VERSION = 3, with one access byte between the share and the root, soMAX_GRANT_ROOTdrops from 55 to 54.userland/fileserver).writesis the grant's access ANDed with the volume's own writability. Both come from the kernel-stamped badge and the server's own volume, never from the client.fileserver::rights::changesclassifies every request on the wire: an open by its flags, ten requests that only read, and eight that change (WRITE,TRUNCATE,MKDIR,RMDIR,UNLINK,RENAME,SYMLINK,STREAM).PermissionDeniedon a connection that may not write, before the volume sees it.None. Its client is let go on every connection, read-only or not, with the same words as before (NO_SUCH_OPERATION).HELLO'sRIGHT_WRITEanswers the access and the volume together.toyos_manifest::Program::view.apps/<name>read-only, andhome/toy/Apps/<name>read-write.whole_tree(), every directory of every role read-write, as before.View. A compile-time assertion holds the longest package folder (MAX_PROGRAM_NAME= 32, underhome/toy/Apps/) withinMAX_GRANT_ROOT, somintandGrants::viewcannot fail andexpectthat bound.Manifest::app_row,row_named)./home/toy/Apps/<name>. The shell keeps its history inApps/shell.supervisor: launcher: the package shell is named after a row the image declares, and /home/toy/Apps/shell is that row's folder.HOMEis its folder, and a launch without it is refused.Program::homeanswers/home/toy/Apps/<name>for a package row.Config Data Cache State(toyos_manifest::APP_FOLDERS), and checks each is a directory (make_app_home).MSG_REFUSED), and the supervisor names why. That includes the case where the file worker is still busy with an earlier call.make_home, unchanged from main).HOMEis still/home/toy.[apps]keeps no DATA-wide grant at all. It is connectors only./appsis an open owner question. No package can write/apps, its own directory included.pkg, the shell, and every other declared row keep the whole tree read-write. Two of the owner's rulings pull against each other here, and they are filed asissues/whether-pkg-alone-writes-apps.md(kind: question), not decided.its badge is no grant.gbae
gbae
bfe8dabf8(gh api repos/Japabu/gbae/tarball/HEAD), started with no ROM, browsesstd::env::current_dir()(src/main.rs:471) withsrc/menu.rs'slist_directory. Its config is$HOME/.config/gbae/config.The measurement was a guest run, under QEMU on
tests/proctreecase, at1c8490e4e. A temporary patch, posted, made theapp_viewpackage run gbae'slist_directoryverbatim from its cwd, its ROM load, and its config save. EXIT=0. TheGBAElines:/, the menu lists/'s nine mount points./systemlistsbin/andetc/. Every other one lists only../, including/homeand/home/toy. Browsable ROMs:[]./in the code (a service gets the supervisor's cwd); I did not measure it./home/toy: the same,[].load_rom(/home/toy/Downloads/measure.gba):NotFound.load_rom(/home/toy/Apps/appview/Data/measure.gba):Ok(3).$HOME/.config/gbae/config: saved and read back in its own folder.I did not run the release binary itself. gbae v0.2.0's ToyOS build was linked on 2026-09-04 against an ABI that has moved since. Its menu is drawn in a window, which no harness here reads.
Checks: this is a security boundary
Negative control. The whole production change (
toyos/src/fs.rs,userland/fileserver,userland/supervisor,toyos-manifest,system.toml) was reverted ontod6298c83e, the base the green arm G0 was measured on. The test,tests/proctreecaseand the QEMU harness were kept.app_viewis red, EXIT=1, and names each hole:home_dir()is/home/toy;fs:/apps,fs:/home,fs:/config,fs:/state,fs:/logandfs:/boot;/apps/other/keptand/home/toy/Apps/other/Data/kept;Ok;/apps,/home,/home/toy,/home/toy/Apps,/stateand/loglist their contents;shellran.shellran and left the app's folder behind, so no file could be planted there. The job now names that as red (0ad87a593), and the control was run again.Mutations. Each was applied as a checked patch, run, and restored in the same script, leaving the tree clean. All of them, and the control, were run at
0ad87a593. Patches, script and logs are posted as comments.RENAMEcounted as a readfileserver::rightshost testrequest 15 writes)toyos::fshost testaccess 2)toyos-manifesthost testtoyos-manifesthost testfileserver::rightshost testrequest 0)app_viewunder QEMUevery arm heldapp_viewunder QEMUfs:/apps/appview says it is writableapp_viewunder QEMUapp_viewunder QEMUa package whose folder is a file ranapp_viewunder QEMUa package named after the shell's row ran, and it held/home/toy/Apps/shellasHOMEapp_viewunder QEMUIndependent oracle. None exists for this boundary: no external specification, differential implementation or third-party checker. The guest test spells every expected path from the owner's layout ruling (
issues/where-everything-lives.md). It does not asktoyos-manifest, so it does not read back the code the supervisor reads. The metal judge readsrow_namedfor the supervisor's refusal line, aslaunch_authority's judge readslaunch::refused.Tests
toyos::fs: a grant round-trips with both accesses; versions 2 and 4 are refused; access bytes 2, 0x80 and 0xff are refused.fileserver::rights: every request number from 1 to 19 is classified; an open is classified by its flags; an unknown number isNone.toyos-manifest: a package's view, spelled out in full and at the longest package name; a package named after any declared row is refused byrow_named,shellincluded; every declared row gets the whole tree; a package'sHOME; the shell'sHOMEis unchanged.app_viewis a metal row ontests/proctreecase, as round 1 ruled:test_rs_app_viewis aPROCTREECASEjob.every arm held; the job's closing line; and the supervisor's two refusals, by name.appview, beside another package and another app's folder.shell, andappviewwhile a file stands where its folder goes.appviewthrough the launcher. As the app, it checks:home_dir()is its folder, with all four sub-folders, and a write there lands in/home;toyos::fs::Dirpast std, as is std's own write;fs:names, reads neither of the other files, and no kernel mount point lists anything.Gates (head
0ad87a593)cargo run -- --ci hostcargo run -- --build-onlycargo test --test toyos-builduptimeload averages 66.42 / 72.94 / 67.15 before, 37.04 / 63.91 / 65.66 aftercargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases boot:proctreecaseproctreecase68e6a454…e5f450bb,testcases3efa3bad…2629e4d4,testcases-watchdog784e6e5c…274e44dd), with their sha256 inrequest.txt. Staging touched no machine; the orchestrator's reading of these images is below.T14 at
0ad87a593, run by the orchestrator (comment 6084824923): all three images' hashes matchedrequest.txt, eachtoyos-metal --fat32-checkexit 0; judge EXIT=0,[metal] 255 passed, 0 failed, 3 boot(s);process_tree,launch_toctou,launch_authority,port_badge,fs_share,app_viewPASS, withapp_view's refusals andevery arm heldin its log; 137 C casesccheck: 0;its badge is no grant0 times. The earlier reading atbb091478fis superseded.Net lines:
git diff --shortstat origin/main...HEADis 16 files, +753 −94.What I am unsure of
HOMEand the whole tree until isolation stage 2 gives every row a view.symlink_metadatasays it is not a directory. No test plants a symlink; the planted file covers the same branch./home/toy/Apps/appviewexisted after the package namedshellran, and that run's write ofApps/appview/Data/keptwas not refused. I did not trace which program made the folder. Nothing in this change rests on it.🤖 Generated with Claude Code
https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C