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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user