feat(storage): scoped grants (subtree, ro/rw), narrow-only, release revokes all holders; IPC KEEP_DELEGATE opt-in; shell cd/pwd, programs get ro cwd

This commit is contained in:
2026-10-27 20:20:34 +01:00
parent c603a578c0
commit 1fc5a54f06
38 changed files with 1237 additions and 170 deletions

View File

@@ -36,6 +36,7 @@ extern "C" fn main() {
match cat(&mut fs, path) {
Ok(()) => {}
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
Err(FsError::Denied) => println!("cat: {}: outside what it was given", path),
Err(_) => println!("cat: {}: cannot read", path),
}
}

View File

@@ -5,7 +5,7 @@
extern crate libkos;
use libkos::fs::Storage;
use libkos::fs::{FsError, Storage};
use libkos::println;
/// Where storage's buffer is mapped.
@@ -29,7 +29,9 @@ extern "C" fn main() {
println!("{:<24} {:>8}", name, e.size);
}
});
if listed.is_err() {
println!("ls: {}: no such directory", dir);
match listed {
Ok(_) => {}
Err(FsError::Denied) => println!("ls: {}: outside what it was given", dir),
Err(_) => println!("ls: {}: no such directory", dir),
}
}

View File

@@ -1,12 +1,14 @@
//! The shell: a few built-ins, and anything else is a program on disk.
//! `name args...` runs `name.elf` through init, handing it the terminal as
//! its output and the storage server, and nothing else; then waits for it.
//! its output and read-only storage rooted at the current directory, and
//! nothing else; then waits for it and takes the storage back.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::fs::Storage;
use libkos::spawn::{SpawnError, spawn};
use libkos::{print, println, syscall};
use types::bounded::CapSlot;
@@ -22,9 +24,76 @@ const INIT: CapSlot = CapSlot::const_new(2);
/// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
const MAX_ARGS: usize = 16;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: usize = 0x0180_0000;
/// Runs `args[0].elf` with `args` and waits for it to finish.
fn run_program(args: &[&str], done: CapSlot) {
/// The current directory, as a path on disk: no leading or trailing `/`.
struct Cwd {
path: [u8; types::storage::MAX_PATH],
len: usize,
}
impl Cwd {
fn as_bytes(&self) -> &[u8] {
&self.path[..self.len]
}
/// Where `cd to` from here leads: `/` starts at the root, `..` goes up
/// (the shell holds the whole disk, so that is its to resolve).
fn join(&self, to: &str) -> Option<Cwd> {
let mut next = match to.starts_with('/') {
true => Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,
},
false => Cwd {
path: self.path,
len: self.len,
},
};
for part in to.split('/') {
match part {
"" | "." => {}
".." => {
next.len = next
.as_bytes()
.iter()
.rposition(|b| *b == b'/')
.unwrap_or(0)
}
name => {
let sep = usize::from(next.len > 0);
let end = next.len + sep + name.len();
let room = next.path.get_mut(next.len..end)?;
room[..sep].copy_from_slice(&b"/"[..sep]);
room[sep..].copy_from_slice(name.as_bytes());
next.len = end;
}
}
}
Some(next)
}
}
impl core::fmt::Display for Cwd {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(
f,
"/{}",
core::str::from_utf8(self.as_bytes()).unwrap_or("?")
)
}
}
struct Shell {
fs: Storage,
cwd: Cwd,
done: CapSlot,
}
/// Runs `args[0].elf` with `args` in the current directory, and waits
/// for it to finish.
fn run_program(sh: &mut Shell, args: &[&str]) {
let mut file = [0u8; types::spawn::MAX_NAME];
let name = args[0].as_bytes();
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
@@ -42,23 +111,34 @@ fn run_program(args: &[&str], done: CapSlot) {
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
};
let call = PermissionSet::empty().with(Permission::Execute);
let grant = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
let arg = StartArg {
log: child.give(TERMINAL, call),
storage: child.give(STORAGE, call),
storage: grant.and_then(|g| child.give(g, call)),
args: child.set_args(args),
console: None,
};
let watched = syscall::watch_death(child.process, done).is_ok();
let watched = syscall::watch_death(child.process, sh.done).is_ok();
if child.start_with(arg) && watched {
let _ = syscall::wait(done);
let _ = syscall::wait(sh.done);
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
}
syscall::drop_cap(child.process);
if let Some(g) = grant {
let _ = libkos::fs::release(g);
}
}
fn run(line: &str, done: CapSlot) {
fn cd(sh: &mut Shell, to: &str) {
match sh.cwd.join(to) {
Some(next) if sh.fs.list_dir(next.as_bytes(), |_| {}).is_ok() => sh.cwd = next,
_ => println!("cd: {}: no such directory", to),
}
}
fn run(sh: &mut Shell, line: &str) {
let mut args = [""; MAX_ARGS];
let mut n = 0;
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
@@ -69,12 +149,15 @@ fn run(line: &str, done: CapSlot) {
match args.first().copied() {
None => {}
Some("help") => {
println!("built-ins: help, echo <text>, clear");
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
println!("anything else runs a program from the disk: try ls, cat readme.txt");
println!("programs see the current directory, read-only, and nothing above it");
}
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd),
Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL),
Some(_) => run_program(args, done),
Some(_) => run_program(sh, args),
}
}
@@ -101,13 +184,25 @@ extern "C" fn main() {
println!("shell: no notification to wait on");
return;
};
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
println!("shell: no storage");
return;
};
let mut sh = Shell {
fs,
cwd: Cwd {
path: [0; types::storage::MAX_PATH],
len: 0,
},
done,
};
println!("k-os shell. Type 'help'.");
let mut buf = [0u8; 256];
loop {
print!("$ ");
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
match core::str::from_utf8(&buf[..n]) {
Ok(line) => run(line.trim_end_matches('\n'), done),
Ok(line) => run(&mut sh, line.trim_end_matches('\n')),
Err(_) => println!("shell: not text"),
}
}

View File

@@ -7,6 +7,7 @@
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::spawn::{SpawnError, spawn};
use libkos::syscall;
use types::bounded::CapSlot;
@@ -17,8 +18,9 @@ use types::budget::Limits;
const STORAGE: CapSlot = CapSlot::const_new(0);
const INIT: CapSlot = CapSlot::const_new(1);
const CONTROL: CapSlot = CapSlot::const_new(2);
/// Where storage's buffer is mapped.
/// Where storage's buffers are mapped: the whole disk's, a narrowed one's.
const STORAGE_BUF_VA: usize = 0x0180_0000;
const NARROW_BUF_VA: usize = 0x0190_0000;
/// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n";
@@ -26,9 +28,14 @@ const NOTE: &[u8] = b"written by k-os on ext4\n";
/// the first has died and its limits came back.
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
/// Spawns hello, runs it, and waits for it to die.
fn run_hello(died: CapSlot) -> Result<(), &'static str> {
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one.
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on");
}
syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?;
if !child.start() {
return Err("cannot start hello");
@@ -38,8 +45,7 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> {
}
/// A file written through storage, which the host then checks with e2fsck.
fn write_a_file() -> Result<(), &'static str> {
let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
if fs.write(h, NOTE) != Ok(NOTE.len()) {
return Err("short write");
@@ -55,12 +61,40 @@ fn write_a_file() -> Result<(), &'static str> {
Ok(())
}
/// A read-only grant to one directory reaches nothing else and changes
/// nothing, can be passed on, and is gone everywhere once released.
fn narrow_storage(fs: &mut Storage, died: CapSlot) -> Result<(), &'static str> {
let grant = fs.grant(b"lost+found", false).map_err(|_| "storage refused a grant")?;
let mut inner = Storage::attach(grant, NARROW_BUF_VA).map_err(|_| "cannot attach through the grant")?;
inner.list_dir(b"", |_| {}).map_err(|_| "cannot list its own directory")?;
if inner.open(b"../selftest.txt").err() != Some(FsError::Denied) {
return Err("a grant reached outside its directory");
}
if inner.create(b"x").err() != Some(FsError::Denied) {
return Err("a read-only grant created a file");
}
let widened = inner.grant(b"", true).and_then(|g| Storage::attach(g, NARROW_BUF_VA + 0x10_0000));
if widened.and_then(|mut w| w.create(b"x")).is_ok() {
return Err("a grant was widened to write");
}
run_hello(died, Some(grant))?;
libkos::fs::release(grant).map_err(|_| "release failed")?;
if inner.list_dir(b"", |_| {}).is_ok() {
return Err("a released grant still works");
}
Ok(())
}
fn check() -> Result<(), &'static str> {
write_a_file()?;
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?;
libkos::println!("spawntest: a file written to ext4 reads back: ok");
let died = syscall::create_notification().map_err(|_| "no notification")?;
run_hello(died)?;
narrow_storage(&mut fs, died)?;
libkos::println!("spawntest: storage grants stay in scope, pass on, release: ok");
run_hello(died, None)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
@@ -73,7 +107,7 @@ fn check() -> Result<(), &'static str> {
}
libkos::println!("spawntest: allowance and missing files refused: ok");
run_hello(died)?;
run_hello(died, None)?;
libkos::println!("spawntest: allowance comes back when a child dies: ok");
Ok(())
}