diff --git a/src/syscall/microkernel/procstat_redact.rs b/src/syscall/microkernel/procstat_redact.rs index 7bcf2de59..90bef5852 100644 --- a/src/syscall/microkernel/procstat_redact.rs +++ b/src/syscall/microkernel/procstat_redact.rs @@ -35,7 +35,13 @@ pub(super) fn sees_all() -> bool { /// `e` as the caller may see it. pub(super) fn visible(mut e: ProcStatEntry, caller: u32, all: bool) -> ProcStatEntry { - if all || e.pid == caller { + /* + * A foreign supervisor answers for the guests it hosts, and reports their + * times and memory to them as Linux's getrusage and /proc do; it sees + * those rows and no one else's. + */ + let hosts = caller != 0 && crate::process::foreign::supervisor_of(e.pid) == Some(caller); + if all || e.pid == caller || hosts { return e; } e.run_ticks = 0; diff --git a/userland/capsule_linux/abi/disclosure.txt b/userland/capsule_linux/abi/disclosure.txt index 73d10c230..0121821cf 100644 --- a/userland/capsule_linux/abi/disclosure.txt +++ b/userland/capsule_linux/abi/disclosure.txt @@ -112,3 +112,44 @@ memfd_create | a descriptor number | its own table statx | as stat | as stat rseq | 0 | a constant faccessat2 | as access | as open +pwrite64 | nothing back but a count | as write +preadv | bytes of its own files, as read | as read +pwritev | nothing back but a count | as write +preadv2 | as preadv | as read +pwritev2 | as pwritev | as write +sendfile | a count of bytes moved between its own descriptors | its own data +copy_file_range | a count of bytes copied between its own files | its own data +truncate | success or refusal on a path | as open +fallocate | success or refusal on its own file | its own state +fadvise64 | nothing | none +close_range | nothing | none +creat | as open | as open +openat2 | as open, and whether a walk left the directory it named | the walk is of the guest's own tree +sync | nothing | none +syncfs | nothing | none +fdatasync | whether its own writes reached the store | its own data +flock | whether another process of the family holds a lock on a file | only the family's own locks are seen +setxattr | success or refusal of an attribute on its own file | its own state +lsetxattr | as setxattr | its own state +fsetxattr | as setxattr | its own state +getxattr | an attribute the family set on a file | only what the family itself set +lgetxattr | as getxattr | only what the family itself set +fgetxattr | as getxattr | only what the family itself set +listxattr | the names of the attributes the family set | only what the family itself set +llistxattr | as listxattr | only what the family itself set +flistxattr | as listxattr | only what the family itself set +removexattr | success or refusal | its own state +lremovexattr | as removexattr | its own state +fremovexattr | as removexattr | its own state +sysinfo | the family's uptime, its memory limit and what it holds, its own load, one CPU | declared figures and the family's own measurements; nothing of the machine +getrusage | its own threads' CPU ticks, switches, faults and resident size, and those of children it waited for | the kernel's counts of the family's own threads only +times | its own and its waited children's CPU ticks, and ticks since the family started | its own threads only +getgroups | no supplementary groups | a fixed identity, the same on every install +setgroups | refusal | the identity cannot change +getresuid | 0, 0, 0 | a fixed identity, the same on every install +getresgid | 0, 0, 0 | a fixed identity, the same on every install +setresuid | success only for the identity already held | the identity cannot change +setresgid | success only for the identity already held | the identity cannot change +getpriority | the nice value set for a process of the family | its own state; no process outside the family is named +setpriority | success or refusal of a nice value for a process of the family | its own state; no process outside the family is named +personality | PER_LINUX, the only persona served | the same on every install diff --git a/userland/capsule_linux/src/linux/abi/errno.rs b/userland/capsule_linux/src/linux/abi/errno.rs index 448c49fa7..6a73af950 100644 --- a/userland/capsule_linux/src/linux/abi/errno.rs +++ b/userland/capsule_linux/src/linux/abi/errno.rs @@ -16,6 +16,7 @@ //! Linux errno values, and the convention for returning them. +pub use super::errno_io::*; pub const EPERM: i64 = 1; pub const ENOENT: i64 = 2; pub const EINTR: i64 = 4; diff --git a/userland/capsule_linux/src/linux/abi/errno_io.rs b/userland/capsule_linux/src/linux/abi/errno_io.rs new file mode 100644 index 000000000..0179faf66 --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/errno_io.rs @@ -0,0 +1,27 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The errnos the file, lock and xattr calls answer with, beyond the ones + * errno.rs has always held. + */ + +pub const ENXIO: i64 = 6; +pub const EXDEV: i64 = 18; +pub const EFBIG: i64 = 27; +pub const ENOLCK: i64 = 37; +pub const ENODATA: i64 = 61; +pub const EOPNOTSUPP: i64 = 95; diff --git a/userland/capsule_linux/src/linux/abi/mod.rs b/userland/capsule_linux/src/linux/abi/mod.rs index 5641aa16d..0eef111f4 100644 --- a/userland/capsule_linux/src/linux/abi/mod.rs +++ b/userland/capsule_linux/src/linux/abi/mod.rs @@ -19,9 +19,11 @@ #![allow(dead_code)] pub mod errno; +pub mod errno_io; pub mod name; pub mod nr; pub mod nr_path; +pub mod nr_file; pub mod nr_high; pub mod nr_sig; pub mod nr_sched; diff --git a/userland/capsule_linux/src/linux/abi/nr_file.rs b/userland/capsule_linux/src/linux/abi/nr_file.rs new file mode 100644 index 000000000..bc114485b --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/nr_file.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Syscall numbers for the file, lock, xattr, id and usage calls, transcribed + * from the x86_64 table. + */ + +/* Files, their data and their locks; from syscall_64.tbl. */ +pub const SENDFILE: u64 = 40; +pub const FLOCK: u64 = 73; +pub const FDATASYNC: u64 = 75; +pub const TRUNCATE: u64 = 76; +pub const CREAT: u64 = 85; +pub const SYNC: u64 = 162; +pub const SETXATTR: u64 = 188; +pub const LSETXATTR: u64 = 189; +pub const FSETXATTR: u64 = 190; +pub const GETXATTR: u64 = 191; +pub const LGETXATTR: u64 = 192; +pub const FGETXATTR: u64 = 193; +pub const LISTXATTR: u64 = 194; +pub const LLISTXATTR: u64 = 195; +pub const FLISTXATTR: u64 = 196; +pub const REMOVEXATTR: u64 = 197; +pub const LREMOVEXATTR: u64 = 198; +pub const FREMOVEXATTR: u64 = 199; +pub const FADVISE64: u64 = 221; +pub const FALLOCATE: u64 = 285; +pub const PREADV: u64 = 295; +pub const PWRITEV: u64 = 296; +pub const SYNCFS: u64 = 306; +pub const COPY_FILE_RANGE: u64 = 326; +pub const PREADV2: u64 = 327; +pub const PWRITEV2: u64 = 328; +pub const CLOSE_RANGE: u64 = 436; +pub const OPENAT2: u64 = 437; + +/* What the system is and what the process used; from syscall_64.tbl. */ +pub const GETRUSAGE: u64 = 98; +pub const SYSINFO: u64 = 99; +pub const TIMES: u64 = 100; +pub const GETGROUPS: u64 = 115; +pub const SETGROUPS: u64 = 116; +pub const SETRESUID: u64 = 117; +pub const SETRESGID: u64 = 119; +pub const PERSONALITY: u64 = 135; +pub const GETPRIORITY: u64 = 140; +pub const SETPRIORITY: u64 = 141; diff --git a/userland/capsule_linux/src/linux/abi/nr_path.rs b/userland/capsule_linux/src/linux/abi/nr_path.rs index ca08f795b..b3653e40e 100644 --- a/userland/capsule_linux/src/linux/abi/nr_path.rs +++ b/userland/capsule_linux/src/linux/abi/nr_path.rs @@ -17,6 +17,7 @@ //! Syscall numbers for the path, time and process calls, transcribed from the //! x86_64 table. +pub use super::nr_file::*; pub const CHDIR: u64 = 80; pub const FCHDIR: u64 = 81; pub const RENAME: u64 = 82; diff --git a/userland/capsule_linux/src/linux/call/ctl.rs b/userland/capsule_linux/src/linux/call/ctl.rs index 4c3c129f6..8bf8dc013 100644 --- a/userland/capsule_linux/src/linux/call/ctl.rs +++ b/userland/capsule_linux/src/linux/call/ctl.rs @@ -17,6 +17,7 @@ //! `fcntl`. use crate::linux::abi::errno; +use crate::linux::file; use crate::linux::file::flags::{O_NONBLOCK, O_RDWR, O_WRONLY}; use crate::linux::guest::{Fd, Guest, Kind}; @@ -25,6 +26,7 @@ const F_GETFD: u64 = 1; const F_SETFD: u64 = 2; const F_GETFL: u64 = 3; const F_SETFL: u64 = 4; +const F_DUPFD_CLOEXEC: u64 = 1030; /// The only descriptor flag there is. const FD_CLOEXEC: u64 = 1; @@ -53,11 +55,10 @@ pub fn fcntl(guest: &mut Guest, fd: u64, cmd: u64, arg: u64) -> u64 { errno::ok(0) } F_GETFL => errno::ok(status(entry)), - /* - * Duplication needs a second handle on the server, which the store - * does not offer yet. - */ - F_DUPFD => errno::fail(errno::ENOSYS), + /* The lowest free number at or above `arg`: where a shell keeps one aside. */ + F_DUPFD | F_DUPFD_CLOEXEC => file::dup_from(guest, fd, arg, cmd == F_DUPFD_CLOEXEC), + /* The record locks; a wait among them is parked before this is reached. */ + c if file::is_lock_cmd(c) => file::fcntl_lock(guest, fd, cmd, arg), _ => errno::fail(errno::EINVAL), } } diff --git a/userland/capsule_linux/src/linux/call/cwd.rs b/userland/capsule_linux/src/linux/call/cwd.rs index 474e0b54d..9bf285e66 100644 --- a/userland/capsule_linux/src/linux/call/cwd.rs +++ b/userland/capsule_linux/src/linux/call/cwd.rs @@ -14,28 +14,29 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Moving the working directory. +/* Moving the working directory. */ use crate::linux::abi::errno; -use crate::linux::file::{look, read_path, visible}; +use crate::linux::file::{follow, join, look, read_path}; use crate::linux::guest::{Guest, Kind}; pub fn chdir(guest: &mut Guest, path: u64) -> u64 { let Some(name) = read_path(guest, path) else { return errno::fail(errno::EFAULT); }; - let at = guest.links.follow(visible(&guest.cwd, &name), true); - // Checked before it is taken. + let at = follow(guest, join(&guest.cwd, &name), true); + /* Checked before it is taken. */ match look(&at) { - Some(_) => { + Some((_, true)) => { guest.cwd = at; errno::ok(0) } + Some(_) => errno::fail(errno::ENOTDIR), None => errno::fail(errno::ENOENT), } } -/// `fchdir`: the same, named by a directory the guest already opened. +/* `fchdir`: the same, named by a directory the guest already opened. */ pub fn fchdir(guest: &mut Guest, fd: u64) -> u64 { let Some(entry) = guest.fds.get(fd as usize).filter(|f| f.kind == Kind::Dir) else { return errno::fail(errno::EBADF); @@ -44,8 +45,10 @@ pub fn fchdir(guest: &mut Guest, fd: u64) -> u64 { errno::ok(0) } -/// `getcwd` writes the path and returns its length including the terminator, -/// which is what a libc uses to tell success from a buffer that was too small. +/* + * `getcwd` writes the path and returns its length including the terminator, + * which is what a libc uses to tell success from a buffer that was too small. + */ pub fn getcwd(guest: &Guest, buf: u64, len: u64) -> u64 { let mut out = guest.cwd.clone(); out.push(0); diff --git a/userland/capsule_linux/src/linux/call/ident.rs b/userland/capsule_linux/src/linux/call/ident.rs index eca7a107b..b8b013b12 100644 --- a/userland/capsule_linux/src/linux/call/ident.rs +++ b/userland/capsule_linux/src/linux/call/ident.rs @@ -16,19 +16,29 @@ //! Who the guest is, and which process group it belongs to. use crate::linux::abi::errno; +use crate::linux::file; use crate::linux::guest::Guest; -/// The identity every guest runs as. +/* The identity every guest runs as. */ const GUEST_UID: u64 = 0; +/* + * The process that forked this one, as /proc//stat names it; the + * personality, the namespace's pid 1, for the program it started. A kernel + * pid: the serve loop gives it the number the namespace knows it by. + */ pub fn getppid(guest: &Guest) -> u64 { - // The personality is the parent of every guest it hosts. - errno::ok(u64::from(guest.parent)) + let parent = file::view_with(|v| { + let me = v.procs.iter().find(|p| p.kernel == guest.pid)?; + v.procs.iter().find(|p| p.ns == me.ppid).map(|p| p.kernel) + }); + errno::ok(u64::from(parent.unwrap_or(guest.parent))) } - -/// Setting the identity to the one already held is the only change -/// that can be honoured, so it is the only one accepted. +/* + * Setting the identity to the one already held is the only change + * that can be honoured, so it is the only one accepted. + */ pub fn setuid(want: u64) -> u64 { match want { GUEST_UID => errno::ok(0), diff --git a/userland/capsule_linux/src/linux/call/limits_table.rs b/userland/capsule_linux/src/linux/call/limits_table.rs index cc1785184..57698535a 100644 --- a/userland/capsule_linux/src/linux/call/limits_table.rs +++ b/userland/capsule_linux/src/linux/call/limits_table.rs @@ -14,28 +14,30 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Which limit each resource number reports. +/* Which limit each resource number reports. */ use crate::linux::file::MAX_FDS; -/// `struct rlimit` is a soft limit then a hard one, both 64-bit. +/* `struct rlimit` is a soft limit then a hard one, both 64-bit. */ pub(super) const RLIMIT: usize = 16; const RLIMIT_STACK: u64 = 3; const RLIMIT_NOFILE: u64 = 7; const RLIMIT_AS: u64 = 9; -/// What a guest's stack is given, from the loader that maps it. +/* What a guest's stack is given, from the loader that maps it. */ const STACK_BYTES: u64 = 1 << 20; -/// The top of the guest's own half, which is the most address space one -/// can hold however it asks. +/* + * The top of the guest's own half, which is the most address space one + * can hold however it asks. + */ const ADDRESS_SPACE: u64 = 0x0000_7FFF_F000; -/// Unlimited, as Linux spells it. +/* Unlimited, as Linux spells it. */ const INFINITY: u64 = u64::MAX; -pub(super) fn limit_for(resource: u64) -> Option<(u64, u64)> { +pub fn limit_for(resource: u64) -> Option<(u64, u64)> { match resource { RLIMIT_STACK => Some((STACK_BYTES, STACK_BYTES)), RLIMIT_NOFILE => Some((MAX_FDS as u64, MAX_FDS as u64)), @@ -47,4 +49,3 @@ pub(super) fn limit_for(resource: u64) -> Option<(u64, u64)> { _ => Some((INFINITY, INFINITY)), } } - diff --git a/userland/capsule_linux/src/linux/call/mod.rs b/userland/capsule_linux/src/linux/call/mod.rs index 53010e330..cb60de81d 100644 --- a/userland/capsule_linux/src/linux/call/mod.rs +++ b/userland/capsule_linux/src/linux/call/mod.rs @@ -41,6 +41,7 @@ mod pipe_end; mod pipe_io; mod pipe_poll; mod pipe_read; +mod process; mod sched; mod session; pub mod sigframe; @@ -76,6 +77,7 @@ mod uname; mod vector; mod vector_read; +pub use process::*; pub use ctl::fcntl; pub use ioctl::ioctl; pub use cwd::{chdir, fchdir, getcwd}; @@ -85,6 +87,7 @@ pub use io::{close, read, write}; pub use life::{exit, exit_thread, killed, set_tid_address}; pub use life_one::exit_one; pub use limits::{getrlimit, prlimit64}; +pub use limits_table::limit_for; pub use glibc::prctl; pub use glibc_sched::{clone3, getcpu, membarrier, sched_getaffinity}; pub use mem::{brk, mmap, mprotect, mremap, munmap, MapReq}; diff --git a/userland/capsule_linux/src/linux/call/process/ids/creds.rs b/userland/capsule_linux/src/linux/call/process/ids/creds.rs new file mode 100644 index 000000000..ac534fef5 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/ids/creds.rs @@ -0,0 +1,70 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The ids a guest runs as, which are root's; its groups, which are none; + * and its execution domain, which is Linux's. + */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/* getresuid and getresgid: real, effective and saved, all 0. */ +pub fn getres(guest: &Guest, ids: [u64; 3]) -> u64 { + for at in ids { + if guest.write(at, &0u32.to_le_bytes()) != 4 { + return errno::fail(errno::EFAULT); + } + } + errno::ok(0) +} + +/* setresuid and setresgid: -1 keeps an id, 0 is the one it has. */ +pub fn setres(ids: [u64; 3]) -> u64 { + match ids.iter().all(|id| *id as u32 == u32::MAX || *id as u32 == 0) { + true => errno::ok(0), + false => errno::fail(errno::EPERM), + } +} + +/* No supplementary groups. */ +pub fn getgroups(size: u64) -> u64 { + match (size as i32) < 0 { + true => errno::fail(errno::EINVAL), + false => errno::ok(0), + } +} + +/* Changing groups needs CAP_SETGID, which no guest holds. */ +pub fn setgroups() -> u64 { + errno::fail(errno::EPERM) +} + +const PER_LINUX: u64 = 0; + +const QUERY: u64 = 0xffff_ffff; + +/* Only asking is served: every guest runs as PER_LINUX. */ +pub fn personality(persona: u64) -> u64 { + match persona & 0xffff_ffff { + QUERY | PER_LINUX => errno::ok(PER_LINUX), + _ => { + let line = b"[LINUX] refused personality: every guest runs as PER_LINUX\n"; + let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); + errno::fail(errno::EINVAL) + } + } +} diff --git a/userland/capsule_linux/src/linux/call/process/ids/mod.rs b/userland/capsule_linux/src/linux/call/process/ids/mod.rs new file mode 100644 index 000000000..28a3c754a --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/ids/mod.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Who the guest is beyond its uid: its three ids, its groups, its nice + * value, and its execution domain. + * + * Every id the personality reports is root's, and a guest holds no + * capability (capget is refused), so Linux's rule for a process without + * CAP_SETUID applies: it may set an id only to one it already has, which + * here is 0. A nice value is kept and reported, not acted on: the + * family's threads are scheduled by NONOS, which does not read it. + */ + +mod creds; +mod nice; +mod who; + +pub use creds::{getgroups, getres, personality, setgroups, setres}; +pub use nice::{getpriority, nice_of, setpriority}; diff --git a/userland/capsule_linux/src/linux/call/process/ids/nice.rs b/userland/capsule_linux/src/linux/call/process/ids/nice.rs new file mode 100644 index 000000000..ea1020d87 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/ids/nice.rs @@ -0,0 +1,68 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Each process's nice value, as setpriority keeps it and getpriority + * reports it. NONOS schedules the family's threads without reading it. + */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::who::named; + +pub(super) struct Nice(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Nice {} + +static NICE: Nice = Nice(RefCell::new(Vec::new())); + +pub fn nice_of(kernel: u32) -> i64 { + NICE.0.borrow().iter().find(|(p, _)| *p == kernel).map_or(0, |(_, n)| *n) +} + +/* The raw syscall answers 20 - nice, so that no success is negative. */ +pub fn getpriority(guest: &Guest, which: u64, who: u64) -> u64 { + match named(guest, which, who) { + Ok(all) => errno::ok((20 - all.iter().map(|k| nice_of(*k)).min().unwrap_or(0)) as u64), + Err(e) => errno::fail(e), + } +} + +/* Without CAP_SYS_NICE a process may only lower its priority: raise nice. */ +pub fn setpriority(guest: &Guest, which: u64, who: u64, value: u64) -> u64 { + let want = (value as i32 as i64).clamp(-20, 19); + let all = match named(guest, which, who) { + Ok(all) => all, + Err(e) => return errno::fail(e), + }; + if all.iter().any(|k| want < nice_of(*k)) { + return errno::fail(errno::EACCES); + } + let mut table = NICE.0.borrow_mut(); + for k in all { + table.retain(|(p, _)| *p != k); + table.push((k, want)); + } + errno::ok(0) +} diff --git a/userland/capsule_linux/src/linux/call/process/ids/who.rs b/userland/capsule_linux/src/linux/call/process/ids/who.rs new file mode 100644 index 000000000..e58dada2b --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/ids/who.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Which processes a priority call names: one, a group, or a user's. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::file; +use crate::linux::guest::Guest; + +const PRIO_PROCESS: u64 = 0; + +const PRIO_PGRP: u64 = 1; + +const PRIO_USER: u64 = 2; + +/* The processes `which` and `who` name, by kernel pid, among the family's. */ +pub(super) fn named(guest: &Guest, which: u64, who: u64) -> Result, i64> { + let who = who as u32; + let found: Vec = file::view_with(|v| match which { + PRIO_PROCESS if who == 0 => alloc::vec![guest.pid], + PRIO_PROCESS => v.procs.iter().filter(|p| p.ns == who).map(|p| p.kernel).collect(), + PRIO_PGRP => { + let group = if who == 0 { v.find(v.me).map_or(0, |p| p.pgid) } else { who }; + v.procs.iter().filter(|p| p.pgid == group).map(|p| p.kernel).collect() + } + /* Every process of the family is root's. */ + PRIO_USER if who == 0 => v.procs.iter().map(|p| p.kernel).collect(), + _ => Vec::new(), + }); + match which { + PRIO_PROCESS | PRIO_PGRP | PRIO_USER if found.is_empty() => Err(errno::ESRCH), + PRIO_PROCESS | PRIO_PGRP | PRIO_USER => Ok(found), + _ => Err(errno::EINVAL), + } +} diff --git a/userland/capsule_linux/src/linux/call/process/mod.rs b/userland/capsule_linux/src/linux/call/process/mod.rs new file mode 100644 index 000000000..ef8693121 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/mod.rs @@ -0,0 +1,28 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What a process is and has used: its ids, groups, nice value + * and execution domain, and its usage. + */ + +pub(super) mod ids; +pub(super) mod usage; + +pub use ids::{ + getgroups, getpriority, getres, nice_of, personality, setgroups, setpriority, setres, +}; +pub use usage::{getrusage, mine as usage_of, sysinfo, times}; diff --git a/userland/capsule_linux/src/linux/call/process/usage/mod.rs b/userland/capsule_linux/src/linux/call/process/usage/mod.rs new file mode 100644 index 000000000..c0d5a4533 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/usage/mod.rs @@ -0,0 +1,34 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * sysinfo, getrusage and times: what the family has used, as the kernel + * measured it for the family's own threads (file/system/cpu/), and the system + * as NONOS declares it (file/system/declared/). Two figures are not what Linux + * means by them: the kernel keeps no peak resident size, so ru_maxrss is + * the resident size at the call, and it does not tell a voluntary switch + * from another, so ru_nvcsw counts every switch and ru_nivcsw none. The + * store's reads and writes are not counted per process, so ru_inblock and + * ru_oublock are zero. The fields Linux itself leaves at zero are zero. + */ + +mod rusage; +mod sysinfo; +mod times; + +pub use rusage::getrusage; +pub use sysinfo::sysinfo; +pub use times::{mine, times}; diff --git a/userland/capsule_linux/src/linux/call/process/usage/rusage.rs b/userland/capsule_linux/src/linux/call/process/usage/rusage.rs new file mode 100644 index 000000000..e4d3162b7 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/usage/rusage.rs @@ -0,0 +1,57 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * getrusage: what a process, a thread or the children it waited for + * used, in the kernel's ticks for the family's own threads. + */ + +use crate::linux::abi::errno; +use crate::linux::file::{self}; +use crate::linux::guest::Guest; + +use super::times::{children, mine, TICK_MS}; + +const RUSAGE_SELF: i64 = 0; + +const RUSAGE_CHILDREN: i64 = -1; + +const RUSAGE_THREAD: i64 = 1; + +pub fn getrusage(guest: &Guest, tid: u32, who: u64, out: u64) -> u64 { + let used = match who as i64 { + RUSAGE_SELF => mine(guest), + RUSAGE_THREAD => file::cpu::usage(&[tid]), + RUSAGE_CHILDREN => children(guest), + _ => return errno::fail(errno::EINVAL), + }; + let mut b = [0u8; 144]; + let mut put = |at: usize, v: u64| b[at..at + 8].copy_from_slice(&v.to_le_bytes()); + let tv = |ticks: u64| ((ticks * TICK_MS) / 1000, (ticks * TICK_MS) % 1000 * 1000); + let (us, uu) = tv(used.user); + let (ss, su) = tv(used.system); + put(0, us); + put(8, uu); + put(16, ss); + put(24, su); + put(32, used.resident_kb); /* ru_maxrss: the resident size now, no peak being kept */ + put(64, used.faults); /* ru_minflt */ + put(128, used.switches); /* ru_nvcsw: every switch the kernel counted */ + match guest.write(out, &b) { + 144 => errno::ok(0), + _ => errno::fail(errno::EFAULT), + } +} diff --git a/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs new file mode 100644 index 000000000..b20107b85 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* sysinfo: the family's uptime, load, memory and threads. */ + +use crate::linux::abi::errno; +use crate::linux::file::{self, declared}; +use crate::linux::guest::Guest; + +use super::super::super::family_ms; + +pub fn sysinfo(guest: &Guest, out: u64) -> u64 { + let (threads, resident, ran) = file::view_with(|v| { + let leaders: alloc::vec::Vec = v.procs.iter().map(|p| p.kernel).collect(); + let all: alloc::vec::Vec<&file::Proc> = v.procs.iter().collect(); + let u = file::cpu::usage(&file::cpu::threads_of(&all)); + let n: usize = v.procs.iter().map(|p| p.tids.len()).sum(); + (n.max(1), file::cpu::usage(&leaders).resident_kb * 1024, u.user + u.system) + }); + let loads = file::load::averages(family_ms(), ran); + let mut b = [0u8; 112]; + let mut put = |at: usize, v: u64| b[at..at + 8].copy_from_slice(&v.to_le_bytes()); + put(0, family_ms() / 1000); /* uptime */ + for (i, avg) in loads.iter().enumerate() { + put(8 + i * 8, avg << 5); /* loads, from Linux's 11 bits to sysinfo's 16 */ + } + put(32, declared::MEMORY); /* totalram */ + let cached = file::cache_bytes(); + put(40, declared::MEMORY.saturating_sub(resident).saturating_sub(cached)); /* freeram */ + put(48, cached); /* sharedram, the family's copies of tmpfs files */ + b[80..82].copy_from_slice(&(threads.min(u16::MAX as usize) as u16).to_le_bytes()); /* procs */ + b[104..108].copy_from_slice(&1u32.to_le_bytes()); /* mem_unit */ + match guest.write(out, &b) { + 112 => errno::ok(0), + _ => errno::fail(errno::EFAULT), + } +} diff --git a/userland/capsule_linux/src/linux/call/process/usage/times.rs b/userland/capsule_linux/src/linux/call/process/usage/times.rs new file mode 100644 index 000000000..619ed3aa2 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/usage/times.rs @@ -0,0 +1,56 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* times, and what the calling process and its waited-for children used. */ + +use crate::linux::abi::errno; +use crate::linux::file::{self, cpu::Usage, declared}; +use crate::linux::guest::Guest; + +use super::super::super::family_ms; + +pub(super) const TICK_MS: u64 = 1000 / declared::HZ; + +/* + * times: the process's and its waited-for children's ticks, and the + * ticks since the family started. + */ +pub fn times(guest: &Guest, out: u64) -> u64 { + if out != 0 { + let (me, kids) = (mine(guest), children(guest)); + let mut b = [0u8; 32]; + for (i, v) in [me.user, me.system, kids.user, kids.system].iter().enumerate() { + b[i * 8..i * 8 + 8].copy_from_slice(&v.to_le_bytes()); + } + if guest.write(out, &b) != 32 { + return errno::fail(errno::EFAULT); + } + } + errno::ok(family_ms() / TICK_MS) +} + +/* Every thread of the calling process. */ +pub fn mine(guest: &Guest) -> Usage { + let mut all = alloc::vec![guest.pid]; + all.extend_from_slice(&guest.threads); + let mut u = file::cpu::usage(&all); + u.resident_kb = file::cpu::usage(&[guest.pid]).resident_kb; + u +} + +pub(super) fn children(guest: &Guest) -> Usage { + file::cpu::children(guest.pid, |c| !guest.children.contains(&c)) +} diff --git a/userland/capsule_linux/src/linux/call/uname.rs b/userland/capsule_linux/src/linux/call/uname.rs index ef6451614..a43c60045 100644 --- a/userland/capsule_linux/src/linux/call/uname.rs +++ b/userland/capsule_linux/src/linux/call/uname.rs @@ -14,33 +14,35 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! `uname`. Six fixed fields of sixty-five bytes, in Linux's order. -//! -//! `sysname` says Linux because it names the ABI this capsule implements, -//! which is the question the caller is asking: a program reads it to -//! decide which syscalls exist. What machine it is really running on is -//! in the other fields, and they say NONOS rather than pretending. +/* + * `uname`. Six fixed fields of sixty-five bytes, in Linux's order. + * + * `sysname` says Linux because it names the ABI this capsule implements, + * which is the question the caller is asking: a program reads it to + * decide which syscalls exist. What machine it is really running on is + * in the other fields, and they say NONOS rather than pretending. + */ use crate::linux::abi::errno; +use crate::linux::file::declared; use crate::linux::guest::Guest; const FIELD: usize = 65; const UTSNAME_LEN: usize = FIELD * 6; -/// The oldest release that has every call this capsule serves. A program -/// gating a feature on the version gets an answer that matches what it -/// will actually find here. -const RELEASE: &[u8] = b"6.1.0"; - +/* + * The oldest release that has every call this capsule serves. A program + * gating a feature on the version gets an answer that matches what it + * will actually find here. + */ pub fn uname(guest: &mut Guest, out: u64) -> u64 { let mut buf = [0u8; UTSNAME_LEN]; - put(&mut buf, 0, b"Linux"); - put(&mut buf, 1, b"nonos"); - put(&mut buf, 2, RELEASE); - put(&mut buf, 3, b"NONOS Linux personality"); - put(&mut buf, 4, b"x86_64"); - put(&mut buf, 5, b"nonos"); + put(&mut buf, 0, declared::OSTYPE); + put(&mut buf, 1, declared::HOSTNAME); + put(&mut buf, 2, declared::RELEASE); + put(&mut buf, 3, declared::VERSION); + put(&mut buf, 4, declared::MACHINE); + put(&mut buf, 5, declared::DOMAIN); if guest.write(out, &buf) < UTSNAME_LEN as i64 { return errno::fail(errno::EFAULT); } diff --git a/userland/capsule_linux/src/linux/file/at.rs b/userland/capsule_linux/src/linux/file/at.rs index 171bfd0d7..af5825e3f 100644 --- a/userland/capsule_linux/src/linux/file/at.rs +++ b/userland/capsule_linux/src/linux/file/at.rs @@ -14,35 +14,48 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! A `dirfd` and a path, resolved to one absolute name. +/* A `dirfd` and a path, resolved to one absolute name. */ use alloc::vec::Vec; +use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; use super::flags::AT_FDCWD; use super::path::read_path; -use super::resolve::visible; -/// The guest-visible absolute path `dirfd` and `path` name together, or `None` -/// when the path cannot be read or the descriptor is not a directory this -/// guest opened. pub fn resolve_at(guest: &Guest, dirfd: u64, path: u64) -> Option> { let name = read_path(guest, path)?; - // The *at calls act on the name, so its own last component is not followed. - let full = match name.first() == Some(&b'/') { - true => visible(b"/", &name), - false => visible(&base_of(guest, dirfd)?, &name), - }; - Some(guest.links.follow(full, false)) + let full = named_at(guest, dirfd, &name).ok()?; + /* The *at calls act on the name, so its own last component is not followed. */ + Some(super::walk::follow(guest, full, false)) } -fn base_of(guest: &Guest, dirfd: u64) -> Option> { +/* + * The name `name` gives against `dirfd`, joined and nothing resolved yet: + * `..` and links are walked in order by `walk::follow`. A directory + * descriptor keeps the path it was opened at, so a chdir made since does + * not move what it names. + */ +pub fn named_at(guest: &Guest, dirfd: u64, name: &[u8]) -> Result, i64> { + let dirfd = super::flags::dirfd(dirfd); + if name.first() == Some(&b'/') { + return Ok(name.to_vec()); + } if dirfd == AT_FDCWD { - return Some(guest.cwd.clone()); + return Ok(join(&guest.cwd, name)); + } + match guest.fds.get(dirfd as usize).filter(|f| f.is_open()) { + Some(fd) if fd.kind == Kind::Dir => Ok(join(&fd.path, name)), + Some(_) => Err(errno::ENOTDIR), + None => Err(errno::EBADF), } - match guest.fds.get(dirfd as usize) { - Some(fd) if fd.kind == Kind::Dir => Some(fd.path.clone()), - _ => None, +} + +/* `name` under the directory `base`; an absolute `name` is itself. */ +pub fn join(base: &[u8], name: &[u8]) -> Vec { + if name.first() == Some(&b'/') { + return name.to_vec(); } + [base, b"/", name].concat() } diff --git a/userland/capsule_linux/src/linux/file/calls/falloc/calls.rs b/userland/capsule_linux/src/linux/file/calls/falloc/calls.rs new file mode 100644 index 000000000..25ce2ba5d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/falloc/calls.rs @@ -0,0 +1,75 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* fallocate, as tmpfs answers it. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::cache; +use super::super::size::resize; +use super::zero::zero; + +const KEEP_SIZE: u64 = 0x01; + +const PUNCH_HOLE: u64 = 0x02; + +/* FALLOC_FL_SUPPORTED_MASK: every mode bit Linux knows. */ +const KNOWN: u64 = 0x7f; + +/* + * As Linux's tmpfs does it, since the family's writable directories are + * its tmpfs mounts: mode 0 makes the file at least `at + len` long, the new + * bytes zero; KEEP_SIZE alone has nothing to allocate; PUNCH_HOLE with + * KEEP_SIZE zeroes the range. tmpfs refuses every other mode. + */ +pub fn fallocate(guest: &mut Guest, fd: u64, mode: u64, at: u64, len: u64) -> u64 { + if (at as i64) < 0 || (len as i64) <= 0 { + return errno::fail(errno::EINVAL); + } + if mode & !KNOWN != 0 { + return errno::fail(errno::EOPNOTSUPP); + } + /* Linux's vfs_fallocate: a hole may only be punched inside the size. */ + if mode & PUNCH_HOLE != 0 && mode & KEEP_SIZE == 0 { + return errno::fail(errno::EOPNOTSUPP); + } + let Some(entry) = guest.fds.get(fd as usize).filter(|f| f.is_open()) else { + return errno::fail(errno::EBADF); + }; + match entry.kind { + Kind::Pipe => return errno::fail(errno::ESPIPE), + Kind::File if !entry.writable => return errno::fail(errno::EBADF), + Kind::File if !super::super::super::synth::owns(&entry.path) => {} + _ => return errno::fail(errno::ENODEV), + } + if mode & !(KEEP_SIZE | PUNCH_HOLE) != 0 { + return errno::fail(errno::EOPNOTSUPP); + } + let path = entry.path.clone(); + let now = cache::size(&path).unwrap_or(entry.size); + let want = at.saturating_add(len); + match mode { + 0 if want > now => { + if let Some(e) = guest.fds.get_mut(fd as usize) { + e.size = want; + } + resize(&path, want, true) + } + m if m & PUNCH_HOLE != 0 && at < now => zero(&path, at, want.min(now)), + _ => errno::ok(0), + } +} diff --git a/userland/capsule_linux/src/linux/file/calls/falloc/mod.rs b/userland/capsule_linux/src/linux/file/calls/falloc/mod.rs new file mode 100644 index 000000000..ea8b041af --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/falloc/mod.rs @@ -0,0 +1,22 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* fallocate. */ + +mod calls; +mod zero; + +pub use calls::fallocate; diff --git a/userland/capsule_linux/src/linux/file/calls/falloc/zero.rs b/userland/capsule_linux/src/linux/file/calls/falloc/zero.rs new file mode 100644 index 000000000..7a46cbab1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/falloc/zero.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Zeros written over a range of the family's copy. */ + +use crate::linux::abi::errno; + +use super::super::super::{cache, resolve, store}; + +/* Zero [from, to) of the family's copy. */ +pub(super) fn zero(path: &[u8], from: u64, to: u64) -> u64 { + let exists = cache::held(path) || store::stat(&resolve::key(path)).is_ok(); + let zeros = alloc::vec![0u8; (to - from) as usize]; + match cache::hold(path, exists).and_then(|()| cache::write(path, from, &zeros)) { + Ok(_) => errno::ok(0), + Err(e) => errno::fail(e), + } +} diff --git a/userland/capsule_linux/src/linux/file/calls/fdrange.rs b/userland/capsule_linux/src/linux/file/calls/fdrange.rs new file mode 100644 index 000000000..b0f446eff --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/fdrange.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * close_range: close, or mark close-on-exec, every descriptor from `first` + * to `last`. + */ + +use crate::linux::abi::errno; +use crate::linux::call; +use crate::linux::guest::Guest; + +/* + * Linux's CLOSE_RANGE_UNSHARE: a guest's table is never shared with + * another process's, so there is nothing to unshare. + */ +const UNSHARE: u64 = 1 << 1; +const CLOEXEC: u64 = 1 << 2; + +pub fn close_range(guest: &mut Guest, first: u64, last: u64, flags: u64) -> u64 { + if flags & !(UNSHARE | CLOEXEC) != 0 || first > last { + return errno::fail(errno::EINVAL); + } + let end = (last as usize).min(guest.fds.len().saturating_sub(1)); + for fd in first as usize..=end { + if !guest.fds.get(fd).is_some_and(|f| f.is_open()) { + continue; + } + match flags & CLOEXEC { + 0 => { + let _ = call::close(guest, fd as u64); + } + _ => guest.fds[fd].cloexec = true, + } + } + errno::ok(0) +} diff --git a/userland/capsule_linux/src/linux/file/calls/fdup.rs b/userland/capsule_linux/src/linux/file/calls/fdup.rs new file mode 100644 index 000000000..c55b6425b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/fdup.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * fcntl's F_DUPFD and F_DUPFD_CLOEXEC: a second descriptor on the same open + * file, at the lowest free number at or above the one asked for. + * + * The copy shares the description (held/desc/) and, for a file, the family's + * copy of its bytes (held/cache/); a read through it opens its own stream + * from the store when it needs one (held/rw/). + */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::super::slot::MAX_FDS; + +pub fn dup_from(guest: &mut Guest, fd: u64, min: u64, cloexec: bool) -> u64 { + let Some(from) = guest.fds.get(fd as usize).filter(|f| f.is_open()) else { + return errno::fail(errno::EBADF); + }; + /* RLIMIT_NOFILE is the table's size. */ + if min >= MAX_FDS as u64 { + return errno::fail(errno::EINVAL); + } + let mut copy = Fd::clone_of(from); + copy.cloexec = cloexec; + let at = (min as usize..MAX_FDS).find(|i| !guest.fds.get(*i).is_some_and(|f| f.is_open())); + let Some(at) = at else { + return errno::fail(errno::EMFILE); + }; + while guest.fds.len() <= at { + guest.fds.push(Fd::empty(Kind::Free)); + } + guest.fds[at] = copy; + errno::ok(at as u64) +} diff --git a/userland/capsule_linux/src/linux/file/calls/mod.rs b/userland/capsule_linux/src/linux/file/calls/mod.rs new file mode 100644 index 000000000..404434cc0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/mod.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The file calls beyond open, read and write. + */ + +pub(super) mod falloc; +pub(super) mod fdrange; +pub(super) mod fdup; +pub(super) mod openat2; +pub(super) mod sendfile; +pub(super) mod size; + +pub use falloc::fallocate; +pub use fdrange::close_range; +pub use fdup::dup_from; +pub use openat2::openat2; +pub use sendfile::{copy_file_range, sendfile}; +pub use size::{fadvise64, ftruncate, truncate}; diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/check.rs b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs new file mode 100644 index 000000000..e13fc60c5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The name walked as open_how's rules allow. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{at, mounts, path, walk}; +use super::how::open_how; +use super::open::{BENEATH, IN_ROOT}; +use super::rooted::rooted; +use super::walked::walked; + +pub(super) fn check( + guest: &Guest, + dirfd: u64, + path_ptr: u64, + how: u64, + size: u64, +) -> Result<(Vec, u64, u64), i64> { + let (flags, mode, rules) = open_how(guest, how, size)?; + let name = path::read_path(guest, path_ptr).ok_or(errno::EFAULT)?; + let base = walk::follow(guest, at::named_at(guest, dirfd, b".")?, true); + if rules & BENEATH != 0 && name.first() == Some(&b'/') { + return Err(errno::EXDEV); + } + if rules & IN_ROOT != 0 { + let mount = mounts::of(&base).0; + let inside = walk::walk_under(guest, &base, name, true, |s| rooted(s, rules, mount))?; + return Ok((inside, flags, mode)); + } + let named = at::named_at(guest, dirfd, &name)?; + walked(guest, &base, &named, rules)?; + Ok((named, flags, mode)) +} diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/how.rs b/userland/capsule_linux/src/linux/file/calls/openat2/how.rs new file mode 100644 index 000000000..3b0e8bd29 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/how.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* struct open_how read and checked as Linux checks it. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::flags::O_CREAT; +use super::open::{ + BENEATH, CACHED, IN_ROOT, NO_MAGICLINKS, NO_SYMLINKS, NO_XDEV, OPEN_HOW, O_TMPFILE, VALID, +}; + +/* + * The flags, mode and RESOLVE_ rules of the struct open_how at `at`, + * checked as Linux checks them. + */ +pub(super) fn open_how(guest: &Guest, at: u64, size: u64) -> Result<(u64, u64, u64), i64> { + if (size as usize) < OPEN_HOW || size > 4096 { + return Err(errno::EINVAL); + } + let raw = guest.read(at, size as usize).ok_or(errno::EFAULT)?; + /* A larger struct from a newer libc is fine while the new part is zero. */ + if raw[OPEN_HOW..].iter().any(|b| *b != 0) { + return Err(7); /* E2BIG */ + } + let word = |i: usize| u64::from_le_bytes(raw[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])); + let (flags, mode, rules) = (word(0), word(1), word(2)); + if flags & !VALID != 0 + || rules & !(NO_XDEV | NO_MAGICLINKS | NO_SYMLINKS | BENEATH | IN_ROOT | CACHED) != 0 + { + return Err(errno::EINVAL); + } + if mode != 0 && flags & (O_CREAT | O_TMPFILE) == 0 || mode & !0o7777 != 0 { + return Err(errno::EINVAL); + } + /* BENEATH and IN_ROOT say opposite things of an absolute name. */ + if rules & BENEATH != 0 && rules & IN_ROOT != 0 { + return Err(errno::EINVAL); + } + if rules & CACHED != 0 { + return Err(errno::EAGAIN); + } + Ok((flags, mode, rules)) +} diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs b/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs new file mode 100644 index 000000000..b127ce2a7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * openat2: openat with a struct open_how, and RESOLVE_ flags that limit + * how the path may be walked. + * + * Served: NO_XDEV, NO_MAGICLINKS, NO_SYMLINKS, BENEATH and IN_ROOT, each + * checked on the same walk open makes (walk/), and CACHED, which Linux + * may always answer with EAGAIN and so does here. + */ + +mod check; +mod how; +mod open; +mod rooted; +mod walked; + +pub use open::openat2; diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/open.rs b/userland/capsule_linux/src/linux/file/calls/openat2/open.rs new file mode 100644 index 000000000..d332d8a4b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/open.rs @@ -0,0 +1,53 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* openat2's entry. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::flags::O_CLOEXEC; +use super::check::check; + +pub(super) const OPEN_HOW: usize = 24; + +pub(super) const NO_XDEV: u64 = 0x01; + +pub(super) const NO_MAGICLINKS: u64 = 0x02; + +pub(super) const NO_SYMLINKS: u64 = 0x04; + +pub(super) const BENEATH: u64 = 0x08; + +pub(super) const IN_ROOT: u64 = 0x10; + +pub(super) const CACHED: u64 = 0x20; + +pub(super) const O_TMPFILE: u64 = 0o20200000; + +/* Every open flag Linux knows (VALID_OPEN_FLAGS). */ +pub(super) const VALID: u64 = 0o37777703; + +pub fn openat2(guest: &mut Guest, dirfd: u64, path_ptr: u64, how: u64, size: u64) -> u64 { + match check(guest, dirfd, path_ptr, how, size) { + Ok((named, flags, mode)) => { + let got = super::super::super::open::open_named(guest, named, flags, mode); + super::super::super::open::mark(guest, got, flags & O_CLOEXEC != 0); + got + } + Err(e) => errno::fail(e), + } +} diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/rooted.rs b/userland/capsule_linux/src/linux/file/calls/openat2/rooted.rs new file mode 100644 index 000000000..1fff1676f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/rooted.rs @@ -0,0 +1,47 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* RESOLVE_IN_ROOT's walk and the magic links it never follows. */ + +use crate::linux::abi::errno; + +use super::super::super::mounts; +use super::super::super::walk::Step; +use super::open::{NO_SYMLINKS, NO_XDEV}; + +/* + * A step of an IN_ROOT walk: the walk keeps itself under the root, so + * only the link and mount rules can refuse a step. A magic link would + * reach past the root, and IN_ROOT never follows one. + */ +pub(super) fn rooted(step: Step, rules: u64, mount: u32) -> Result<(), i64> { + match step { + Step::Link { at, .. } if rules & NO_SYMLINKS != 0 || magic(at) => Err(errno::ELOOP), + Step::At(p) if rules & NO_XDEV != 0 && mounts::of(p).0 != mount => Err(errno::EXDEV), + _ => Ok(()), + } +} + +/* + * A /proc link that names an object rather than a path: fd/N, exe, cwd, + * root. /proc/self, thread-self and mounts are ordinary links. + */ +pub(super) fn magic(at: &[u8]) -> bool { + at.starts_with(b"/proc/") + && !at.ends_with(b"/self") + && !at.ends_with(b"/thread-self") + && !at.ends_with(b"/mounts") +} diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs b/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs new file mode 100644 index 000000000..71f90c5dc --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs @@ -0,0 +1,64 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The walk open makes, refused at the first step the rules forbid. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::walk::Step; +use super::super::super::{mounts, walk}; +use super::open::{BENEATH, NO_MAGICLINKS, NO_SYMLINKS, NO_XDEV}; +use super::rooted::magic; + +/* + * Walk the name as open will, and refuse the first step `rules` forbid: + * a link at all, a /proc magic link, an absolute link or a step out of the + * starting directory under BENEATH, a step onto another mount. + */ +pub(super) fn walked(guest: &Guest, base: &[u8], named: &[u8], rules: u64) -> Result<(), i64> { + let mount = mounts::of(base).0; + let under = |p: &[u8]| { + base == b"/" || p.starts_with(base) && matches!(p.get(base.len()), None | Some(b'/')) + }; + /* The walk passes base's own parents on its way down to it. */ + let mut reached = false; + let step = |s: Step| { + match s { + Step::Link { at, to } => { + let magic = magic(at); + if rules & NO_SYMLINKS != 0 || (rules & NO_MAGICLINKS != 0 && magic) { + return Err(errno::ELOOP); + } + /* BENEATH allows neither an absolute link nor a magic one. */ + if rules & BENEATH != 0 && (to.first() == Some(&b'/') || magic) { + return Err(errno::EXDEV); + } + } + Step::At(p) => { + reached |= under(p); + if reached && rules & BENEATH != 0 && !under(p) { + return Err(errno::EXDEV); + } + if reached && rules & NO_XDEV != 0 && mounts::of(p).0 != mount { + return Err(errno::EXDEV); + } + } + } + Ok(()) + }; + walk::walk(guest, named.to_vec(), true, step).map(|_| ()) +} diff --git a/userland/capsule_linux/src/linux/file/calls/sendfile/bytes.rs b/userland/capsule_linux/src/linux/file/calls/sendfile/bytes.rs new file mode 100644 index 000000000..78c39bb8c --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/sendfile/bytes.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The bytes moved from one descriptor to the other. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::desc; +use super::super::super::rw::{read_at, MAX_IO}; +use super::offset::read_offset; + +/* + * Read up to `count` bytes of `input` at `*offset`, or at its own offset + * when `offset` is null, hand them to `put`, and move the right offset on. + */ +pub(super) fn move_bytes( + guest: &mut Guest, + input: u64, + offset: u64, + count: u64, + put: impl FnOnce(&mut Guest, &[u8]) -> Result, +) -> u64 { + match guest.fds.get(input as usize).filter(|f| f.is_open()).map(|f| f.kind) { + None => return errno::fail(errno::EBADF), + Some(Kind::File) => {} + Some(_) => return errno::fail(errno::EINVAL), + } + let at = match read_offset(guest, offset) { + Ok(Some(at)) => at, + Ok(None) => desc::pos(&guest.fds[input as usize]), + Err(e) => return e, + }; + let bytes = match read_at(guest, input, at, (count as usize).min(MAX_IO)) { + Ok(bytes) => bytes, + Err(e) => return errno::fail(e), + }; + if bytes.is_empty() { + return errno::ok(0); + } + let n = match put(guest, &bytes) { + Ok(n) => n, + Err(e) => return errno::fail(e), + }; + let end = at + n as u64; + if offset == 0 { + desc::set_pos(&mut guest.fds[input as usize], end); + } else if guest.write(offset, &end.to_le_bytes()) < 8 { + return errno::fail(errno::EFAULT); + } + errno::ok(n as u64) +} diff --git a/userland/capsule_linux/src/linux/file/calls/sendfile/copy.rs b/userland/capsule_linux/src/linux/file/calls/sendfile/copy.rs new file mode 100644 index 000000000..07ecd29eb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/sendfile/copy.rs @@ -0,0 +1,64 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* copy_file_range. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::desc; +use super::super::super::rw::write_at; +use super::bytes::move_bytes; +use super::offset::read_offset; + +pub fn copy_file_range(guest: &mut Guest, a: [u64; 6]) -> u64 { + let (input, off_in, out, off_out, len, flags) = (a[0], a[1], a[2], a[3], a[4], a[5]); + if flags != 0 { + return errno::fail(errno::EINVAL); + } + let files = + [input, out].map(|fd| guest.fds.get(fd as usize).filter(|f| f.is_open()).map(|f| f.kind)); + match files { + [None, _] | [_, None] => return errno::fail(errno::EBADF), + [Some(Kind::File), Some(Kind::File)] => {} + [Some(Kind::Dir), _] | [_, Some(Kind::Dir)] => return errno::fail(errno::EISDIR), + _ => return errno::fail(errno::EINVAL), + } + let target = &guest.fds[out as usize]; + if !target.writable || super::super::super::desc::appends(target) { + return errno::fail(errno::EBADF); + } + let mut at_out = match read_offset(guest, off_out) { + Ok(Some(at)) => at, + Ok(None) => desc::pos(target), + Err(e) => return e, + }; + let start_out = at_out; + let got = move_bytes(guest, input, off_in, len, |g, bytes| { + let (n, end) = write_at(g, out, at_out, bytes)?; + at_out = end; + Ok(n) + }); + if (got as i64) > 0 { + let moved = at_out - start_out; + if off_out == 0 { + desc::set_pos(&mut guest.fds[out as usize], start_out + moved); + } else if guest.write(off_out, &at_out.to_le_bytes()) < 8 { + return errno::fail(errno::EFAULT); + } + } + got +} diff --git a/userland/capsule_linux/src/linux/file/calls/sendfile/mod.rs b/userland/capsule_linux/src/linux/file/calls/sendfile/mod.rs new file mode 100644 index 000000000..014d7b572 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/sendfile/mod.rs @@ -0,0 +1,34 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * sendfile and copy_file_range: bytes from one descriptor to another + * without passing through the guest. + * + * Both read a file through the same path read(2) does. sendfile writes to + * a file or to the console; to a pipe or a socket it answers EINVAL, as + * Linux answers for an output it cannot splice into, and every caller + * then falls back to read and write, which reach those. copy_file_range + * is between two files, as on Linux. + */ + +mod bytes; +mod copy; +mod offset; +mod send; + +pub use copy::copy_file_range; +pub use send::sendfile; diff --git a/userland/capsule_linux/src/linux/file/calls/sendfile/offset.rs b/userland/capsule_linux/src/linux/file/calls/sendfile/offset.rs new file mode 100644 index 000000000..a96f4facd --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/sendfile/offset.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The offset a call names, or the descriptor's own. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +pub(super) fn read_offset(guest: &Guest, ptr: u64) -> Result, u64> { + if ptr == 0 { + return Ok(None); + } + let raw = guest.read(ptr, 8).ok_or(errno::fail(errno::EFAULT))?; + let at = i64::from_le_bytes(raw.try_into().unwrap_or([0; 8])); + if at < 0 { + return Err(errno::fail(errno::EINVAL)); + } + Ok(Some(at as u64)) +} diff --git a/userland/capsule_linux/src/linux/file/calls/sendfile/send.rs b/userland/capsule_linux/src/linux/file/calls/sendfile/send.rs new file mode 100644 index 000000000..44730b9fd --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/sendfile/send.rs @@ -0,0 +1,48 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* sendfile. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::desc; +use super::super::super::rw::write_at; +use super::bytes::move_bytes; + +pub fn sendfile(guest: &mut Guest, out: u64, input: u64, offset: u64, count: u64) -> u64 { + let Some(kind) = guest.fds.get(out as usize).filter(|f| f.is_open()).map(|f| f.kind) else { + return errno::fail(errno::EBADF); + }; + if !matches!(kind, Kind::File | Kind::Stdout | Kind::Stderr) { + return errno::fail(errno::EINVAL); + } + if kind == Kind::File && !guest.fds[out as usize].writable { + return errno::fail(errno::EBADF); + } + move_bytes(guest, input, offset, count, |g, bytes| match kind { + Kind::File => { + let at = desc::pos(&g.fds[out as usize]); + let (n, end) = write_at(g, out, at, bytes)?; + desc::set_pos(&mut g.fds[out as usize], end); + Ok(n) + } + _ => { + let _ = nonos_libc::mk_debug(bytes.as_ptr(), bytes.len()); + Ok(bytes.len()) + } + }) +} diff --git a/userland/capsule_linux/src/linux/file/calls/size/advice.rs b/userland/capsule_linux/src/linux/file/calls/size/advice.rs new file mode 100644 index 000000000..b897580b4 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/size/advice.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The length a file takes, and fadvise64. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{cache, resolve, store}; + +/* + * Resize the family's copy; with no descriptor to close later, put it in + * the store now. + */ +pub(crate) fn resize(path: &[u8], len: u64, kept: bool) -> u64 { + let exists = cache::held(path) || store::stat(&resolve::key(path)).is_ok(); + let done = cache::hold(path, exists).and_then(|()| cache::resize(path, len)).and_then(|()| { + if kept { + Ok(()) + } else { + cache::flush(path, false) + } + }); + match done { + Ok(()) => errno::ok(0), + Err(e) => errno::fail(e), + } +} + +/* POSIX_FADV_NORMAL to POSIX_FADV_NOREUSE are accepted, and change nothing. */ +pub fn fadvise64(guest: &Guest, fd: u64, advice: u64) -> u64 { + match guest.fds.get(fd as usize).filter(|f| f.is_open()).map(|f| f.kind) { + None => errno::fail(errno::EBADF), + Some(Kind::Pipe) => errno::fail(errno::ESPIPE), + Some(_) if advice > 5 => errno::fail(errno::EINVAL), + Some(_) => errno::ok(0), + } +} diff --git a/userland/capsule_linux/src/linux/file/calls/size/mod.rs b/userland/capsule_linux/src/linux/file/calls/size/mod.rs new file mode 100644 index 000000000..ba6f411e5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/size/mod.rs @@ -0,0 +1,27 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * A file's length: ftruncate and truncate; and fadvise, which is only + * advice. + */ + +mod advice; +mod truncate; + +pub use advice::fadvise64; +pub(crate) use advice::resize; +pub use truncate::{ftruncate, truncate}; diff --git a/userland/capsule_linux/src/linux/file/calls/size/truncate.rs b/userland/capsule_linux/src/linux/file/calls/size/truncate.rs new file mode 100644 index 000000000..7a2ab0438 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/size/truncate.rs @@ -0,0 +1,73 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* ftruncate and truncate. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{at, cache, resolve, store, walk}; +use super::advice::resize; + +pub fn ftruncate(guest: &mut Guest, fd: u64, len: u64) -> u64 { + if (len as i64) < 0 { + return errno::fail(errno::EINVAL); + } + let Some(entry) = guest.fds.get_mut(fd as usize).filter(|f| f.is_open()) else { + return errno::fail(errno::EBADF); + }; + match entry.kind { + /* + * Sizing a memfd records the size and nothing else. The pages appear + * when the client maps it, because that is when their address is decided. + */ + Kind::Memfd => { + entry.size = len; + errno::ok(0) + } + /* Linux answers EINVAL for anything but a regular file open to write. */ + Kind::File if entry.writable && !super::super::super::synth::owns(&entry.path) => { + let path = entry.path.clone(); + entry.size = len; + resize(&path, len, true) + } + _ => errno::fail(errno::EINVAL), + } +} + +pub fn truncate(guest: &Guest, path: u64, len: u64) -> u64 { + if (len as i64) < 0 { + return errno::fail(errno::EINVAL); + } + let Some(named) = at::resolve_at(guest, super::super::super::flags::AT_FDCWD, path) else { + return errno::fail(errno::EFAULT); + }; + let full = walk::follow(guest, named, true); + if super::super::super::synth::owns(&full) { + return errno::fail(errno::EACCES); + } + match store::stat(&resolve::key(&full)) { + _ if cache::held(&full) => {} + Ok((_, true)) => return errno::fail(errno::EISDIR), + Ok(_) => {} + Err(_) => return errno::fail(errno::ENOENT), + } + if resolve::key(&full).writable().is_err() { + return errno::fail(errno::EROFS); + } + let kept = cache::held(&full); + resize(&full, len, kept) +} diff --git a/userland/capsule_linux/src/linux/file/close.rs b/userland/capsule_linux/src/linux/file/close.rs index 5b7d71049..afafe8718 100644 --- a/userland/capsule_linux/src/linux/file/close.rs +++ b/userland/capsule_linux/src/linux/file/close.rs @@ -14,35 +14,49 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Closing a descriptor, and writing out anything it was holding. +/* Closing a descriptor, and writing out anything it was holding. */ use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest, Kind}; pub fn close(guest: &mut Guest, fd: u64) -> u64 { - let Some(entry) = guest.fds.get_mut(fd as usize) else { + let Some(entry) = guest.fds.get(fd as usize) else { return errno::fail(errno::EBADF); }; if !entry.is_open() { return errno::fail(errno::EBADF); } + super::lock_calls::closing(guest, fd); + let flushed = flush(guest, fd); /* * The store handle is dropped with the descriptor, which closes it on the * server. */ - let flushed = flush(entry); - *entry = Fd::empty(Kind::Free); + guest.fds[fd as usize] = Fd::empty(Kind::Free); super::epoll::forget(guest, fd); match flushed { - true => errno::ok(0), - false => errno::fail(errno::EIO), + Ok(()) => errno::ok(0), + Err(e) => errno::fail(e), } } -/// Write a descriptor's buffered bytes out. -pub(super) fn flush(entry: &Fd) -> bool { - if entry.kind != Kind::File || !entry.writable { - return true; +/* + * A written file's bytes to the store. The family's copy is let go when + * this process holds no other descriptor on it; another process that + * still does reads the store, which now has every byte. + */ +pub(super) fn flush(guest: &Guest, fd: u64) -> Result<(), i64> { + let Some(entry) = guest.fds.get(fd as usize) else { + return Ok(()); + }; + if entry.kind != Kind::File || !entry.writable || super::synth::owns(&entry.path) { + return Ok(()); } - super::store::write(&super::resolve::key(&entry.path), &entry.pending).is_ok() + let path = &entry.path; + let others = guest + .fds + .iter() + .enumerate() + .any(|(i, f)| i as u64 != fd && f.kind == Kind::File && f.path == *path); + super::cache::flush(path, others) } diff --git a/userland/capsule_linux/src/linux/file/dev.rs b/userland/capsule_linux/src/linux/file/dev.rs index f47420bd1..a400fb4c8 100644 --- a/userland/capsule_linux/src/linux/file/dev.rs +++ b/userland/capsule_linux/src/linux/file/dev.rs @@ -22,7 +22,7 @@ use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest, Kind}; -use super::flags::wants_write; +use super::flags::writes; use super::slot::install; /// Path, major, minor. The handle of an open device is its index here. @@ -50,7 +50,7 @@ pub fn open_path(guest: &mut Guest, full: &[u8], flags: u64) -> u64 { let mut fd = Fd::empty(Kind::Device); fd.handle = dev; fd.path = full.to_vec(); - fd.writable = wants_write(flags); + fd.writable = writes(flags); match install(guest, fd) { Some(n) => errno::ok(n), None => errno::fail(errno::EMFILE), diff --git a/userland/capsule_linux/src/linux/file/dev_stat.rs b/userland/capsule_linux/src/linux/file/dev_stat.rs index f64043710..13adf52d4 100644 --- a/userland/capsule_linux/src/linux/file/dev_stat.rs +++ b/userland/capsule_linux/src/linux/file/dev_stat.rs @@ -14,37 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! What stat, fstat and statx say about a character device: S_IFCHR with -//! read and write for everyone, as Linux's devtmpfs makes them, and the -//! device's major and minor numbers. +/* + * What stat, fstat and statx say about a character device: S_IFCHR with + * read and write for everyone, as Linux's devtmpfs makes them, and the + * device's major and minor numbers. + */ use super::dev::numbers; -const MODE: u32 = 0o020666; -/// st_mode and st_rdev in a `struct stat`. -const STAT_MODE: usize = 24; -const STAT_RDEV: usize = 40; -/// stx_mode, stx_rdev_major and stx_rdev_minor in a `struct statx`. -const STATX_MODE: usize = 28; -const STATX_RDEV: usize = 128; +pub const MODE: u32 = 0o020666; -/// A `struct stat` made for a file, turned into the device's. st_rdev is -/// Linux's encoding of the two numbers. -pub fn as_device(stat: &mut [u8], dev: u32) { - let Some((major, minor)) = numbers(dev) else { - return; - }; - let rdev = (minor & 0xff) | ((major & 0xfff) << 8) | ((minor & !0xff) << 12); - stat[STAT_MODE..STAT_MODE + 4].copy_from_slice(&MODE.to_le_bytes()); - stat[STAT_RDEV..STAT_RDEV + 8].copy_from_slice(&rdev.to_le_bytes()); -} - -/// The same for a `struct statx`, which keeps the two numbers apart. -pub fn statx_device(buf: &mut [u8], dev: u32) { - let Some((major, minor)) = numbers(dev) else { - return; - }; - buf[STATX_MODE..STATX_MODE + 2].copy_from_slice(&(MODE as u16).to_le_bytes()); - buf[STATX_RDEV..STATX_RDEV + 4].copy_from_slice(&(major as u32).to_le_bytes()); - buf[STATX_RDEV + 4..STATX_RDEV + 8].copy_from_slice(&(minor as u32).to_le_bytes()); +/* Linux's st_rdev for the device: the minor's low byte, the major, the rest. */ +pub fn rdev(dev: u32) -> Option { + let (major, minor) = numbers(dev)?; + Some((minor & 0xff) | ((major & 0xfff) << 8) | ((minor & !0xff) << 12)) } diff --git a/userland/capsule_linux/src/linux/file/dir.rs b/userland/capsule_linux/src/linux/file/dir.rs index a71e5a6b9..d8504036f 100644 --- a/userland/capsule_linux/src/linux/file/dir.rs +++ b/userland/capsule_linux/src/linux/file/dir.rs @@ -14,30 +14,44 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Directory open. The listing is snapshotted here, which is all POSIX -//! promises a directory stream. +/* + * Directory open. The listing is snapshotted here, which is all POSIX + * promises a directory stream. + */ +use alloc::string::String; use alloc::vec::Vec; use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest}; use super::dir_children::children; -use super::{resolve, slot, store}; +use super::{cache, desc, resolve, slot, store, synth}; pub fn open(guest: &mut Guest, path: Vec) -> u64 { let at = resolve::key(&path); let Ok(keys) = store::list(&at) else { return errno::fail(errno::EACCES); }; - // Cut against the store key, not against the path the guest named. - let mut names = children(at.as_bytes(), keys); - for link in guest.links.names_in(&path) { - if !names.contains(&link) { - names.push(link); + /* + * Cut against the store key, not against the path the guest named. + * Every Linux directory lists itself and its parent first. + */ + let mut names = alloc::vec![String::from("."), String::from("..")]; + names.extend(children(at.as_bytes(), keys)); + let made = if path == b"/" { + synth::ROOTS.iter().map(|r| String::from(*r)).collect() + } else { + Vec::new() + }; + for name in guest.links.names_in(&path).into_iter().chain(cache::names_in(&path)).chain(made) { + if !names.contains(&name) { + names.push(name); } } - match slot::install(guest, Fd::dir(path, names)) { + let mut fd = Fd::dir(path, names); + fd.handle = desc::fresh(false, false); + match slot::install(guest, fd) { Some(n) => errno::ok(n), None => errno::fail(errno::EMFILE), } diff --git a/userland/capsule_linux/src/linux/file/dirops/dirs.rs b/userland/capsule_linux/src/linux/file/dirops/dirs.rs new file mode 100644 index 000000000..3d71f74c2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/dirops/dirs.rs @@ -0,0 +1,75 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* mkdir and rmdir, in the family's private directories. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::at::resolve_at; +use super::super::meta::look; +use super::super::resolve::key; +use super::super::{cache, modes, store_name, synth}; + +pub fn mkdirat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { + let Some(at) = resolve_at(guest, dirfd, path) else { + return errno::fail(errno::EFAULT); + }; + if look(&at).is_some() || guest.links.target(&at).is_some() { + return errno::fail(errno::EEXIST); + } + if synth::owns(&at) || key(&at).writable().is_err() { + return errno::fail(errno::EROFS); + } + match store_name::mkdir(&key(&at)) { + Ok(()) => { + modes::set(&at, mode as u32 & !u32::from(guest.umask)); + errno::ok(0) + } + Err(_) => errno::fail(errno::ENOENT), + } +} + +pub fn rmdir(guest: &Guest, path: u64) -> u64 { + let Some(at) = resolve_at(guest, super::super::flags::AT_FDCWD, path) else { + return errno::fail(errno::EFAULT); + }; + remove_dir(&at) +} + +/* + * Not recursive: POSIX rmdir refuses a populated directory, and a recursive + * delete behind that name is data loss. A file the family holds and has not + * yet put in the store is in the directory all the same. + */ +pub(super) fn remove_dir(at: &[u8]) -> u64 { + match look(at) { + None => return errno::fail(errno::ENOENT), + Some((_, false)) => return errno::fail(errno::ENOTDIR), + Some(_) if synth::owns(at) || key(at).writable().is_err() => { + return errno::fail(errno::EROFS) + } + Some(_) if !cache::names_in(at).is_empty() => return errno::fail(errno::ENOTEMPTY), + Some(_) => {} + } + match store_name::rmdir(&key(at)) { + Ok(()) => { + modes::forget(at); + errno::ok(0) + } + Err(_) => errno::fail(errno::ENOTEMPTY), + } +} diff --git a/userland/capsule_linux/src/linux/file/dirops/mod.rs b/userland/capsule_linux/src/linux/file/dirops/mod.rs new file mode 100644 index 000000000..bb2429c58 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/dirops/mod.rs @@ -0,0 +1,23 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Making, removing and moving names in the store. */ + +mod dirs; +mod unlink; + +pub use dirs::{mkdirat, rmdir}; +pub use unlink::unlinkat; diff --git a/userland/capsule_linux/src/linux/file/dirops.rs b/userland/capsule_linux/src/linux/file/dirops/unlink.rs similarity index 52% rename from userland/capsule_linux/src/linux/file/dirops.rs rename to userland/capsule_linux/src/linux/file/dirops/unlink.rs index 26eeb6cab..c36353ce4 100644 --- a/userland/capsule_linux/src/linux/file/dirops.rs +++ b/userland/capsule_linux/src/linux/file/dirops/unlink.rs @@ -14,38 +14,16 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Making, removing and moving names in the store. +/* unlinkat: a file, a link, or with AT_REMOVEDIR a directory. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::at::resolve_at; -use super::resolve::key; -use super::store_name; - -pub fn mkdirat(guest: &Guest, dirfd: u64, path: u64) -> u64 { - let Some(at) = resolve_at(guest, dirfd, path) else { - return errno::fail(errno::EFAULT); - }; - match store_name::mkdir(&key(&at)) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::EEXIST), - } -} - -pub fn rmdir(guest: &Guest, path: u64) -> u64 { - let Some(at) = resolve_at(guest, super::flags::AT_FDCWD, path) else { - return errno::fail(errno::EFAULT); - }; - /* - * Not recursive: POSIX rmdir refuses a populated directory, and a - * recursive delete behind that name is data loss. - */ - match store_name::rmdir(&key(&at)) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::ENOTEMPTY), - } -} +use super::super::at::resolve_at; +use super::super::meta::look; +use super::super::resolve::key; +use super::super::{cache, modes, store_name, synth}; +use super::dirs::remove_dir; pub fn unlinkat(guest: &Guest, dirfd: u64, path: u64, flags: u64) -> u64 { let Some(at) = resolve_at(guest, dirfd, path) else { @@ -56,13 +34,32 @@ pub fn unlinkat(guest: &Guest, dirfd: u64, path: u64, flags: u64) -> u64 { * rmdir on top of one syscall. */ const AT_REMOVEDIR: u64 = 0x200; - let at = key(&at); - let done = match flags & AT_REMOVEDIR { - 0 => store_name::unlink(&at), - _ => store_name::rmdir(&at), - }; - match done { + if flags & AT_REMOVEDIR != 0 { + return remove_dir(&at); + } + if guest.links.target(&at).is_some() { + return match key(&at).writable() { + Ok(()) if guest.links.remove(&at) => errno::ok(0), + _ => errno::fail(errno::EROFS), + }; + } + let held = cache::held(&at); + match look(&at) { + None => return errno::fail(errno::ENOENT), + Some((_, true)) => return errno::fail(errno::EISDIR), + Some(_) if synth::owns(&at) || key(&at).writable().is_err() => { + return errno::fail(errno::EROFS) + } + Some(_) => {} + } + cache::forget(&at); + modes::forget(&at); + super::super::times::forget(&at); + super::super::xattr_table::forget(&at); + /* A file only the family held was never in the store. */ + match store_name::unlink(&key(&at)) { Ok(()) => errno::ok(0), + Err(_) if held => errno::ok(0), Err(_) => errno::fail(errno::ENOENT), } } diff --git a/userland/capsule_linux/src/linux/file/flags.rs b/userland/capsule_linux/src/linux/file/flags.rs index 36a29e9ef..dbda75f5c 100644 --- a/userland/capsule_linux/src/linux/file/flags.rs +++ b/userland/capsule_linux/src/linux/file/flags.rs @@ -14,25 +14,40 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! The open flags and the special directory descriptor, as Linux defines -//! them on x86_64. Transcribed, never chosen. +/* + * The open flags and the special directory descriptor, as Linux defines + * them on x86_64. Transcribed, never chosen. + */ pub const O_WRONLY: u64 = 0o1; pub const O_RDWR: u64 = 0o2; pub const O_CREAT: u64 = 0o100; +pub const O_EXCL: u64 = 0o200; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const O_NONBLOCK: u64 = 0o4000; pub const O_DIRECTORY: u64 = 0o200000; +pub const O_NOFOLLOW: u64 = 0o400000; pub const O_CLOEXEC: u64 = 0o2000000; -/// `openat` with this as the directory means "relative to the working -/// directory", which is the only relative form a static binary uses. +/* + * `openat` with this as the directory means "relative to the working + * directory", which is the only relative form a static binary uses. + */ pub const AT_FDCWD: u64 = (-100i64) as u64; -/// A guest asked to write if it asked for anything but read. -pub fn wants_write(flags: u64) -> bool { - flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND) != 0 +/* + * A directory descriptor as the kernel reads it: an int, so only the low 32 + * bits count. A C program calling syscall() with an int leaves the high + * half of the register undefined, and Linux never looks at it. + */ +pub fn dirfd(raw: u64) -> u64 { + raw as u32 as i32 as i64 as u64 +} + +/* Opened O_WRONLY or O_RDWR: what a write through the descriptor needs. */ +pub fn writes(flags: u64) -> bool { + flags & (O_WRONLY | O_RDWR) != 0 } pub fn wants_read(flags: u64) -> bool { diff --git a/userland/capsule_linux/src/linux/file/fsync.rs b/userland/capsule_linux/src/linux/file/fsync.rs index f5c66c497..4a86051ee 100644 --- a/userland/capsule_linux/src/linux/file/fsync.rs +++ b/userland/capsule_linux/src/linux/file/fsync.rs @@ -16,14 +16,41 @@ //! Getting a descriptor's buffered bytes onto the store. use crate::linux::abi::errno; -use crate::linux::guest::Guest; +use crate::linux::guest::{Guest, Kind}; +/* + * fsync and fdatasync: the store keeps no metadata apart from the bytes, + * so the two are one. + */ pub fn fsync(guest: &Guest, fd: u64) -> u64 { let Some(entry) = guest.fds.get(fd as usize).filter(|f| f.is_open()) else { return errno::fail(errno::EBADF); }; - match super::close::flush(entry) { - true => errno::ok(0), - false => errno::fail(errno::EIO), + /* Linux answers EINVAL for what cannot be synced: a pipe, a socket. */ + if !matches!(entry.kind, Kind::File | Kind::Dir) { + return errno::fail(errno::EINVAL); + } + if entry.kind == Kind::Dir || super::synth::owns(&entry.path) { + return errno::ok(0); + } + match super::cache::flush(&entry.path, true) { + Ok(()) => errno::ok(0), + Err(e) => errno::fail(e), + } +} + +/* sync(2) cannot fail; syncfs answers its errors, and EBADF for a bad fd. */ +pub fn sync() -> u64 { + let _ = super::cache::flush_all(); + errno::ok(0) +} + +pub fn syncfs(guest: &Guest, fd: u64) -> u64 { + if !guest.fds.get(fd as usize).is_some_and(|f| f.is_open()) { + return errno::fail(errno::EBADF); + } + match super::cache::flush_all() { + Ok(()) => errno::ok(0), + Err(e) => errno::fail(e), } } diff --git a/userland/capsule_linux/src/linux/file/held/cache/change.rs b/userland/capsule_linux/src/linux/file/held/cache/change.rs new file mode 100644 index 000000000..db7662b3d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/change.rs @@ -0,0 +1,47 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Writing into a copy, and cutting or growing it. */ + +use crate::linux::abi::errno; + +use super::table::{now, with, MAX_FILE}; + +/* Write `bytes` at `at`, filling any gap with zeros, as a sparse write reads. */ +pub fn write(path: &[u8], at: u64, bytes: &[u8]) -> Result { + let end = (at as usize).checked_add(bytes.len()).filter(|e| *e <= MAX_FILE); + let end = end.ok_or(errno::EFBIG)?; + with(path, |e| { + if e.data.len() < end { + e.data.resize(end, 0); + } + e.data[at as usize..end].copy_from_slice(bytes); + e.dirty = true; + e.mtime_ms = now(); + bytes.len() + }) + .ok_or(errno::EBADF) +} + +pub fn resize(path: &[u8], len: u64) -> Result<(), i64> { + let len = usize::try_from(len).ok().filter(|l| *l <= MAX_FILE).ok_or(errno::EFBIG)?; + with(path, |e| { + e.data.resize(len, 0); + e.dirty = true; + e.mtime_ms = now(); + }) + .ok_or(errno::EBADF) +} diff --git a/userland/capsule_linux/src/linux/file/held/cache/flush.rs b/userland/capsule_linux/src/linux/file/held/cache/flush.rs new file mode 100644 index 000000000..7fb01a59a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.rs @@ -0,0 +1,56 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A copy put in the store: at close, fsync and sync, and at exit. */ + +use alloc::vec::Vec; + +use super::super::super::{resolve, store}; +use super::table::CACHE; + +/* + * Put the copy of `path` in the store, if it changed; `keep` false lets + * it go afterwards. + */ +pub fn flush(path: &[u8], keep: bool) -> Result<(), i64> { + let mut all = CACHE.0.borrow_mut(); + let Some(i) = all.iter().position(|e| e.path == path) else { + return Ok(()); + }; + if all[i].dirty { + let (len, stored) = (all[i].data.len() as u64, all[i].stored); + if len > stored { + drop(all); + super::super::super::space::within()?; + all = CACHE.0.borrow_mut(); + } + store::write(&resolve::key(path), &all[i].data) + .map_err(super::super::store_err::errno_of)?; + all[i].dirty = false; + all[i].stored = len; + } + if !keep { + all.remove(i); + } + Ok(()) +} + +/* Every changed file to the store: sync, and a process's exit. */ +pub fn flush_all() -> Result<(), i64> { + let paths: Vec> = + CACHE.0.borrow().iter().filter(|e| e.dirty).map(|e| e.path.clone()).collect(); + paths.iter().try_for_each(|p| flush(p, true)) +} diff --git a/userland/capsule_linux/src/linux/file/held/cache/mod.rs b/userland/capsule_linux/src/linux/file/held/cache/mod.rs new file mode 100644 index 000000000..dcfeac4d6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -0,0 +1,39 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The family's copy of each file it is writing, one per path. + * + * On Linux every descriptor on a file, in every process, reads and writes + * the same page cache, so a write through one is seen at once through the + * others and by stat. The store is written whole, so the bytes a family is + * changing are kept here, once per path, and every descriptor on the path + * reads and writes this copy: a dup, a fork's copy and a second open all + * meet the same bytes. The copy goes to the store at close, at fsync and + * sync, and when a process exits. + */ + +mod change; +mod flush; +mod names; +mod table; +mod take; + +pub use change::{resize, write}; +pub use flush::{flush, flush_all}; +pub use names::{bytes, forget, growth, names_in, renamed}; +pub use table::{held, mtime, size}; +pub use take::{hold, read}; diff --git a/userland/capsule_linux/src/linux/file/held/cache/names.rs b/userland/capsule_linux/src/linux/file/held/cache/names.rs new file mode 100644 index 000000000..79d62299b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/names.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Copies that follow their names, and what the copies add up to. */ + +use alloc::vec::Vec; + +use super::table::CACHE; + +/* + * The bytes the family's copies hold in memory, which /proc/meminfo and + * sysinfo report as the page cache and shared memory a tmpfs file takes. + */ +pub fn bytes() -> u64 { + CACHE.0.borrow().iter().map(|e| e.data.len() as u64).sum() +} + +/* + * What the family's copies add to the store's bytes once written, less + * what they take away. + */ +pub fn growth() -> i64 { + let all = CACHE.0.borrow(); + all.iter().map(|e| e.data.len() as i64 - e.stored as i64).sum() +} + +/* The name went away or moved: the copy follows it. */ +pub fn forget(path: &[u8]) { + CACHE.0.borrow_mut().retain(|e| e.path != path); +} + +pub fn renamed(from: &[u8], to: &[u8]) { + let mut all = CACHE.0.borrow_mut(); + all.retain(|e| e.path != to); + if let Some(e) = all.iter_mut().find(|e| e.path == from) { + e.path = to.to_vec(); + } +} + +/* + * The files directly in `dir` that the family holds, which a listing must + * show although the store may not have them yet. + */ +pub fn names_in(dir: &[u8]) -> Vec { + let dir = if dir == b"/" { &b""[..] } else { dir }; + let all = CACHE.0.borrow(); + let leaves = all.iter().filter_map(|e| e.path.strip_prefix(dir)?.strip_prefix(b"/")); + leaves + .filter(|l| !l.contains(&b'/')) + .filter_map(|l| alloc::string::String::from_utf8(l.to_vec()).ok()) + .collect() +} diff --git a/userland/capsule_linux/src/linux/file/held/cache/table.rs b/userland/capsule_linux/src/linux/file/held/cache/table.rs new file mode 100644 index 000000000..3c0429422 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/table.rs @@ -0,0 +1,63 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The family's copies, one per path, and the lookups on them. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +/* The most a family may hold of one file while writing it. */ +pub const MAX_FILE: usize = 8 << 20; + +pub(super) struct Entry { + pub(super) path: Vec, + pub(super) data: Vec, + pub(super) dirty: bool, + /* How long the file is in the store, which `data` replaces at flush. */ + pub(super) stored: u64, + /* Wall-clock milliseconds of the last change, which stat reports. */ + pub(super) mtime_ms: u64, +} + +pub(super) fn now() -> u64 { + u64::try_from(nonos_libc::mk_time_millis()).unwrap_or(0) +} + +pub(super) struct Cache(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Cache {} + +pub(super) static CACHE: Cache = Cache(RefCell::new(Vec::new())); + +pub(super) fn with(path: &[u8], f: impl FnOnce(&mut Entry) -> T) -> Option { + CACHE.0.borrow_mut().iter_mut().find(|e| e.path == path).map(f) +} + +pub fn held(path: &[u8]) -> bool { + with(path, |_| ()).is_some() +} + +pub fn size(path: &[u8]) -> Option { + with(path, |e| e.data.len() as u64) +} + +pub fn mtime(path: &[u8]) -> Option { + with(path, |e| e.mtime_ms) +} diff --git a/userland/capsule_linux/src/linux/file/held/cache/take.rs b/userland/capsule_linux/src/linux/file/held/cache/take.rs new file mode 100644 index 000000000..8a3625adb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/take.rs @@ -0,0 +1,55 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A file's bytes taken into the family's copy, and read from it. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; + +use super::super::super::{resolve, store}; +use super::table::{held, now, with, Entry, CACHE, MAX_FILE}; + +/* Hold `path`: its bytes from the store, or none for a file being made. */ +pub fn hold(path: &[u8], exists: bool) -> Result<(), i64> { + if held(path) { + return Ok(()); + } + let key = resolve::key(path); + let (data, mtime_ms) = match exists { + true => { + let at = store::stat_full(&key).map(|s| s.2).unwrap_or_else(|_| now()); + (store::read(&key, MAX_FILE as u32).map_err(|_| errno::EIO)?, at) + } + false => (Vec::new(), now()), + }; + let stored = data.len() as u64; + CACHE.0.borrow_mut().push(Entry { + path: path.to_vec(), + data, + dirty: !exists, + stored, + mtime_ms, + }); + Ok(()) +} + +pub fn read(path: &[u8], at: u64, len: usize) -> Option> { + with(path, |e| { + let from = (at as usize).min(e.data.len()); + e.data[from..(from + len).min(e.data.len())].to_vec() + }) +} diff --git a/userland/capsule_linux/src/linux/file/held/desc/handle.rs b/userland/capsule_linux/src/linux/file/held/desc/handle.rs new file mode 100644 index 000000000..af3b2efc6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/desc/handle.rs @@ -0,0 +1,52 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What a descriptor's handle holds: the description's number and how + * it was opened. + */ + +use core::sync::atomic::{AtomicU32, Ordering}; + +use crate::linux::guest::{Fd, Kind}; + +const APPEND: u32 = 1 << 31; + +const READS: u32 = 1 << 30; + +const FLAGS: u32 = APPEND | READS; + +static NEXT: AtomicU32 = AtomicU32::new(1); + +/* A new description's handle. */ +pub fn fresh(append: bool, reads: bool) -> u32 { + let id = NEXT.fetch_add(1, Ordering::Relaxed) & !FLAGS; + id | if append { APPEND } else { 0 } | if reads { READS } else { 0 } +} + +/* The description's number, for a file or directory descriptor. */ +pub fn of(fd: &Fd) -> Option { + matches!(fd.kind, Kind::File | Kind::Dir).then_some(fd.handle & !FLAGS) +} + +pub fn appends(fd: &Fd) -> bool { + fd.kind == Kind::File && fd.handle & APPEND != 0 +} + +/* Opened O_RDONLY or O_RDWR: a read of a write-only descriptor is EBADF. */ +pub fn reads(fd: &Fd) -> bool { + fd.kind == Kind::File && fd.handle & READS != 0 +} diff --git a/userland/capsule_linux/src/linux/file/held/desc/mod.rs b/userland/capsule_linux/src/linux/file/held/desc/mod.rs new file mode 100644 index 000000000..3070880ee --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/desc/mod.rs @@ -0,0 +1,36 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The open file description behind a descriptor on a file or directory. + * + * A store file has no server handle number of its own, so its `handle` + * holds what belongs to the description rather than to the descriptor: a + * number naming the description, which dup and fork copy with the rest, + * whether it was opened O_APPEND, and whether it was opened to read. The + * description's offset is kept here too: after a fork, a child's write + * moves the parent's offset, which is how a shell's output and its + * commands' output land one after the other in the same file. Two descriptors share a description + * exactly when a dup or a fork made one from the other, as on Linux. + */ + +mod handle; +mod offset; +mod shared; + +pub use handle::{appends, fresh, of, reads}; +pub use offset::{gone, pos, set_pos}; +pub use shared::held_elsewhere; diff --git a/userland/capsule_linux/src/linux/file/held/desc/offset.rs b/userland/capsule_linux/src/linux/file/held/desc/offset.rs new file mode 100644 index 000000000..c5b36f318 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/desc/offset.rs @@ -0,0 +1,60 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Each description's offset, shared by the descriptors on it. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use crate::linux::guest::{Fd, Kind}; + +use super::handle::of; + +pub(super) struct Offsets(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Offsets {} + +static OFFSETS: Offsets = Offsets(RefCell::new(Vec::new())); + +/* Where the next read or write of a file goes: its description's offset. */ +pub fn pos(fd: &Fd) -> u64 { + let Some(d) = of(fd).filter(|_| fd.kind == Kind::File) else { + return fd.offset; + }; + OFFSETS.0.borrow().iter().find(|(x, _)| *x == d).map_or(fd.offset, |(_, at)| *at) +} + +/* Move the description's offset, and the descriptor's copy of it. */ +pub fn set_pos(fd: &mut Fd, at: u64) { + fd.offset = at; + let Some(d) = of(fd).filter(|_| fd.kind == Kind::File) else { + return; + }; + let mut all = OFFSETS.0.borrow_mut(); + match all.iter_mut().find(|(x, _)| *x == d) { + Some(entry) => entry.1 = at, + None => all.push((d, at)), + } +} + +/* The description is closed everywhere: its offset goes with it. */ +pub fn gone(d: u32) { + OFFSETS.0.borrow_mut().retain(|(x, _)| *x != d); +} diff --git a/userland/capsule_linux/src/linux/file/held/desc/shared.rs b/userland/capsule_linux/src/linux/file/held/desc/shared.rs new file mode 100644 index 000000000..5be73aad7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/desc/shared.rs @@ -0,0 +1,35 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Whether another descriptor holds the same description. */ + +use super::handle::of; + +/* + * Whether a descriptor other than `fd` holds `fd`'s description: in this + * process, or, when the family's view is lent, in any process of it. + */ +pub fn held_elsewhere(guest: &crate::linux::guest::Guest, fd: u64, d: u32) -> bool { + let me = guest.pid; + let here = guest + .fds + .iter() + .enumerate() + .any(|(i, o)| i as u64 != fd && o.is_open() && of(o) == Some(d)); + here || super::super::super::view::with(|v| { + v.procs.iter().any(|p| p.kernel != me && p.fds.iter().any(|o| o.desc == Some(d))) + }) +} diff --git a/userland/capsule_linux/src/linux/file/held/mod.rs b/userland/capsule_linux/src/linux/file/held/mod.rs new file mode 100644 index 000000000..e0df7c25d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What the family holds of the files it uses: one copy of each file + * it writes, the open file descriptions, and the modes and times it + * set. + */ + +pub(super) mod cache; +pub(super) mod desc; +pub(super) mod modes; +pub(super) mod rw; +pub(super) mod store_err; +pub(super) mod times; + +pub use cache::{bytes as cache_bytes, flush_all}; diff --git a/userland/capsule_linux/src/linux/file/held/modes.rs b/userland/capsule_linux/src/linux/file/held/modes.rs new file mode 100644 index 000000000..b417e4a44 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/modes.rs @@ -0,0 +1,71 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The permission bits of the family's files, which the store does not keep. + * + * A file or directory the family makes gets the mode it was made with, less + * the umask, as on Linux, and chmod changes it; each lives as long as the + * family, which is as long as its private directories do. The shared tree + * is read-only to a guest, so its modes never change: a directory is 0755, + * and a file is 0755 too, because the store cannot say which of its files + * are programs and a program must be executable. + */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +struct Modes(RefCell, u32)>>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Modes {} + +static MODES: Modes = Modes(RefCell::new(Vec::new())); + +pub const SHARED: u32 = 0o755; +/* A private file or directory that predates the family's record of it. */ +pub const FILE: u32 = 0o644; +pub const DIR: u32 = 0o755; + +pub fn of(path: &[u8]) -> Option { + MODES.0.borrow().iter().find(|(p, _)| p == path).map(|(_, m)| *m) +} + +pub fn set(path: &[u8], mode: u32) { + let mut all = MODES.0.borrow_mut(); + all.retain(|(p, _)| p != path); + all.push((path.to_vec(), mode & 0o7777)); +} + +pub fn forget(path: &[u8]) { + MODES.0.borrow_mut().retain(|(p, _)| p != path); +} + +/* The name moved, and everything below it with it. */ +pub fn renamed(from: &[u8], to: &[u8]) { + let mut all = MODES.0.borrow_mut(); + all.retain(|(p, _)| p != to); + for (p, _) in all.iter_mut() { + if p.starts_with(from) && matches!(p.get(from.len()), None | Some(b'/')) { + let mut moved = to.to_vec(); + moved.extend_from_slice(&p[from.len()..]); + *p = moved; + } + } +} diff --git a/userland/capsule_linux/src/linux/file/held/rw/mod.rs b/userland/capsule_linux/src/linux/file/held/rw/mod.rs new file mode 100644 index 000000000..40b6f7b5b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/rw/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The bytes of a file descriptor at an offset, in and out. read, write, + * the p- and v- forms, sendfile and copy_file_range all come here, so a + * file reads the same whichever call asks. + * + * In order: a made file (/dev, /proc, /sys); the family's copy of a file + * it is writing; the store, through the descriptor's stream, opened again + * for a descriptor that dup or fork made without one. + */ + +mod read; +mod write; + +pub use read::{read_at, MAX_IO}; +pub use write::write_at; diff --git a/userland/capsule_linux/src/linux/file/held/rw/read.rs b/userland/capsule_linux/src/linux/file/held/rw/read.rs new file mode 100644 index 000000000..35537cfb7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/rw/read.rs @@ -0,0 +1,56 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Reading a file at an offset: a made file, the family's copy, or the store. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{cache, desc, resolve, store, synth_ops}; + +/* The most one call moves. */ +pub const MAX_IO: usize = 1 << 20; + +pub fn read_at(guest: &mut Guest, fd: u64, at: u64, len: usize) -> Result, i64> { + let entry = guest.fds.get_mut(fd as usize).filter(|f| f.is_open()).ok_or(errno::EBADF)?; + match entry.kind { + Kind::File => {} + Kind::Dir => return Err(errno::EISDIR), + _ => return Err(errno::EINVAL), + } + if !desc::reads(entry) { + return Err(errno::EBADF); + } + let len = len.min(MAX_IO); + if let Some(made) = synth_ops::read(&entry.path, at, len) { + return made; + } + if let Some(held) = cache::read(&entry.path, at, len) { + return Ok(held); + } + if entry.stream.is_none() { + entry.stream = Some(store::open(&resolve::key(&entry.path)).map_err(|_| errno::EIO)?); + } + let size = store::stat(&resolve::key(&entry.path)).map(|(s, _)| s).unwrap_or(entry.size); + if len == 0 || at >= size { + return Ok(Vec::new()); + } + let want = (len as u64).min(size - at) as u32; + let stream = entry.stream.as_mut().ok_or(errno::EBADF)?; + stream.read_window(at, want).map_err(|_| errno::EIO) +} diff --git a/userland/capsule_linux/src/linux/file/held/rw/write.rs b/userland/capsule_linux/src/linux/file/held/rw/write.rs new file mode 100644 index 000000000..50da16e91 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/rw/write.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Writing a file at an offset, into the family's copy. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{cache, desc, resolve, store, synth_ops}; + +/* + * Write `bytes` at `at`, or at the end for a descriptor opened O_APPEND; + * the count taken and where the write ended. + */ +pub fn write_at(guest: &mut Guest, fd: u64, at: u64, bytes: &[u8]) -> Result<(usize, u64), i64> { + let entry = guest.fds.get(fd as usize).filter(|f| f.is_open()).ok_or(errno::EBADF)?; + if entry.kind == Kind::Dir { + return Err(errno::EISDIR); + } + if entry.kind != Kind::File || !entry.writable { + return Err(errno::EBADF); + } + let path = entry.path.clone(); + if let Some(made) = synth_ops::write(&path) { + return made.map(|n| (n, at)); + } + let exists = store::stat(&resolve::key(&path)).is_ok(); + cache::hold(&path, exists)?; + let at = if desc::appends(entry) { cache::size(&path).unwrap_or(0) } else { at }; + let n = cache::write(&path, at, bytes)?; + let end = at + n as u64; + if let Some(e) = guest.fds.get_mut(fd as usize) { + e.size = cache::size(&path).unwrap_or(end); + } + Ok((n, end)) +} diff --git a/userland/capsule_linux/src/linux/file/held/store_err.rs b/userland/capsule_linux/src/linux/file/held/store_err.rs new file mode 100644 index 000000000..ab35cc83f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/store_err.rs @@ -0,0 +1,37 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The errno for a store request that failed, from the reason the store + * gave. A full store is ENOSPC and a file too large for it EFBIG, as a + * Linux filesystem says; a reason with no Linux name is EIO. + */ + +use crate::linux::abi::errno; + +pub fn errno_of(reason: &str) -> i64 { + match reason { + "no space left" => errno::ENOSPC, + "too large" => errno::EFBIG, + "read-only file system" => errno::EROFS, + "not found" => errno::ENOENT, + "access denied" => errno::EACCES, + "already exists" => errno::EEXIST, + "is a directory" => errno::EISDIR, + "directory not empty" => errno::ENOTEMPTY, + _ => errno::EIO, + } +} diff --git a/userland/capsule_linux/src/linux/file/held/times.rs b/userland/capsule_linux/src/linux/file/held/times.rs new file mode 100644 index 000000000..a7172862b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/times.rs @@ -0,0 +1,66 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The times the family set on its files with utimensat, which the store + * cannot keep: it records one time, the last write's. + * + * A time set here stands until the file is written again, which moves the + * store's own time past it, as a write moves mtime on Linux. + */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +#[derive(Clone, Copy)] +pub struct Set { + pub atime_ms: u64, + pub mtime_ms: u64, + /* The store's time when these were set: a later write replaces them. */ + pub written_ms: u64, +} + +struct Times(RefCell, Set)>>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Times {} + +static TIMES: Times = Times(RefCell::new(Vec::new())); + +pub fn of(path: &[u8]) -> Option { + TIMES.0.borrow().iter().find(|(p, _)| p == path).map(|(_, s)| *s) +} + +pub fn set(path: &[u8], times: Set) { + let mut all = TIMES.0.borrow_mut(); + all.retain(|(p, _)| p != path); + all.push((path.to_vec(), times)); +} + +pub fn forget(path: &[u8]) { + TIMES.0.borrow_mut().retain(|(p, _)| p != path); +} + +pub fn renamed(from: &[u8], to: &[u8]) { + let mut all = TIMES.0.borrow_mut(); + all.retain(|(p, _)| p != to); + if let Some((p, _)) = all.iter_mut().find(|(p, _)| p == from) { + *p = to.to_vec(); + } +} diff --git a/userland/capsule_linux/src/linux/file/link.rs b/userland/capsule_linux/src/linux/file/link/calls.rs similarity index 63% rename from userland/capsule_linux/src/linux/file/link.rs rename to userland/capsule_linux/src/linux/file/link/calls.rs index 9dda297b2..ec5fef1ab 100644 --- a/userland/capsule_linux/src/linux/file/link.rs +++ b/userland/capsule_linux/src/linux/file/link/calls.rs @@ -14,22 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `symlinkat` and `linkat`; the plain forms are these at AT_FDCWD. -//! -//! A symbolic link joins the family's link table, where the image's own links -//! are, and only where the guest may write. A hard link is the same bytes -//! under a second name, copied: the store has no inodes to share, and a copy -//! keeps what programs rely on, that removing the old name leaves the new. +/* symlinkat and linkat. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::at::resolve_at; -use super::path::read_path; -use super::resolve::key; -use super::{meta::stat, store_read, store_write}; +use super::super::at::resolve_at; +use super::super::path::read_path; +use super::super::resolve::key; +use super::super::{store_read, store_write}; +use super::free::free_and_writable; -/// Largest file a hard link copies; the same bound an exec image has. +/* Largest file a hard link copies; the same bound an exec image has. */ const MAX_LINKED: u32 = 64 << 20; pub fn symlinkat(guest: &Guest, target: u64, dirfd: u64, path: u64) -> u64 { @@ -50,24 +46,19 @@ pub fn linkat(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64) -> u6 else { return errno::fail(errno::EFAULT); }; - let from = guest.links.follow(from, true); + let from = super::super::walk::follow(guest, from, true); if let Err(e) = free_and_writable(guest, &at) { return errno::fail(e); } + /* The store copies what it has: the family's copy goes in first. */ + if let Err(e) = super::super::cache::flush(&from, true) { + return errno::fail(e); + } let Ok(bytes) = store_read(&key(&from), MAX_LINKED) else { return errno::fail(errno::ENOENT); }; match store_write(&key(&at), &bytes) { Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::EIO), - } -} - -// A new name must not exist as a file or a link, and must be somewhere the -// guest may write: the shared tree is read-only to it. -fn free_and_writable(guest: &Guest, at: &[u8]) -> Result<(), i64> { - if stat::look(at).is_some() || guest.links.target(at).is_some() { - return Err(errno::EEXIST); + Err(e) => errno::fail(super::super::store_err::errno_of(e)), } - key(at).writable().map_err(|_| errno::EROFS) } diff --git a/userland/capsule_linux/src/linux/file/link/free.rs b/userland/capsule_linux/src/linux/file/link/free.rs new file mode 100644 index 000000000..c7cb598c3 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/link/free.rs @@ -0,0 +1,34 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Whether a new name may be made where it is asked for. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::meta::stat; +use super::super::resolve::key; + +/* + * A new name must not exist as a file or a link, and must be somewhere the + * guest may write: the shared tree is read-only to it. + */ +pub(super) fn free_and_writable(guest: &Guest, at: &[u8]) -> Result<(), i64> { + if stat::look(at).is_some() || guest.links.target(at).is_some() { + return Err(errno::EEXIST); + } + key(at).writable().map_err(|_| errno::EROFS) +} diff --git a/userland/capsule_linux/src/linux/file/link/mod.rs b/userland/capsule_linux/src/linux/file/link/mod.rs new file mode 100644 index 000000000..864dec52b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/link/mod.rs @@ -0,0 +1,29 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * `symlinkat` and `linkat`; the plain forms are these at AT_FDCWD. + * + * A symbolic link joins the family's link table, where the image's own links + * are, and only where the guest may write. A hard link is the same bytes + * under a second name, copied: the store has no inodes to share, and a copy + * keeps what programs rely on, that removing the old name leaves the new. + */ + +mod calls; +mod free; + +pub use calls::{linkat, symlinkat}; diff --git a/userland/capsule_linux/src/linux/file/locks/lock/apply.rs b/userland/capsule_linux/src/linux/file/locks/lock/apply.rs new file mode 100644 index 000000000..f0c525620 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock/apply.rs @@ -0,0 +1,53 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A lock taken or dropped, splitting and merging the ranges it meets. */ + +use alloc::vec::Vec; + +use super::table::{Lock, LOCKS}; + +/* + * Clear `want`'s owner from `want`'s range of the file, then, if `add`, + * hold that range as `want` says. Pieces of an old lock outside the range + * stay, as Linux splits a record lock. + */ +pub fn apply(want: &Lock, add: bool) { + let mut all = LOCKS.0.borrow_mut(); + let mut kept: Vec = Vec::with_capacity(all.len() + 2); + for l in all.drain(..) { + let mine = l.file == want.file && l.owner == want.owner; + if !mine || l.end <= want.start || want.end <= l.start { + kept.push(l); + continue; + } + if l.start < want.start { + kept.push(Lock { end: want.start, ..l.clone() }); + } + if want.end < l.end { + kept.push(Lock { start: want.end, ..l }); + } + } + if add { + kept.push(want.clone()); + } + *all = kept; +} + +/* Drop every lock `gone` says has lost its owner. */ +pub fn drop_where(gone: impl Fn(&Lock) -> bool) { + LOCKS.0.borrow_mut().retain(|l| !gone(l)); +} diff --git a/userland/capsule_linux/src/linux/file/locks/lock/mod.rs b/userland/capsule_linux/src/linux/file/locks/lock/mod.rs new file mode 100644 index 000000000..0ae24c12f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock/mod.rs @@ -0,0 +1,36 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The family's file locks, with Linux's rules for who conflicts with whom. + * + * Three kinds, as on Linux. A flock lock belongs to an open file + * description, so a dup or a fork shares it, and it goes when the last + * descriptor on that description closes. A POSIX record lock (F_SETLK) + * belongs to a process, and goes when that process closes any descriptor + * on the file, or exits. An OFD record lock (F_OFD_SETLK) is a record lock + * owned by a description. flock locks never meet record locks; POSIX and + * OFD locks meet each other. A process's own POSIX locks never conflict + * with each other: a new one replaces the old over the range it covers. + */ + +mod apply; +mod rules; +mod table; + +pub use apply::{apply, drop_where}; +pub use rules::{blocker, take}; +pub use table::{Lock, Owner}; diff --git a/userland/capsule_linux/src/linux/file/locks/lock/rules.rs b/userland/capsule_linux/src/linux/file/locks/lock/rules.rs new file mode 100644 index 000000000..30a1c800f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock/rules.rs @@ -0,0 +1,47 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Linux's rules for when two locks meet. */ + +use super::apply::apply; +use super::table::{record, Lock, LOCKS}; + +/* Whether a lock held by `held` stands in the way of one `want` asks for. */ +fn conflicts(held: &Lock, want: &Lock) -> bool { + held.file == want.file + && held.owner != want.owner + && record(held.owner) == record(want.owner) + && (held.write || want.write) + && held.start < want.end + && want.start < held.end +} + +/* The first lock in the way of `want`, as F_GETLK reports it. */ +pub fn blocker(want: &Lock) -> Option { + LOCKS.0.borrow().iter().find(|l| conflicts(l, want)).cloned() +} + +/* + * Take `want`, or give back what stands in the way. An unlock is a `want` + * that `apply` is told to only remove. + */ +pub fn take(want: Lock) -> Result<(), Lock> { + if let Some(b) = blocker(&want) { + return Err(b); + } + apply(&want, true); + Ok(()) +} diff --git a/userland/capsule_linux/src/linux/file/locks/lock/table.rs b/userland/capsule_linux/src/linux/file/locks/lock/table.rs new file mode 100644 index 000000000..96f3b307f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock/table.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The family's locks and what each covers. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Owner { + /* flock, by open file description. */ + Flock(u32), + /* F_SETLK, by the kernel pid of the process. */ + Posix(u32), + /* F_OFD_SETLK, by open file description. */ + Ofd(u32), +} + +#[derive(Clone)] +pub struct Lock { + pub file: Vec, + pub owner: Owner, + pub write: bool, + /* Bytes [start, end); end is u64::MAX for "to the end, however long". */ + pub start: u64, + pub end: u64, +} + +pub(super) struct Table(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Table {} + +pub(super) static LOCKS: Table = Table(RefCell::new(Vec::new())); + +pub(super) fn record(a: Owner) -> bool { + !matches!(a, Owner::Flock(_)) +} diff --git a/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs b/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs new file mode 100644 index 000000000..b20af76b5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What a close and an exit take away: POSIX locks and flock's. */ + +use crate::linux::guest::Guest; + +use super::super::super::desc; +use super::super::lock::{self, Owner}; +use super::flock::file_of; + +/* + * What a close of `fd` releases: every POSIX lock the process holds on the + * file, and a description's flock and OFD locks once no other descriptor + * anywhere in the family holds that description. + */ +pub fn closing(guest: &Guest, fd: u64) { + let Ok((f, d)) = file_of(guest, fd) else { return }; + let (file, me) = (f.path.clone(), guest.pid); + let last = !desc::held_elsewhere(guest, fd, d); + lock::drop_where(|l| { + l.file == file + && match l.owner { + Owner::Posix(pid) => pid == me, + Owner::Flock(x) | Owner::Ofd(x) => x == d && last, + } + }); + if last { + desc::gone(d); + } +} + +/* Every lock `pid` holds as a process, when it exits. */ +pub fn exiting(pid: u32) { + lock::drop_where(|l| l.owner == Owner::Posix(pid)); +} diff --git a/userland/capsule_linux/src/linux/file/locks/lock_calls/flock.rs b/userland/capsule_linux/src/linux/file/locks/lock_calls/flock.rs new file mode 100644 index 000000000..cb8adc049 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/flock.rs @@ -0,0 +1,70 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* flock: a lock on an open file description. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::super::super::desc; +use super::super::lock::{self, Lock, Owner}; +use super::purge::purge; + +const LOCK_SH: u64 = 1; + +const LOCK_EX: u64 = 2; + +const LOCK_NB: u64 = 4; + +const LOCK_UN: u64 = 8; + +/* The answer that means "not yet": parked, not replied. */ +pub const WAIT: u64 = u64::MAX - 1000; + +pub(super) fn file_of(guest: &Guest, fd: u64) -> Result<(&Fd, u32), u64> { + let f = guest.fds.get(fd as usize).filter(|f| f.is_open()).ok_or(errno::fail(errno::EBADF))?; + let d = desc::of(f).ok_or(errno::fail(errno::EINVAL))?; + Ok((f, d)) +} + +pub fn flock(guest: &Guest, fd: u64, op: u64) -> u64 { + let (f, d) = match file_of(guest, fd) { + Ok(x) => x, + Err(e) => return e, + }; + let whole = |write| Lock { + file: f.path.clone(), + owner: Owner::Flock(d), + write, + start: 0, + end: u64::MAX, + }; + match op & !LOCK_NB { + LOCK_UN => { + lock::apply(&whole(false), false); + errno::ok(0) + } + LOCK_SH | LOCK_EX => { + purge(); + match lock::take(whole(op & LOCK_EX != 0)) { + Ok(()) => errno::ok(0), + Err(_) if op & LOCK_NB != 0 => errno::fail(errno::EAGAIN), + Err(_) => WAIT, + } + } + _ => errno::fail(errno::EINVAL), + } +} diff --git a/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs b/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs new file mode 100644 index 000000000..5283506a6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * flock(2), and fcntl's record-lock commands, on the family's lock table. + * + * A lock that must wait answers `WAIT`, which the serve loop parks and + * tries again after every call, as it does a read on an empty pipe. + */ + +mod closing; +mod flock; +mod purge; + +pub use closing::{closing, exiting}; +pub use flock::{flock, WAIT}; +pub(crate) use purge::owners_ns; +pub use purge::purge; diff --git a/userland/capsule_linux/src/linux/file/locks/lock_calls/purge.rs b/userland/capsule_linux/src/linux/file/locks/lock_calls/purge.rs new file mode 100644 index 000000000..92a07fbf9 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/purge.rs @@ -0,0 +1,47 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Locks whose owner is gone, and the namespace's numbers of the owners. */ + +use super::super::super::view; +use super::super::lock::{self, Owner}; + +/* + * Drop the locks whose owners are gone from the family, when the family's + * view is lent: a POSIX lock whose process has exited, and a flock or OFD + * lock whose description no process holds any more. + */ +pub fn purge() { + view::with(|v| { + if v.procs.is_empty() { + return; + } + let holds = |d: u32| v.procs.iter().any(|p| p.fds.iter().any(|o| o.desc == Some(d))); + lock::drop_where(|l| match l.owner { + Owner::Posix(pid) => !v.procs.iter().any(|p| p.kernel == pid), + Owner::Flock(d) | Owner::Ofd(d) => !holds(d), + }); + }); +} + +pub(crate) fn owners_ns(owner: Owner) -> i32 { + match owner { + Owner::Posix(k) => { + view::with(|v| v.procs.iter().find(|p| p.kernel == k).map_or(0, |p| p.ns as i32)) + } + _ => -1, + } +} diff --git a/userland/capsule_linux/src/linux/file/locks/mod.rs b/userland/capsule_linux/src/linux/file/locks/mod.rs new file mode 100644 index 000000000..1834bb49a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/mod.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * flock and fcntl's record locks. + */ + +pub(super) mod lock; +pub(super) mod lock_calls; +pub(super) mod record; + +pub use lock_calls::{exiting as locks_exiting, flock, WAIT as LOCK_WAIT}; +pub use record::{fcntl_lock, is_lock as is_lock_cmd}; diff --git a/userland/capsule_linux/src/linux/file/locks/record/cmds.rs b/userland/capsule_linux/src/linux/file/locks/record/cmds.rs new file mode 100644 index 000000000..4edea53f0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/record/cmds.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The commands and lock types fcntl's record locks take. */ + +pub const F_GETLK: u64 = 5; + +pub const F_SETLK: u64 = 6; + +pub const F_SETLKW: u64 = 7; + +pub const F_OFD_GETLK: u64 = 36; + +pub const F_OFD_SETLK: u64 = 37; + +pub const F_OFD_SETLKW: u64 = 38; + +pub(super) const F_RDLCK: i16 = 0; + +pub(super) const F_WRLCK: i16 = 1; + +pub(super) const F_UNLCK: i16 = 2; + +pub(super) const FLOCK: usize = 32; + +pub fn is_lock(cmd: u64) -> bool { + matches!(cmd, F_GETLK | F_SETLK | F_SETLKW | F_OFD_GETLK | F_OFD_SETLK | F_OFD_SETLKW) +} diff --git a/userland/capsule_linux/src/linux/file/locks/record/fcntl.rs b/userland/capsule_linux/src/linux/file/locks/record/fcntl.rs new file mode 100644 index 000000000..99c7e5405 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/record/fcntl.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* fcntl's record locks: F_GETLK, F_SETLK and F_SETLKW, and the OFD forms. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::desc; +use super::super::lock::{self}; +use super::super::lock_calls::{purge, WAIT}; +use super::cmds::{F_GETLK, F_OFD_GETLK, F_OFD_SETLKW, F_RDLCK, F_SETLKW, F_UNLCK, F_WRLCK}; +use super::range::report; +use super::want::wanted; + +pub fn fcntl_lock(guest: &Guest, fd: u64, cmd: u64, arg: u64) -> u64 { + let Some(f) = guest.fds.get(fd as usize).filter(|f| f.is_open()) else { + return errno::fail(errno::EBADF); + }; + let (want, kind) = match wanted(guest, f, cmd, arg) { + Ok(w) => w, + Err(e) => return e, + }; + purge(); + match (cmd, kind) { + (F_GETLK | F_OFD_GETLK, F_RDLCK | F_WRLCK) => report(guest, arg, lock::blocker(&want)), + (_, F_UNLCK) if !matches!(cmd, F_GETLK | F_OFD_GETLK) => { + lock::apply(&want, false); + errno::ok(0) + } + /* Linux wants the descriptor open for what the lock is for. */ + (_, F_WRLCK) if !f.writable => errno::fail(errno::EBADF), + (_, F_RDLCK) if !desc::reads(f) => errno::fail(errno::EBADF), + (_, F_RDLCK | F_WRLCK) => match lock::take(want) { + Ok(()) => errno::ok(0), + Err(_) if matches!(cmd, F_SETLKW | F_OFD_SETLKW) => WAIT, + Err(_) => errno::fail(errno::EAGAIN), + }, + _ => errno::fail(errno::EINVAL), + } +} diff --git a/userland/capsule_linux/src/linux/file/locks/record/mod.rs b/userland/capsule_linux/src/linux/file/locks/record/mod.rs new file mode 100644 index 000000000..c12b474cf --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/record/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * fcntl's record locks: F_GETLK, F_SETLK, F_SETLKW and their OFD forms. + * + * struct flock on x86_64: l_type and l_whence as shorts, then l_start, + * l_len as 64-bit offsets, then l_pid; 32 bytes. + */ + +mod cmds; +mod fcntl; +mod range; +mod want; + +pub use cmds::is_lock; +pub use fcntl::fcntl_lock; diff --git a/userland/capsule_linux/src/linux/file/locks/record/range.rs b/userland/capsule_linux/src/linux/file/locks/record/range.rs new file mode 100644 index 000000000..86a03eb5f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/record/range.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The range a struct flock names, and the lock reported back in it. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::lock::Lock; +use super::super::lock_calls::owners_ns; +use super::cmds::{FLOCK, F_RDLCK, F_UNLCK, F_WRLCK}; + +/* + * [from, to) for a start and a length from `base`; a negative length + * counts back from the start, and zero means "to the end". + */ +pub(super) fn range(base: i64, start: i64, len: i64) -> Option<(u64, u64)> { + let at = base.checked_add(start)?; + let (from, to) = match len { + 0 => (at, i64::MAX), + l if l > 0 => (at, at.checked_add(l)?), + l => (at.checked_add(l)?, at), + }; + (from >= 0).then(|| (from as u64, if to == i64::MAX { u64::MAX } else { to as u64 })) +} + +/* F_GETLK: the lock in the way, or F_UNLCK when there is none. */ +pub(super) fn report(guest: &Guest, arg: u64, found: Option) -> u64 { + let mut out = [0u8; FLOCK]; + match found { + None => out[0..2].copy_from_slice(&F_UNLCK.to_le_bytes()), + Some(l) => { + let kind = if l.write { F_WRLCK } else { F_RDLCK }; + let len = if l.end == u64::MAX { 0 } else { l.end - l.start }; + out[0..2].copy_from_slice(&kind.to_le_bytes()); + out[8..16].copy_from_slice(&(l.start as i64).to_le_bytes()); + out[16..24].copy_from_slice(&(len as i64).to_le_bytes()); + out[24..28].copy_from_slice(&owners_ns(l.owner).to_le_bytes()); + } + } + match guest.write(arg, &out) { + n if n < FLOCK as i64 => errno::fail(errno::EFAULT), + _ => errno::ok(0), + } +} diff --git a/userland/capsule_linux/src/linux/file/locks/record/want.rs b/userland/capsule_linux/src/linux/file/locks/record/want.rs new file mode 100644 index 000000000..71e92a521 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/record/want.rs @@ -0,0 +1,52 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The lock a struct flock asks for. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::super::super::desc; +use super::super::lock::{Lock, Owner}; +use super::cmds::{FLOCK, F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW, F_WRLCK}; +use super::range::range; + +/* + * The lock the struct flock at `arg` asks for on `f`, and its l_type: the + * range from l_whence, l_start and l_len, owned by the process or, for the + * OFD commands, by the open file description. + */ +pub(super) fn wanted(guest: &Guest, f: &Fd, cmd: u64, arg: u64) -> Result<(Lock, i16), u64> { + let d = desc::of(f).ok_or_else(|| errno::fail(errno::EINVAL))?; + let raw = guest.read(arg, FLOCK).ok_or_else(|| errno::fail(errno::EFAULT))?; + let short = |at: usize| i16::from_le_bytes([raw[at], raw[at + 1]]); + let long = |at: usize| i64::from_le_bytes(raw[at..at + 8].try_into().unwrap_or([0; 8])); + let (kind, whence, start, len) = (short(0), short(2), long(8), long(16)); + let ofd = matches!(cmd, F_OFD_GETLK | F_OFD_SETLK | F_OFD_SETLKW); + /* An OFD lock must say l_pid 0. */ + if ofd && long(24) as i32 != 0 { + return Err(errno::fail(errno::EINVAL)); + } + let base = match whence { + 0 => 0, + 1 => desc::pos(f) as i64, + 2 => f.size.max(f.pending.len() as u64) as i64, + _ => return Err(errno::fail(errno::EINVAL)), + }; + let (from, to) = range(base, start, len).ok_or_else(|| errno::fail(errno::EINVAL))?; + let owner = if ofd { Owner::Ofd(d) } else { Owner::Posix(guest.pid) }; + Ok((Lock { file: f.path.clone(), owner, write: kind == F_WRLCK, start: from, end: to }, kind)) +} diff --git a/userland/capsule_linux/src/linux/file/made/boot_id.rs b/userland/capsule_linux/src/linux/file/made/boot_id.rs new file mode 100644 index 000000000..97b3a54c4 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/boot_id.rs @@ -0,0 +1,72 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The boot id a family sees, and the fresh uuid Linux gives at each read. + * + * Neither is the machine's: the boot id is drawn once, when the family + * first asks, so it stays the same for the family's whole life as Linux's + * does for a boot, and no two families share one. + */ + +use alloc::vec::Vec; +use core::cell::Cell; + +struct Once(Cell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Once {} + +static BOOT: Once = Once(Cell::new(None)); + +pub fn boot_id() -> Vec { + let id = BOOT.0.get().unwrap_or_else(|| { + let fresh = random(); + BOOT.0.set(Some(fresh)); + fresh + }); + format(id) +} + +pub fn uuid() -> Vec { + format(random()) +} + +/* Sixteen random bytes as a version 4, variant 1 uuid, as Linux makes one. */ +fn random() -> [u8; 16] { + let mut b = [0u8; 16]; + let _ = nonos_libc::crypto_random(b.as_mut_ptr(), b.len()); + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + b +} + +fn format(b: [u8; 16]) -> Vec { + let hex = + |r: &[u8]| r.iter().map(|x| alloc::format!("{x:02x}")).collect::(); + alloc::format!( + "{}-{}-{}-{}-{}", + hex(&b[..4]), + hex(&b[4..6]), + hex(&b[6..8]), + hex(&b[8..10]), + hex(&b[10..]) + ) + .into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/dev/mod.rs b/userland/capsule_linux/src/linux/file/made/dev/mod.rs new file mode 100644 index 000000000..1f987d842 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/dev/mod.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /dev: the names a Linux program finds there, with Linux's numbers, and + * the links into /proc/self/fd. The devices themselves, null, zero, full, + * random and urandom, are descriptors file/dev.rs answers. + * + * There is no terminal: a guest's console is a stream, as a pipe is, so + * /dev/tty answers ENXIO, which is what Linux answers a process with no + * controlling terminal. /dev/shm is the family's own private directory in + * the store, and is not made here. + */ + +mod number; +mod tree; + +pub use number::{at, rdev}; +pub use tree::{node, Dev}; diff --git a/userland/capsule_linux/src/linux/file/made/dev/number.rs b/userland/capsule_linux/src/linux/file/made/dev/number.rs new file mode 100644 index 000000000..e8ea5356f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/dev/number.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The device numbers stat reports, and /dev/fd's links. */ + +use super::tree::{Dev, DEVICES}; + +/* st_rdev, in the encoding glibc's and musl's makedev use. */ +pub fn rdev(dev: Dev) -> u64 { + let (_, _, major, minor) = DEVICES.iter().find(|d| d.1 == dev).copied().unwrap_or(DEVICES[0]); + let (major, minor) = (u64::from(major), u64::from(minor)); + ((major & 0xfff) << 8) | (minor & 0xff) | ((minor & !0xff) << 12) | ((major & !0xfff) << 32) +} + +/* The device a path names, for a descriptor already open on it. */ +pub fn at(path: &[u8]) -> Option { + let name = path.strip_prefix(b"/dev/")?; + DEVICES.iter().find(|d| d.0 == name).map(|d| d.1) +} diff --git a/userland/capsule_linux/src/linux/file/made/dev/tree.rs b/userland/capsule_linux/src/linux/file/made/dev/tree.rs new file mode 100644 index 000000000..71cac09bd --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/dev/tree.rs @@ -0,0 +1,66 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /dev's names and what each is. */ + +use alloc::vec::Vec; + +use super::super::synth::Node; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Dev { + Null, + Zero, + Full, + Random, + Urandom, + Tty, +} + +/* Name, device, major and minor, from Linux's Documentation/admin-guide/devices.txt. */ +pub(super) const DEVICES: [(&[u8], Dev, u32, u32); 6] = [ + (b"null", Dev::Null, 1, 3), + (b"zero", Dev::Zero, 1, 5), + (b"full", Dev::Full, 1, 7), + (b"random", Dev::Random, 1, 8), + (b"urandom", Dev::Urandom, 1, 9), + (b"tty", Dev::Tty, 5, 0), +]; + +/* udev's links, which a shell's /dev/stdin redirection opens. */ +const LINKS: [(&[u8], &[u8]); 4] = [ + (b"fd", b"/proc/self/fd"), + (b"stdin", b"/proc/self/fd/0"), + (b"stdout", b"/proc/self/fd/1"), + (b"stderr", b"/proc/self/fd/2"), +]; + +pub fn node(rest: &[&[u8]]) -> Option { + match rest { + [] => { + let mut names: Vec> = DEVICES.iter().map(|d| d.0.to_vec()).collect(); + names.extend(LINKS.iter().map(|l| l.0.to_vec())); + names.push(b"shm".to_vec()); + Some(Node::Dir(names)) + } + [name] => DEVICES + .iter() + .find(|d| d.0 == *name) + .map(|d| Node::Dev(d.1)) + .or_else(|| LINKS.iter().find(|l| l.0 == *name).map(|l| Node::Link(l.1.to_vec()))), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/file/made/exe/mod.rs b/userland/capsule_linux/src/linux/file/made/exe/mod.rs new file mode 100644 index 000000000..4ea96d4e7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What each program of the family was started as: the file, its name, and + * where its arguments and environment lie in its own memory. + * + * Recorded when an image is built, for both a first start and an exec, + * and kept by kernel pid for the life of the personality, which is the life + * of the family. A forked child has no record of its own until it execs: + * it runs its parent's image, so /proc answers for it from the parent's. + */ + +mod shape; +mod table; + +pub use shape::Exe; +pub use table::{comm_of, of, record}; diff --git a/userland/capsule_linux/src/linux/file/made/exe/shape.rs b/userland/capsule_linux/src/linux/file/made/exe/shape.rs new file mode 100644 index 000000000..c0a5fc427 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe/shape.rs @@ -0,0 +1,44 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What a process runs, as /proc//exe, comm, cmdline and environ read it. */ + +use alloc::vec::Vec; + +#[derive(Clone, Default)] +pub struct Exe { + /* The file, as the guest names it: /proc//exe. */ + pub path: Vec, + /* + * The last component of the name it was started by, cut to fifteen + * bytes as Linux cuts it: /proc//comm. + */ + pub comm: Vec, + /* + * Where argv's strings begin and the environment's end: the two runs + * are contiguous on the stack, as on Linux, split at `env`. + */ + pub args: u64, + pub env: u64, + pub end: u64, + /* + * The stack pointer it started with, where argc is: Linux's + * start_stack. + */ + pub stack: u64, + /* Family milliseconds when it started. */ + pub start_ms: u64, +} diff --git a/userland/capsule_linux/src/linux/file/made/exe/table.rs b/userland/capsule_linux/src/linux/file/made/exe/table.rs new file mode 100644 index 000000000..64c740b0e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe/table.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The family's record of what each process runs. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use super::shape::Exe; + +const COMM: usize = 15; + +pub(super) struct Table(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Table {} + +static TABLE: Table = Table(RefCell::new(Vec::new())); + +/* The image `pid` now runs, replacing what it ran before. */ +pub fn record(pid: u32, exe: Exe) { + let mut all = TABLE.0.borrow_mut(); + all.retain(|(p, _)| *p != pid); + all.push((pid, exe)); +} + +pub fn of(pid: u32) -> Option { + TABLE.0.borrow().iter().find(|(p, _)| *p == pid).map(|(_, e)| e.clone()) +} + +pub fn comm_of(name: &[u8]) -> Vec { + let last = name.rsplit(|b| *b == b'/').next().unwrap_or(name); + last[..last.len().min(COMM)].to_vec() +} diff --git a/userland/capsule_linux/src/linux/file/made/exe_image.rs b/userland/capsule_linux/src/linux/file/made/exe_image.rs new file mode 100644 index 000000000..99a4ed786 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe_image.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What exec tells /proc about the image it built: the file, its name, and + * where its arguments and environment lie. + */ + +use alloc::vec::Vec; + +use super::exe::{comm_of, record, Exe}; + +/* + * argv's strings run up from the first, then the environment's, then + * `top`, as the stack builder placed them at `at`; the program starts + * with its stack pointer at `stack`. + */ +pub fn record_image(pid: u32, argv: &[Vec], at: &[u64], top: u64, stack: u64) { + let (Some(first), Some(name)) = (at.first(), argv.first()) else { + return; + }; + let env = at.get(argv.len()).copied().unwrap_or(top); + /* A name with no directory is not yet the file it names; exec says which. */ + let path = if name.first() == Some(&b'/') { name.clone() } else { Vec::new() }; + let start_ms = crate::linux::call::family_ms(); + let comm = comm_of(name); + record(pid, Exe { path, comm, args: *first, env, end: top, stack, start_ms }); +} diff --git a/userland/capsule_linux/src/linux/file/made/fdopen.rs b/userland/capsule_linux/src/linux/file/made/fdopen.rs new file mode 100644 index 000000000..bc8bb6a21 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/fdopen.rs @@ -0,0 +1,61 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Opening /proc//fd/ when it names no path: a pipe, the console, a + * socket, or an object with no file behind it. + * + * On Linux such an open reaches the same pipe again, and fails with ENXIO + * for a socket or an anonymous object. For the asking process's own pipe + * that is a second descriptor on the same pipe, which is what dup makes. + * Another process's descriptors are not reached this way here: that would + * hand one process a way into what another holds. + */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::super::flags::O_CLOEXEC; +use super::super::slot; +use super::proc::number; +use super::view; + +pub fn reopen(guest: &mut Guest, path: &[u8], to: &[u8], flags: u64) -> u64 { + if !to.starts_with(b"pipe:") { + return errno::fail(errno::ENXIO); + } + let parts: alloc::vec::Vec<&[u8]> = + path.split(|b| *b == b'/').filter(|p| !p.is_empty()).collect(); + let (Some(pid), Some(n)) = + (parts.get(1).and_then(|p| number(p)), parts.last().and_then(|p| number(p))) + else { + return errno::fail(errno::ENOENT); + }; + if pid != view::with(|v| v.me) { + let line = b"[LINUX] refused /proc//fd of another process: a way into what it holds\n"; + let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); + return errno::fail(errno::EACCES); + } + let Some(from) = guest.fds.get(n as usize).filter(|f| f.is_open()) else { + return errno::fail(errno::ENOENT); + }; + let mut fd = Fd::clone_of(from); + fd.cloexec = flags & O_CLOEXEC != 0; + match slot::install(guest, fd) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} diff --git a/userland/capsule_linux/src/linux/file/made/mod.rs b/userland/capsule_linux/src/linux/file/made/mod.rs new file mode 100644 index 000000000..1284dbb9e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/mod.rs @@ -0,0 +1,39 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The trees the personality makes, /dev, /proc and /sys, and what + * they read of the family. + */ + +pub(super) mod boot_id; +pub(super) mod dev; +pub(super) mod exe; +pub(super) mod exe_image; +pub(super) mod fdopen; +pub(super) mod need; +pub(super) mod proc; +pub(super) mod synth; +pub(super) mod synth_ops; +pub(super) mod sys; +pub(super) mod view; + +pub use exe::{of as exe_of, Exe}; +pub use exe_image::record_image; +pub use need::needs_view; +pub(crate) use proc::mounts; +pub use proc::open_fds; +pub use view::{lend as lend_view, with as view_with, Proc, View}; diff --git a/userland/capsule_linux/src/linux/file/made/need.rs b/userland/capsule_linux/src/linux/file/made/need.rs new file mode 100644 index 000000000..fc9f8a577 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/need.rs @@ -0,0 +1,53 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Which calls need the family's view lent before they are answered: the + * ones that may name a path under /proc, read a /proc descriptor, or + * release a lock another process may share. Every other call skips it. + */ + +use crate::linux::guest::{Guest, Kind}; + +/* + * open, stat, lstat, access, execve, truncate, chdir, readlink, chmod, + * statfs, utime, the xattr calls, flock, fcntl, close and the calls that + * close, sysinfo, getppid, the priority calls, exit, and the *at forms. + */ +const ALWAYS: [u64; 45] = [ + 2, 3, 4, 6, 21, 33, 59, 60, 72, 73, 76, 80, 89, 90, 99, 110, 132, 137, 140, 141, 188, 189, 190, + 191, 192, 193, 194, 195, 196, 197, 198, 199, 231, 235, 257, 262, 267, 268, 269, 280, 292, 332, + 436, 437, 439, +]; + +pub fn needs_view(guest: &Guest, nr: u64, a: [u64; 6]) -> bool { + if ALWAYS.contains(&nr) { + return true; + } + let proc_fd = |fd: u64| { + guest + .fds + .get(fd as usize) + .is_some_and(|f| f.kind == Kind::File && f.path.starts_with(b"/proc")) + }; + match nr { + /* read, fstat, pread64, readv, preadv, preadv2, copy_file_range */ + 0 | 5 | 17 | 19 | 295 | 326 | 327 => proc_fd(a[0]), + /* sendfile reads its second descriptor */ + 40 => proc_fd(a[1]), + _ => false, + } +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/fds/info.rs b/userland/capsule_linux/src/linux/file/made/proc/fds/info.rs new file mode 100644 index 000000000..169075aab --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/fds/info.rs @@ -0,0 +1,72 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What each descriptor names, and fdinfo's lines. */ + +use alloc::vec::Vec; + +use crate::linux::file::flags::{O_CLOEXEC, O_NONBLOCK, O_RDWR, O_WRONLY}; +use crate::linux::guest::{Fd, Kind}; + +use super::super::super::view::Proc; +use super::list::{CONSOLE_IN, CONSOLE_OUT, PIPES, SOCKETS}; + +pub fn fd_target(f: &Fd) -> Vec { + match f.kind { + Kind::File | Kind::Dir | Kind::Device => f.path.clone(), + Kind::Stdin => alloc::format!("pipe:[{CONSOLE_IN}]").into_bytes(), + Kind::Stdout | Kind::Stderr => alloc::format!("pipe:[{CONSOLE_OUT}]").into_bytes(), + Kind::Pipe => alloc::format!("pipe:[{}]", PIPES + u64::from(f.handle)).into_bytes(), + Kind::Socket | Kind::Unix | Kind::Resolver => { + alloc::format!("socket:[{}]", SOCKETS + u64::from(f.handle)).into_bytes() + } + Kind::Memfd => b"/memfd: (deleted)".to_vec(), + Kind::Epoll => b"anon_inode:[eventpoll]".to_vec(), + Kind::Timer => b"anon_inode:[timerfd]".to_vec(), + Kind::Event => b"anon_inode:[eventfd]".to_vec(), + Kind::Signal => b"anon_inode:[signalfd]".to_vec(), + Kind::Free => Vec::new(), + } +} + +pub(super) fn flags_of(f: &Fd) -> u64 { + let mode = match (f.kind, f.writable) { + (Kind::Stdout | Kind::Stderr, _) => O_WRONLY, + (Kind::Stdin | Kind::Dir, _) => 0, + (Kind::Pipe | Kind::File, true) => O_WRONLY, + (Kind::Pipe | Kind::File, false) => 0, + _ => O_RDWR, + }; + let nonblock = if f.nonblock { O_NONBLOCK } else { 0 }; + mode | nonblock | if f.cloexec { O_CLOEXEC } else { 0 } +} + +pub fn target_of(proc: &Proc, fd: u32) -> Option> { + proc.fds.iter().find(|f| f.fd == fd).map(|f| f.target.clone()) +} + +/* + * fdinfo: the position and the flags, in octal as Linux prints them, and + * the mount a file is on. Nothing else is claimed. + */ +pub fn info(proc: &Proc, fd: u32) -> Option> { + let f = proc.fds.iter().find(|f| f.fd == fd)?; + let mut out = alloc::format!("pos:\t{}\nflags:\t0{:o}\n", f.offset, f.flags); + if f.target.first() == Some(&b'/') { + out.push_str(&alloc::format!("mnt_id:\t{}\n", super::super::mounts::of(&f.target).0)); + } + Some(out.into_bytes()) +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/fds/list.rs b/userland/capsule_linux/src/linux/file/made/proc/fds/list.rs new file mode 100644 index 000000000..b99a63afc --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/fds/list.rs @@ -0,0 +1,48 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A process's descriptors as /proc//fd lists them. */ + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::super::super::view::Open; +use super::info::{fd_target, flags_of}; + +/* + * Inode numbers for what has no file: the console's two streams, then + * each pipe by its buffer, then each socket by its handle. + */ +pub const CONSOLE_IN: u64 = 1; + +pub const CONSOLE_OUT: u64 = 2; + +pub const PIPES: u64 = 0x1000; + +pub const SOCKETS: u64 = 0x10_0000; + +pub fn open_fds(guest: &Guest) -> Vec { + let open = guest.fds.iter().enumerate().filter(|(_, f)| f.is_open()); + open.map(|(i, f)| Open { + fd: i as u32, + target: fd_target(f), + offset: super::super::super::super::desc::pos(f), + flags: flags_of(f), + desc: super::super::super::super::desc::of(f), + }) + .collect() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/fds/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/fds/mod.rs new file mode 100644 index 000000000..d32bffe26 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/fds/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /proc//fd and fdinfo: each descriptor, named as Linux names it. + * + * A file or directory is its path. What has no path is named by kind and + * inode as Linux does: a pipe `pipe:[n]`, a socket `socket:[n]`, and the + * objects with no file behind them `anon_inode:[kind]`. The console is a + * stream with no terminal behind it, as a pipe is, so it is shown as one. + */ + +mod info; +mod list; + +pub use info::{info, target_of}; +pub use list::{open_fds, CONSOLE_IN, CONSOLE_OUT, PIPES, SOCKETS}; diff --git a/userland/capsule_linux/src/linux/file/made/proc/maps.rs b/userland/capsule_linux/src/linux/file/made/proc/maps.rs new file mode 100644 index 000000000..7acbc611b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/maps.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /proc//maps: one line for each region the personality keeps for the + * process, in address order, in Linux's layout. + * + * The region list is the truth about the address space (guest/region.rs): + * nothing is added to it here and nothing merged. A PROT_NONE reservation + * shows as ---p. The stack and the heap are named, as Linux names them; + * the rest is shown as anonymous, which is all the list records of it. + */ + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::linux::guest::STACK_TOP; + +use super::super::view::Proc; + +/* + * Where Linux starts the name: 25 columns plus six for each of the two + * addresses on a 64-bit machine, less one. + */ +const NAME_AT: usize = 25 + 6 * 8 - 1; + +pub fn maps(p: &Proc) -> Vec { + let mut regions = p.regions.clone(); + regions.sort_by_key(|r| r.at); + let mut out = String::new(); + for r in regions { + let bit = |on: bool, c: char| if on && r.backed { c } else { '-' }; + let perms = alloc::format!("{}{}{}p", bit(true, 'r'), bit(r.write, 'w'), bit(r.exec, 'x')); + let mut line = alloc::format!("{:08x}-{:08x} {perms} 00000000 00:00 0", r.at, r.at + r.len); + let end = r.at + r.len; + let name = match () { + _ if end == STACK_TOP => Some("[stack]"), + _ if r.at >= p.brk.0 && end <= p.brk.1.max(p.brk.0) && r.len > 0 => Some("[heap]"), + _ => None, + }; + if let Some(name) = name { + while line.len() < NAME_AT { + line.push(' '); + } + line.push(' '); + line.push_str(name); + } + out.push_str(&line); + out.push('\n'); + } + out.into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/mod.rs new file mode 100644 index 000000000..75ba652d0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/mod.rs @@ -0,0 +1,38 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /proc: the family's own processes, and the system as NONOS declares it. + * + * A pid directory exists only for a process of the asking guest's family, + * under the number its pid namespace gave; any other number is ENOENT, as + * it would be for a pid that does not exist. The personality itself, the + * namespace's pid 1, is not shown: it is not one of the family's programs. + */ + +mod fds; +mod maps; +pub mod mounts; +mod names; +mod pid_files; +mod pid_status; +mod sysctl; +mod system; +mod tree; + +pub use fds::{open_fds, CONSOLE_IN, CONSOLE_OUT, PIPES, SOCKETS}; +pub use names::number; +pub use tree::{content, node}; diff --git a/userland/capsule_linux/src/linux/file/made/proc/mounts/files.rs b/userland/capsule_linux/src/linux/file/made/proc/mounts/files.rs new file mode 100644 index 000000000..522afdbaf --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/mounts/files.rs @@ -0,0 +1,55 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* mounts, mountinfo and filesystems, written from the table. */ + +use alloc::vec::Vec; + +use super::table::{of, MOUNTS}; + +pub fn mounts() -> Vec { + let mut out = alloc::string::String::new(); + for (_, src, point, kind, opts, _) in MOUNTS { + out.push_str(&alloc::format!("{src} {point} {kind} {opts} 0 0\n")); + } + out.into_bytes() +} + +pub fn mountinfo() -> Vec { + let mut out = alloc::string::String::new(); + for (id, src, point, kind, opts, _) in MOUNTS { + let parent = if id == 1 { 1 } else { of(parent_of(point)).0 }; + let flags = if opts.starts_with("ro") { "ro" } else { "rw" }; + out.push_str(&alloc::format!( + "{id} {parent} 0:{id} / {point} {opts} - {kind} {src} {flags}\n" + )); + } + out.into_bytes() +} + +fn parent_of(point: &str) -> &[u8] { + let p = point.as_bytes(); + let cut = p.iter().rposition(|b| *b == b'/').unwrap_or(0); + if cut == 0 { + b"/" + } else { + &p[..cut] + } +} + +pub fn filesystems() -> Vec { + b"nodev\tsysfs\nnodev\ttmpfs\nnodev\tdevtmpfs\nnodev\tproc\n\tnonos\n".to_vec() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/mounts/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/mounts/mod.rs new file mode 100644 index 000000000..7e2d33b62 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/mounts/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The mounts a family sees, in one table: /proc//mounts, mountinfo, + * /proc/filesystems, statfs's f_type and stat's st_dev all come from it, + * so they agree with each other. + * + * The tree itself is the store, read-only to a guest; the family's private + * directories are writable, and are the tmpfs mounts a Linux system has in + * the same places; /dev, /proc and /sys are made by the personality. + */ + +mod files; +mod table; + +pub use files::{filesystems, mountinfo, mounts}; +pub use table::{dev, of, MOUNTS}; diff --git a/userland/capsule_linux/src/linux/file/made/proc/mounts/table.rs b/userland/capsule_linux/src/linux/file/made/proc/mounts/table.rs new file mode 100644 index 000000000..866184408 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/mounts/table.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The mount table, and the mount a path is on. */ + +/* Mount id, source, mount point, type, options, statfs magic. */ +pub const MOUNTS: [(u32, &str, &str, &str, &str, u64); 10] = [ + (1, "nonos", "/", "nonos", "ro,relatime", 0x6E6F_6E6F), + (2, "devtmpfs", "/dev", "devtmpfs", "ro,nosuid,relatime", 0x0102_1994), + (3, "proc", "/proc", "proc", "ro,nosuid,nodev,noexec,relatime", 0x9FA0), + (4, "sysfs", "/sys", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0x6265_6572), + (5, "tmpfs", "/dev/shm", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), + (6, "tmpfs", "/home", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), + (7, "tmpfs", "/root", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), + (8, "tmpfs", "/run", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), + (9, "tmpfs", "/tmp", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), + (10, "tmpfs", "/var/tmp", "tmpfs", "rw,nosuid,nodev", 0x0102_1994), +]; + +/* The mount `path` is on: the longest mount point that contains it. */ +pub fn of(path: &[u8]) -> (u32, &'static str, u64) { + let within = |point: &str| { + let p = point.as_bytes(); + p == b"/" || (path.starts_with(p) && matches!(path.get(p.len()), None | Some(b'/'))) + }; + let best = MOUNTS.iter().filter(|m| within(m.2)).max_by_key(|m| m.2.len()); + best.map_or((1, "nonos", 0x6E6F_6E6F), |m| (m.0, m.3, m.5)) +} + +/* + * st_dev for a file on mount `id`: an anonymous device, 0:id, as Linux + * gives every filesystem with no block device. + */ +pub fn dev(id: u32) -> u64 { + u64::from(id) +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/names/at.rs b/userland/capsule_linux/src/linux/file/made/proc/names/at.rs new file mode 100644 index 000000000..cafddad11 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/names/at.rs @@ -0,0 +1,37 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The things a /proc path can name. */ + +use alloc::vec::Vec; + +use super::super::super::view::Proc; + +/* Where a /proc path lands. */ +pub enum At<'a> { + Root, + System(&'a [u8]), + Sysctl(&'a [&'a [u8]]), + /* A process's directory, or one of its threads' under task/. */ + PidDir(Option), + Task(&'a Proc), + Pid { proc: &'a Proc, tid: u32, file: &'a [u8] }, + FdDir(&'a Proc), + Fd { proc: &'a Proc, fd: u32 }, + FdInfoDir(&'a Proc), + FdInfo { proc: &'a Proc, fd: u32 }, + Link(Vec), +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/names/lists.rs b/userland/capsule_linux/src/linux/file/made/proc/names/lists.rs new file mode 100644 index 000000000..848abeba7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/names/lists.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The names each /proc directory holds. */ + +/* Linux's system-wide files that NONOS declares. */ +pub const SYSTEM: [&[u8]; 7] = + [b"cpuinfo", b"filesystems", b"loadavg", b"meminfo", b"stat", b"uptime", b"version"]; + +/* Each process's files; `mem` is listed, as on Linux, and refused at open. */ +pub const FILES: [&[u8]; 12] = [ + b"cgroup", + b"cmdline", + b"comm", + b"environ", + b"limits", + b"maps", + b"mem", + b"mountinfo", + b"mounts", + b"stat", + b"statm", + b"status", +]; + +pub(super) const LINKS: [&[u8]; 3] = [b"cwd", b"exe", b"root"]; + +pub(super) const DIRS: [&[u8]; 3] = [b"fd", b"fdinfo", b"task"]; diff --git a/userland/capsule_linux/src/linux/file/made/proc/names/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/names/mod.rs new file mode 100644 index 000000000..6ff502ed8 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/names/mod.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What each path under /proc names. */ + +mod at; +mod lists; +mod node; +mod parse; + +pub use at::At; +pub use node::{node, number}; +pub use parse::parse; diff --git a/userland/capsule_linux/src/linux/file/made/proc/names/node.rs b/userland/capsule_linux/src/linux/file/made/proc/names/node.rs new file mode 100644 index 000000000..1f3db914d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/names/node.rs @@ -0,0 +1,67 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The node a parsed /proc path is, for the asking process's view. */ + +use alloc::vec::Vec; + +use super::super::super::synth::{num, Node}; +use super::super::super::view::{Proc, View}; +use super::at::At; +use super::lists::{DIRS, FILES, LINKS, SYSTEM}; +use super::parse::parse; + +pub fn node(v: &View, rest: &[&[u8]]) -> Option { + Some(match parse(v, rest)? { + At::Root => Node::Dir(root(v)), + At::Sysctl(path) => super::super::sysctl::node(path)?, + At::PidDir(tid) => Node::Dir(pid_names(tid.is_none())), + At::Task(p) => Node::Dir(p.tids.iter().map(|(ns, _)| num(u64::from(*ns))).collect()), + At::FdDir(p) | At::FdInfoDir(p) => { + Node::Dir(p.fds.iter().map(|f| num(u64::from(f.fd))).collect()) + } + At::Fd { proc, fd } => Node::Link(super::super::fds::target_of(proc, fd)?), + At::Link(to) => Node::Link(to), + At::Pid { file: b"mem", .. } => Node::Refused("/proc//mem: a second way into memory"), + At::Pid { file: b"environ", .. } => Node::Text(0o400), + _ => Node::Text(0o444), + }) +} + +fn root(v: &View) -> Vec> { + let mut names: Vec> = v.procs.iter().map(|p| num(u64::from(p.ns))).collect(); + names.extend(SYSTEM.iter().map(|s| s.to_vec())); + names.extend([&b"mounts"[..], b"self", b"sys", b"thread-self"].iter().map(|s| s.to_vec())); + names +} + +fn pid_names(leader: bool) -> Vec> { + let dirs = DIRS.iter().filter(|d| leader || **d != b"task"); + FILES.iter().chain(LINKS.iter()).chain(dirs).map(|n| n.to_vec()).collect() +} + +pub(super) fn open_fd(proc: &Proc, n: &[u8]) -> Option { + let fd = number(n)?; + proc.fds.iter().any(|f| f.fd == fd).then_some(fd) +} + +/* A decimal with no sign and no leading zero, as /proc names are. */ +pub fn number(s: &[u8]) -> Option { + if s.is_empty() || (s[0] == b'0' && s.len() > 1) || !s.iter().all(u8::is_ascii_digit) { + return None; + } + core::str::from_utf8(s).ok()?.parse().ok() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/names/parse.rs b/userland/capsule_linux/src/linux/file/made/proc/names/parse.rs new file mode 100644 index 000000000..7d3c39d8e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/names/parse.rs @@ -0,0 +1,61 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What each path under /proc names. */ + +use super::super::super::synth::num; +use super::super::super::view::{Proc, View}; +use super::at::At; +use super::lists::{FILES, SYSTEM}; +use super::node::{number, open_fd}; + +pub fn parse<'a>(v: &'a View, rest: &'a [&'a [u8]]) -> Option> { + let Some((first, more)) = rest.split_first() else { + return Some(At::Root); + }; + match (*first, more) { + (b"self", []) => Some(At::Link(num(u64::from(v.me)))), + (b"thread-self", []) => Some(At::Link(alloc::format!("{}/task/{}", v.me, v.thread).into())), + (b"mounts", []) => Some(At::Link(b"self/mounts".to_vec())), + (b"sys", _) => Some(At::Sysctl(more)), + (name, []) if SYSTEM.contains(&name) => Some(At::System(name)), + (pid, _) => { + let proc = v.find(number(pid)?)?; + in_pid(proc, proc.ns, more, true) + } + } +} + +fn in_pid<'a>(proc: &'a Proc, tid: u32, more: &'a [&'a [u8]], leader: bool) -> Option> { + match more { + [] => Some(At::PidDir((!leader).then_some(tid))), + [b"task"] if leader => Some(At::Task(proc)), + [b"task", t, rest @ ..] if leader => { + let t = number(t)?; + proc.tids.iter().any(|(ns, _)| *ns == t).then_some(())?; + in_pid(proc, t, rest, false) + } + [b"fd"] => Some(At::FdDir(proc)), + [b"fd", n] => Some(At::Fd { proc, fd: open_fd(proc, n)? }), + [b"fdinfo"] => Some(At::FdInfoDir(proc)), + [b"fdinfo", n] => Some(At::FdInfo { proc, fd: open_fd(proc, n)? }), + [b"root"] => Some(At::Link(b"/".to_vec())), + [b"cwd"] => Some(At::Link(proc.cwd.clone())), + [b"exe"] => (!proc.exe.path.is_empty()).then(|| At::Link(proc.exe.path.clone())), + [file] if FILES.contains(file) => Some(At::Pid { proc, tid, file }), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_files/files.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_files/files.rs new file mode 100644 index 000000000..c48dab183 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/files.rs @@ -0,0 +1,73 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The files in /proc// that are text, and a process's usage. */ + +use alloc::vec::Vec; +use nonos_libc::peer::mk_peer_read; + +use super::super::super::super::cpu::{self, Usage}; +use super::super::super::view::Proc; +use super::super::{maps, mounts, pid_status}; +use super::stat::stat; +use super::statm::statm; + +pub fn content(p: &Proc, tid: u32, file: &[u8]) -> Option> { + Some(match file { + b"stat" => stat(p, tid), + b"statm" => statm(p), + b"status" => pid_status::status(p, tid), + b"cmdline" => memory(p, p.exe.args, p.exe.env), + b"environ" => memory(p, p.exe.env, p.exe.end), + b"comm" => [&p.exe.comm[..], b"\n"].concat(), + b"limits" => pid_status::limits(), + b"maps" => maps::maps(p), + b"mounts" => mounts::mounts(), + b"mountinfo" => mounts::mountinfo(), + /* One family, one cgroup: the root of its own hierarchy, as v2 says it. */ + b"cgroup" => b"0::/\n".to_vec(), + _ => return None, + }) +} + +/* + * The bytes of the process's own memory from `from` to `to`, where the + * image put argv and the environment. Empty if the record has none. + */ +fn memory(p: &Proc, from: u64, to: u64) -> Vec { + let len = to.saturating_sub(from) as usize; + let mut out = alloc::vec![0u8; len.min(64 << 10)]; + if len == 0 || mk_peer_read(p.kernel, from, &mut out) < 0 { + return Vec::new(); + } + out +} + +/* The whole process's measured use, or one thread's. */ +pub fn usage(p: &Proc, tid: u32) -> Usage { + let all: Vec = match p.tids.iter().find(|(ns, _)| *ns == tid && tid != p.ns) { + Some((_, k)) => alloc::vec![*k], + None => cpu::threads_of(&[p]), + }; + let mut u = cpu::usage(&all); + /* Threads share one address space: its resident size is the leader's. */ + u.resident_kb = cpu::usage(&[p.kernel]).resident_kb; + u +} + +pub fn vsize(p: &Proc) -> u64 { + p.regions.iter().map(|r| r.len).sum() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_files/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_files/mod.rs new file mode 100644 index 000000000..c0d1f5432 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The files in a process's /proc directory. + * + * Every figure is the family's own: the image record for the name and the + * argument block, the region list for the address space, and the kernel's + * own count of the process's ticks, faults and resident pages. What the + * personality cannot measure is left out, never made up. + */ + +mod files; +mod stat; +mod statm; + +pub use files::{content, usage, vsize}; diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.rs new file mode 100644 index 000000000..ff4c99587 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc//stat. */ + +use alloc::vec::Vec; + +use super::super::super::view::Proc; +use super::files::{usage, vsize}; +use super::statm::segment; + +/* + * Linux's 52 fields, in proc(5)'s order, a row to a group: ids, faults + * and times, scheduling and memory, code and stack, signals, data and the + * argument and environment bounds. Zero where Linux has zero too: no + * terminal, no major faults (nothing is paged in from a disk), no timer, + * no swap, no delay accounting, one CPU, SCHED_OTHER, no mask of blocked + * signals, and no exit code while it runs. Two zeros are this view's + * limits, not Linux's: no kernel flags are kept, and the pending signals + * are lane C's queue, which the view does not carry yet. + */ +pub(super) fn stat(p: &Proc, tid: u32) -> Vec { + let (u, e, r) = (usage(p, tid), &p.exe, &p.reaped); + let state = if p.sleeping { 'S' } else { 'R' }; + let comm = core::str::from_utf8(&e.comm).unwrap_or(""); + let nice = crate::linux::call::nice_of(p.kernel); + let (code, data) = (segment(p, true), segment(p, false)); + let start = e.start_ms / (1000 / super::super::super::super::declared::HZ); + let (threads, rss, vsize) = (p.tids.len(), u.resident_kb / 4, vsize(p)); + let (utime, stime, cutime, cstime) = (u.user, u.system, r.user, r.system); + let brk = p.brk.0; + let rows = [ + alloc::format!("{tid} ({comm}) {state} {} {} {} 0 -1 0", p.ppid, p.pgid, p.sid), + alloc::format!("{} {} 0 0 {utime} {stime} {cutime} {cstime}", u.faults, r.faults), + alloc::format!("{} {nice} {threads} 0 {start} {vsize} {rss} {}", 20 + nice, u64::MAX), + alloc::format!("{} {} {} 0 0", code.0, code.1, e.stack), + alloc::format!("0 0 {} {} 0 0 0 17 0 0 0 0 0 0", p.ignored, p.caught), + alloc::format!("{} {} {brk} {} {} {} {} 0", data.0, data.1, e.args, e.env, e.env, e.end), + ]; + (rows.join(" ") + "\n").into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_files/statm.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_files/statm.rs new file mode 100644 index 000000000..4262d251d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/statm.rs @@ -0,0 +1,48 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc//statm, and where the program's code and data lie. */ + +use alloc::vec::Vec; + +use super::super::super::view::Proc; +use super::files::usage; + +/* + * Where the program's own code, or its data, starts and ends: its + * image's executable or writable regions, not the interpreter's. The + * loader puts a program at its own address below the heap, or a + * position-independent one at EXEC_BASE, and the interpreter higher. + */ +pub(super) fn segment(p: &Proc, code: bool) -> (u64, u64) { + use crate::linux::guest::{BRK_BASE, EXEC_BASE, INTERP_BASE}; + let own = + p.regions.iter().filter(|r| r.at < BRK_BASE || (EXEC_BASE..INTERP_BASE).contains(&r.at)); + let mine: Vec<_> = own.filter(|r| if code { r.exec } else { r.write && !r.exec }).collect(); + let from = mine.iter().map(|r| r.at).min().unwrap_or(0); + (from, mine.iter().map(|r| r.at + r.len).max().unwrap_or(0)) +} + +pub(super) fn statm(p: &Proc) -> Vec { + let pages = |f: &dyn Fn(&crate::linux::guest::Region) -> bool| { + p.regions.iter().filter(|r| f(r)).map(|r| r.len / 4096).sum::() + }; + let size = pages(&|_| true); + let text = pages(&|r| r.exec); + let data = pages(&|r| r.write); + let rss = usage(p, p.ns).resident_kb / 4; + alloc::format!("{size} {rss} 0 {text} 0 {data} 0\n").into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_status/limits.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_status/limits.rs new file mode 100644 index 000000000..e723a8bad --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_status/limits.rs @@ -0,0 +1,64 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc//limits. */ + +use alloc::string::String; +use alloc::vec::Vec; + +/* Linux's names and units, in its order of RLIMIT numbers 0 to 15. */ +const NAMES: [(&str, &str); 16] = [ + ("Max cpu time", "seconds"), + ("Max file size", "bytes"), + ("Max data size", "bytes"), + ("Max stack size", "bytes"), + ("Max core file size", "bytes"), + ("Max resident set", "bytes"), + ("Max processes", "processes"), + ("Max open files", "files"), + ("Max locked memory", "bytes"), + ("Max address space", "bytes"), + ("Max file locks", "locks"), + ("Max pending signals", "signals"), + ("Max msgqueue size", "bytes"), + ("Max nice priority", ""), + ("Max realtime priority", ""), + ("Max realtime timeout", "us"), +]; + +/* The same limits getrlimit answers. */ +pub fn limits() -> Vec { + let mut s = alloc::format!( + "{:<25} {:<20} {:<20} {:<10}\n", + "Limit", + "Soft Limit", + "Hard Limit", + "Units" + ); + let shown = |v: u64| match v { + u64::MAX => String::from("unlimited"), + n => alloc::format!("{n}"), + }; + for (i, (name, unit)) in NAMES.iter().enumerate() { + let (soft, hard) = crate::linux::call::limit_for(i as u64).unwrap_or((u64::MAX, u64::MAX)); + s += &alloc::format!("{:<25} {:<20} {:<20} ", name, shown(soft), shown(hard)); + s += &match unit.is_empty() { + true => String::from("\n"), + false => alloc::format!("{unit:<10}\n"), + }; + } + s.into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_status/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_status/mod.rs new file mode 100644 index 000000000..0644894eb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_status/mod.rs @@ -0,0 +1,23 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc//status and limits, in Linux's own layout. */ + +mod limits; +mod status; + +pub use limits::limits; +pub use status::status; diff --git a/userland/capsule_linux/src/linux/file/made/proc/pid_status/status.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_status/status.rs new file mode 100644 index 000000000..cc73fd530 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_status/status.rs @@ -0,0 +1,67 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc//status. */ + +use alloc::string::String; +use alloc::vec::Vec; + +use super::super::super::view::Proc; +use super::super::pid_files::{usage, vsize}; + +pub fn status(p: &Proc, tid: u32) -> Vec { + let u = usage(p, tid); + let comm = core::str::from_utf8(&p.exe.comm).unwrap_or(""); + let state = if p.sleeping { "S (sleeping)" } else { "R (running)" }; + let kb = |n: u64| alloc::format!("{:8} kB", n / 1024); + let stack: u64 = p + .regions + .iter() + .filter(|r| r.at + r.len == crate::linux::guest::STACK_TOP) + .map(|r| r.len) + .sum(); + let data: u64 = + p.regions.iter().filter(|r| r.write).map(|r| r.len).sum::().saturating_sub(stack); + let fdsize = p.fds.iter().map(|f| f.fd + 1).max().unwrap_or(0).div_ceil(64).max(1) * 64; + let mut s = String::new(); + s += &alloc::format!("Name:\t{comm}\nUmask:\t{:04o}\nState:\t{state}\n", p.umask); + s += &alloc::format!( + "Tgid:\t{}\nNgid:\t0\nPid:\t{tid}\nPPid:\t{}\nTracerPid:\t0\n", + p.ns, + p.ppid + ); + s += "Uid:\t0\t0\t0\t0\nGid:\t0\t0\t0\t0\n"; + s += &alloc::format!("FDSize:\t{fdsize}\nGroups:\t\n"); + s += &alloc::format!( + "NStgid:\t{}\nNSpid:\t{tid}\nNSpgid:\t{}\nNSsid:\t{}\n", + p.ns, + p.pgid, + p.sid + ); + s += &alloc::format!("VmSize:\t{}\nVmRSS:\t{:8} kB\n", kb(vsize(p)), u.resident_kb); + s += &alloc::format!("VmData:\t{}\nVmStk:\t{}\n", kb(data), kb(stack)); + s += &alloc::format!("Threads:\t{}\n", p.tids.len()); + s += &alloc::format!( + "SigBlk:\t{:016x}\nSigIgn:\t{:016x}\nSigCgt:\t{:016x}\n", + 0, + p.ignored, + p.caught + ); + s += "CapInh:\t0000000000000000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\n"; + s += "CapBnd:\t0000000000000000\nCapAmb:\t0000000000000000\nNoNewPrivs:\t1\nSeccomp:\t0\n"; + s += "Cpus_allowed:\t1\nCpus_allowed_list:\t0\n"; + s.into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/sysctl.rs b/userland/capsule_linux/src/linux/file/made/proc/sysctl.rs new file mode 100644 index 000000000..43b1195a6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/sysctl.rs @@ -0,0 +1,72 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /proc/sys: the few settings programs read, each from the declared surface. + * + * All are read-only to a guest: a family cannot rename the system or + * change how it is run, so each file's mode says so, and a write is + * refused at open. + */ + +use alloc::vec::Vec; + +use super::super::super::declared as d; +use super::super::synth::{num, Node}; + +type Made = fn() -> Vec; + +const FILES: [(&[u8], Made); 8] = [ + (b"fs/pipe-max-size", || line(num(d::PIPE_MAX))), + (b"kernel/hostname", || line(d::HOSTNAME.to_vec())), + (b"kernel/osrelease", || line(d::RELEASE.to_vec())), + (b"kernel/ostype", || line(d::OSTYPE.to_vec())), + (b"kernel/pid_max", || line(num(d::PID_MAX))), + (b"kernel/random/boot_id", || line(super::super::boot_id::boot_id())), + (b"kernel/random/uuid", || line(super::super::boot_id::uuid())), + (b"vm/overcommit_memory", || line(num(d::OVERCOMMIT))), +]; + +fn line(mut v: Vec) -> Vec { + v.push(b'\n'); + v +} + +fn joined(path: &[&[u8]]) -> Vec { + path.join(&b'/') +} + +pub fn node(path: &[&[u8]]) -> Option { + let at = joined(path); + if FILES.iter().any(|(p, _)| *p == &at[..]) { + return Some(Node::Text(0o444)); + } + let prefix = if at.is_empty() { at.clone() } else { [&at[..], b"/"].concat() }; + let mut names: Vec> = Vec::new(); + for (p, _) in FILES.iter() { + let Some(rest) = p.strip_prefix(&prefix[..]) else { continue }; + let first = rest.split(|b| *b == b'/').next().unwrap_or(rest).to_vec(); + if !names.contains(&first) { + names.push(first); + } + } + (!names.is_empty()).then_some(Node::Dir(names)) +} + +pub fn content(path: &[&[u8]]) -> Option> { + let at = joined(path); + FILES.iter().find(|(p, _)| *p == &at[..]).map(|(_, made)| made()) +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/system/files.rs b/userland/capsule_linux/src/linux/file/made/proc/system/files.rs new file mode 100644 index 000000000..7b18bb241 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/files.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc's system-wide files, and the family's use behind them. */ + +use alloc::vec::Vec; + +use super::super::super::super::cpu::{self, Usage}; +use super::super::super::super::declared::{self as d}; +use super::super::super::view::View; +use super::memory::{cpuinfo, meminfo}; +use super::stat::stat; +use super::time::{loadavg, uptime}; + +pub fn content(v: &View, name: &[u8]) -> Option> { + Some(match name { + b"cpuinfo" => cpuinfo(), + b"filesystems" => super::super::mounts::filesystems(), + b"loadavg" => loadavg(v), + b"meminfo" => meminfo(v), + b"stat" => stat(v), + b"uptime" => uptime(v), + b"version" => { + [b"Linux version ", d::RELEASE, b" (", d::HOSTNAME, b") ", d::VERSION, b"\n"].concat() + } + _ => return None, + }) +} + +/* What the family's threads used, and each process's resident memory. */ +pub(super) fn family(v: &View) -> Usage { + let all: Vec<&super::super::super::view::Proc> = v.procs.iter().collect(); + let mut u = cpu::usage(&cpu::threads_of(&all)); + let leaders: Vec = v.procs.iter().map(|p| p.kernel).collect(); + u.resident_kb = cpu::usage(&leaders).resident_kb; + u +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/system/memory.rs b/userland/capsule_linux/src/linux/file/made/proc/system/memory.rs new file mode 100644 index 000000000..0c054ad3b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/memory.rs @@ -0,0 +1,64 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc/meminfo and /proc/cpuinfo. */ + +use alloc::string::String; +use alloc::vec::Vec; + +use super::super::super::super::declared::{self as d}; +use super::super::super::view::View; +use super::files::family; + +/* The x86-64 baseline every x86_64 Linux program may assume, and no more. */ +pub(super) fn cpuinfo() -> Vec { + let mut s = String::new(); + for n in 0..d::CPUS { + s += &alloc::format!( + "processor\t: {n}\nvendor_id\t: NONOS\nmodel name\t: NONOS virtual CPU\n" + ); + s += "flags\t\t: fpu tsc cx8 cmov mmx fxsr sse sse2 syscall nx lm\n\n"; + } + s.into_bytes() +} + +/* + * The family's memory: what its processes hold resident, and the copies + * of files it is writing (held/cache/), which are its page cache and, as a + * tmpfs's pages are on Linux, its shared memory. Those copies can be put + * in the store, so they count as available. No swap and no block-device + * buffers exist. + */ +pub(super) fn meminfo(v: &View) -> Vec { + let total = d::MEMORY / 1024; + let cached = super::super::super::super::cache::bytes() / 1024; + let free = total.saturating_sub(family(v).resident_kb).saturating_sub(cached); + let mut s = String::new(); + for (name, kb) in [ + ("MemTotal:", total), + ("MemFree:", free), + ("MemAvailable:", free + cached), + ("Buffers:", 0), + ("Cached:", cached), + ("SwapCached:", 0), + ("SwapTotal:", 0), + ("SwapFree:", 0), + ("Shmem:", cached), + ] { + s += &alloc::format!("{name:<16}{kb:>8} kB\n"); + } + s.into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/system/mod.rs b/userland/capsule_linux/src/linux/file/made/proc/system/mod.rs new file mode 100644 index 000000000..f7237ba4b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /proc's system-wide files, as NONOS declares the system to a family. + * + * The machine is one CPU and the family's memory limit, up since the + * family started. What the family used of it is the kernel's measure of + * the family's own threads; nothing of any other process, capsule or + * family, and nothing of the hardware, is in any of these files. + */ + +mod files; +mod memory; +mod stat; +mod time; + +pub use files::content; diff --git a/userland/capsule_linux/src/linux/file/made/proc/system/stat.rs b/userland/capsule_linux/src/linux/file/made/proc/system/stat.rs new file mode 100644 index 000000000..23ce21a8d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/stat.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc/stat, and the ticks the family ran and did not. */ + +use alloc::vec::Vec; + +use crate::linux::call::family_ms; + +use super::super::super::super::cpu::Usage; +use super::super::super::super::declared::{self as d, HZ}; +use super::super::super::view::View; +use super::files::family; +use super::time::last; + +/* Ticks since the family started, and the part no thread of it ran. */ +pub(super) fn ticks(u: &Usage) -> (u64, u64) { + let up = family_ms() / (1000 / HZ); + (up, up.saturating_sub(u.user + u.system)) +} + +pub(super) fn stat(v: &View) -> Vec { + let u = family(v); + let (_, idle) = ticks(&u); + let cpu = alloc::format!("{} 0 {} {idle} 0 0 0 0 0 0", u.user, u.system); + let wall = u64::try_from(nonos_libc::mk_time_millis()).unwrap_or(0); + let btime = wall.saturating_sub(family_ms()) / 1000; + let running = v.procs.iter().filter(|p| !p.sleeping).count(); + /* The one CPU is the whole machine, so it and the total are the same line. */ + let mut s = alloc::format!("cpu {cpu}\n"); + for n in 0..d::CPUS { + s += &alloc::format!("cpu{n} {cpu}\n"); + } + s += &alloc::format!("intr 0\nctxt {}\nbtime {btime}\n", u.switches); + s += &alloc::format!( + "processes {}\nprocs_running {running}\nprocs_blocked 0\n", + last(v).saturating_sub(1) + ); + s += "softirq 0 0 0 0 0 0 0 0 0 0 0\n"; + s.into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/system/time.rs b/userland/capsule_linux/src/linux/file/made/proc/system/time.rs new file mode 100644 index 000000000..f61fee7d2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/time.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* /proc/uptime and /proc/loadavg. */ + +use alloc::vec::Vec; + +use crate::linux::call::family_ms; + +use super::super::super::super::declared::HZ; +use super::super::super::super::load; +use super::super::super::synth::num; +use super::super::super::view::View; +use super::files::family; +use super::stat::ticks; + +pub(super) fn uptime(v: &View) -> Vec { + let (up, idle) = ticks(&family(v)); + let two = |t: u64| alloc::format!("{}.{:02}", t / HZ, t % HZ); + alloc::format!("{} {}\n", two(up), two(idle)).into_bytes() +} + +/* The family's measured load (system/load/), then its running and all threads. */ +pub(super) fn loadavg(v: &View) -> Vec { + let threads: usize = v.procs.iter().map(|p| p.tids.len()).sum(); + let running = v.procs.iter().filter(|p| !p.sleeping).count(); + let u = family(v); + let [a, b, c] = load::averages(family_ms(), u.user + u.system).map(load::text); + let mut s = alloc::format!("{a} {b} {c} {running}/{threads} ").into_bytes(); + s.extend_from_slice(&num(u64::from(last(v)))); + s.push(b'\n'); + s +} + +/* The highest number the namespace has given. */ +pub(super) fn last(v: &View) -> u32 { + v.procs.iter().flat_map(|p| p.tids.iter().map(|(ns, _)| *ns)).max().unwrap_or(0) +} diff --git a/userland/capsule_linux/src/linux/file/made/proc/tree.rs b/userland/capsule_linux/src/linux/file/made/proc/tree.rs new file mode 100644 index 000000000..5b0289699 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/tree.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * A /proc path's node, and a /proc file's bytes, made now from the view the + * family lent for the call. + */ + +use alloc::vec::Vec; + +use super::super::synth::Node; +use super::super::view::{self, View}; +use super::names::{self, parse, At}; +use super::{fds, pid_files, sysctl, system}; + +pub fn node(rest: &[&[u8]]) -> Option { + view::with(|v| names::node(v, rest)) +} + +/* The bytes of the /proc file at `path`, made now; Err is the errno. */ +pub fn content(path: &[u8]) -> Result, i64> { + let parts: Vec<&[u8]> = path.split(|b| *b == b'/').filter(|p| !p.is_empty()).collect(); + let missing = crate::linux::abi::errno::ENOENT; + let rest = parts.get(1..).ok_or(missing)?; + view::with(|v| made(v, rest)).ok_or(missing) +} + +fn made(v: &View, rest: &[&[u8]]) -> Option> { + match parse(v, rest)? { + At::System(name) => system::content(v, name), + At::Sysctl(path) => sysctl::content(path), + At::Pid { proc, tid, file } => pid_files::content(proc, tid, file), + At::FdInfo { proc, fd } => fds::info(proc, fd), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/file/made/synth/mod.rs b/userland/capsule_linux/src/linux/file/made/synth/mod.rs new file mode 100644 index 000000000..657cb060a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The files NONOS makes rather than keeps: /dev, /proc and /sys. + * + * None of them is in the store. Each is answered from the declared surface + * (`declared`), from the family's view (`view`), or from the asking guest + * itself, and is made again at every read, so a descriptor carried through + * dup or fork reads what is true when it reads, as on Linux. + */ + +mod node; +mod roots; + +pub use node::{node, owns, Node, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG}; +pub use roots::{num, ROOTS}; diff --git a/userland/capsule_linux/src/linux/file/made/synth/node.rs b/userland/capsule_linux/src/linux/file/made/synth/node.rs new file mode 100644 index 000000000..7526d5132 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth/node.rs @@ -0,0 +1,69 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The trees the personality makes, and what a path in them is. */ + +use alloc::vec::Vec; + +use super::super::dev::Dev; + +pub const S_IFDIR: u32 = 0o040000; + +pub const S_IFREG: u32 = 0o100000; + +pub const S_IFLNK: u32 = 0o120000; + +pub const S_IFCHR: u32 = 0o020000; + +pub enum Node { + /* A directory and the names in it. */ + Dir(Vec>), + /* A file whose bytes are made at each read, and its permission bits. */ + Text(u32), + /* A symbolic link, as readlink gives it. */ + Link(Vec), + Dev(Dev), + /* There, and refused on purpose: the reason is said when it is opened. */ + Refused(&'static str), +} + +/* Which tree a path is in, if any: the store keeps /dev/shm, not /dev. */ +pub fn owns(path: &[u8]) -> bool { + let under = + |root: &[u8]| path.starts_with(root) && matches!(path.get(root.len()), None | Some(b'/')); + (under(b"/proc") || under(b"/sys") || under(b"/dev")) && !under(b"/dev/shm") +} + +/* + * The node at `path`: None outside these trees, Err(ENOENT) inside them + * where there is nothing. + */ +pub fn node(path: &[u8]) -> Option> { + if !owns(path) { + return None; + } + let parts: Vec<&[u8]> = path.split(|b| *b == b'/').filter(|p| !p.is_empty()).collect(); + let missing = crate::linux::abi::errno::ENOENT; + Some( + match parts.split_first() { + Some((&b"dev", rest)) => super::super::dev::node(rest), + Some((&b"sys", rest)) => super::super::sys::node(rest), + Some((_, rest)) => super::super::proc::node(rest), + None => None, + } + .ok_or(missing), + ) +} diff --git a/userland/capsule_linux/src/linux/file/made/synth/roots.rs b/userland/capsule_linux/src/linux/file/made/synth/roots.rs new file mode 100644 index 000000000..983d85310 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth/roots.rs @@ -0,0 +1,27 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The trees at /, and a number as text. */ + +use alloc::vec::Vec; + +/* The three trees, for a listing of `/`. */ +pub const ROOTS: [&str; 3] = ["dev", "proc", "sys"]; + +/* `n` in decimal. */ +pub fn num(n: u64) -> Vec { + alloc::format!("{n}").into_bytes() +} diff --git a/userland/capsule_linux/src/linux/file/made/synth_ops/io.rs b/userland/capsule_linux/src/linux/file/made/synth_ops/io.rs new file mode 100644 index 000000000..51ea235e5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth_ops/io.rs @@ -0,0 +1,48 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Reading and writing a made file. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; + +use super::super::super::{proc, sys}; +use super::super::synth::{self}; + +/* The bytes at `offset` of the made file at `path`; None if it is not one. */ +pub fn read(path: &[u8], offset: u64, len: usize) -> Option, i64>> { + if !synth::owns(path) { + return None; + } + let whole = match sys::content(path) { + Some(bytes) => Ok(bytes), + None => proc::content(path), + }; + Some(whole.map(|all| { + let from = (offset as usize).min(all.len()); + all[from..(from + len).min(all.len())].to_vec() + })) +} + +/* A write to the made file at `path`, which none of them takes. */ +pub fn write(path: &[u8]) -> Option> { + if !synth::owns(path) { + return None; + } + /* The devices are answered by file/dev.rs; nothing made here takes a write. */ + Some(Err(errno::EACCES)) +} diff --git a/userland/capsule_linux/src/linux/file/made/synth_ops/made.rs b/userland/capsule_linux/src/linux/file/made/synth_ops/made.rs new file mode 100644 index 000000000..c650830d2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth_ops/made.rs @@ -0,0 +1,34 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A descriptor on a made file, and a made path refused by name. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Fd; + +/* A descriptor on a made file: its bytes come from its path at each read. */ +pub(super) fn made_file(path: &[u8], writing: bool, flags: u64) -> Fd { + let mut fd = Fd::file(path.to_vec(), 0, None, writing); + fd.handle = + super::super::super::desc::fresh(false, super::super::super::flags::wants_read(flags)); + fd +} + +pub(super) fn refuse(why: &str) -> u64 { + let line = alloc::format!("[LINUX] refused {why}\n"); + let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); + errno::fail(errno::EACCES) +} diff --git a/userland/capsule_linux/src/linux/file/made/synth_ops/mod.rs b/userland/capsule_linux/src/linux/file/made/synth_ops/mod.rs new file mode 100644 index 000000000..b55ae618f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth_ops/mod.rs @@ -0,0 +1,24 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Opening, reading and writing the files NONOS makes. */ + +mod io; +mod made; +mod open; + +pub use io::{read, write}; +pub use open::open; diff --git a/userland/capsule_linux/src/linux/file/made/synth_ops/open.rs b/userland/capsule_linux/src/linux/file/made/synth_ops/open.rs new file mode 100644 index 000000000..010647425 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/synth_ops/open.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Opening a path in a made tree. */ + +use alloc::string::String; + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::super::super::flags::{O_CREAT, O_DIRECTORY}; +use super::super::super::slot; +use super::super::dev; +use super::super::synth::{self, Node}; +use super::made::{made_file, refuse}; + +/* + * Open `path`, already followed, if it is one of these files; None if it + * is not in /dev, /proc or /sys. + */ +pub fn open(guest: &mut Guest, path: &[u8], flags: u64) -> Option { + let node = match synth::node(path)? { + Ok(node) => node, + /* Those trees are mounted read-only: nothing is made in them. */ + Err(_) if flags & O_CREAT != 0 => return Some(errno::fail(errno::EROFS)), + Err(e) => return Some(errno::fail(e)), + }; + /* O_WRONLY or O_RDWR. */ + let writing = flags & 3 != 0; + let fd = match node { + Node::Dir(_) if writing => return Some(errno::fail(errno::EISDIR)), + Node::Dir(names) => { + let dots = [String::from("."), String::from("..")]; + let names = + dots.into_iter().chain(names.into_iter().filter_map(|n| String::from_utf8(n).ok())); + let mut fd = Fd::dir(path.to_vec(), names.collect()); + fd.handle = super::super::super::desc::fresh(false, false); + fd + } + _ if flags & O_DIRECTORY != 0 => return Some(errno::fail(errno::ENOTDIR)), + Node::Dev(dev::Dev::Tty) => return Some(errno::fail(errno::ENXIO)), + Node::Dev(_) => made_file(path, writing, flags), + Node::Text(_) if writing => return Some(errno::fail(errno::EACCES)), + Node::Text(_) => made_file(path, false, flags), + Node::Refused(why) => return Some(refuse(why)), + Node::Link(to) => return Some(super::super::fdopen::reopen(guest, path, &to, flags)), + }; + Some(match slot::install(guest, fd) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + }) +} diff --git a/userland/capsule_linux/src/linux/file/made/sys.rs b/userland/capsule_linux/src/linux/file/made/sys.rs new file mode 100644 index 000000000..c1d557846 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/sys.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * /sys: only what programs are known to read, and nothing else. + * + * Go reads the huge-page size at start to size its heap arenas; musl reads + * nothing here. Every other path answers ENOENT, which is what a program + * meets on a Linux with no sysfs mounted, and which every sysfs reader + * already handles. + */ + +use alloc::vec::Vec; + +use super::super::declared; +use super::synth::{num, Node}; + +const THP: [&[u8]; 4] = [b"kernel", b"mm", b"transparent_hugepage", b"hpage_pmd_size"]; + +pub fn node(rest: &[&[u8]]) -> Option { + if rest.len() > THP.len() || rest.iter().zip(THP.iter()).any(|(a, b)| a != b) { + return None; + } + Some(match THP.get(rest.len()) { + Some(next) => Node::Dir(alloc::vec![next.to_vec()]), + None => Node::Text(0o444), + }) +} + +pub fn content(path: &[u8]) -> Option> { + let want = b"/sys/kernel/mm/transparent_hugepage/hpage_pmd_size"; + (path == want).then(|| { + let mut out = num(declared::HPAGE_PMD); + out.push(b'\n'); + out + }) +} diff --git a/userland/capsule_linux/src/linux/file/made/view/lent.rs b/userland/capsule_linux/src/linux/file/made/view/lent.rs new file mode 100644 index 000000000..88265211d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/view/lent.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The view the family lends /proc for one call. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use super::shape::View; + +pub(super) struct Lent(pub(super) RefCell); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Lent {} + +static LENT: Lent = Lent(RefCell::new(View { me: 0, thread: 0, procs: Vec::new() })); + +/* Lend the view for one call; an empty view takes it back. */ +pub fn lend(view: View) { + *LENT.0.borrow_mut() = view; +} + +pub fn with(f: impl FnOnce(&View) -> T) -> T { + f(&LENT.0.borrow()) +} diff --git a/userland/capsule_linux/src/linux/file/made/view/mod.rs b/userland/capsule_linux/src/linux/file/made/view/mod.rs new file mode 100644 index 000000000..e2e397032 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/view/mod.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The family as /proc shows it to the guest being answered. + * + * Only the family's own processes are here, each under the number the + * guest's pid namespace gives it; nothing outside the family is, so no + * path under /proc can name it. The serve loop fills this before a call + * that may read /proc and empties it after, so it is never stale. + */ + +mod lent; +mod proc; +mod shape; + +pub use lent::{lend, with}; +pub use proc::Proc; +pub use shape::{Open, View}; diff --git a/userland/capsule_linux/src/linux/file/made/view/proc.rs b/userland/capsule_linux/src/linux/file/made/view/proc.rs new file mode 100644 index 000000000..e2d27d9d6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/view/proc.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* One process of the family, as /proc shows it. */ + +use alloc::vec::Vec; + +use crate::linux::guest::Region; + +use super::super::exe::Exe; +use super::shape::Open; + +#[derive(Clone)] +pub struct Proc { + /* The pid in the family's namespace, and the kernel's behind it. */ + pub ns: u32, + pub kernel: u32, + pub ppid: u32, + pub pgid: u32, + pub sid: u32, + /* Every thread's number, the leader first. */ + pub tids: Vec<(u32, u32)>, + /* Waiting in a call rather than on the CPU. */ + pub sleeping: bool, + pub exe: Exe, + pub cwd: Vec, + pub fds: Vec, + pub regions: Vec, + pub brk: (u64, u64), + pub umask: u16, + /* Bit n-1 set for each signal n it catches, and for each it ignores. */ + pub caught: u64, + pub ignored: u64, + /* What its children used, those it has waited for. */ + pub reaped: super::super::super::cpu::Usage, +} diff --git a/userland/capsule_linux/src/linux/file/made/view/shape.rs b/userland/capsule_linux/src/linux/file/made/view/shape.rs new file mode 100644 index 000000000..d223fdd44 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/view/shape.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What a family looks like from inside: its processes' files. */ + +use alloc::vec::Vec; + +use super::proc::Proc; + +/* A descriptor as /proc//fd shows it. */ +#[derive(Clone)] +pub struct Open { + pub fd: u32, + /* What readlink says it names. */ + pub target: Vec, + pub offset: u64, + /* O_ACCMODE, O_NONBLOCK and O_CLOEXEC, as fdinfo's flags. */ + pub flags: u64, + /* The open file description, for a file or a directory. */ + pub desc: Option, +} + +#[derive(Default)] +pub struct View { + /* The asking process's own number. */ + pub me: u32, + /* The asking thread's number. */ + pub thread: u32, + pub procs: Vec, +} + +impl View { + pub fn find(&self, ns: u32) -> Option<&Proc> { + self.procs.iter().find(|p| p.ns == ns) + } +} diff --git a/userland/capsule_linux/src/linux/file/memfd.rs b/userland/capsule_linux/src/linux/file/memfd.rs index 36bc7afa0..d23fede19 100644 --- a/userland/capsule_linux/src/linux/file/memfd.rs +++ b/userland/capsule_linux/src/linux/file/memfd.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `memfd_create` and `ftruncate`. +/* `memfd_create` and `ftruncate`. */ use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest, Kind}; @@ -31,16 +31,3 @@ pub fn memfd_create(guest: &mut Guest) -> u64 { None => errno::fail(errno::EMFILE), } } - -/// Sizing one records the size and nothing else. The pages appear when -/// the client maps it, because that is when their address is decided. -pub fn ftruncate(guest: &mut Guest, fd: u64, len: u64) -> u64 { - match guest.fds.get_mut(fd as usize) { - Some(entry) if entry.kind == Kind::Memfd => { - entry.size = len; - errno::ok(0) - } - Some(_) => errno::fail(errno::EINVAL), - None => errno::fail(errno::EBADF), - } -} diff --git a/userland/capsule_linux/src/linux/file/meta/mod.rs b/userland/capsule_linux/src/linux/file/meta/mod.rs index 6921433df..20e209bd2 100644 --- a/userland/capsule_linux/src/linux/file/meta/mod.rs +++ b/userland/capsule_linux/src/linux/file/meta/mod.rs @@ -14,8 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! What the store knows about a name, and what a program may do with it. +/* What the store knows about a name, and what a program may do with it. */ +mod node; mod perms; mod query; pub(super) mod stat; @@ -23,8 +24,9 @@ mod statbuf; mod statfs; mod statx; -pub use perms::{chmod, faccessat, fchmod, fchmodat}; -pub use query::{access, readlinkat}; +pub use node::{now as now_ms, of as meta_of}; +pub use perms::{chmod, fchmod, fchmodat}; +pub use query::{access, faccessat, is_link, readlinkat}; pub use stat::{fstat, look, newfstatat}; -pub use statfs::statfs; +pub use statfs::{fstatfs, statfs}; pub use statx::statx; diff --git a/userland/capsule_linux/src/linux/file/meta/node/device.rs b/userland/capsule_linux/src/linux/file/meta/node/device.rs new file mode 100644 index 000000000..efc492597 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/device.rs @@ -0,0 +1,28 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The metadata of a character device this capsule answers. */ + +use super::super::super::dev_stat; +use super::super::statbuf::Meta; +use super::fd::now; +use super::path::at; + +/* The device `dev`, by the path it was opened at. */ +pub(super) fn device(path: &[u8], dev: u32) -> Option { + let rdev = dev_stat::rdev(dev)?; + Some(Meta { rdev, ..at(path, dev_stat::MODE, 0, now()) }) +} diff --git a/userland/capsule_linux/src/linux/file/meta/node/fd.rs b/userland/capsule_linux/src/linux/file/meta/node/fd.rs new file mode 100644 index 000000000..c77469e8f --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/fd.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The metadata for a descriptor, and the kind of file it is on. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::super::super::modes; +use super::super::super::proc::{CONSOLE_IN, CONSOLE_OUT, PIPES, SOCKETS}; +use super::super::super::synth::S_IFREG; +use super::super::statbuf::Meta; +use super::device::device; +use super::made::named; +use super::path::{at, of}; + +/* fstat: a file by its path, and the rest by kind, as /proc names them. */ +pub fn of_fd(guest: &Guest, f: &Fd) -> Result { + match f.kind { + Kind::Free => Err(errno::EBADF), + Kind::File | Kind::Dir => { + let m = of(guest, f.path.clone(), true); + /* A file this family is making exists before the store holds it. */ + m.or_else(|_| Ok(at(&f.path, S_IFREG | modes::FILE, f.size, now()))) + } + Kind::Stdin => Ok(named(&alloc::format!("pipe:[{CONSOLE_IN}]").into_bytes(), b"/")), + Kind::Stdout | Kind::Stderr => { + Ok(named(&alloc::format!("pipe:[{CONSOLE_OUT}]").into_bytes(), b"/")) + } + Kind::Pipe => { + Ok(named(&alloc::format!("pipe:[{}]", PIPES + u64::from(f.handle)).into_bytes(), b"/")) + } + Kind::Socket | Kind::Unix | Kind::Resolver => Ok(named( + &alloc::format!("socket:[{}]", SOCKETS + u64::from(f.handle)).into_bytes(), + b"/", + )), + Kind::Memfd => Ok(Meta { size: f.size, ..at(b"/memfd:", S_IFREG | 0o777, 0, now()) }), + Kind::Device => device(&f.path, f.handle).ok_or(errno::EBADF), + Kind::Epoll | Kind::Timer | Kind::Event | Kind::Signal => Ok(named(b"anon_inode:[]", b"/")), + } +} + +pub fn now() -> u64 { + u64::try_from(nonos_libc::mk_time_millis()).unwrap_or(0) +} diff --git a/userland/capsule_linux/src/linux/file/meta/node/made.rs b/userland/capsule_linux/src/linux/file/meta/node/made.rs new file mode 100644 index 000000000..35ada8c89 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/made.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The metadata for a made node, and for a name the store keeps. */ + +use super::super::super::made::dev; +use super::super::super::synth::{Node, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG}; +use super::super::statbuf::Meta; +use super::fd::now; +use super::path::{at, S_IFIFO, S_IFSOCK}; + +pub(super) fn made(path: &[u8], node: Node) -> Result { + let t = now(); + Ok(match node { + Node::Dir(_) if path.starts_with(b"/dev") => at(path, S_IFDIR | 0o755, 0, t), + Node::Dir(_) => at(path, S_IFDIR | 0o555, 0, t), + Node::Text(mode) => at(path, S_IFREG | mode, 0, t), + Node::Refused(_) => at(path, S_IFREG | 0o600, 0, t), + Node::Dev(d) => Meta { rdev: dev::rdev(d), ..at(path, S_IFCHR | 0o666, 0, t) }, + /* Followed already, so a link here names no path: a pipe or a socket. */ + Node::Link(to) if to.contains(&b':') => named(&to, path), + Node::Link(to) => at(path, S_IFLNK | 0o777, to.len() as u64, t), + }) +} + +/* + * What has no path, by the name /proc gives it: `pipe:[n]`, `socket:[n]`, + * `anon_inode:[...]`. + */ +pub(super) fn named(to: &[u8], path: &[u8]) -> Meta { + let number = |skip: usize| { + let digits = to.get(skip..to.len().saturating_sub(1)).unwrap_or(b""); + core::str::from_utf8(digits).ok().and_then(|s| s.parse().ok()).unwrap_or(0) + }; + let t = now(); + match to { + _ if to.starts_with(b"pipe:[") => { + Meta { ino: number(6), ..at(path, S_IFIFO | 0o600, 0, t) } + } + _ if to.starts_with(b"socket:[") => { + Meta { ino: number(8), ..at(path, S_IFSOCK | 0o777, 0, t) } + } + _ => Meta { ino: 0, ..at(path, 0o600, 0, t) }, + } +} diff --git a/userland/capsule_linux/src/linux/file/meta/node/mod.rs b/userland/capsule_linux/src/linux/file/meta/node/mod.rs new file mode 100644 index 000000000..9ff2a0eb7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/mod.rs @@ -0,0 +1,29 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What stat says about a path or a descriptor, from one place, so stat, + * lstat, fstat, fstatat and statx never disagree. + */ + +mod device; +mod fd; +mod made; +mod path; + +pub use super::statbuf::Meta; +pub use fd::{now, of_fd}; +pub use path::of; diff --git a/userland/capsule_linux/src/linux/file/meta/node/path.rs b/userland/capsule_linux/src/linux/file/meta/node/path.rs new file mode 100644 index 000000000..75de5e9eb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/path.rs @@ -0,0 +1,73 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The metadata for a path, from the store, the family's copies and + * the trees the personality makes. + */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::synth::{self, S_IFDIR, S_IFLNK, S_IFREG}; +use super::super::super::{cache, dev, modes, mounts, resolve, store, times, walk}; +use super::super::statbuf::inode; +use super::super::statbuf::Meta; +use super::device::device; +use super::fd::now; +use super::made::made; + +pub const S_IFIFO: u32 = 0o010000; + +pub const S_IFSOCK: u32 = 0o140000; + +pub(super) fn at(path: &[u8], mode: u32, size: u64, mtime_ms: u64) -> Meta { + let dev = mounts::dev(mounts::of(path).0); + let (atime_ms, mtime_ms) = match times::of(path) { + Some(t) if t.written_ms >= mtime_ms => (t.atime_ms, t.mtime_ms), + _ => (mtime_ms, mtime_ms), + }; + Meta { mode, size, ino: inode(path), nlink: 1, rdev: 0, dev, mtime_ms, atime_ms } +} + +/* The path's metadata; its last component followed when `follow` is set. */ +pub fn of(guest: &Guest, named: alloc::vec::Vec, follow: bool) -> Result { + let full = walk::follow(guest, named, follow); + if !follow { + if let Some(to) = guest.links.target(&full) { + return Ok(at(&full, S_IFLNK | 0o777, to.len() as u64, now())); + } + } + if let Some(m) = dev::device_of(&full).and_then(|d| device(&full, d)) { + return Ok(m); + } + if let Some(node) = synth::node(&full) { + return made(&full, node?); + } + if let (Some(size), Some(t)) = (cache::size(&full), cache::mtime(&full)) { + let mode = modes::of(&full).unwrap_or(modes::FILE); + return Ok(at(&full, S_IFREG | mode, size, t)); + } + let (size, is_dir, mtime, writable) = + store::stat_full(&resolve::key(&full)).map_err(|_| errno::ENOENT)?; + let kind = if is_dir { S_IFDIR } else { S_IFREG }; + let default = match (writable, is_dir) { + (false, _) => modes::SHARED, + (true, true) => modes::DIR, + (true, false) => modes::FILE, + }; + Ok(at(&full, kind | modes::of(&full).unwrap_or(default), size, mtime)) +} diff --git a/userland/capsule_linux/src/linux/file/meta/perms.rs b/userland/capsule_linux/src/linux/file/meta/perms.rs index affea08c3..4a00b1c8b 100644 --- a/userland/capsule_linux/src/linux/file/meta/perms.rs +++ b/userland/capsule_linux/src/linux/file/meta/perms.rs @@ -14,28 +14,23 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Mode bits, and whether a path can be reached. +/* Mode bits, and whether a path can be reached. */ use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; use super::super::at::resolve_at; use super::super::flags::AT_FDCWD; -use super::super::resolve::key; -use super::super::{store, store_name}; +use super::super::{cache, modes, resolve, synth, walk}; +use super::stat::look; pub fn fchmodat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { let Some(at) = resolve_at(guest, dirfd, path) else { return errno::fail(errno::EFAULT); }; - match store_name::chmod(&key(&at), mode as u16) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::ENOENT), - } + change(&walk::follow(guest, at, true), mode) } -/// `fchmod` names the file by a descriptor the guest already holds, so -/// the path comes from the descriptor rather than from the caller. pub fn fchmod(guest: &Guest, fd: u64, mode: u64) -> u64 { let Some(entry) = guest.fds.get(fd as usize).filter(|f| f.is_open()) else { return errno::fail(errno::EBADF); @@ -43,21 +38,22 @@ pub fn fchmod(guest: &Guest, fd: u64, mode: u64) -> u64 { if entry.kind != Kind::File && entry.kind != Kind::Dir { return errno::fail(errno::EINVAL); } - match store_name::chmod(&key(&entry.path), mode as u16) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::ENOENT), - } + change(&entry.path.clone(), mode) } -/// `faccessat`: does the path exist and is it reachable. -pub fn faccessat(guest: &Guest, dirfd: u64, path: u64) -> u64 { - let Some(at) = resolve_at(guest, dirfd, path) else { - return errno::fail(errno::EFAULT); - }; - match store::stat(&key(&at)) { - Ok(_) => errno::ok(0), - Err(_) => errno::fail(errno::ENOENT), +/* + * The store keeps no modes, so the family does (held/modes.rs). The shared + * tree and /dev, /proc and /sys are mounted read-only. + */ +fn change(full: &[u8], mode: u64) -> u64 { + if look(full).is_none() { + return errno::fail(errno::ENOENT); + } + if synth::owns(full) || (!cache::held(full) && resolve::key(full).writable().is_err()) { + return errno::fail(errno::EROFS); } + modes::set(full, mode as u32); + errno::ok(0) } pub fn chmod(guest: &Guest, path: u64, mode: u64) -> u64 { diff --git a/userland/capsule_linux/src/linux/file/meta/query.rs b/userland/capsule_linux/src/linux/file/meta/query.rs deleted file mode 100644 index ddb706697..000000000 --- a/userland/capsule_linux/src/linux/file/meta/query.rs +++ /dev/null @@ -1,54 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! `getcwd`, `access` and `readlink`: the three questions a program asks -//! about a path without opening it. - -use crate::linux::abi::errno; -use crate::linux::guest::Guest; - -use super::super::{path, resolve}; -use super::stat; - -pub fn access(guest: &Guest, path_ptr: u64) -> u64 { - let Some(name) = path::read_path(guest, path_ptr) else { - return errno::fail(errno::EFAULT); - }; - let full = guest.links.follow(resolve::visible(&guest.cwd, &name), true); - match stat::look(&full) { - Some(_) => errno::ok(0), - None => errno::fail(errno::ENOENT), - } -} - -/// A link's target, from the family's table. A path that exists and is not a -/// link is EINVAL, as Linux answers. `readlink` is this at AT_FDCWD. -pub fn readlinkat(guest: &Guest, dirfd: u64, path_ptr: u64, buf: u64, len: u64) -> u64 { - let Some(full) = super::super::at::resolve_at(guest, dirfd, path_ptr) else { - return errno::fail(errno::EFAULT); - }; - if let Some(to) = guest.links.target(&full) { - let n = to.len().min(len as usize); - return match guest.write(buf, &to[..n]) < n as i64 { - true => errno::fail(errno::EFAULT), - false => errno::ok(n as u64), - }; - } - match stat::look(&full) { - Some(_) => errno::fail(errno::EINVAL), - None => errno::fail(errno::ENOENT), - } -} diff --git a/userland/capsule_linux/src/linux/file/meta/query/access.rs b/userland/capsule_linux/src/linux/file/meta/query/access.rs new file mode 100644 index 000000000..40b0428de --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/query/access.rs @@ -0,0 +1,70 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* access and faccessat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::made::dev; +use super::super::super::synth::{self}; +use super::super::super::{at, cache, path, resolve, walk}; + +const X_OK: u64 = 1; + +const W_OK: u64 = 2; + +pub fn access(guest: &Guest, path_ptr: u64, mode: u64) -> u64 { + faccessat(guest, super::super::super::flags::AT_FDCWD, path_ptr, mode, 0) +} + +/* + * The guest is root, so only two things stand in the way: a read-only + * mount for W_OK, and a file no one may execute for X_OK. + */ +pub fn faccessat(guest: &Guest, dirfd: u64, path_ptr: u64, mode: u64, flags: u64) -> u64 { + if mode & !7 != 0 { + return errno::fail(errno::EINVAL); + } + let m = match super::super::stat::meta_at( + guest, + dirfd, + path_ptr, + flags & super::super::stat::AT_SYMLINK_NOFOLLOW, + ) { + Ok(m) => m, + Err(e) => return errno::fail(e), + }; + let is_dir = m.mode & 0o170000 == synth::S_IFDIR; + if mode & X_OK != 0 && !is_dir && m.mode & 0o111 == 0 { + return errno::fail(errno::EACCES); + } + if mode & W_OK != 0 && !writable(guest, dirfd, path_ptr) { + return errno::fail(errno::EROFS); + } + errno::ok(0) +} + +fn writable(guest: &Guest, dirfd: u64, path_ptr: u64) -> bool { + let Some(name) = path::read_path(guest, path_ptr) else { return false }; + let Ok(named) = at::named_at(guest, dirfd, &name) else { return false }; + let full = walk::follow(guest, named, true); + if synth::owns(&full) { + /* A device ignores its mount's read-only flag; nothing else there is writable. */ + return dev::at(&full).is_some(); + } + cache::held(&full) || resolve::key(&full).writable().is_ok() +} diff --git a/userland/capsule_linux/src/linux/file/meta/query/link.rs b/userland/capsule_linux/src/linux/file/meta/query/link.rs new file mode 100644 index 000000000..d737c07be --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/query/link.rs @@ -0,0 +1,57 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Whether a name is a link, and readlinkat. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::synth::{self, Node}; +use super::super::super::{at, walk}; +use super::super::node; + +/* Whether the name is itself a link, of the image's or a made one. */ +pub fn is_link(guest: &Guest, named: &[u8]) -> bool { + let full = walk::follow(guest, named.to_vec(), false); + guest.links.target(&full).is_some() || matches!(synth::node(&full), Some(Ok(Node::Link(_)))) +} + +pub fn readlinkat(guest: &Guest, dirfd: u64, path_ptr: u64, buf: u64, len: u64) -> u64 { + if (len as i64) <= 0 { + return errno::fail(errno::EINVAL); + } + let Some(full) = at::resolve_at(guest, dirfd, path_ptr) else { + return errno::fail(errno::EFAULT); + }; + let to: Vec = match (guest.links.target(&full), synth::node(&full)) { + (Some(to), _) => to, + (None, Some(Ok(Node::Link(to)))) => to, + (None, Some(Err(e))) => return errno::fail(e), + _ => { + return match node::of(guest, full, false) { + Ok(_) => errno::fail(errno::EINVAL), + Err(e) => errno::fail(e), + }; + } + }; + let n = to.len().min(len as usize); + match guest.write(buf, &to[..n]) < n as i64 { + true => errno::fail(errno::EFAULT), + false => errno::ok(n as u64), + } +} diff --git a/userland/capsule_linux/src/linux/file/meta/query/mod.rs b/userland/capsule_linux/src/linux/file/meta/query/mod.rs new file mode 100644 index 000000000..40a96a6af --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/query/mod.rs @@ -0,0 +1,23 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* `access`, `faccessat` and `readlink`: questions about a name. */ + +mod access; +mod link; + +pub use access::{access, faccessat}; +pub use link::{is_link, readlinkat}; diff --git a/userland/capsule_linux/src/linux/file/meta/stat.rs b/userland/capsule_linux/src/linux/file/meta/stat.rs deleted file mode 100644 index f8f032ac2..000000000 --- a/userland/capsule_linux/src/linux/file/meta/stat.rs +++ /dev/null @@ -1,75 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! What the store knows about a path, and the two calls that ask. - -use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; - -use super::super::dev::device_of; -use super::super::flags::AT_FDCWD; -use super::super::{path, resolve, store}; -use super::statbuf::{build, inode, STAT_LEN}; - -/// Size and whether it is a directory, or nothing when the path is -/// absent. `full` is guest-visible and is confined here. -pub fn look(full: &[u8]) -> Option<(u64, bool)> { - match store::stat_full(&resolve::key(full)) { - _ if device_of(full).is_some() => Some((0, false)), - Ok((size, is_dir, _, _)) => Some((size, is_dir)), - Err(_) => None, - } -} - -pub fn fstat(guest: &mut Guest, fd: u64, out: u64) -> u64 { - let Some(entry) = guest.fds.get(fd as usize) else { - return errno::fail(errno::EBADF); - }; - let (size, is_dir) = match entry.kind { - Kind::Free => return errno::fail(errno::EBADF), - Kind::Dir => (0, true), - Kind::File => (entry.size.max(entry.pending.len() as u64), false), - _ => (0, false), - }; - let ino = inode(&entry.path); - let dev = (entry.kind == Kind::Device).then_some(entry.handle); - write_out(guest, out, size, is_dir, ino, dev) -} - -pub fn newfstatat(guest: &mut Guest, dirfd: u64, path_ptr: u64, out: u64) -> u64 { - let Some(name) = path::read_path(guest, path_ptr) else { - return errno::fail(errno::EFAULT); - }; - if dirfd != AT_FDCWD { - return errno::fail(errno::ENOSYS); - } - let full = guest.links.follow(resolve::visible(&guest.cwd, &name), true); - match look(&full) { - Some((size, is_dir)) => write_out(guest, out, size, is_dir, inode(&full), device_of(&full)), - None => errno::fail(errno::ENOENT), - } -} - -fn write_out(guest: &Guest, out: u64, size: u64, is_dir: bool, ino: u64, dev: Option) -> u64 { - let mut stat = build(size, is_dir, ino); - if let Some(d) = dev { - super::super::dev_stat::as_device(&mut stat, d); - } - if guest.write(out, &stat) < STAT_LEN as i64 { - return errno::fail(errno::EFAULT); - } - errno::ok(0) -} diff --git a/userland/capsule_linux/src/linux/file/meta/stat/at.rs b/userland/capsule_linux/src/linux/file/meta/stat/at.rs new file mode 100644 index 000000000..b14d79228 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/stat/at.rs @@ -0,0 +1,55 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The metadata a *at call names, after its flags. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{at, path}; +use super::super::node::{self, Meta}; +use super::super::statbuf::{build, STAT_LEN}; +use super::calls::{AT_EMPTY_PATH, AT_SYMLINK_NOFOLLOW}; + +/* + * The *at form's metadata: the name against the directory descriptor, + * or the descriptor itself for an empty name with AT_EMPTY_PATH. + */ +pub fn meta_at(guest: &Guest, dirfd: u64, path_ptr: u64, flags: u64) -> Result { + let name = path::read_path(guest, path_ptr).ok_or(errno::EFAULT)?; + let dirfd = super::super::super::flags::dirfd(dirfd); + if name.is_empty() { + if flags & AT_EMPTY_PATH == 0 { + return Err(errno::ENOENT); + } + if dirfd == super::super::super::flags::AT_FDCWD { + return node::of(guest, guest.cwd.clone(), true); + } + let f = guest.fds.get(dirfd as usize).filter(|f| f.is_open()).ok_or(errno::EBADF)?; + return node::of_fd(guest, f); + } + let named: Vec = at::named_at(guest, dirfd, &name)?; + node::of(guest, named, flags & AT_SYMLINK_NOFOLLOW == 0) +} + +pub(super) fn write_out(guest: &Guest, out: u64, m: &Meta) -> u64 { + if guest.write(out, &build(m)) < STAT_LEN as i64 { + return errno::fail(errno::EFAULT); + } + errno::ok(0) +} diff --git a/userland/capsule_linux/src/linux/file/meta/stat/calls.rs b/userland/capsule_linux/src/linux/file/meta/stat/calls.rs new file mode 100644 index 000000000..0e1b52b29 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/stat/calls.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* stat, fstat and newfstatat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{cache, dev, resolve, store, synth}; +use super::super::node::{self}; +use super::at::{meta_at, write_out}; + +pub const AT_SYMLINK_NOFOLLOW: u64 = 0x100; + +pub const AT_EMPTY_PATH: u64 = 0x1000; + +/* Size and whether it is a directory, for a path already followed. */ +pub fn look(full: &[u8]) -> Option<(u64, bool)> { + if dev::device_of(full).is_some() { + return Some((0, false)); + } + if let Some(node) = synth::node(full) { + return node.ok().map(|n| (0, matches!(n, synth::Node::Dir(_)))); + } + if let Some(size) = cache::size(full) { + return Some((size, false)); + } + store::stat_full(&resolve::key(full)).ok().map(|(size, is_dir, _, _)| (size, is_dir)) +} + +pub fn fstat(guest: &mut Guest, fd: u64, out: u64) -> u64 { + let Some(entry) = guest.fds.get(fd as usize) else { + return errno::fail(errno::EBADF); + }; + match node::of_fd(guest, entry) { + Ok(m) => write_out(guest, out, &m), + Err(e) => errno::fail(e), + } +} + +pub fn newfstatat(guest: &mut Guest, dirfd: u64, path_ptr: u64, out: u64, flags: u64) -> u64 { + match meta_at(guest, dirfd, path_ptr, flags) { + Ok(m) => write_out(guest, out, &m), + Err(e) => errno::fail(e), + } +} diff --git a/userland/capsule_linux/src/linux/file/meta/stat/mod.rs b/userland/capsule_linux/src/linux/file/meta/stat/mod.rs new file mode 100644 index 000000000..bc120089d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/stat/mod.rs @@ -0,0 +1,23 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* `stat`, `lstat`, `fstat` and `newfstatat`. */ + +mod at; +mod calls; + +pub use at::meta_at; +pub use calls::{fstat, look, newfstatat, AT_SYMLINK_NOFOLLOW}; diff --git a/userland/capsule_linux/src/linux/file/meta/statbuf.rs b/userland/capsule_linux/src/linux/file/meta/statbuf.rs deleted file mode 100644 index a3c0876a1..000000000 --- a/userland/capsule_linux/src/linux/file/meta/statbuf.rs +++ /dev/null @@ -1,60 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! The `struct stat` an x86_64 Linux program expects, filled by hand. - -/// Bytes of a `struct stat` on this architecture. -pub const STAT_LEN: usize = 144; - -pub const S_IFREG: u32 = 0o100000; -pub const S_IFDIR: u32 = 0o040000; - -const OFF_INO: usize = 8; -const OFF_NLINK: usize = 16; -const OFF_MODE: usize = 24; -const OFF_SIZE: usize = 48; -const OFF_BLKSIZE: usize = 56; -const OFF_BLOCKS: usize = 64; - -/// A file's number, stable for its path and never zero. Distinct numbers are -/// how a loader tells two libraries apart; zero for every file made each -/// dlopen after the first hand back the library already loaded. -pub fn inode(path: &[u8]) -> u64 { - let fold = path - .iter() - .fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ u64::from(*b)).wrapping_mul(0x100_0000_01b3)); - fold | 1 -} - -pub fn build(size: u64, is_dir: bool, ino: u64) -> [u8; STAT_LEN] { - let mut out = [0u8; STAT_LEN]; - let mode = if is_dir { S_IFDIR | 0o755 } else { S_IFREG | 0o644 }; - put64(&mut out, OFF_INO, ino); - put64(&mut out, OFF_NLINK, 1); - put32(&mut out, OFF_MODE, mode); - put64(&mut out, OFF_SIZE, size); - put64(&mut out, OFF_BLKSIZE, 4096); - put64(&mut out, OFF_BLOCKS, size.div_ceil(512)); - out -} - -fn put32(out: &mut [u8; STAT_LEN], at: usize, value: u32) { - out[at..at + 4].copy_from_slice(&value.to_le_bytes()); -} - -fn put64(out: &mut [u8; STAT_LEN], at: usize, value: u64) { - out[at..at + 8].copy_from_slice(&value.to_le_bytes()); -} diff --git a/userland/capsule_linux/src/linux/file/meta/statbuf/build.rs b/userland/capsule_linux/src/linux/file/meta/statbuf/build.rs new file mode 100644 index 000000000..5803952ed --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statbuf/build.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* struct stat, x86_64 layout. */ + +use super::shape::{blocks, Meta, STAT_LEN}; + +/* + * struct stat, x86_64 layout. Owner and group are root, as the ids the + * personality reports are. + */ +pub fn build(m: &Meta) -> [u8; STAT_LEN] { + let mut out = [0u8; STAT_LEN]; + let split = |ms: u64| (ms / 1000, (ms % 1000) * 1_000_000); + put64(&mut out, 0, m.dev); + put64(&mut out, 8, m.ino); + put64(&mut out, 16, m.nlink); + put32(&mut out, 24, m.mode); + put64(&mut out, 40, m.rdev); + put64(&mut out, 48, m.size); + put64(&mut out, 56, 4096); + put64(&mut out, 64, blocks(m.size)); + /* atime, then mtime and ctime, which the store does not tell apart. */ + for (at, ms) in [(72, m.atime_ms), (88, m.mtime_ms), (104, m.mtime_ms)] { + let (secs, nanos) = split(ms); + put64(&mut out, at, secs); + put64(&mut out, at + 8, nanos); + } + out +} + +fn put32(out: &mut [u8; STAT_LEN], at: usize, value: u32) { + out[at..at + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put64(out: &mut [u8; STAT_LEN], at: usize, value: u64) { + out[at..at + 8].copy_from_slice(&value.to_le_bytes()); +} diff --git a/userland/capsule_linux/src/linux/file/meta/statbuf/mod.rs b/userland/capsule_linux/src/linux/file/meta/statbuf/mod.rs new file mode 100644 index 000000000..d9c746571 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statbuf/mod.rs @@ -0,0 +1,23 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The `struct stat` an x86_64 Linux program expects, filled by hand. */ + +mod build; +mod shape; + +pub use build::build; +pub use shape::{blocks, inode, Meta, STAT_LEN}; diff --git a/userland/capsule_linux/src/linux/file/meta/statbuf/shape.rs b/userland/capsule_linux/src/linux/file/meta/statbuf/shape.rs new file mode 100644 index 000000000..32bb96eeb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statbuf/shape.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What stat says about a file, and a file's number and blocks. */ + +/* Bytes of a `struct stat` on this architecture. */ +pub const STAT_LEN: usize = 144; + +/* What stat says about a file, in the units struct stat takes. */ +#[derive(Clone, Copy, Default)] +pub struct Meta { + pub mode: u32, + pub size: u64, + pub ino: u64, + pub nlink: u64, + pub rdev: u64, + pub dev: u64, + /* + * Wall-clock milliseconds. The store keeps one time; a time the family + * set (held/times.rs) stands in for it until the next write. + */ + pub mtime_ms: u64, + pub atime_ms: u64, +} + +/* + * A file's number, stable for its path and never zero. Distinct numbers are + * how a loader tells two libraries apart; zero for every file made each + * dlopen after the first hand back the library already loaded. + */ +pub fn inode(path: &[u8]) -> u64 { + let fold = path + .iter() + .fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ u64::from(*b)).wrapping_mul(0x100_0000_01b3)); + fold | 1 +} + +/* st_blocks as tmpfs counts them: whole pages, in 512-byte units. */ +pub fn blocks(size: u64) -> u64 { + size.div_ceil(4096) * 8 +} diff --git a/userland/capsule_linux/src/linux/file/meta/statfs.rs b/userland/capsule_linux/src/linux/file/meta/statfs.rs deleted file mode 100644 index a4c9a04a4..000000000 --- a/userland/capsule_linux/src/linux/file/meta/statfs.rs +++ /dev/null @@ -1,53 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! How much room there is, in the shape `statfs` expects. -//! -//! The store's real usage is shared by everything on the machine: read here, -//! it would let a guest watch a sibling write, and it sizes this install. So -//! every guest sees the same plausible figures, and a write that does not fit -//! still fails where it is made, with ENOSPC. - -use crate::linux::abi::errno; -use crate::linux::guest::Guest; - -/// `struct statfs` on x86_64 is 120 bytes. -const STATFS: usize = 120; -/// The store addresses bytes, not blocks, so a block size is a fiction either -/// way. -const BSIZE: u64 = 1024; -/// A 1 GiB volume, half free, on every machine. -const BLOCKS: u64 = 1 << 20; -const FREE: u64 = BLOCKS / 2; - -pub fn statfs(guest: &Guest, out: u64) -> u64 { - let mut buf = [0u8; STATFS]; - put(&mut buf, 0, 0x6E6F6E6F); // f_type, "nono" - put(&mut buf, 8, BSIZE); // f_bsize - put(&mut buf, 16, BLOCKS); // f_blocks - put(&mut buf, 24, FREE); // f_bfree - put(&mut buf, 32, FREE); // f_bavail - put(&mut buf, 56, 255); // f_namelen, the vfs path limit - put(&mut buf, 64, BSIZE); // f_frsize - match guest.write(out, &buf) { - n if n < 0 => errno::fail(errno::EFAULT), - _ => errno::ok(0), - } -} - -fn put(buf: &mut [u8; STATFS], at: usize, v: u64) { - buf[at..at + 8].copy_from_slice(&v.to_le_bytes()); -} diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs new file mode 100644 index 000000000..b74e88223 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs @@ -0,0 +1,43 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* statfs and fstatfs. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{at, walk}; +use super::fill::fill; + +pub fn statfs(guest: &Guest, path: u64, out: u64) -> u64 { + let Some(named) = at::resolve_at(guest, super::super::super::flags::AT_FDCWD, path) else { + return errno::fail(errno::EFAULT); + }; + let full = walk::follow(guest, named, true); + if super::super::stat::look(&full).is_none() { + return errno::fail(errno::ENOENT); + } + fill(guest, &full, out) +} + +pub fn fstatfs(guest: &Guest, fd: u64, out: u64) -> u64 { + match guest.fds.get(fd as usize).filter(|f| f.is_open()) { + Some(f) if matches!(f.kind, Kind::File | Kind::Dir) => fill(guest, &f.path.clone(), out), + /* What has no path is on no mount a guest can name: the root's. */ + Some(_) => fill(guest, b"/", out), + None => errno::fail(errno::EBADF), + } +} diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs new file mode 100644 index 000000000..9db15b701 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* struct statfs, from the mount table and the room on the mount. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::mounts; +use super::super::super::space::{self, BSIZE}; + +const STATFS: usize = 120; + +/* f_flags: ST_VALID, which Linux always sets, and the mount's options. */ +const ST_VALID: u64 = 0x20; + +const OPTS: [(&str, u64); 5] = + [("ro", 1), ("nosuid", 2), ("nodev", 4), ("noexec", 8), ("relatime", 0x1000)]; + +pub(super) fn fill(guest: &Guest, path: &[u8], out: u64) -> u64 { + let (id, _, magic) = mounts::of(path); + let (point, opts) = mounts::MOUNTS.iter().find(|m| m.0 == id).map_or(("/", ""), |m| (m.2, m.4)); + let room = match space::of(point, opts) { + Ok(room) => room, + Err(e) => return errno::fail(e), + }; + let mut buf = [0u8; STATFS]; + put(&mut buf, 0, magic); /* f_type */ + put(&mut buf, 8, BSIZE); /* f_bsize */ + put(&mut buf, 16, room.blocks); /* f_blocks */ + put(&mut buf, 24, room.free); /* f_bfree */ + put(&mut buf, 32, room.free); /* f_bavail */ + put(&mut buf, 40, room.files); /* f_files */ + put(&mut buf, 48, 0); /* f_ffree: none, or no limit when f_files is none */ + put(&mut buf, 56, u64::from(id)); /* f_fsid */ + put(&mut buf, 64, 255); /* f_namelen, the vfs path limit */ + put(&mut buf, 72, BSIZE); /* f_frsize */ + put(&mut buf, 80, flags(opts)); /* f_flags */ + match guest.write(out, &buf) { + n if n < 0 => errno::fail(errno::EFAULT), + _ => errno::ok(0), + } +} + +fn put(buf: &mut [u8; STATFS], at: usize, v: u64) { + buf[at..at + 8].copy_from_slice(&v.to_le_bytes()); +} + +pub(super) fn flags(opts: &str) -> u64 { + let set = |name: &str| opts.split(',').any(|o| o == name); + OPTS.iter().filter(|(name, _)| set(name)).fold(ST_VALID, |f, (_, bit)| f | bit) +} diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs b/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs new file mode 100644 index 000000000..bb61ae8d1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * How much room there is, in the shape `statfs` expects: the mount's type + * and flags from the family's mount table, and its sizes from the family's + * own files (system/space/). + */ + +mod calls; +mod fill; + +pub use calls::{fstatfs, statfs}; diff --git a/userland/capsule_linux/src/linux/file/meta/statx.rs b/userland/capsule_linux/src/linux/file/meta/statx.rs index 0ba7c4558..df5b707d9 100644 --- a/userland/capsule_linux/src/linux/file/meta/statx.rs +++ b/userland/capsule_linux/src/linux/file/meta/statx.rs @@ -14,51 +14,43 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `statx`, which a current libc reaches for before it tries `stat`. +/* `statx`, which a current libc reaches for before it tries `stat`. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::at::resolve_at; -use super::super::resolve::key; -use super::super::store; -use super::statbuf::inode; +use super::stat::meta_at; +use super::statbuf::blocks; -/// `struct statx` is 256 bytes. const STATX: usize = 256; -/// The bits for the fields answered: type, mode, inode and size. Times are -/// not claimed; a real mtime on a shared file would date its install. -const STATX_TYPE: u32 = 0x0001; -const STATX_MODE: u32 = 0x0002; -const STATX_SIZE: u32 = 0x0200; -const STATX_INO: u32 = 0x0100; +/* Type, mode, nlink, uid, gid, times, ino, size and blocks: STATX_BASIC_STATS. */ +const STATX_BASIC_STATS: u32 = 0x07ff; -const S_IFDIR: u16 = 0o040_000; -const S_IFREG: u16 = 0o100_000; - -pub fn statx(guest: &Guest, dirfd: u64, path: u64, out: u64) -> u64 { - let Some(at) = resolve_at(guest, dirfd, path) else { - return errno::fail(errno::EFAULT); - }; - let dev = super::super::dev::device_of(&at); - let Some((size, is_dir, _, readonly)) = - store::stat_full(&key(&at)).ok().or(dev.map(|_| (0, false, 0, false))) - else { - return errno::fail(errno::ENOENT); +pub fn statx(guest: &Guest, dirfd: u64, path: u64, flags: u64, out: u64) -> u64 { + let m = match meta_at(guest, dirfd, path, flags) { + Ok(m) => m, + Err(e) => return errno::fail(e), }; - let mode = if is_dir { S_IFDIR } else { S_IFREG } | if readonly { 0o555 } else { 0o755 }; - let mut buf = [0u8; STATX]; - buf[0..4].copy_from_slice(&(STATX_TYPE | STATX_MODE | STATX_INO | STATX_SIZE).to_le_bytes()); - buf[4..8].copy_from_slice(&4096u32.to_le_bytes()); // stx_blksize - buf[28..30].copy_from_slice(&mode.to_le_bytes()); // stx_mode - buf[32..40].copy_from_slice(&inode(&at).to_le_bytes()); // stx_ino - buf[40..48].copy_from_slice(&size.to_le_bytes()); // stx_size - buf[48..56].copy_from_slice(&size.div_ceil(512).to_le_bytes()); // stx_blocks - if let Some(d) = dev { - super::super::dev_stat::statx_device(&mut buf, d); + let mut put = |at: usize, v: &[u8]| buf[at..at + v.len()].copy_from_slice(v); + put(0, &STATX_BASIC_STATS.to_le_bytes()); + put(4, &4096u32.to_le_bytes()); /* stx_blksize */ + put(16, &(m.nlink as u32).to_le_bytes()); /* stx_nlink */ + put(28, &(m.mode as u16).to_le_bytes()); /* stx_mode */ + put(32, &m.ino.to_le_bytes()); /* stx_ino */ + put(40, &m.size.to_le_bytes()); /* stx_size */ + put(48, &blocks(m.size).to_le_bytes()); /* stx_blocks */ + let split = |ms: u64| ((ms / 1000) as i64, ((ms % 1000) * 1_000_000) as u32); + /* atime, then ctime and mtime, which the store does not tell apart. */ + for (at, ms) in [(64, m.atime_ms), (96, m.mtime_ms), (112, m.mtime_ms)] { + let (secs, nanos) = split(ms); + put(at, &secs.to_le_bytes()); + put(at + 8, &nanos.to_le_bytes()); } + put(128, &((m.rdev >> 8) as u32 & 0xfff).to_le_bytes()); /* stx_rdev_major */ + put(132, &((m.rdev & 0xff) as u32).to_le_bytes()); /* stx_rdev_minor */ + put(140, &(m.dev as u32).to_le_bytes()); /* stx_dev_minor */ match guest.write(out, &buf) { n if n < 0 => errno::fail(errno::EFAULT), _ => errno::ok(0), diff --git a/userland/capsule_linux/src/linux/file/mknod.rs b/userland/capsule_linux/src/linux/file/mknod.rs index 891240eec..8342ba890 100644 --- a/userland/capsule_linux/src/linux/file/mknod.rs +++ b/userland/capsule_linux/src/linux/file/mknod.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `mknodat`: a regular file is an empty file; no device node or fifo is made. +/* `mknodat`: a regular file is an empty file; no device node or fifo is made. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; @@ -27,7 +27,7 @@ use super::resolve::key; const S_IFMT: u64 = 0o170000; const S_IFREG: u64 = 0o100000; -/// A regular file is an empty file; devices and fifos are not made here. +/* A regular file is an empty file; devices and fifos are not made here. */ pub fn mknodat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { if mode & S_IFMT != S_IFREG && mode & S_IFMT != 0 { return refused(b"[LINUX] refused mknod: no device nodes or fifos\n"); @@ -38,11 +38,14 @@ pub fn mknodat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { if stat::look(&at).is_some() { return errno::fail(errno::EEXIST); } - if key(&at).writable().is_err() { + if super::synth::owns(&at) || key(&at).writable().is_err() { return errno::fail(errno::EROFS); } match super::store_write(&key(&at), &[]) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::EIO), + Ok(()) => { + super::modes::set(&at, mode as u32 & 0o7777 & !u32::from(guest.umask)); + errno::ok(0) + } + Err(e) => errno::fail(super::store_err::errno_of(e)), } } diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index c9db777f0..27039bada 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! The filesystem a guest sees. +/* The filesystem a guest sees. */ mod at; +mod calls; mod clamp; pub(super) mod close; mod cstr; @@ -36,7 +37,10 @@ mod eventfd_io; pub mod family; pub mod flags; mod fsync; +mod held; mod link; +mod locks; +mod made; mod memfd; mod memfd_map; mod meta; @@ -55,11 +59,15 @@ mod seek; mod slot; mod store; mod store_name; +mod system; mod timerfd; mod timerfd_read; mod timerfd_spec; +mod walk; mod write; +mod xattrs; +pub use at::join; pub use close::close; pub use cstr::read_cstr; pub use dev_io::{read as dev_read, write as dev_write}; @@ -70,25 +78,28 @@ pub use epoll_arm::rearm; pub use epoll_wait::epoll_wait; pub use eventfd::{bits as event_bits, eventfd2}; pub use eventfd_io::{read as event_read, write as event_write}; -pub use fsync::fsync; +pub use fsync::{fsync, sync, syncfs}; pub use link::{linkat, symlinkat}; -pub use memfd::{ftruncate, is_memfd, memfd_create}; +pub use memfd::{is_memfd, memfd_create}; pub use memfd_map::{mapped_at, set_mapped, staged}; pub use meta::{ - access, chmod, faccessat, fchmod, fchmodat, fstat, look, newfstatat, readlinkat, statfs, statx, + access, chmod, faccessat, fchmod, fchmodat, fstat, fstatfs, look, newfstatat, readlinkat, + statfs, statx, }; pub use mknod::mknodat; pub use open::openat; -pub use owner::{fchown_ids, fchownat, utimensat}; +pub use owner::{fchown_ids, fchownat, utimensat, utimes}; pub use path::read_path; -pub use pread::pread64; +pub use pread::{pread64, preadv, pwrite64, pwritev}; pub use private::{allow_shared_writes, clear as clear_private, prepare as prepare_private}; pub use read::read; -pub use rename::{rename, renameat2}; +pub use rename::renameat2; pub use resolve::{key, visible}; pub use seek::lseek; pub use slot::{install, MAX_FDS}; pub use store::{read as store_read, write as store_write}; pub use timerfd::{timerfd_create, timerfd_gettime, timerfd_settime}; pub use timerfd_read::{bits as timer_bits, read as timerfd_read}; +pub use walk::follow; pub use write::write; +pub use {calls::*, held::*, locks::*, made::*, system::*, xattrs::*}; diff --git a/userland/capsule_linux/src/linux/file/open.rs b/userland/capsule_linux/src/linux/file/open.rs deleted file mode 100644 index 5ef6051a9..000000000 --- a/userland/capsule_linux/src/linux/file/open.rs +++ /dev/null @@ -1,69 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! `openat`. - -use alloc::vec::Vec; - -use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; - -use super::flags::{wants_write, AT_FDCWD, O_CLOEXEC, O_CREAT, O_DIRECTORY}; -use super::{dev, dir, path, regular, resolve, store}; - -pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64) -> u64 { - let Some(name) = path::read_path(guest, path_ptr) else { - return errno::fail(errno::EFAULT); - }; - let base = match base_of(guest, dirfd) { - Ok(base) => base, - Err(e) => return e, - }; - let full = guest.links.follow(resolve::visible(&base, &name), true); - let got = match store::stat(&resolve::key(&full)).ok() { - _ if dev::device_of(&full).is_some() => dev::open_path(guest, &full, flags), - Some((_, true)) => dir::open(guest, full), - Some((_, false)) if flags & O_DIRECTORY != 0 => errno::fail(errno::ENOTDIR), - Some((size, false)) => regular::open(guest, full, size, flags), - None if flags & O_CREAT != 0 && wants_write(flags) => regular::create(guest, full), - None => errno::fail(errno::ENOENT), - }; - mark(guest, got, flags & O_CLOEXEC != 0); - got -} - -/// O_CLOEXEC is a property of the descriptor, not of the open, so it is set -/// once the number is known rather than threaded through every one of the -/// paths above. -fn mark(guest: &mut Guest, got: u64, on: bool) { - if let Some(slot) = errno::slot(got).filter(|_| on) { - if let Some(fd) = guest.fds.get_mut(slot) { - fd.cloexec = true; - } - } -} - -/// AT_FDCWD or a dirfd the guest itself opened. No other dirfd resolves. -fn base_of(guest: &Guest, dirfd: u64) -> Result, u64> { - if dirfd == AT_FDCWD { - return Ok(guest.cwd.clone()); - } - match guest.fds.get(dirfd as usize) { - Some(fd) if fd.kind == Kind::Dir => Ok(fd.path.clone()), - Some(_) => Err(errno::fail(errno::ENOTDIR)), - None => Err(errno::fail(errno::EBADF)), - } -} diff --git a/userland/capsule_linux/src/linux/file/open/mark.rs b/userland/capsule_linux/src/linux/file/open/mark.rs new file mode 100644 index 000000000..7d8e8eb44 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/mark.rs @@ -0,0 +1,36 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The mark a descriptor carries of how it was opened, and the directory + * a relative name starts from. + */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/* + * O_CLOEXEC is a property of the descriptor, not of the open, so it is set + * once the number is known rather than threaded through every one of the + * paths above. + */ +pub(crate) fn mark(guest: &mut Guest, got: u64, on: bool) { + if let Some(slot) = errno::slot(got).filter(|_| on) { + if let Some(fd) = guest.fds.get_mut(slot) { + fd.cloexec = true; + } + } +} diff --git a/userland/capsule_linux/src/linux/file/open/mod.rs b/userland/capsule_linux/src/linux/file/open/mod.rs new file mode 100644 index 000000000..a3081c7cf --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/mod.rs @@ -0,0 +1,25 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* `openat`. */ + +mod mark; +mod named; +mod openat; + +pub(super) use mark::mark; +pub use named::open_named; +pub use openat::openat; diff --git a/userland/capsule_linux/src/linux/file/open/named.rs b/userland/capsule_linux/src/linux/file/open/named.rs new file mode 100644 index 000000000..07e47b5a1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/named.rs @@ -0,0 +1,54 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Opening a name once it is walked: links, made trees, the store. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::flags::{writes, O_CREAT, O_DIRECTORY, O_EXCL, O_NOFOLLOW}; +use super::super::{cache, dev, dir, regular, resolve, store, synth_ops, walk}; + +/* Open the path the guest named, once made absolute. */ +pub fn open_named(guest: &mut Guest, named: Vec, flags: u64, mode: u64) -> u64 { + /* O_NOFOLLOW refuses a link in the last place, with ELOOP, as Linux does. */ + if flags & O_NOFOLLOW != 0 && super::super::meta::is_link(guest, &named) { + return errno::fail(errno::ELOOP); + } + let full = walk::follow(guest, named, true); + /* /dev/null and its kin are descriptors this capsule answers itself. */ + if dev::device_of(&full).is_some() { + return dev::open_path(guest, &full, flags); + } + if let Some(got) = synth_ops::open(guest, &full, flags) { + return got; + } + let found = match cache::size(&full) { + Some(size) => Some((size, false)), + None => store::stat(&resolve::key(&full)).ok(), + }; + match found { + Some(_) if flags & O_CREAT != 0 && flags & O_EXCL != 0 => errno::fail(errno::EEXIST), + Some((_, true)) if writes(flags) => errno::fail(errno::EISDIR), + Some((_, true)) => dir::open(guest, full), + Some((_, false)) if flags & O_DIRECTORY != 0 => errno::fail(errno::ENOTDIR), + Some((size, false)) => regular::open(guest, full, size, flags), + None if flags & O_CREAT != 0 => regular::create(guest, full, flags, mode), + None => errno::fail(errno::ENOENT), + } +} diff --git a/userland/capsule_linux/src/linux/file/open/openat.rs b/userland/capsule_linux/src/linux/file/open/openat.rs new file mode 100644 index 000000000..ce57934e5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/openat.rs @@ -0,0 +1,38 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* openat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::flags::O_CLOEXEC; +use super::super::path; +use super::mark::mark; +use super::named::open_named; + +pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64, mode: u64) -> u64 { + let Some(name) = path::read_path(guest, path_ptr) else { + return errno::fail(errno::EFAULT); + }; + let named = match super::super::at::named_at(guest, dirfd, &name) { + Ok(named) => named, + Err(e) => return errno::fail(e), + }; + let got = open_named(guest, named, flags, mode); + mark(guest, got, flags & O_CLOEXEC != 0); + got +} diff --git a/userland/capsule_linux/src/linux/file/owner.rs b/userland/capsule_linux/src/linux/file/owner/chown.rs similarity index 55% rename from userland/capsule_linux/src/linux/file/owner.rs rename to userland/capsule_linux/src/linux/file/owner/chown.rs index e579fbcaa..eba0313ee 100644 --- a/userland/capsule_linux/src/linux/file/owner.rs +++ b/userland/capsule_linux/src/linux/file/owner/chown.rs @@ -14,36 +14,32 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Owners and times: what the store does not record. -//! -//! The store keeps bytes under names and nothing else, so every file reports -//! uid 0, gid 0 and time zero, and the guest runs as uid 0. A change that -//! would leave that true is answered; one that would need the store to keep -//! something it cannot is refused by name, never reported done. +/* chown and its forms: every file is root's, and stays so. */ use nonos_libc::mk_debug; use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::at::resolve_at; -use super::meta::stat; +use super::super::at::resolve_at; +use super::super::meta::stat; const KEEP: u32 = u32::MAX; -const UTIME_OMIT: u64 = (1 << 30) - 2; -/// `fchownat`; `chown`, `lchown` and `fchown` are this at other bases. +pub(super) const AT_SYMLINK_NOFOLLOW: u64 = 0x100; + +/* `fchownat`; `chown`, `lchown` and `fchown` are this at other bases. */ pub fn fchownat(guest: &Guest, dirfd: u64, path: u64, uid: u64, gid: u64) -> u64 { let Some(at) = resolve_at(guest, dirfd, path) else { return errno::fail(errno::EFAULT); }; - if stat::look(&guest.links.follow(at, true)).is_none() { + if stat::look(&super::super::walk::follow(guest, at, true)).is_none() { return errno::fail(errno::ENOENT); } fchown_ids(uid, gid) } -/// `fchown` on an open descriptor: only the owner every file already has. +/* `fchown` on an open descriptor: only the owner every file already has. */ pub fn fchown_ids(uid: u64, gid: u64) -> u64 { match [uid as u32, gid as u32].iter().all(|id| *id == 0 || *id == KEEP) { true => errno::ok(0), @@ -51,17 +47,7 @@ pub fn fchown_ids(uid: u64, gid: u64) -> u64 { } } -/// Times are not kept, so only a call that changes neither is answered. -pub fn utimensat(guest: &Guest, times: u64) -> u64 { - let omitted = - |at: u64| guest.read(at + 8, 8).map(|n| u64::from_le_bytes(n.try_into().unwrap_or([0; 8]))); - if times != 0 && omitted(times) == Some(UTIME_OMIT) && omitted(times + 16) == Some(UTIME_OMIT) { - return errno::ok(0); - } - refused(b"[LINUX] refused utimensat: times are not recorded\n") -} - -pub(super) fn refused(line: &[u8]) -> u64 { +pub(crate) fn refused(line: &[u8]) -> u64 { let _ = mk_debug(line.as_ptr(), line.len()); errno::fail(errno::EPERM) } diff --git a/userland/capsule_linux/src/linux/file/owner/mod.rs b/userland/capsule_linux/src/linux/file/owner/mod.rs new file mode 100644 index 000000000..52553709b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/owner/mod.rs @@ -0,0 +1,32 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Owners and times: what the store does not record. + * + * The store keeps bytes under names and nothing else, so every file reports + * uid 0, gid 0 and time zero, and the guest runs as uid 0. A change that + * would leave that true is answered; one that would need the store to keep + * something it cannot is refused by name, never reported done. + */ + +mod chown; +mod stamp; +mod times; + +pub(super) use chown::refused; +pub use chown::{fchown_ids, fchownat}; +pub use times::{utimensat, utimes}; diff --git a/userland/capsule_linux/src/linux/file/owner/stamp.rs b/userland/capsule_linux/src/linux/file/owner/stamp.rs new file mode 100644 index 000000000..9ff2408e2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/owner/stamp.rs @@ -0,0 +1,69 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A time utimensat names, set on the family's record of the file. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::at::resolve_at; +use super::super::resolve::key; +use super::super::times; +use super::chown::AT_SYMLINK_NOFOLLOW; + +pub(super) fn stamp( + guest: &Guest, + dirfd: u64, + path: u64, + flags: u64, + a: Option, + m: Option, +) -> u64 { + let full = match path { + 0 => { + match guest.fds.get(super::super::flags::dirfd(dirfd) as usize).filter(|f| f.is_open()) + { + Some(f) => f.path.clone(), + None => return errno::fail(errno::EBADF), + } + } + p => match resolve_at(guest, dirfd, p) { + Some(named) => { + super::super::walk::follow(guest, named, flags & AT_SYMLINK_NOFOLLOW == 0) + } + None => return errno::fail(errno::EFAULT), + }, + }; + let now = match super::super::meta::meta_of(guest, full.clone(), true) { + Ok(now) => now, + Err(e) => return errno::fail(e), + }; + if super::super::synth::owns(&full) + || (!super::super::cache::held(&full) && key(&full).writable().is_err()) + { + return errno::fail(errno::EROFS); + } + let written_ms = super::super::cache::mtime(&full) + .or_else(|| super::super::store::stat_full(&key(&full)).ok().map(|s| s.2)) + .unwrap_or(0); + let set = times::Set { + atime_ms: a.unwrap_or(now.atime_ms), + mtime_ms: m.unwrap_or(now.mtime_ms), + written_ms, + }; + times::set(&full, set); + errno::ok(0) +} diff --git a/userland/capsule_linux/src/linux/file/owner/times.rs b/userland/capsule_linux/src/linux/file/owner/times.rs new file mode 100644 index 000000000..24929967b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/owner/times.rs @@ -0,0 +1,68 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* utimensat and utimes: the times the family sets on its own files. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::flags::AT_FDCWD; +use super::stamp::stamp; + +const UTIME_NOW: u64 = (1 << 30) - 1; + +const UTIME_OMIT: u64 = (1 << 30) - 2; + +/* + * Times are not kept, so only a call that changes neither is answered. + * utimensat and futimens (a null path names `dirfd` itself). Each time is + * a timespec, UTIME_NOW or UTIME_OMIT; a null array means now for both. + */ +pub fn utimensat(guest: &Guest, dirfd: u64, path: u64, times: u64, flags: u64) -> u64 { + let spec = |at: u64| -> Result, i64> { + let raw = guest.read(at, 16).ok_or(errno::EFAULT)?; + let secs = u64::from_le_bytes(raw[..8].try_into().unwrap_or([0; 8])); + let nanos = u64::from_le_bytes(raw[8..].try_into().unwrap_or([0; 8])); + match nanos { + UTIME_OMIT => Ok(None), + UTIME_NOW => Ok(Some(super::super::meta::now_ms())), + n if n >= 1_000_000_000 => Err(errno::EINVAL), + n => Ok(Some(secs.saturating_mul(1000) + n / 1_000_000)), + } + }; + let pair = match times { + 0 => Ok((Some(super::super::meta::now_ms()), Some(super::super::meta::now_ms()))), + t => spec(t).and_then(|a| spec(t + 16).map(|m| (a, m))), + }; + match pair { + Ok((a, m)) => stamp(guest, dirfd, path, flags, a, m), + Err(e) => errno::fail(e), + } +} + +/* utimes and utime: seconds and microseconds, or whole seconds. */ +pub fn utimes(guest: &Guest, path: u64, times: u64, micros: bool) -> u64 { + if times == 0 { + return utimensat(guest, AT_FDCWD, path, 0, 0); + } + let width = if micros { 16 } else { 8 }; + let Some(raw) = guest.read(times, width * 2) else { + return errno::fail(errno::EFAULT); + }; + let word = |at: usize| u64::from_le_bytes(raw[at..at + 8].try_into().unwrap_or([0; 8])); + let ms = |at: usize| word(at) * 1000 + if micros { word(at + 8) / 1000 } else { 0 }; + stamp(guest, AT_FDCWD, path, 0, Some(ms(0)), Some(ms(width))) +} diff --git a/userland/capsule_linux/src/linux/file/pread.rs b/userland/capsule_linux/src/linux/file/pread.rs deleted file mode 100644 index 524add2b9..000000000 --- a/userland/capsule_linux/src/linux/file/pread.rs +++ /dev/null @@ -1,41 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -//! `pread64`: a read at an offset that leaves the descriptor's own -//! position alone, which is what a program doing its own seeking relies -//! on and the reason it uses this call instead of seek and read. - -use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; - -use super::read::read; - -pub fn pread64(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> u64 { - let saved = match guest.fds.get(fd as usize) { - Some(entry) if entry.kind == Kind::File => entry.offset, - Some(_) => return errno::fail(errno::ESPIPE), - None => return errno::fail(errno::EBADF), - }; - if let Some(entry) = guest.fds.get_mut(fd as usize) { - entry.offset = at; - } - let out = read(guest, fd, buf, len); - if let Some(entry) = guest.fds.get_mut(fd as usize) { - entry.offset = saved; - } - out -} diff --git a/userland/capsule_linux/src/linux/file/pread/mod.rs b/userland/capsule_linux/src/linux/file/pread/mod.rs new file mode 100644 index 000000000..cc0a66b51 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The positional forms: pread64, pwrite64, preadv, pwritev, preadv2 and + * pwritev2. Each reads or writes at the offset it is given and leaves the + * descriptor's own offset where it was; a pipe, a socket or a console has + * no offset, which Linux calls ESPIPE. + */ + +mod place; +mod plain; +mod sync; +mod vector; + +pub use plain::{pread64, pwrite64}; +pub use sync::{preadv, pwritev}; diff --git a/userland/capsule_linux/src/linux/file/pread/place.rs b/userland/capsule_linux/src/linux/file/pread/place.rs new file mode 100644 index 000000000..5bce6a0f9 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/place.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Where a v2 call reads or writes, and where the file ends. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::plain::seekable; + +/* + * Where a v2 call reads or writes: at `at`, or at the descriptor's own + * offset (u64::MAX) for -1. With RWF_APPEND a write goes at the end, and + * at -1 the descriptor's offset moves there first and on past what is + * written, as Linux moves it. + */ +pub(super) fn place(guest: &mut Guest, fd: u64, at: u64, append: bool) -> Result { + match (append, at as i64) { + (true, n) => { + seekable(guest, fd)?; + let end = end_of(guest, fd); + if n != -1 { + return Ok(end); + } + super::super::desc::set_pos(&mut guest.fds[fd as usize], end); + Ok(u64::MAX) + } + (false, -1) => Ok(u64::MAX), + (false, n) if n < 0 => Err(errno::fail(errno::EINVAL)), + (false, _) => Ok(at), + } +} + +/* Where the file ends now, the family's copy's end if it holds one. */ +fn end_of(guest: &Guest, fd: u64) -> u64 { + let f = &guest.fds[fd as usize]; + super::super::cache::size(&f.path).unwrap_or(f.size) +} diff --git a/userland/capsule_linux/src/linux/file/pread/plain.rs b/userland/capsule_linux/src/linux/file/pread/plain.rs new file mode 100644 index 000000000..7c4d00f63 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/plain.rs @@ -0,0 +1,68 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* pread64 and pwrite64, and the offset they use and give back. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +pub fn pread64(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> u64 { + at_offset(guest, fd, at, |g| super::super::read::read(g, fd, buf, len)) +} + +pub fn pwrite64(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> u64 { + if (at as i64) < 0 { + return errno::fail(errno::EINVAL); + } + if let Err(e) = seekable(guest, fd) { + return e; + } + match super::super::write::whole(guest, fd, buf, len, at) { + Ok((n, _)) => errno::ok(n), + Err(e) => errno::fail(e), + } +} + +pub(super) fn seekable(guest: &Guest, fd: u64) -> Result { + match guest.fds.get(fd as usize).filter(|f| f.is_open()) { + Some(f) if f.kind == Kind::File => Ok(super::super::desc::pos(f)), + Some(f) if f.kind == Kind::Dir => Err(errno::fail(errno::EISDIR)), + Some(_) => Err(errno::fail(errno::ESPIPE)), + None => Err(errno::fail(errno::EBADF)), + } +} + +/* Run `go` with the descriptor's offset set to `at`, then put it back. */ +pub(super) fn at_offset( + guest: &mut Guest, + fd: u64, + at: u64, + go: impl FnOnce(&mut Guest) -> u64, +) -> u64 { + if (at as i64) < 0 { + return errno::fail(errno::EINVAL); + } + let saved = match seekable(guest, fd) { + Ok(saved) => saved, + Err(e) => return e, + }; + super::super::desc::set_pos(&mut guest.fds[fd as usize], at); + let out = go(guest); + if let Some(entry) = guest.fds.get_mut(fd as usize) { + super::super::desc::set_pos(entry, saved); + } + out +} diff --git a/userland/capsule_linux/src/linux/file/pread/sync.rs b/userland/capsule_linux/src/linux/file/pread/sync.rs new file mode 100644 index 000000000..d634eb81b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/sync.rs @@ -0,0 +1,38 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* preadv and pwritev, and a write put in the store as RWF_DSYNC asks. */ + +use crate::linux::call; +use crate::linux::guest::{Guest, Kind}; + +use super::vector::vectored; + +pub fn preadv(guest: &mut Guest, fd: u64, iov: u64, count: u64, at: u64, flags: u64) -> u64 { + vectored(guest, fd, at, flags, false, |g| call::readv(g, fd, iov, count)) +} + +pub fn pwritev(guest: &mut Guest, fd: u64, iov: u64, count: u64, at: u64, flags: u64) -> u64 { + vectored(guest, fd, at, flags, true, |g| call::writev(g, fd, iov, count)) +} + +pub(super) fn synced(guest: &Guest, fd: u64) -> Result<(), i64> { + let f = &guest.fds[fd as usize]; + match f.kind == Kind::File && !super::super::synth::owns(&f.path) { + true => super::super::cache::flush(&f.path, true), + false => Ok(()), + } +} diff --git a/userland/capsule_linux/src/linux/file/pread/vector.rs b/userland/capsule_linux/src/linux/file/pread/vector.rs new file mode 100644 index 000000000..17e258e83 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/vector.rs @@ -0,0 +1,67 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* preadv2 and pwritev2 with their RWF_ flags. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::place::place; +use super::plain::at_offset; +use super::sync::synced; + +/* The RWF_ flags Linux 6.1 knows: HIPRI, DSYNC, SYNC, NOWAIT and APPEND. */ +const RWF_DSYNC: u64 = 0x02; + +const RWF_SYNC: u64 = 0x04; + +const RWF_APPEND: u64 = 0x10; + +const RWF_KNOWN: u64 = 0x1f; + +/* + * The v2 forms: an offset of -1 means the descriptor's own, which then + * moves. HIPRI asks to poll for completion and NOWAIT not to block, and a + * read or write here never blocks, so both hold as they are. DSYNC and + * SYNC put a write in the store before answering, as fsync would. APPEND + * writes at the end whatever the offset. Any other flag is EOPNOTSUPP. + */ +pub(super) fn vectored( + guest: &mut Guest, + fd: u64, + at: u64, + flags: u64, + write: bool, + go: impl FnOnce(&mut Guest) -> u64, +) -> u64 { + if flags & !RWF_KNOWN != 0 { + return errno::fail(errno::EOPNOTSUPP); + } + let at = match place(guest, fd, at, write && flags & RWF_APPEND != 0) { + Ok(at) => at, + Err(e) => return e, + }; + let got = match at { + u64::MAX => go(guest), + _ => at_offset(guest, fd, at, go), + }; + if write && flags & (RWF_DSYNC | RWF_SYNC) != 0 && (got as i64) >= 0 { + if let Err(e) = synced(guest, fd) { + return errno::fail(e); + } + } + got +} diff --git a/userland/capsule_linux/src/linux/file/read.rs b/userland/capsule_linux/src/linux/file/read.rs index ebeb50327..d3b4b24cc 100644 --- a/userland/capsule_linux/src/linux/file/read.rs +++ b/userland/capsule_linux/src/linux/file/read.rs @@ -14,23 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! Reading from a file a guest has open. -//! -//! The descriptor is inspected, released, and only then are the bytes put -//! into the guest: writing into the guest needs the guest itself, and the -//! descriptor is a part of it. +/* `read` on a file: the bytes at the descriptor's offset, which moves on. */ use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; +use crate::linux::guest::Guest; -/// One transfer, matching the kernel's own peer-copy ceiling. -const MAX_IO: u64 = 1 << 20; +use super::rw::read_at; pub fn read(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { - let bytes = match take(guest, fd, len) { + let at = guest.fds.get(fd as usize).map_or(0, super::desc::pos); + let bytes = match read_at(guest, fd, at, len as usize) { Ok(bytes) => bytes, - Err(e) => return e, + Err(e) => return errno::fail(e), }; if bytes.is_empty() { return errno::ok(0); @@ -39,26 +34,7 @@ pub fn read(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { return errno::fail(errno::EFAULT); } if let Some(entry) = guest.fds.get_mut(fd as usize) { - entry.offset += bytes.len() as u64; + super::desc::set_pos(entry, at + bytes.len() as u64); } errno::ok(bytes.len() as u64) } - -fn take(guest: &mut Guest, fd: u64, len: u64) -> Result, u64> { - let Some(entry) = guest.fds.get_mut(fd as usize) else { - return Err(errno::fail(errno::EBADF)); - }; - if entry.kind != Kind::File { - return Err(errno::fail(errno::EBADF)); - } - if len == 0 || entry.offset >= entry.size { - return Ok(alloc::vec::Vec::new()); - } - let want = len.min(MAX_IO).min(entry.size - entry.offset); - let at = entry.offset; - // Opened to write only: Linux answers EBADF, not end of file. - let Some(stream) = entry.stream.as_mut() else { - return Err(errno::fail(errno::EBADF)); - }; - stream.read_window(at, want as u32).map_err(|_| errno::fail(errno::EIO)) -} diff --git a/userland/capsule_linux/src/linux/file/regular/create.rs b/userland/capsule_linux/src/linux/file/regular/create.rs new file mode 100644 index 000000000..a5902177d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/regular/create.rs @@ -0,0 +1,42 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* A file made by O_CREAT, with the mode it was asked for. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::super::flags::writes; +use super::super::{cache, modes, resolve}; +use super::open::install; + +/* + * Linux makes the file at open, so stat sees it before anything is written; + * here it is held empty in the family's copy until close puts it in the store. + */ +pub fn create(guest: &mut Guest, path: Vec, flags: u64, mode: u64) -> u64 { + if resolve::key(&path).writable().is_err() { + return errno::fail(errno::EROFS); + } + if let Err(e) = cache::hold(&path, false) { + return errno::fail(e); + } + /* The mode it is made with, less the umask, as open(2) says. */ + modes::set(&path, mode as u32 & 0o7777 & !u32::from(guest.umask)); + install(guest, Fd::file(path, 0, None, writes(flags)), flags) +} diff --git a/userland/capsule_linux/src/linux/file/regular/mod.rs b/userland/capsule_linux/src/linux/file/regular/mod.rs new file mode 100644 index 000000000..510621d74 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/regular/mod.rs @@ -0,0 +1,29 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Opening a regular file, and creating one that is not there yet. + * + * A write-only or truncating open needs no stream from the store: nothing + * will be read from what is there. A write goes to the family's copy of + * the file (held/cache/), which is taken when the first write needs it. + */ + +mod create; +mod open; + +pub use create::create; +pub use open::open; diff --git a/userland/capsule_linux/src/linux/file/regular.rs b/userland/capsule_linux/src/linux/file/regular/open.rs similarity index 53% rename from userland/capsule_linux/src/linux/file/regular.rs rename to userland/capsule_linux/src/linux/file/regular/open.rs index 3e0f091af..b8eca6d17 100644 --- a/userland/capsule_linux/src/linux/file/regular.rs +++ b/userland/capsule_linux/src/linux/file/regular/open.rs @@ -14,38 +14,40 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Opening a regular file, and creating one that is not there yet. +/* Opening a regular file of the store or of the family's copies. */ use alloc::vec::Vec; use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest}; -use super::flags::{wants_read, wants_write, O_TRUNC}; -use super::{resolve, slot, store}; +use super::super::flags::{wants_read, writes, O_APPEND, O_TRUNC}; +use super::super::{cache, desc, resolve, slot, store}; pub fn open(guest: &mut Guest, path: Vec, size: u64, flags: u64) -> u64 { - // No server handle for write-only or O_TRUNC: nothing will read it. - let truncating = flags & O_TRUNC != 0; - let stream = if wants_read(flags) && !truncating { - match store::open(&resolve::key(&path)) { + let writing = writes(flags); + if writing && resolve::key(&path).writable().is_err() { + return errno::fail(errno::EROFS); + } + let truncating = flags & O_TRUNC != 0 && writing; + let stream = match wants_read(flags) && !cache::held(&path) { + true => match store::open(&resolve::key(&path)) { Ok(s) => Some(s), Err(_) => return errno::fail(errno::EACCES), - } - } else { - None + }, + false => None, }; - let size = if truncating { 0 } else { size }; - let fd = Fd::file(path, size, stream, wants_write(flags)); - match slot::install(guest, fd) { - Some(n) => errno::ok(n), - None => errno::fail(errno::EMFILE), + if truncating { + if let Err(e) = cache::hold(&path, false).and_then(|()| cache::resize(&path, 0)) { + return errno::fail(e); + } } + let size = if truncating { 0 } else { cache::size(&path).unwrap_or(size) }; + install(guest, Fd::file(path, size, stream, writing), flags) } -/// Nothing hits the store until close, so a create-then-die leaves no file. -pub fn create(guest: &mut Guest, path: Vec) -> u64 { - let fd = Fd::file(path, 0, None, true); +pub(super) fn install(guest: &mut Guest, mut fd: Fd, flags: u64) -> u64 { + fd.handle = desc::fresh(flags & O_APPEND != 0, wants_read(flags)); match slot::install(guest, fd) { Some(n) => errno::ok(n), None => errno::fail(errno::EMFILE), diff --git a/userland/capsule_linux/src/linux/file/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs index f97e27994..14b508375 100644 --- a/userland/capsule_linux/src/linux/file/rename.rs +++ b/userland/capsule_linux/src/linux/file/rename.rs @@ -14,33 +14,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Moving a name. +/* Moving a name. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; use super::at::resolve_at; +use super::meta::look; use super::resolve::key; -use super::store_name; - -pub fn rename(guest: &Guest, old: u64, new: u64) -> u64 { - let (Some(from), Some(to)) = ( - resolve_at(guest, super::flags::AT_FDCWD, old), - resolve_at(guest, super::flags::AT_FDCWD, new), - ) else { - return errno::fail(errno::EFAULT); - }; - match store_name::rename(&key(&from), &key(&to)) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::ENOENT), - } -} +use super::{cache, modes, store_name, synth}; const RENAME_NOREPLACE: u64 = 1; -/// `renameat` and `renameat2`. NOREPLACE refuses an existing target; -/// EXCHANGE would need two names swapped at once, which the store cannot -/// do, so it is refused rather than done as two renames that could half-fail. pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, flags: u64) -> u64 { if flags & !RENAME_NOREPLACE != 0 { return errno::fail(errno::EINVAL); @@ -49,11 +34,39 @@ pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, fl else { return errno::fail(errno::EFAULT); }; - if flags & RENAME_NOREPLACE != 0 && super::meta::stat::look(&to).is_some() { + let there = look(&to).is_some() || guest.links.target(&to).is_some(); + if flags & RENAME_NOREPLACE != 0 && there { return errno::fail(errno::EEXIST); } + if [&from, &to].iter().any(|p| synth::owns(p) || key(p).writable().is_err()) { + return errno::fail(errno::EROFS); + } + if from == to { + return errno::ok(0); + } + if guest.links.rename(&from, to.clone()) { + return errno::ok(0); + } + if look(&from).is_none() { + return errno::fail(errno::ENOENT); + } + /* A file in the way is replaced, as rename(2) replaces it. */ + if let Some((_, false)) = look(&to) { + cache::forget(&to); + let _ = store_name::unlink(&key(&to)); + } + /* The store renames what it has: the family's copy goes in first. */ + if let Err(e) = cache::flush(&from, true) { + return errno::fail(e); + } match store_name::rename(&key(&from), &key(&to)) { - Ok(()) => errno::ok(0), - Err(_) => errno::fail(errno::ENOENT), + Ok(()) => { + cache::renamed(&from, &to); + modes::renamed(&from, &to); + super::times::renamed(&from, &to); + super::xattr_table::renamed(&from, &to); + errno::ok(0) + } + Err(e) => errno::fail(super::store_err::errno_of(e)), } } diff --git a/userland/capsule_linux/src/linux/file/seek.rs b/userland/capsule_linux/src/linux/file/seek.rs index 66002939f..23998cbc6 100644 --- a/userland/capsule_linux/src/linux/file/seek.rs +++ b/userland/capsule_linux/src/linux/file/seek.rs @@ -14,8 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `lseek`. The position is this capsule's, not the server's: a read takes -//! a window at an offset, so the descriptor's offset is the whole of it. +/* + * `lseek`. The position is this capsule's, not the server's: a read takes + * a window at an offset, so the descriptor's offset is the whole of it. + */ use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; @@ -33,19 +35,19 @@ pub fn lseek(guest: &mut Guest, fd: u64, offset: u64, whence: u64) -> u64 { return errno::ok(0); } if entry.kind != Kind::File { - // A pipe or a console has no position, which Linux calls ESPIPE. + /* A pipe or a console has no position, which Linux calls ESPIPE. */ return errno::fail(errno::ESPIPE); } let delta = offset as i64; let base = match whence { SEEK_SET => 0, - SEEK_CUR => entry.offset as i64, + SEEK_CUR => super::desc::pos(entry) as i64, SEEK_END => entry.size as i64, _ => return errno::fail(errno::EINVAL), }; let Some(at) = base.checked_add(delta).filter(|v| *v >= 0) else { return errno::fail(errno::EINVAL); }; - entry.offset = at as u64; - errno::ok(entry.offset) + super::desc::set_pos(entry, at as u64); + errno::ok(at as u64) } diff --git a/userland/capsule_linux/src/linux/file/store_name.rs b/userland/capsule_linux/src/linux/file/store_name.rs index bbca4c43a..bd8b0fab3 100644 --- a/userland/capsule_linux/src/linux/file/store_name.rs +++ b/userland/capsule_linux/src/linux/file/store_name.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Store operations that change the namespace rather than content. +/* Store operations that change the namespace rather than content. */ use nonos_app_skeleton::clients::vfs; use nonos_libc::mk_getpid; @@ -43,8 +43,3 @@ pub fn rename(from: &Key, to: &Key) -> Result<(), Fail> { to.writable()?; vfs::rename(mk_getpid(), from.as_bytes(), to.as_bytes()) } - -pub fn chmod(at: &Key, mode: u16) -> Result<(), Fail> { - at.writable()?; - vfs::chmod(mk_getpid(), at.as_bytes(), mode) -} diff --git a/userland/capsule_linux/src/linux/file/system/cpu/ended.rs b/userland/capsule_linux/src/linux/file/system/cpu/ended.rs new file mode 100644 index 000000000..03d67e8d5 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/cpu/ended.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What each process that has exited used, kept for its parent. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use super::usage::Usage; + +/* + * What each process that has exited used, itself and the children it + * waited for, kept for its parent's RUSAGE_CHILDREN. + */ +pub(super) struct Ended(pub(super) RefCell>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Ended {} + +static ENDED: Ended = Ended(RefCell::new(Vec::new())); + +/* `pid`, child of `parent`, is exiting having used `used`. */ +pub fn ended(pid: u32, parent: u32, used: Usage) { + let mut all = ENDED.0.borrow_mut(); + all.retain(|(p, _, _)| *p != pid); + all.push((pid, parent, used)); +} + +/* + * What `pid`'s children used, those it has waited for, as Linux counts + * RUSAGE_CHILDREN: `waited` says which. + */ +pub fn children(pid: u32, waited: impl Fn(u32) -> bool) -> Usage { + let all = ENDED.0.borrow(); + let mut sum = Usage::default(); + for (_, _, u) in all.iter().filter(|(c, p, _)| *p == pid && waited(*c)) { + sum.user += u.user; + sum.system += u.system; + sum.faults += u.faults; + sum.switches += u.switches; + sum.resident_kb = sum.resident_kb.max(u.resident_kb); + } + sum +} + +/* Every thread of the processes in `procs`, by kernel pid. */ +pub fn threads_of(procs: &[&crate::linux::file::Proc]) -> Vec { + procs.iter().flat_map(|p| p.tids.iter().map(|(_, k)| *k)).collect() +} diff --git a/userland/capsule_linux/src/linux/file/system/cpu/mod.rs b/userland/capsule_linux/src/linux/file/system/cpu/mod.rs new file mode 100644 index 000000000..f244f3508 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/cpu/mod.rs @@ -0,0 +1,31 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What the kernel measured of the family's own threads: CPU ticks split + * into user and kernel, page faults, context switches and resident pages. + * + * The kernel shows a supervisor these for the guests it hosts and for no + * one else's, so every figure is one of the family's own. Only the rows + * for the pids asked about are kept; the rest of the machine's table is + * read past and dropped, and nothing of it reaches a guest. + */ + +mod ended; +mod usage; + +pub use ended::{children, ended, threads_of}; +pub use usage::{usage, Usage}; diff --git a/userland/capsule_linux/src/linux/file/system/cpu/usage.rs b/userland/capsule_linux/src/linux/file/system/cpu/usage.rs new file mode 100644 index 000000000..115d2698e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/cpu/usage.rs @@ -0,0 +1,67 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The kernel's counts for the threads asked about, from its table. */ + +use alloc::vec; +use core::mem::size_of; +use nonos_libc::{mk_proc_stat, ProcStatEntry, ProcStatHeader}; + +/* + * Sums over the threads asked about. Ticks are the kernel's 100 Hz ticks, + * which is also the guest's clock tick (AT_CLKTCK, declared::HZ). + */ +#[derive(Clone, Copy, Default)] +pub struct Usage { + pub user: u64, + pub system: u64, + pub faults: u64, + pub switches: u64, + pub resident_kb: u64, +} + +const HEADER: usize = size_of::(); + +const ENTRY: usize = size_of::(); + +pub fn usage(pids: &[u32]) -> Usage { + let mut sum = Usage::default(); + let count = mk_proc_stat(core::ptr::null_mut(), 0); + if count <= 0 || pids.is_empty() { + return sum; + } + /* Room for a few more, in case the machine starts one between the calls. */ + let room = count as usize + 8; + let mut buf = vec![0u8; HEADER + room * ENTRY]; + let written = mk_proc_stat(buf.as_mut_ptr(), room as u32); + for i in 0..written.max(0) as usize { + let at = HEADER + i * ENTRY; + let Some(raw) = buf.get(at..at + ENTRY) else { break }; + /* + * SAFETY: the slice holds ENTRY bytes, and every bit pattern is a + * valid ProcStatEntry, which is plain integers and bytes. + */ + let e: ProcStatEntry = unsafe { core::ptr::read_unaligned(raw.as_ptr().cast()) }; + if pids.contains(&e.pid) { + sum.user += e.user_ticks; + sum.system += e.run_ticks.saturating_sub(e.user_ticks); + sum.faults += e.faults; + sum.switches += e.switches; + sum.resident_kb += e.mem_kb; + } + } + sum +} diff --git a/userland/capsule_linux/src/linux/file/system/declared/mod.rs b/userland/capsule_linux/src/linux/file/system/declared/mod.rs new file mode 100644 index 000000000..2cb0fdb5a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/declared/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Everything a guest can learn about the system it runs on, in one place. + * + * uname, /proc, /sys and sysinfo all answer from these values and from + * nothing else. Each is what NONOS declares to a Linux family, never a fact + * of the machine underneath: the host's CPU model, memory size, boot id or + * name never reach a guest, so no two families can tell they share a host. + */ + +mod names; +mod sizes; + +pub use names::{DOMAIN, HOSTNAME, MACHINE, OSTYPE, RELEASE, VERSION}; +pub use sizes::{CPUS, HPAGE_PMD, HZ, MEMORY, OVERCOMMIT, PID_MAX, PIPE_MAX, PRIVATE}; diff --git a/userland/capsule_linux/src/linux/file/system/declared/names.rs b/userland/capsule_linux/src/linux/file/system/declared/names.rs new file mode 100644 index 000000000..3c0e09591 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/declared/names.rs @@ -0,0 +1,35 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The names a guest is told: the kernel, the host and the machine. */ + +/* The kernel a guest is told it runs on: uname's release. */ +pub const RELEASE: &[u8] = b"6.1.0"; + +/* uname's nodename and the hostname files: the family's name for itself. */ +pub const HOSTNAME: &[u8] = b"nonos"; + +/* uname's version field. */ +pub const VERSION: &[u8] = b"NONOS Linux personality"; + +/* uname's sysname and /proc/sys/kernel/ostype. */ +pub const OSTYPE: &[u8] = b"Linux"; + +/* uname's machine. */ +pub const MACHINE: &[u8] = b"x86_64"; + +/* uname's domainname. */ +pub const DOMAIN: &[u8] = b"nonos"; diff --git a/userland/capsule_linux/src/linux/file/system/declared/sizes.rs b/userland/capsule_linux/src/linux/file/system/declared/sizes.rs new file mode 100644 index 000000000..e4a30dc02 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/declared/sizes.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The sizes a guest is told: CPUs, memory, pids, ticks, pipes and pages. */ + +/* + * One CPU, as sched_getaffinity says: a family never learns how many the + * machine has. + */ +pub const CPUS: u64 = 1; + +/* + * The memory a family may address, which is RLIMIT_AS: MemTotal and + * sysinfo's totalram are this, not the machine's memory. + */ +pub const MEMORY: u64 = 0x0000_7FFF_F000; + +/* Linux's own default for a machine of 32 CPUs or fewer. */ +pub const PID_MAX: u64 = 32768; + +/* Clock ticks a second, as AT_CLKTCK tells the C runtime. */ +pub const HZ: u64 = 100; + +/* + * A pipe holds 64 KiB (call/pipe_io.rs), and F_SETPIPE_SZ is not served, + * so that is also the most a pipe can be given. + */ +pub const PIPE_MAX: u64 = 64 << 10; + +/* + * Anonymous memory is reserved without frames and filled on first touch, + * so any reservation below the limit succeeds: Linux's "always" (1). + */ +pub const OVERCOMMIT: u64 = 1; + +/* + * x86_64's second-level page, which is what Go reads to size its heap + * arenas. An architectural constant, the same on every x86_64 machine. + */ +pub const HPAGE_PMD: u64 = 2 << 20; + +/* + * The most the family may keep in its private directories, all of them + * together: half its memory, as Linux sizes a tmpfs it is given no size. + */ +pub const PRIVATE: u64 = MEMORY / 2; diff --git a/userland/capsule_linux/src/linux/file/system/load/average.rs b/userland/capsule_linux/src/linux/file/system/load/average.rs new file mode 100644 index 000000000..4982cafca --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/load/average.rs @@ -0,0 +1,67 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The three averages, decayed as Linux decays them. */ + +use super::super::declared::{CPUS, HZ}; +use super::state::LOAD; + +const FIXED_1: u64 = 1 << 11; + +/* Linux's EXP_1, EXP_5 and EXP_15: 2048/exp(5s/1min), /exp(5s/5min), /exp(5s/15min). */ +const EXP: [u64; 3] = [1884, 2014, 2037]; + +const PERIOD_MS: u64 = 5000; + +/* After this many periods every average has reached the share it decays to. */ +const SETTLED: u64 = 4096; + +/* + * The three averages in Linux's fixed point, at `now_ms` since the family + * started, with the family's live threads having run `live` ticks. + */ +pub fn averages(now_ms: u64, live: u64) -> [u64; 3] { + let mut s = LOAD.0.borrow_mut(); + let ran = live + s.gone; + let periods = now_ms.saturating_sub(s.at_ms) / PERIOD_MS; + if periods > 0 { + let span = periods * PERIOD_MS * HZ / 1000; + let share = (ran.saturating_sub(s.ran) * FIXED_1 / span).min(FIXED_1 * CPUS); + for (avg, exp) in s.avg.iter_mut().zip(EXP) { + for _ in 0..periods.min(SETTLED) { + *avg = decay(*avg, exp, share); + } + } + s.at_ms += periods * PERIOD_MS; + s.ran = ran; + } + s.avg +} + +/* + * Linux's calc_load: one period's decay toward `share`, rounded up while + * rising. + */ +fn decay(avg: u64, exp: u64, share: u64) -> u64 { + let next = avg * exp + share * (FIXED_1 - exp); + (next + if share >= avg { FIXED_1 - 1 } else { 0 }) / FIXED_1 +} + +/* "0.42", as /proc/loadavg writes an average, rounded as Linux rounds it. */ +pub fn text(avg: u64) -> alloc::string::String { + let v = avg + FIXED_1 / 200; + alloc::format!("{}.{:02}", v >> 11, ((v & (FIXED_1 - 1)) * 100) >> 11) +} diff --git a/userland/capsule_linux/src/linux/file/system/load/mod.rs b/userland/capsule_linux/src/linux/file/system/load/mod.rs new file mode 100644 index 000000000..edcca8666 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/load/mod.rs @@ -0,0 +1,34 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The family's load average, measured. Linux averages the number of tasks + * running or waiting to run, sampled every five seconds and decayed by + * fixed factors for one, five and fifteen minutes. The kernel does not say + * how long a thread waited for the CPU, so this averages what it does + * say: the share of each period the family's threads ran, from their + * ticks. A family whose threads wait for a CPU another holds reads lower + * than Linux would show. + * + * Nothing samples between reads: at a read, the periods since the last + * one each get the share the family ran over all of them. + */ + +mod average; +mod state; + +pub use average::{averages, text}; +pub use state::exited; diff --git a/userland/capsule_linux/src/linux/file/system/load/state.rs b/userland/capsule_linux/src/linux/file/system/load/state.rs new file mode 100644 index 000000000..75ef2f647 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/load/state.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The load's state: when it was last taken, and what had run by then. */ + +use core::cell::RefCell; + +pub(super) struct State { + pub(super) at_ms: u64, + pub(super) ran: u64, + pub(super) gone: u64, + pub(super) avg: [u64; 3], +} + +pub(super) struct Load(pub(super) RefCell); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Load {} + +pub(super) static LOAD: Load = Load(RefCell::new(State { at_ms: 0, ran: 0, gone: 0, avg: [0; 3] })); + +/* A process of the family ran `ticks` in all before it exited. */ +pub fn exited(ticks: u64) { + LOAD.0.borrow_mut().gone += ticks; +} diff --git a/userland/capsule_linux/src/linux/file/system/mod.rs b/userland/capsule_linux/src/linux/file/system/mod.rs new file mode 100644 index 000000000..61913ca39 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/mod.rs @@ -0,0 +1,25 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What the family is told about the system it runs on, and what it + * has used of it. + */ + +pub mod cpu; +pub mod declared; +pub mod load; +pub(super) mod space; diff --git a/userland/capsule_linux/src/linux/file/system/space/mod.rs b/userland/capsule_linux/src/linux/file/system/space/mod.rs new file mode 100644 index 000000000..f49bef43c --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/space/mod.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * How much room a mount has, for statfs, from the family's own files and + * nothing else: the store's use as a whole would let a guest watch a + * sibling write, so it is never read. + * + * The tree at / is read-only to a guest: its size is the bytes the store + * holds under the family's root, and none of it is free. The private + * directories share one quota, declared::PRIVATE, less the bytes the + * family keeps there, in the store or still in its cache. /dev, /proc and + * /sys hold no bytes, which Linux reports as no blocks. + */ + +mod room; +mod used; + +pub use room::{of, BSIZE}; +pub use used::within; diff --git a/userland/capsule_linux/src/linux/file/system/space/room.rs b/userland/capsule_linux/src/linux/file/system/space/room.rs new file mode 100644 index 000000000..1832bdaa0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/space/room.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The room a mount has. */ + +use super::super::super::resolve; +use super::super::declared::PRIVATE; +use super::used::{under, used}; + +/* statfs's block size, the page size tmpfs counts in. */ +pub const BSIZE: u64 = 4096; + +/* Blocks in all, blocks free, and inodes in all, as statfs reports them. */ +pub struct Room { + pub blocks: u64, + pub free: u64, + pub files: u64, +} + +/* The room on the mount with options `opts`, mounted at `point`. */ +pub fn of(point: &str, opts: &str) -> Result { + let none = Room { blocks: 0, free: 0, files: 0 }; + match (point, opts.starts_with("rw")) { + ("/", _) => { + let (bytes, entries) = under(resolve::key(b"/").as_bytes())?; + Ok(Room { blocks: bytes.div_ceil(BSIZE), free: 0, files: entries }) + } + /* No inode limit is kept, which Linux says with no inodes at all. */ + (_, true) => Ok(Room { + blocks: PRIVATE / BSIZE, + free: PRIVATE.saturating_sub(used()?) / BSIZE, + files: 0, + }), + _ => Ok(none), + } +} diff --git a/userland/capsule_linux/src/linux/file/system/space/used.rs b/userland/capsule_linux/src/linux/file/system/space/used.rs new file mode 100644 index 000000000..a4e266661 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/space/used.rs @@ -0,0 +1,50 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What the family keeps in its private directories, and its quota. */ + +use nonos_app_skeleton::clients::vfs; +use nonos_libc::mk_getpid; + +use super::super::super::{cache, private}; +use super::super::declared::PRIVATE; + +/* The bytes the family keeps in its private directories. */ +pub fn used() -> Result { + let stored = under(&private::root())?.0; + Ok(stored.saturating_add_signed(cache::growth())) +} + +/* + * Whether what the family keeps, its unwritten copies counted, is within + * its quota. + */ +pub fn within() -> Result<(), i64> { + match used()? <= PRIVATE { + true => Ok(()), + false => Err(crate::linux::abi::errno::ENOSPC), + } +} + +/* + * Bytes and entries under a store prefix. A walk the store cut short at + * its node cap counts less than is there. + */ +pub(super) fn under(prefix: &[u8]) -> Result<(u64, u64), i64> { + let (files, dirs, bytes, _) = + vfs::dirstat(mk_getpid(), prefix).map_err(super::super::super::store_err::errno_of)?; + Ok((bytes, u64::from(files) + u64::from(dirs))) +} diff --git a/userland/capsule_linux/src/linux/file/walk/link.rs b/userland/capsule_linux/src/linux/file/walk/link.rs new file mode 100644 index 000000000..927c5febe --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/link.rs @@ -0,0 +1,35 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The link at a name, if a walk can go through it. */ + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::super::synth::{self, Node}; + +/* Where the link at `at` leads, if `at` is one that can be walked through. */ +pub(super) fn link_at(guest: &Guest, at: &[u8]) -> Option> { + if let Some(to) = guest.links.target(at) { + return Some(to); + } + match synth::node(at)? { + /* A pipe or a socket: there is nothing to walk through. */ + Ok(Node::Link(to)) if to.first() == Some(&b'/') || !to.contains(&b':') => Some(to), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/file/walk/mod.rs b/userland/capsule_linux/src/linux/file/walk/mod.rs new file mode 100644 index 000000000..b3a95fc6a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/mod.rs @@ -0,0 +1,35 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Following a path through the links in it: the image's own, and the ones + * /dev and /proc make (/proc/self, /dev/fd, /proc//cwd, /root, /exe). + * + * The path is walked a name at a time, as Linux walks it: a link is + * followed where it stands, and a `..` after it goes to the parent of + * where the link led, not of the link. `..` at the root stays at the root, + * so every result is a path of the family's own tree, and /proc//root + * is that root. A descriptor's link that names no path, a pipe or a + * socket, is not walked through, as Linux cannot walk through it either. + */ + +mod link; +mod path; +mod state; +mod step; + +pub use path::{walk, walk_under}; +pub use step::{follow, Step}; diff --git a/userland/capsule_linux/src/linux/file/walk/path.rs b/userland/capsule_linux/src/linux/file/walk/path.rs new file mode 100644 index 000000000..def8c7c04 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/path.rs @@ -0,0 +1,75 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Following a path, a name at a time, under a root. */ + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::link::link_at; +use super::state::{joined, names, Walk}; +use super::step::{Step, MAX_HOPS}; + +/* + * The walk `follow` makes, with `check` asked about each step first; its + * refusal ends the walk. + */ +pub fn walk( + guest: &Guest, + path: Vec, + last: bool, + check: impl FnMut(Step) -> Result<(), i64>, +) -> Result, i64> { + walk_under(guest, b"/", path, last, check) +} + +/* + * The walk with `root` standing in for `/`: an absolute name or link + * target starts from it, and `..` never climbs above it. openat2's + * RESOLVE_IN_ROOT is this with the directory descriptor's path as root. + */ +pub fn walk_under( + guest: &Guest, + root: &[u8], + path: Vec, + last: bool, + mut check: impl FnMut(Step) -> Result<(), i64>, +) -> Result, i64> { + let last = last || path.last() == Some(&b'/'); + let done: Vec> = names(root).collect(); + let mut w = Walk { todo: names(&path).collect(), floor: done.len(), done, hops: 0 }; + while let Some(name) = w.todo.pop_front() { + if name == b".." { + w.up(&path); + check(Step::At(&joined(&w.done)))?; + continue; + } + w.done.push(name); + let at = joined(&w.done); + if w.todo.is_empty() && !last { + check(Step::At(&at))?; + break; + } + let Some(to) = link_at(guest, &at).filter(|_| w.hops < MAX_HOPS) else { + check(Step::At(&at))?; + continue; + }; + check(Step::Link { at: &at, to: &to })?; + w.into_link(&to); + } + Ok(joined(&w.done)) +} diff --git a/userland/capsule_linux/src/linux/file/walk/state.rs b/userland/capsule_linux/src/linux/file/walk/state.rs new file mode 100644 index 000000000..1ce4169bf --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/state.rs @@ -0,0 +1,69 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* Where a walk has got to, and the names of a path. */ + +use alloc::collections::VecDeque; +use alloc::vec::Vec; + +/* + * Where a walk has got to: the names still to walk, the names walked, the + * root's names at the bottom of those, and how many links it has followed. + */ +pub(super) struct Walk { + pub(super) todo: VecDeque>, + pub(super) done: Vec>, + pub(super) floor: usize, + pub(super) hops: usize, +} + +impl Walk { + /* Up one name; at the root there is none to go up from, so it stays. */ + pub(super) fn up(&mut self, path: &[u8]) { + if self.done.len() == self.floor { + super::super::clamp::note(path); + } else { + self.done.pop(); + } + } + + /* + * Into the link just walked: its target's names come next, from the + * root for an absolute one. + */ + pub(super) fn into_link(&mut self, to: &[u8]) { + self.hops += 1; + self.done.pop(); + if to.first() == Some(&b'/') { + self.done.truncate(self.floor); + } + for (i, n) in names(to).enumerate() { + self.todo.insert(i, n); + } + } +} + +/* The names in `path`, with the empty ones and `.` left out. */ +pub(super) fn names(path: &[u8]) -> impl Iterator> + '_ { + path.split(|b| *b == b'/').filter(|n| !n.is_empty() && *n != b".").map(<[u8]>::to_vec) +} + +pub(super) fn joined(names: &[Vec]) -> Vec { + if names.is_empty() { + return alloc::vec![b'/']; + } + names.iter().flat_map(|n| [&b"/"[..], n].concat()).collect() +} diff --git a/userland/capsule_linux/src/linux/file/walk/step.rs b/userland/capsule_linux/src/linux/file/walk/step.rs new file mode 100644 index 000000000..164f84523 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/step.rs @@ -0,0 +1,44 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* What a walk's caller is told at each step, and the walk with no limit. */ + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::path::walk; + +/* Linux's MAXSYMLINKS: more links than this in one walk is a loop. */ +pub(super) const MAX_HOPS: usize = 40; + +/* One step of a walk, for a caller that limits where a walk may go. */ +pub enum Step<'a> { + /* The walk stands at this path. */ + At(&'a [u8]), + /* The walk is about to follow the link at `at`, which leads to `to`. */ + Link { at: &'a [u8], to: &'a [u8] }, +} + +/* + * `path` with every link in it followed, its last name too when `last` is + * set, and every `.` and `..` resolved. A trailing slash asks for the last + * name to be followed, as it does on Linux. + */ +pub fn follow(guest: &Guest, path: Vec, last: bool) -> Vec { + /* With no check to refuse a step, the walk always ends somewhere. */ + walk(guest, path, last, |_| Ok(())).unwrap_or_else(|_| alloc::vec![b'/']) +} diff --git a/userland/capsule_linux/src/linux/file/write.rs b/userland/capsule_linux/src/linux/file/write.rs index 2c29f6e55..992fe7dd1 100644 --- a/userland/capsule_linux/src/linux/file/write.rs +++ b/userland/capsule_linux/src/linux/file/write.rs @@ -14,45 +14,50 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! Writing to a file a guest has open. -//! -//! Bytes are held here until the descriptor is closed, then written as one -//! file. The store takes whole values rather than a stream of positioned -//! writes, and a program that writes a file expects it to appear whole or -//! not at all, which is the same thing. +/* + * `write` on a file: at the descriptor's offset, or at the end when it + * was opened O_APPEND, and the offset moves to where the write ended. + */ use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; - -/// One transfer, matching the kernel's own peer-copy ceiling. -const MAX_IO: u64 = 1 << 20; +use crate::linux::guest::Guest; -/// What a single guest may hold unwritten. A program that produces more -/// than this without closing is refused rather than allowed to grow this -/// capsule's heap without bound. -const MAX_PENDING: usize = 8 << 20; +use super::rw::{write_at, MAX_IO}; pub fn write(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { - let take = len.min(MAX_IO); - let Some(bytes) = guest.read(buf, take as usize) else { - return errno::fail(errno::EFAULT); - }; - let Some(entry) = guest.fds.get_mut(fd as usize) else { - return errno::fail(errno::EBADF); + let at = guest.fds.get(fd as usize).map_or(0, super::desc::pos); + let (n, end) = match whole(guest, fd, buf, len, at) { + Ok(done) => done, + Err(e) => return errno::fail(e), }; - if entry.kind != Kind::File || !entry.writable { - return errno::fail(errno::EBADF); + if let Some(entry) = guest.fds.get_mut(fd as usize) { + super::desc::set_pos(entry, end); } - let at = entry.offset as usize; - if at + bytes.len() > MAX_PENDING { - return errno::fail(errno::ENOSPC); - } - if entry.pending.len() < at + bytes.len() { - entry.pending.resize(at + bytes.len(), 0); + errno::ok(n) +} + +/* + * All `len` bytes at `buf` into the file at `at`, as a Linux file takes a + * whole write: MAX_IO bounds one copy out of the guest, not the call. The + * count written and where the write ended; a failure after some bytes + * landed is the count so far, as on Linux. + */ +pub fn whole(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> Result<(u64, u64), i64> { + let (mut done, mut end) = (0u64, at); + loop { + let take = ((len - done) as usize).min(MAX_IO); + let got = match guest.read(buf + done, take) { + Some(bytes) => write_at(guest, fd, end, &bytes), + None => Err(errno::EFAULT), + }; + match got { + Ok((0, _)) if take > 0 => return Ok((done, end)), + Ok((n, to)) => (done, end) = (done + n as u64, to), + Err(e) if done == 0 => return Err(e), + Err(_) => return Ok((done, end)), + } + if done >= len { + return Ok((done, end)); + } } - entry.pending[at..at + bytes.len()].copy_from_slice(&bytes); - entry.offset += bytes.len() as u64; - entry.size = entry.size.max(entry.pending.len() as u64); - errno::ok(bytes.len() as u64) } diff --git a/userland/capsule_linux/src/linux/file/xattrs/mod.rs b/userland/capsule_linux/src/linux/file/xattrs/mod.rs new file mode 100644 index 000000000..d976ddc42 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/mod.rs @@ -0,0 +1,22 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Extended attributes, kept by the family for its own files. + */ + +pub mod xattr; +pub(super) mod xattr_table; diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr/answer.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr/answer.rs new file mode 100644 index 000000000..3bb6031ff --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr/answer.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* One xattr call answered from the family's table. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{cache, cstr, resolve, synth}; +use super::super::xattr_table as table; +use super::calls::{Args, Op}; +use super::give::give; + +pub(super) fn answer(guest: &Guest, path: &[u8], op: Op, a: Args) -> u64 { + if synth::owns(path) { + return match op { + Op::List => errno::ok(0), + _ => errno::fail(errno::EOPNOTSUPP), + }; + } + if let Op::List = op { + return give(guest, a.value, a.size, table::list(path)); + } + let name = match cstr::read_cstr(guest, a.name, 256) { + Some(name) => name, + None => return errno::fail(errno::EFAULT), + }; + if let Err(e) = table::check_name(&name) { + return errno::fail(e); + } + let writable = cache::held(path) || resolve::key(path).writable().is_ok(); + let done = match op { + Op::Get => { + return match table::get(path, &name) { + Ok(value) => give(guest, a.value, a.size, value), + Err(e) => errno::fail(e), + }; + } + Op::Set if !writable => Err(errno::EROFS), + Op::Remove if !writable => Err(errno::EROFS), + Op::Set => match guest.read(a.value, (a.size as usize).min(65537)) { + Some(value) => table::set(path, &name, value, a.flags), + None => Err(errno::EFAULT), + }, + Op::Remove => table::remove(path, &name), + Op::List => Ok(()), + }; + match done { + Ok(()) => errno::ok(0), + Err(e) => errno::fail(e), + } +} diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr/calls.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr/calls.rs new file mode 100644 index 000000000..07974ac13 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr/calls.rs @@ -0,0 +1,60 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The xattr calls by path and by descriptor. */ + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::super::{at, flags::AT_FDCWD, meta, walk}; +use super::answer::answer; + +pub enum Op { + Get, + Set, + List, + Remove, +} + +/* The call's own arguments after the path or descriptor. */ +pub struct Args { + pub name: u64, + pub value: u64, + pub size: u64, + pub flags: u64, +} + +pub fn by_path(guest: &Guest, path: u64, op: Op, args: Args, follow: bool) -> u64 { + let Some(named) = at::resolve_at(guest, AT_FDCWD, path) else { + return errno::fail(errno::EFAULT); + }; + let full = walk::follow(guest, named, follow); + if let Err(e) = meta::meta_of(guest, full.clone(), false) { + return errno::fail(e); + } + answer(guest, &full, op, args) +} + +pub fn by_fd(guest: &Guest, fd: u64, op: Op, args: Args) -> u64 { + match guest.fds.get(fd as usize).filter(|f| f.is_open()) { + Some(f) if matches!(f.kind, Kind::File | Kind::Dir) => { + answer(guest, &f.path.clone(), op, args) + } + /* A pipe, a socket: nothing a guest names has attributes there. */ + Some(_) => errno::fail(errno::EOPNOTSUPP), + None => errno::fail(errno::EBADF), + } +} diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr/give.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr/give.rs new file mode 100644 index 000000000..184003982 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr/give.rs @@ -0,0 +1,39 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* An attribute's value or the list, given back as Linux gives it. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/* + * Copy `bytes` out: size 0 asks only how long they are; a buffer too + * small is ERANGE. + */ +pub(super) fn give(guest: &Guest, buf: u64, size: u64, bytes: Vec) -> u64 { + if size == 0 { + return errno::ok(bytes.len() as u64); + } + if (size as usize) < bytes.len() { + return errno::fail(errno::ERANGE); + } + match guest.write(buf, &bytes) { + n if n < bytes.len() as i64 => errno::fail(errno::EFAULT), + _ => errno::ok(bytes.len() as u64), + } +} diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr/mod.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr/mod.rs new file mode 100644 index 000000000..c6e1686df --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr/mod.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The xattr calls: get, set, list and remove, by path (following the last + * link or not) and by descriptor, on the family's table (xattrs/xattr_table/). + */ + +mod answer; +mod calls; +mod give; + +pub use calls::{by_fd, by_path, Args, Op}; diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr_table/edit.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/edit.rs new file mode 100644 index 000000000..ce568f2c3 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/edit.rs @@ -0,0 +1,55 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* An attribute removed or listed, and attributes that follow their file. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; + +use super::table::ATTRS; + +pub fn remove(path: &[u8], name: &[u8]) -> Result<(), i64> { + let mut all = ATTRS.0.borrow_mut(); + let before = all.len(); + all.retain(|(p, n, _)| !(p == path && n == name)); + if all.len() == before { + Err(errno::ENODATA) + } else { + Ok(()) + } +} + +/* Every name, each followed by a NUL, as listxattr gives them. */ +pub fn list(path: &[u8]) -> Vec { + let all = ATTRS.0.borrow(); + all.iter() + .filter(|(p, _, _)| p == path) + .flat_map(|(_, n, _)| n.iter().copied().chain([0])) + .collect() +} + +pub fn forget(path: &[u8]) { + ATTRS.0.borrow_mut().retain(|(p, _, _)| p != path); +} + +pub fn renamed(from: &[u8], to: &[u8]) { + let mut all = ATTRS.0.borrow_mut(); + all.retain(|(p, _, _)| p != to); + for (p, _, _) in all.iter_mut().filter(|(p, _, _)| p == from) { + *p = to.to_vec(); + } +} diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr_table/mod.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/mod.rs new file mode 100644 index 000000000..a3f967309 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/mod.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Extended attributes, kept by the family as tmpfs keeps them. + * + * The store holds a file's bytes and nothing beside them, so the family's + * files keep their attributes here, for the family's life, which is the + * life of its private directories. The shared tree is read-only: its files + * have none and can be given none (EROFS). /proc and /dev files do not + * support them, as procfs does not (EOPNOTSUPP). + */ + +mod edit; +mod set; +mod table; + +pub use edit::{forget, list, remove, renamed}; +pub use set::set; +pub use table::{check_name, get}; diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr_table/set.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/set.rs new file mode 100644 index 000000000..f778bf2a1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/set.rs @@ -0,0 +1,46 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* An attribute set, as setxattr's flags allow. */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; + +use super::table::{ATTRS, SIZE_MAX, XATTR_CREATE, XATTR_REPLACE}; + +pub fn set(path: &[u8], name: &[u8], value: Vec, flags: u64) -> Result<(), i64> { + if flags & !(XATTR_CREATE | XATTR_REPLACE) != 0 { + return Err(errno::EINVAL); + } + if value.len() > SIZE_MAX { + return Err(7); /* E2BIG */ + } + let mut all = ATTRS.0.borrow_mut(); + let at = all.iter().position(|(p, n, _)| p == path && n == name); + match (at, flags) { + (Some(_), XATTR_CREATE) => Err(errno::EEXIST), + (None, XATTR_REPLACE) => Err(errno::ENODATA), + (Some(i), _) => { + all[i].2 = value; + Ok(()) + } + (None, _) => { + all.push((path.to_vec(), name.to_vec(), value)); + Ok(()) + } + } +} diff --git a/userland/capsule_linux/src/linux/file/xattrs/xattr_table/table.rs b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/table.rs new file mode 100644 index 000000000..a33937975 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/xattrs/xattr_table/table.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The family's extended attributes, by path. */ + +use alloc::vec::Vec; +use core::cell::RefCell; + +use crate::linux::abi::errno; + +pub(super) const XATTR_CREATE: u64 = 1; + +pub(super) const XATTR_REPLACE: u64 = 2; + +/* XATTR_SIZE_MAX. */ +pub(super) const SIZE_MAX: usize = 65536; + +const NAMESPACES: [&[u8]; 3] = [b"user.", b"trusted.", b"security."]; + +pub(super) struct Attrs(pub(super) RefCell, Vec, Vec)>>); + +/* + * SAFETY: one personality process serves one family from one serve loop, + * answering one call at a time, so no two borrows can overlap. + */ +unsafe impl Sync for Attrs {} + +pub(super) static ATTRS: Attrs = Attrs(RefCell::new(Vec::new())); + +/* The name is one tmpfs keeps: a known namespace and something after it. */ +pub fn check_name(name: &[u8]) -> Result<(), i64> { + if name.is_empty() || name.len() > 255 { + return Err(errno::ERANGE); + } + match NAMESPACES.iter().any(|ns| name.len() > ns.len() && name.starts_with(ns)) { + true => Ok(()), + false => Err(errno::EOPNOTSUPP), + } +} + +pub fn get(path: &[u8], name: &[u8]) -> Result, i64> { + let all = ATTRS.0.borrow(); + let found = all.iter().find(|(p, n, _)| p == path && n == name); + found.map(|(_, _, v)| v.clone()).ok_or(errno::ENODATA) +} diff --git a/userland/capsule_linux/src/linux/guest/links.rs b/userland/capsule_linux/src/linux/guest/links.rs index 1a710dedf..dc49384df 100644 --- a/userland/capsule_linux/src/linux/guest/links.rs +++ b/userland/capsule_linux/src/linux/guest/links.rs @@ -24,6 +24,9 @@ //! link cannot point out of the guest's tree, and a program reached through //! one is proved by its own path, never the link's. +/* Removing and moving a link, for unlink and rename. */ +mod edit; + use alloc::vec::Vec; use core::cell::RefCell; diff --git a/userland/capsule_linux/src/linux/guest/links/edit.rs b/userland/capsule_linux/src/linux/guest/links/edit.rs new file mode 100644 index 000000000..f559f1bf7 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/links/edit.rs @@ -0,0 +1,48 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Removing and moving a symbolic link, for unlink and rename: a link made + * by symlink lives in this table, not in the store. + */ + +use alloc::vec::Vec; + +use super::Links; + +impl Links { + /* Remove the link at `path`; false when there is none. */ + pub fn remove(&self, path: &[u8]) -> bool { + let mut all = self.0.borrow_mut(); + let before = all.len(); + all.retain(|(from, _)| from != path); + all.len() != before + } + + /* + * Move the link at `from` to `to`, replacing any there; false when + * `from` is no link. + */ + pub fn rename(&self, from: &[u8], to: Vec) -> bool { + let Some(target) = self.target(from) else { + return false; + }; + let mut all = self.0.borrow_mut(); + all.retain(|(p, _)| p != from && p[..] != to[..]); + all.push((to, target)); + true + } +} diff --git a/userland/capsule_linux/src/linux/image/stack.rs b/userland/capsule_linux/src/linux/image/stack.rs index ddeac8e20..746d47684 100644 --- a/userland/capsule_linux/src/linux/image/stack.rs +++ b/userland/capsule_linux/src/linux/image/stack.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! The stack a Linux program wakes up on: argc, then argv, then the //! environment, then the auxiliary vector, each list ended by a null. @@ -57,6 +56,9 @@ pub fn build( } match guest.write(rsp, &blob) { n if n < 0 => None, - _ => Some(rsp), + _ => { + crate::linux::file::record_image(guest.pid, argv, &placed.at, top, rsp); + Some(rsp) + } } } diff --git a/userland/capsule_linux/src/linux/serve/dispatch.rs b/userland/capsule_linux/src/linux/serve/dispatch.rs index e292a5f27..c5758a7b3 100644 --- a/userland/capsule_linux/src/linux/serve/dispatch.rs +++ b/userland/capsule_linux/src/linux/serve/dispatch.rs @@ -21,6 +21,7 @@ use nonos_libc::ForeignFrame; use super::answer::Answer; use super::table::plain; +use super::waits_lock; use crate::linux::abi::{nr, nr_path as np}; use crate::linux::call::{clone, exit_thread, futex}; use crate::linux::guest::Guest; @@ -58,6 +59,7 @@ fn route(guest: &mut Guest, frame: &ForeignFrame) -> Answer { np::CLOCK_NANOSLEEP => { crate::linux::call::clock_nanosleep(guest, frame.pid, a[0], a[1], a[2]) } + np::FLOCK | nr::FCNTL if waits_lock::wants(frame) => waits_lock::lock(guest, frame), nr::READ | nr::WRITE if super::waits::may_wait(guest, frame.nr, a[0]) => { super::waits::io(guest, frame.pid, frame.nr, a) } diff --git a/userland/capsule_linux/src/linux/serve/family.rs b/userland/capsule_linux/src/linux/serve/family.rs index 92cb042e0..42157c2ea 100644 --- a/userland/capsule_linux/src/linux/serve/family.rs +++ b/userland/capsule_linux/src/linux/serve/family.rs @@ -63,7 +63,9 @@ impl Family { return; }; self.lend(i); + self.lend_view(i, &frame); let got = answer(&mut self.guests[i], &frame); + self.take_view(); self.take_back(i); let g = &mut self.guests[i]; let born = mem::take(&mut g.forked); diff --git a/userland/capsule_linux/src/linux/serve/family_exit.rs b/userland/capsule_linux/src/linux/serve/family_exit.rs new file mode 100644 index 000000000..7622c1b41 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_exit.rs @@ -0,0 +1,53 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What a process leaves behind when it exits, settled on its last call, + * while the family still holds it. + */ + +use nonos_libc::ForeignFrame; + +use super::family::Family; +use crate::linux::abi::nr; +use crate::linux::file; + +impl Family { + /* + * A process about to exit: what it used, with all that the children it + * waited for used, goes to its parent's RUSAGE_CHILDREN once waited + * for, as Linux adds them; its POSIX locks go, and every file + * the family is writing reaches the store, as the exit's closes would. + */ + pub(super) fn note_exit(&self, i: usize, frame: &ForeignFrame) { + let g = &self.guests[i]; + let leaving = frame.nr == nr::EXIT_GROUP || (frame.nr == nr::EXIT && g.threads.is_empty()); + if !leaving { + return; + } + let parent = self.guests.iter().find(|p| p.children.contains(&g.pid)).map_or(0, |p| p.pid); + let mut used = crate::linux::call::usage_of(g); + file::load::exited(used.user + used.system); + let kids = file::cpu::children(g.pid, |c| !g.children.contains(&c)); + used.user += kids.user; + used.system += kids.system; + used.faults += kids.faults; + used.switches += kids.switches; + file::cpu::ended(g.pid, parent, used); + file::locks_exiting(g.pid); + let _ = file::flush_all(); + } +} diff --git a/userland/capsule_linux/src/linux/serve/family_view/facts.rs b/userland/capsule_linux/src/linux/serve/family_view/facts.rs new file mode 100644 index 000000000..23e187eaa --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/facts.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What the view says of one process: its image, whether it waits, + * and its dispositions. + */ + +use crate::linux::file::{self}; +use crate::linux::guest::sigstate::NSIG; +use crate::linux::guest::Guest; + +/* A forked child runs its parent's image until it execs. */ +pub(super) fn image_of(all: &[Guest], g: &Guest) -> file::Exe { + let mut at = g.pid; + for _ in 0..all.len() + 1 { + if let Some(exe) = file::exe_of(at) { + return exe; + } + match all.iter().find(|p| p.children.contains(&at)) { + Some(p) => at = p.pid, + None => break, + } + } + file::Exe::default() +} + +/* Some thread of it is parked in a call. */ +pub(super) fn parked(g: &Guest) -> bool { + let mut tids = core::iter::once(g.pid).chain(g.threads.iter().copied()); + tids.any(|t| g.parked(t).is_some()) || g.signals.vfork.is_some() +} + +/* The signals it catches and the ones it ignores, as status's masks. */ +pub(super) fn dispositions(g: &Guest) -> (u64, u64) { + let (mut caught, mut ignored) = (0u64, 0u64); + for n in 1..=NSIG { + match g.signals.action(n) { + Some(a) if a.catches() => caught |= 1 << (n - 1), + Some(a) if a.ignores() => ignored |= 1 << (n - 1), + _ => {} + } + } + (caught, ignored) +} diff --git a/userland/capsule_linux/src/linux/serve/family_view/lend.rs b/userland/capsule_linux/src/linux/serve/family_view/lend.rs new file mode 100644 index 000000000..e8d461d54 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/lend.rs @@ -0,0 +1,45 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* The view a family lends /proc for a call. */ + +use nonos_libc::ForeignFrame; + +use crate::linux::file::{self, View}; + +use super::super::family::Family; +use super::proc::proc_of; + +/* The personality itself: the namespace's pid 1, never shown under /proc. */ +pub(super) const HOST_NS: u32 = 1; + +impl Family { + pub(crate) fn lend_view(&mut self, i: usize, frame: &ForeignFrame) { + self.note_exit(i, frame); + if !file::needs_view(&self.guests[i], frame.nr, frame.args()) { + return; + } + let me = self.ns.outward(self.guests[i].pid); + let thread = self.ns.outward(frame.pid); + let n = self.guests.len(); + let procs = (0..n).map(|j| proc_of(&self.guests, &mut self.ns, j, j == i)).collect(); + file::lend_view(View { me, thread, procs }); + } + + pub(crate) fn take_view(&mut self) { + file::lend_view(View::default()); + } +} diff --git a/userland/capsule_linux/src/linux/serve/family_view/mod.rs b/userland/capsule_linux/src/linux/serve/family_view/mod.rs new file mode 100644 index 000000000..60e2265e7 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/mod.rs @@ -0,0 +1,28 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Lending /proc its view of the family, for the one call that may read it. + * + * Only the family knows which processes it holds and the numbers its pid + * namespace gave them, and /proc must show exactly those and nothing else. + * Built only for a call that names a path or reads a /proc descriptor, so + * every other call costs one test. + */ + +mod facts; +mod lend; +mod proc; diff --git a/userland/capsule_linux/src/linux/serve/family_view/proc.rs b/userland/capsule_linux/src/linux/serve/family_view/proc.rs new file mode 100644 index 000000000..8736931f6 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/proc.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* One process as /proc shows it, under the family's numbers. */ + +use alloc::vec::Vec; + +use crate::linux::file::{self, Proc}; +use crate::linux::guest::{Guest, BRK_BASE}; + +use super::super::pid_ns::PidNs; +use super::facts::{dispositions, image_of, parked}; +use super::lend::HOST_NS; + +/* One process as /proc shows it, under its numbers in the family's namespace. */ +pub(super) fn proc_of(guests: &[Guest], ns: &mut PidNs, j: usize, asking: bool) -> Proc { + let g = &guests[j]; + let parent = guests.iter().find(|p| p.children.contains(&g.pid)).map(|p| p.pid); + let exe = image_of(guests, g); + let (kernel, pgid, sid) = (g.pid, g.pgid, g.sid); + let sleeping = !asking && parked(g); + let (cwd, fds, regions) = (g.cwd.clone(), file::open_fds(g), g.regions.clone()); + let (brk, umask) = ((BRK_BASE, g.brk), g.umask); + let (caught, ignored) = dispositions(g); + let reaped = file::cpu::children(g.pid, |c| !g.children.contains(&c)); + let members: Vec = [g.pid].iter().chain(g.threads.iter()).copied().collect(); + let tids = members.iter().map(|t| (ns.outward(*t), *t)).collect(); + Proc { + ns: ns.outward(kernel), + kernel, + ppid: parent.map_or(HOST_NS, |p| ns.outward(p)), + pgid: ns.outward(pgid), + sid: ns.outward(sid), + tids, + sleeping, + exe, + cwd, + fds, + regions, + brk, + umask, + caught, + ignored, + reaped, + } +} diff --git a/userland/capsule_linux/src/linux/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index 917245ecf..d329fbdf8 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -19,8 +19,8 @@ mod answer; mod deliver; mod deliver_enter; -mod deliver_pipe; mod deliver_interrupt; +mod deliver_pipe; mod deliver_rem; mod deliver_restart; mod deliver_say; @@ -29,6 +29,7 @@ mod deliver_stack; mod deliver_wait; mod dispatch; mod family; +mod family_exit; mod family_futex; mod family_lend; mod family_reap; @@ -37,6 +38,7 @@ mod family_signal; mod family_signal_fire; mod family_signal_route; mod family_sleep; +mod family_view; mod family_wait; mod family_wait_report; mod family_wait_try; @@ -44,21 +46,25 @@ mod family_waits; mod loop_impl; mod pid_map; mod pid_ns; +mod pid_out; mod pid_space; mod refused; -mod pid_out; mod route_life; mod table; +mod table_data; mod table_file; mod table_link; mod table_mem; +mod table_meta; mod table_net; mod table_proc; mod table_sig; +mod table_sys; mod tally; mod unserved; mod waits; mod waits_fds; +mod waits_lock; mod waits_time; pub use answer::Answer; diff --git a/userland/capsule_linux/src/linux/serve/refused.rs b/userland/capsule_linux/src/linux/serve/refused.rs index 05e44228c..0350d6264 100644 --- a/userland/capsule_linux/src/linux/serve/refused.rs +++ b/userland/capsule_linux/src/linux/serve/refused.rs @@ -36,6 +36,10 @@ const REFUSED: &[(u64, i64, &str)] = &[ (425, errno::ENOSYS, "io_uring_setup: a second call path around the gate"), (426, errno::ENOSYS, "io_uring_enter: a second call path around the gate"), (427, errno::ENOSYS, "io_uring_register: a second call path around the gate"), + (253, errno::ENOSYS, "inotify_init: the store sends no change events to watch"), + (294, errno::ENOSYS, "inotify_init1: the store sends no change events to watch"), + (254, errno::ENOSYS, "inotify_add_watch: the store sends no change events to watch"), + (255, errno::ENOSYS, "inotify_rm_watch: the store sends no change events to watch"), ]; /// The errno for a call refused on purpose, after saying why; None otherwise. diff --git a/userland/capsule_linux/src/linux/serve/table_data.rs b/userland/capsule_linux/src/linux/serve/table_data.rs new file mode 100644 index 000000000..35ed80185 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_data.rs @@ -0,0 +1,74 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * A file's data beyond read and write: the positional and vector forms, + * copies between descriptors, a file's length, syncing, and the opens + * with more to say than openat. + */ + +use crate::linux::abi::{nr, nr_path as np}; +use crate::linux::file::{self, flags::AT_FDCWD, xattr}; +use crate::linux::guest::Guest; + +/* creat is open with O_CREAT | O_WRONLY | O_TRUNC. */ +const CREAT_FLAGS: u64 = 0o1101; + +/* + * The xattr calls' arguments after the path or descriptor: name, value, + * size, flags; list has only a buffer and its size. + */ +fn args(a: [u64; 6]) -> xattr::Args { + xattr::Args { name: a[1], value: a[2], size: a[3], flags: a[4] } +} + +fn list_args(a: [u64; 6]) -> xattr::Args { + xattr::Args { name: 0, value: a[1], size: a[2], flags: 0 } +} + +pub fn data_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { + Some(match nr { + nr::PWRITE64 => file::pwrite64(guest, a[0], a[1], a[2], a[3]), + np::PREADV => file::preadv(guest, a[0], a[1], a[2], a[3], 0), + np::PWRITEV => file::pwritev(guest, a[0], a[1], a[2], a[3], 0), + np::PREADV2 => file::preadv(guest, a[0], a[1], a[2], a[3], a[5]), + np::PWRITEV2 => file::pwritev(guest, a[0], a[1], a[2], a[3], a[5]), + np::SENDFILE => file::sendfile(guest, a[0], a[1], a[2], a[3]), + np::COPY_FILE_RANGE => file::copy_file_range(guest, a), + np::TRUNCATE => file::truncate(guest, a[0], a[1]), + np::FALLOCATE => file::fallocate(guest, a[0], a[1], a[2], a[3]), + np::FADVISE64 => file::fadvise64(guest, a[0], a[3]), + np::CLOSE_RANGE => file::close_range(guest, a[0], a[1], a[2]), + np::SYNC => file::sync(), + np::SYNCFS => file::syncfs(guest, a[0]), + np::CREAT => file::openat(guest, AT_FDCWD, a[0], CREAT_FLAGS, a[1]), + np::OPENAT2 => file::openat2(guest, a[0], a[1], a[2], a[3]), + np::GETXATTR => xattr::by_path(guest, a[0], xattr::Op::Get, args(a), true), + np::LGETXATTR => xattr::by_path(guest, a[0], xattr::Op::Get, args(a), false), + np::FGETXATTR => xattr::by_fd(guest, a[0], xattr::Op::Get, args(a)), + np::SETXATTR => xattr::by_path(guest, a[0], xattr::Op::Set, args(a), true), + np::LSETXATTR => xattr::by_path(guest, a[0], xattr::Op::Set, args(a), false), + np::FSETXATTR => xattr::by_fd(guest, a[0], xattr::Op::Set, args(a)), + np::LISTXATTR => xattr::by_path(guest, a[0], xattr::Op::List, list_args(a), true), + np::LLISTXATTR => xattr::by_path(guest, a[0], xattr::Op::List, list_args(a), false), + np::FLISTXATTR => xattr::by_fd(guest, a[0], xattr::Op::List, list_args(a)), + np::REMOVEXATTR => xattr::by_path(guest, a[0], xattr::Op::Remove, args(a), true), + np::LREMOVEXATTR => xattr::by_path(guest, a[0], xattr::Op::Remove, args(a), false), + np::FREMOVEXATTR => xattr::by_fd(guest, a[0], xattr::Op::Remove, args(a)), + np::FLOCK => super::waits_lock::answer_now(file::flock(guest, a[0], a[1])), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs index d0da0fe5e..ceca90bd4 100644 --- a/userland/capsule_linux/src/linux/serve/table_file.rs +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Calls that name a file or a descriptor. +/* Calls that name a file or a descriptor. */ use crate::linux::abi::{errno, nr, nr_path as np}; use crate::linux::call; @@ -31,15 +31,12 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option call::close(guest, a[0]), nr::MEMFD_CREATE => file::memfd_create(guest), nr::FTRUNCATE => file::ftruncate(guest, a[0], a[1]), - nr::OPENAT => file::openat(guest, a[0], a[1], a[2]), - nr::OPEN => file::openat(guest, flags::AT_FDCWD, a[0], a[1]), + nr::OPENAT => file::openat(guest, a[0], a[1], a[2], a[3]), + nr::OPEN => file::openat(guest, flags::AT_FDCWD, a[0], a[1], a[2]), nr::LSEEK => file::lseek(guest, a[0], a[1], a[2]), - nr::FSTAT => file::fstat(guest, a[0], a[1]), - nr::STAT | nr::LSTAT => file::newfstatat(guest, flags::AT_FDCWD, a[0], a[1]), - nr::NEWFSTATAT => file::newfstatat(guest, a[0], a[1], a[2]), nr::GETDENTS64 => file::getdents64(guest, a[0], a[1], a[2]), nr::EPOLL_CREATE1 => file::epoll_create(guest), - // The size is a hint Linux ignores past checking it is positive. + /* The size is a hint Linux ignores past checking it is positive. */ nr::EPOLL_CREATE if a[0] as u32 as i32 <= 0 => errno::fail(errno::EINVAL), nr::EPOLL_CREATE => file::epoll_create(guest), nr::EVENTFD2 => file::eventfd2(guest, a[0], a[1]), @@ -56,22 +53,20 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option call::getcwd(guest, a[0], a[1]), np::CHDIR => call::chdir(guest, a[0]), np::FCHDIR => call::fchdir(guest, a[0]), - np::MKDIR => file::mkdirat(guest, flags::AT_FDCWD, a[0]), - np::MKDIRAT => file::mkdirat(guest, a[0], a[1]), + np::MKDIR => file::mkdirat(guest, flags::AT_FDCWD, a[0], a[1]), + np::MKDIRAT => file::mkdirat(guest, a[0], a[1], a[2]), np::RMDIR => file::rmdir(guest, a[0]), np::UNLINK => file::unlinkat(guest, flags::AT_FDCWD, a[0], 0), np::UNLINKAT => file::unlinkat(guest, a[0], a[1], a[2]), - np::RENAME => file::rename(guest, a[0], a[1]), - np::FSYNC => file::fsync(guest, a[0]), + np::RENAME => file::renameat2(guest, flags::AT_FDCWD, a[0], flags::AT_FDCWD, a[1], 0), + np::FSYNC | np::FDATASYNC => file::fsync(guest, a[0]), np::READV => call::readv(guest, a[0], a[1], a[2]), np::CHMOD => file::chmod(guest, a[0], a[1]), np::FCHMOD => file::fchmod(guest, a[0], a[1]), np::FCHMODAT => file::fchmodat(guest, a[0], a[1], a[2]), - np::FACCESSAT | np::FACCESSAT2 => file::faccessat(guest, a[0], a[1]), - np::STATFS | np::FSTATFS => file::statfs(guest, a[1]), - np::STATX => file::statx(guest, a[0], a[1], a[4]), - nr::ACCESS => file::access(guest, a[0]), - nr::READLINK => file::readlinkat(guest, flags::AT_FDCWD, a[0], a[1], a[2]), - _ => return None, + _ => { + let looked = super::table_meta::meta_ops(guest, nr, a); + return looked.or_else(|| super::table_sys::sys_ops(guest, tid, nr, a)); + } }) } diff --git a/userland/capsule_linux/src/linux/serve/table_link.rs b/userland/capsule_linux/src/linux/serve/table_link.rs index f4a10b06a..d3c7dec77 100644 --- a/userland/capsule_linux/src/linux/serve/table_link.rs +++ b/userland/capsule_linux/src/linux/serve/table_link.rs @@ -14,8 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Links, renames, owners, times and nodes. The plain calls are their *at -//! forms at AT_FDCWD, so each property is decided in one place. +/* + * Links, renames, owners, times and nodes. The plain calls are their *at + * forms at AT_FDCWD, so each property is decided in one place. + */ use crate::linux::abi::nr_path as np; use crate::linux::file; @@ -34,9 +36,9 @@ pub fn link_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { np::CHOWN | np::LCHOWN => file::fchownat(guest, CWD, a[0], a[1], a[2]), np::FCHOWNAT => file::fchownat(guest, a[0], a[1], a[2], a[3]), np::FCHOWN => file::fchown_ids(a[1], a[2]), - np::UTIMENSAT => file::utimensat(guest, a[2]), - // A timeval cannot say "leave this time alone", so these always change one. - np::UTIME | np::UTIMES => file::utimensat(guest, 0), + np::UTIMENSAT => file::utimensat(guest, a[0], a[1], a[2], a[3]), + np::UTIMES => file::utimes(guest, a[0], a[1], true), + np::UTIME => file::utimes(guest, a[0], a[1], false), np::MKNOD => file::mknodat(guest, CWD, a[0], a[1]), np::MKNODAT => file::mknodat(guest, a[0], a[1], a[2]), _ => return None, diff --git a/userland/capsule_linux/src/linux/serve/table_meta.rs b/userland/capsule_linux/src/linux/serve/table_meta.rs new file mode 100644 index 000000000..87fc12a7f --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_meta.rs @@ -0,0 +1,45 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * Calls that only look at a file: stat and its forms, access, statfs, + * statx and readlink. Reached from the file table when it has no arm. + */ + +use crate::linux::abi::{nr, nr_path as np}; +use crate::linux::file; +use crate::linux::file::flags; +use crate::linux::guest::Guest; + +/* fstatat's AT_SYMLINK_NOFOLLOW, which lstat is. */ +const NOFOLLOW: u64 = 0x100; + +pub fn meta_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { + Some(match nr { + nr::FSTAT => file::fstat(guest, a[0], a[1]), + nr::STAT => file::newfstatat(guest, flags::AT_FDCWD, a[0], a[1], 0), + nr::LSTAT => file::newfstatat(guest, flags::AT_FDCWD, a[0], a[1], NOFOLLOW), + nr::NEWFSTATAT => file::newfstatat(guest, a[0], a[1], a[2], a[3]), + np::FACCESSAT => file::faccessat(guest, a[0], a[1], a[2], 0), + np::FACCESSAT2 => file::faccessat(guest, a[0], a[1], a[2], a[3]), + np::STATFS => file::statfs(guest, a[0], a[1]), + np::FSTATFS => file::fstatfs(guest, a[0], a[1]), + np::STATX => file::statx(guest, a[0], a[1], a[2], a[4]), + nr::ACCESS => file::access(guest, a[0], a[1]), + nr::READLINK => file::readlinkat(guest, flags::AT_FDCWD, a[0], a[1], a[2]), + _ => return super::table_data::data_ops(guest, nr, a), + }) +} diff --git a/userland/capsule_linux/src/linux/serve/table_sys.rs b/userland/capsule_linux/src/linux/serve/table_sys.rs new file mode 100644 index 000000000..53725701b --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_sys.rs @@ -0,0 +1,43 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * What the system is and what a process has used, and who it runs as; + * then the file calls in table_data. + */ + +use crate::linux::abi::nr_path as np; +use crate::linux::call; +use crate::linux::guest::Guest; + +pub fn sys_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option { + if let Some(v) = super::table_data::data_ops(guest, nr, a) { + return Some(v); + } + Some(match nr { + np::SYSINFO => call::sysinfo(guest, a[0]), + np::GETRUSAGE => call::getrusage(guest, tid, a[0], a[1]), + np::TIMES => call::times(guest, a[0]), + np::GETGROUPS => call::getgroups(a[0]), + np::SETGROUPS => call::setgroups(), + np::GETRESUID | np::GETRESGID => call::getres(guest, [a[0], a[1], a[2]]), + np::SETRESUID | np::SETRESGID => call::setres([a[0], a[1], a[2]]), + np::GETPRIORITY => call::getpriority(guest, a[0], a[1]), + np::SETPRIORITY => call::setpriority(guest, a[0], a[1], a[2]), + np::PERSONALITY => call::personality(a[0]), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/serve/waits.rs b/userland/capsule_linux/src/linux/serve/waits.rs index 6de3c6d7a..2cfc7e9cb 100644 --- a/userland/capsule_linux/src/linux/serve/waits.rs +++ b/userland/capsule_linux/src/linux/serve/waits.rs @@ -85,6 +85,7 @@ pub fn attempt(guest: &mut Guest, wait: &Blocked) -> Option { np::SELECT | np::PSELECT6 => { Some(net::select(guest, a[0], [a[1], a[2], a[3]])).filter(|&v| v != 0) } + np::FLOCK | nr::FCNTL => super::waits_lock::retry(guest, wait), _ => Some(file::epoll_wait(guest, a[0], a[1], a[2])).filter(|&v| v != 0), } } diff --git a/userland/capsule_linux/src/linux/serve/waits_lock.rs b/userland/capsule_linux/src/linux/serve/waits_lock.rs new file mode 100644 index 000000000..236e07bd0 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/waits_lock.rs @@ -0,0 +1,71 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * A lock that must wait: flock without LOCK_NB and F_SETLKW, which Linux + * blocks until the lock in the way goes. Parked like a read on an empty + * pipe (waits.rs), and tried again after every call the family makes, so + * it is taken as soon as the close, unlock or exit that frees it is served. + */ + +use nonos_libc::ForeignFrame; + +use crate::linux::abi::{errno, nr, nr_path as np}; +use crate::linux::file; +use crate::linux::guest::{Blocked, Guest}; + +use super::answer::Answer; + +pub fn wants(frame: &ForeignFrame) -> bool { + frame.nr == np::FLOCK || (frame.nr == nr::FCNTL && file::is_lock_cmd(frame.args()[1])) +} + +pub fn lock(guest: &mut Guest, frame: &ForeignFrame) -> Answer { + let (tid, nr, a) = (frame.pid, frame.nr, frame.args()); + match try_lock(guest, nr, a) { + file::LOCK_WAIT => { + guest.blocked.push(Blocked { tid, nr, args: a, deadline: None }); + Answer::Park + } + v => Answer::value(v), + } +} + +/* A parked lock call tried again: its answer once it no longer waits. */ +pub fn retry(guest: &mut Guest, wait: &Blocked) -> Option { + Some(try_lock(guest, wait.nr, wait.args)).filter(|&v| v != file::LOCK_WAIT) +} + +fn try_lock(guest: &mut Guest, nr: u64, a: [u64; 6]) -> u64 { + match nr { + np::FLOCK => file::flock(guest, a[0], a[1]), + _ => file::fcntl_lock(guest, a[0], a[1], a[2]), + } +} + +/* + * A lock call answered where nothing can park. The serve loop sends every + * lock call to `lock` above; one that arrives here anyway and would wait + * is said by name rather than answered with a value no caller knows. + */ +pub fn answer_now(v: u64) -> u64 { + if v != file::LOCK_WAIT { + return v; + } + let line = b"[LINUX] unserved lock wait: this path cannot park the caller\n"; + let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); + errno::fail(errno::ENOLCK) +} diff --git a/userland/capsule_linux_proofs/src/calls.rs b/userland/capsule_linux_proofs/src/calls.rs index 7fb933908..d383fc320 100644 --- a/userland/capsule_linux_proofs/src/calls.rs +++ b/userland/capsule_linux_proofs/src/calls.rs @@ -36,3 +36,7 @@ pub mod sigtimer_rearm; #[path = "../../capsule_linux/src/linux/call/spawn/exec_shebang.rs"] pub mod exec_shebang; + +/* The load average's arithmetic: file code, not a call, but as pure. */ +#[path = "load/mod.rs"] +pub mod loadavg; diff --git a/userland/capsule_linux_proofs/src/lib.rs b/userland/capsule_linux_proofs/src/lib.rs index 0bbaa500e..e9073627b 100644 --- a/userland/capsule_linux_proofs/src/lib.rs +++ b/userland/capsule_linux_proofs/src/lib.rs @@ -45,7 +45,7 @@ pub mod dir_children; #[path = "../../capsule_linux/src/linux/file/dirent.rs"] pub mod dirent; -#[path = "../../capsule_linux/src/linux/file/meta/statbuf.rs"] +#[path = "../../capsule_linux/src/linux/file/meta/statbuf/mod.rs"] pub mod statbuf; #[path = "../../capsule_linux/src/linux/net/host_body.rs"] diff --git a/userland/capsule_linux_proofs/src/load/mod.rs b/userland/capsule_linux_proofs/src/load/mod.rs new file mode 100644 index 000000000..74bc3829c --- /dev/null +++ b/userland/capsule_linux_proofs/src/load/mod.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * The load average's module shape: system/load/ reads its constants from a + * sibling `declared`, as it does in the capsule. + */ + +#[path = "../../../capsule_linux/src/linux/file/system/declared/mod.rs"] +pub mod declared; + +#[path = "../../../capsule_linux/src/linux/file/system/load/mod.rs"] +pub mod load; diff --git a/userland/capsule_linux_proofs/src/tests.rs b/userland/capsule_linux_proofs/src/tests.rs index c28d4a409..02684bf2c 100644 --- a/userland/capsule_linux_proofs/src/tests.rs +++ b/userland/capsule_linux_proofs/src/tests.rs @@ -34,6 +34,7 @@ mod index_tests; mod kali_anchor_tests; mod key_tests; mod listing_family_tests; +mod load_tests; mod mutation; mod mutation_tests; mod pacman_desc_tests; diff --git a/userland/capsule_linux_proofs/src/tests/load_tests.rs b/userland/capsule_linux_proofs/src/tests/load_tests.rs new file mode 100644 index 000000000..9530e7191 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/load_tests.rs @@ -0,0 +1,38 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* + * A family whose threads run all the time reads as Linux reads one task + * that never sleeps: after a minute from idle, 1 - (1884/2048)^12 on the + * one-minute average, and the longer averages behind it. One test only, + * because the averages are the family's one state. + */ + +use crate::calls::loadavg::load::{averages, exited, text}; + +#[test] +fn a_minute_busy_then_a_minute_idle_reads_as_linux_does() { + assert_eq!(averages(0, 0).map(text), ["0.00", "0.00", "0.00"]); + /* 60 s at 100 Hz, every tick run. */ + let busy = averages(60_000, 6000); + assert_eq!(busy.map(text), ["0.63", "0.18", "0.06"]); + /* A process that exited took its ticks with it; they still count. */ + exited(6000); + let idle = averages(120_000, 0); + assert_eq!(idle.map(text), ["0.23", "0.15", "0.06"]); + /* A read inside the same five seconds changes nothing. */ + assert_eq!(averages(124_999, 0), idle); +} diff --git a/userland/capsule_linux_proofs/src/tests/stat_tests.rs b/userland/capsule_linux_proofs/src/tests/stat_tests.rs index a4dc00eb4..0a5459407 100644 --- a/userland/capsule_linux_proofs/src/tests/stat_tests.rs +++ b/userland/capsule_linux_proofs/src/tests/stat_tests.rs @@ -14,10 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +/* The struct a libc reads out of fstat. */ -//! The struct a libc reads out of fstat. - -use crate::statbuf::{build, S_IFDIR, S_IFREG, STAT_LEN}; +use crate::statbuf::{blocks, build, inode, Meta, STAT_LEN}; fn u32_at(b: &[u8], at: usize) -> u32 { u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) @@ -27,46 +26,48 @@ fn u64_at(b: &[u8], at: usize) -> u64 { u64::from_le_bytes(b[at..at + 8].try_into().unwrap()) } -/// Offsets are the x86_64 layout: nlink at 16, mode at 24, size at 48, blksize -/// at 56, blocks at 64. -#[test] -fn a_regular_file_lands_in_the_right_fields() { - let s = build(4096, false, 7); - assert_eq!(s.len(), STAT_LEN); - assert_eq!(u64_at(&s, 16), 1); - assert_eq!(u32_at(&s, 24), S_IFREG | 0o644); - assert_eq!(u64_at(&s, 48), 4096); - assert_eq!(u64_at(&s, 56), 4096); - assert_eq!(u64_at(&s, 64), 8); +fn file() -> Meta { + let (mode, size, ino, nlink, rdev, dev) = (0o100640, 5000, 0xdead_beef, 2, 0x0103, 9); + Meta { mode, size, ino, nlink, rdev, dev, mtime_ms: 1_234_567, atime_ms: 7_000_001 } } +/* + * x86_64: dev 0, ino 8, nlink 16, mode 24, uid/gid 28/32, rdev 40, size 48, blksize 56, blocks 64. + */ #[test] -fn a_directory_says_so_in_the_mode() { - let s = build(0, true, 7); - assert_eq!(u32_at(&s, 24), S_IFDIR | 0o755); - assert_eq!(u64_at(&s, 48), 0); +fn each_field_lands_where_x86_64_linux_has_it() { + let s = build(&file()); + assert_eq!(s.len(), STAT_LEN); + assert_eq!(u64_at(&s, 0), 9); + assert_eq!(u64_at(&s, 8), 0xdead_beef); + assert_eq!(u64_at(&s, 16), 2); + assert_eq!(u32_at(&s, 24), 0o100640); + assert_eq!((u32_at(&s, 28), u32_at(&s, 32)), (0, 0), "owner and group are root"); + assert_eq!(u64_at(&s, 40), 0x0103); + assert_eq!(u64_at(&s, 48), 5000); + assert_eq!(u64_at(&s, 56), 4096); + assert_eq!(u64_at(&s, 64), 16); } +/* atime at 72, mtime at 88 and ctime at 104, each seconds then nanoseconds. */ #[test] -fn block_count_rounds_up_to_the_next_five_hundred_and_twelve() { - assert_eq!(u64_at(&build(1, false, 7), 64), 1); - assert_eq!(u64_at(&build(512, false, 7), 64), 1); - assert_eq!(u64_at(&build(513, false, 7), 64), 2); +fn times_are_split_into_seconds_and_nanoseconds() { + let s = build(&file()); + assert_eq!((u64_at(&s, 72), u64_at(&s, 80)), (7000, 1_000_000)); + assert_eq!((u64_at(&s, 88), u64_at(&s, 96)), (1234, 567_000_000)); + assert_eq!((u64_at(&s, 104), u64_at(&s, 112)), (1234, 567_000_000)); } +/* tmpfs counts whole pages, in 512-byte units. */ #[test] -fn everything_unknown_is_left_at_zero() { - let s = build(10, false, 7); - // st_ino at 8 is known now: `the_inode_given_is_the_inode_reported`. - for at in [0, 40, 72, 88, 104] { - assert_eq!(u64_at(&s, at), 0, "offset {at} should be untouched"); - } +fn blocks_are_whole_pages() { + assert_eq!([blocks(0), blocks(1), blocks(4096), blocks(4097)], [0, 8, 8, 16]); } +/* Every file once reported inode 0, and musl's loader took two libraries for one file. */ #[test] -fn the_inode_given_is_the_inode_reported() { - // st_ino sits after st_dev, at byte 8. Every file used to report 0, and - // musl's loader took two libraries with one inode for the same file. - assert_eq!(u64_at(&build(10, false, 0xdead_beef), 8), 0xdead_beef); - assert_ne!(u64_at(&build(10, false, 1), 8), u64_at(&build(10, false, 2), 8)); +fn an_inode_is_never_zero_and_differs_by_path() { + assert_ne!(inode(b"/lib/a.so"), inode(b"/lib/b.so")); + assert_eq!(inode(b"/"), inode(b"/")); + assert_ne!(inode(b""), 0); } diff --git a/userland/linux_guests/GuestFiles.mk b/userland/linux_guests/GuestFiles.mk index 75a418455..01163737e 100644 --- a/userland/linux_guests/GuestFiles.mk +++ b/userland/linux_guests/GuestFiles.mk @@ -41,3 +41,30 @@ LINUX_GUEST_STORE_ENTRIES += --entry /linux/lib/libprobe_bad.so=$(LINUX_GUEST_BA --entry /linux/lib/libprobe_bad.so.nonos_id_cert.bin=$(linux-guest-libprobe_CERT) \ --entry /linux/lib/libprobe_bad.so.manifest.bin=$(linux-guest-libprobe_MANIFEST) \ --entry /linux/lib/libprobe_bad.so.zk_trailer.bin=$(linux-guest-libprobe_ATTESTATION) + +# bbsuite: busybox runs 40 and more applets from a script, and what it prints +# must equal what the same busybox printed on the host through the same links +# (sh/bbsuite-host.sh). Plain data files: the programs are busybox's own. +LINUX_GUEST_BB := $(TARGET_DIR)/linux-guests/bbsuite.expect +$(LINUX_GUEST_BB): $(LINUX_GUESTS_DIR)/sh/bbsuite-body.sh $(LINUX_GUESTS_DIR)/sh/bbsuite-host.sh \ + userland/capsule_linux/guests/busybox.elf + @mkdir -p $(@D) && sh $(LINUX_GUESTS_DIR)/sh/bbsuite-host.sh \ + userland/capsule_linux/guests/busybox.elf $(LINUX_GUESTS_DIR)/sh/bbsuite-body.sh > $@ +LINUX_GUEST_STORE_DEPS += $(LINUX_GUEST_BB) +LINUX_GUEST_STORE_ENTRIES += --entry /linux/etc/bbsuite.expect=$(LINUX_GUEST_BB) \ + --entry /linux/etc/bbsuite.sh=$(LINUX_GUESTS_DIR)/sh/bbsuite.sh \ + --entry /linux/etc/bbsuite-body.sh=$(LINUX_GUESTS_DIR)/sh/bbsuite-body.sh + +# The users and groups an Alpine tree names, so ls and ps print root as root. +LINUX_GUEST_STORE_ENTRIES += --entry /linux/etc/passwd=$(LINUX_GUESTS_DIR)/etc/passwd \ + --entry /linux/etc/group=$(LINUX_GUESTS_DIR)/etc/group + +# The build host's facts, which cproc checks no /proc or /sys file names: +# its CPU model, its boot id and its name. +LINUX_GUEST_HOST_FACTS := $(TARGET_DIR)/linux-guests/cproc-host +.PHONY: $(LINUX_GUEST_HOST_FACTS) +$(LINUX_GUEST_HOST_FACTS): + @mkdir -p $(@D) && { grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //'; \ + cat /proc/sys/kernel/random/boot_id; hostname; } > $@ +LINUX_GUEST_STORE_DEPS += $(LINUX_GUEST_HOST_FACTS) +LINUX_GUEST_STORE_ENTRIES += --entry /linux/etc/cproc-host=$(LINUX_GUEST_HOST_FACTS) diff --git a/userland/linux_guests/GuestOnly.mk b/userland/linux_guests/GuestOnly.mk new file mode 100644 index 000000000..e4cdf1f32 --- /dev/null +++ b/userland/linux_guests/GuestOnly.mk @@ -0,0 +1,13 @@ +# Which guests an image carries. Included by Guests.mk after every guest. + +# The store loads at most 16 MiB and each guest's proof alone is 320 KiB, so +# an image cannot carry every guest at once. LINUX_GUEST_ONLY names the +# programs an image carries, by their names under /linux/bin; every file +# outside /linux/bin (libraries, /etc) stays. Unset, the image carries all. +ifneq ($(strip $(LINUX_GUEST_ONLY)),) +linux-guest-words := $(subst --entry ,--entry@,$(strip $(LINUX_GUEST_STORE_ENTRIES))) +linux-guest-kept := $(filter-out --entry@/linux/bin/%,$(linux-guest-words)) \ + $(foreach g,$(LINUX_GUEST_ONLY),$(filter --entry@/linux/bin/$(g)=% \ + --entry@/linux/bin/$(g).%,$(linux-guest-words))) +LINUX_GUEST_STORE_ENTRIES := $(subst --entry@,--entry ,$(linux-guest-kept)) +endif diff --git a/userland/linux_guests/GuestProofs.mk b/userland/linux_guests/GuestProofs.mk new file mode 100644 index 000000000..7d9446cee --- /dev/null +++ b/userland/linux_guests/GuestProofs.mk @@ -0,0 +1,19 @@ +# The guests that prove the file calls, /proc and Go's os against the +# host. Included by Guests.mk. + +# The file calls and the system-information calls as Linux answers them, +# part by part (cfiles). It was run on the host first through sh/oracle.sh, +# which is the oracle. +$(LINUX_GUESTS_C)/cfiles: $(wildcard $(LINUX_GUESTS_DIR)/c/cfiles/*.[ch]) + @mkdir -p $(@D) && musl-gcc -O2 -static -o $@ $(filter %.c,$^) +$(eval $(call LINUX_GUEST,cfiles,5020,5021,$(LINUX_GUESTS_C)/cfiles)) + +# /dev, /proc and /sys as a program reads them, isolation, and no host fact +# in any file (cproc, run as "cproc one two"); oracle as for cfiles. +$(LINUX_GUESTS_C)/cproc: $(wildcard $(LINUX_GUESTS_DIR)/c/cproc/*.[ch]) + @mkdir -p $(@D) && musl-gcc -O2 -static -o $@ $(filter %.c,$^) +$(eval $(call LINUX_GUEST,cproc,5022,5023,$(LINUX_GUESTS_C)/cproc)) + +# Go's os, io/fs and path/filepath with flock (goos); oracle as for cfiles. +$(GO_OUT)/goos: $(wildcard $(LINUX_GUESTS_DIR)/go/goos/*.go) +$(eval $(call LINUX_GUEST,goos,5024,5025,$(GO_OUT)/goos)) diff --git a/userland/linux_guests/Guests.mk b/userland/linux_guests/Guests.mk index 9499ab0b1..bd5bf1555 100644 --- a/userland/linux_guests/Guests.mk +++ b/userland/linux_guests/Guests.mk @@ -130,3 +130,5 @@ override NONOS_STORE_MEDIA_ENTRIES := override NONOS_STORE_DEMO_ENTRIES := include $(LINUX_GUESTS_DIR)/GuestFiles.mk +include $(LINUX_GUESTS_DIR)/GuestProofs.mk +include $(LINUX_GUESTS_DIR)/GuestOnly.mk diff --git a/userland/linux_guests/c/cfiles/cfiles.h b/userland/linux_guests/c/cfiles/cfiles.h new file mode 100644 index 000000000..4020d520e --- /dev/null +++ b/userland/linux_guests/c/cfiles/cfiles.h @@ -0,0 +1,60 @@ +/* What every part of cfiles shares: the harness and each part. */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef CFILES_H +#define CFILES_H + +#define DIR "/tmp/cfiles" + +#define CHECK(p, cond, a, b) if (!check(p, __LINE__, cond, #cond, (long)(a), (long)(b))) return + +#define ERR(p, call, e) do { errno = 0; long r_ = (long)(call); \ + if (!check(p, __LINE__, r_ == -1 && errno == (e), #call " -> " #e, r_, errno)) return; } while (0) + +extern int parts, failed; +extern char bad[512]; + +void nap_ms(long ms); +int check(const char *part, int line, int good, const char *what, long a, long b); +void done(const char *part, const char *detail); +int mk(const char *name, const char *text); +void part_pwrite(void); +void part_vec(void); +void part_sendfile(void); +void part_cfr(void); +void part_trunc(void); +void part_falloc(void); +void part_flock(void); +void part_fcntl(void); +void part_close_range(void); +void part_openat2(void); +void part_sync(void); +void part_statfs(void); +void part_xattr(void); +void part_usage(void); +void part_ids(void); + +#endif diff --git a/userland/linux_guests/c/cfiles/cfr.c b/userland/linux_guests/c/cfiles/cfr.c new file mode 100644 index 000000000..176923611 --- /dev/null +++ b/userland/linux_guests/c/cfiles/cfr.c @@ -0,0 +1,22 @@ +#include "cfiles.h" + +void part_cfr(void) { + const char *p = "copy_file_range"; + int in = mk("cfr-in", "0123456789"); + int out = mk("cfr-out", ".........."); + loff_t oi = 2, oo = 5; + CHECK(p, copy_file_range(in, &oi, out, &oo, 4, 0) == 4 && oi == 6 && oo == 9, oi, oo); + char b[11] = {0}; + pread(out, b, 10, 0); + CHECK(p, memcmp(b, ".....2345.", 10) == 0, b[5], b[8]); + CHECK(p, lseek(in, 0, SEEK_CUR) == 10 && lseek(out, 0, SEEK_CUR) == 10, 0, 0); + ERR(p, copy_file_range(in, &oi, out, &oo, 1, 1), EINVAL); + int pp[2]; + pipe(pp); + ERR(p, copy_file_range(in, 0, pp[1], 0, 1, 0), EINVAL); + close(pp[0]); + close(pp[1]); + close(in); + close(out); + done(p, "between two files at offsets; flags and a pipe refused"); +} diff --git a/userland/linux_guests/c/cfiles/close_range.c b/userland/linux_guests/c/cfiles/close_range.c new file mode 100644 index 000000000..b1f7276a0 --- /dev/null +++ b/userland/linux_guests/c/cfiles/close_range.c @@ -0,0 +1,23 @@ +#include "cfiles.h" + +#ifndef SYS_close_range +#define SYS_close_range 436 +#endif + +#define CLOSE_RANGE_CLOEXEC (1U << 2) + +void part_close_range(void) { + const char *p = "close_range"; + int base = open("/", O_RDONLY); + int a = dup(base), b = dup(base); + CHECK(p, syscall(SYS_close_range, a, b, CLOSE_RANGE_CLOEXEC) == 0, errno, 0); + CHECK(p, fcntl(a, F_GETFD) == FD_CLOEXEC && fcntl(b, F_GETFD) == FD_CLOEXEC, a, b); + CHECK(p, fcntl(base, F_GETFD) == 0, base, 0); + CHECK(p, syscall(SYS_close_range, a, ~0U, 0) == 0, errno, 0); + ERR(p, fcntl(a, F_GETFD), EBADF); + ERR(p, fcntl(b, F_GETFD), EBADF); + ERR(p, syscall(SYS_close_range, 5, 4, 0), EINVAL); + ERR(p, syscall(SYS_close_range, 3, 4, 0x80), EINVAL); + close(base); + done(p, "marks close-on-exec, closes a range, refuses a bad one"); +} diff --git a/userland/linux_guests/c/cfiles/falloc.c b/userland/linux_guests/c/cfiles/falloc.c new file mode 100644 index 000000000..0c4e72dcb --- /dev/null +++ b/userland/linux_guests/c/cfiles/falloc.c @@ -0,0 +1,28 @@ +#include "cfiles.h" + +#ifndef FALLOC_FL_COLLAPSE_RANGE +#define FALLOC_FL_COLLAPSE_RANGE 0x08 +#endif + +void part_falloc(void) { + const char *p = "fallocate"; + int fd = mk("fa", "abcdef"); + CHECK(p, fallocate(fd, 0, 4, 10) == 0, 0, 0); + struct stat st; + fstat(fd, &st); + CHECK(p, st.st_size == 14, st.st_size, 14); + CHECK(p, fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 100) == 0, 0, 0); + fstat(fd, &st); + CHECK(p, st.st_size == 14, st.st_size, 14); + CHECK(p, fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 1, 2) == 0, 0, 0); + char b[4]; + pread(fd, b, 4, 0); + CHECK(p, b[0] == 'a' && b[1] == 0 && b[2] == 0 && b[3] == 'd', b[1], b[3]); + ERR(p, fallocate(fd, FALLOC_FL_PUNCH_HOLE, 0, 1), EOPNOTSUPP); + ERR(p, fallocate(fd, FALLOC_FL_COLLAPSE_RANGE, 0, 4096), EOPNOTSUPP); + ERR(p, fallocate(fd, 0, 0, 0), EINVAL); + CHECK(p, posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL) == 0, 0, 0); + CHECK(p, syscall(SYS_fadvise64, fd, 0, 0, 99) == -1 && errno == EINVAL, errno, EINVAL); + close(fd); + done(p, "mode 0 grows, KEEP_SIZE keeps, PUNCH_HOLE zeroes, the rest refused"); +} diff --git a/userland/linux_guests/c/cfiles/fcntl.c b/userland/linux_guests/c/cfiles/fcntl.c new file mode 100644 index 000000000..29f9802dd --- /dev/null +++ b/userland/linux_guests/c/cfiles/fcntl.c @@ -0,0 +1,70 @@ +#include "cfiles.h" + +static struct flock span(short type, off_t start, off_t len) { + struct flock f; + memset(&f, 0, sizeof f); + f.l_type = type; + f.l_whence = SEEK_SET; + f.l_start = start; + f.l_len = len; + return f; +} + +void part_fcntl(void) { + const char *p = "fcntl_lock"; + int fd = mk("rl", "0123456789abcdefghij"); + struct flock f = span(F_WRLCK, 0, 10); + CHECK(p, fcntl(fd, F_SETLK, &f) == 0, errno, 0); + int ready[2], go[2]; + pipe(ready); + pipe(go); + pid_t me = getpid(); + pid_t kid = fork(); + if (kid == 0) { + int mine = open(DIR "/rl", O_RDWR); + int res = 0; + struct flock q = span(F_WRLCK, 5, 10); + fcntl(mine, F_GETLK, &q); + res |= !(q.l_type == F_WRLCK && q.l_pid == me && q.l_start == 0 && q.l_len == 10) << 0; + q = span(F_WRLCK, 5, 10); + res |= !(fcntl(mine, F_SETLK, &q) == -1 && (errno == EAGAIN || errno == EACCES)) << 1; + q = span(F_WRLCK, 10, 10); + res |= !(fcntl(mine, F_SETLK, &q) == 0) << 2; + write(ready[1], "r", 1); + q = span(F_RDLCK, 0, 5); + res |= !(fcntl(mine, F_SETLKW, &q) == 0) << 3; + char c; + read(go[0], &c, 1); + q = span(F_WRLCK, 0, 0); + res |= !(fcntl(mine, F_SETLK, &q) == 0) << 4; + _exit(res); + } + char c; + read(ready[0], &c, 1); + nap_ms(200); + int st = 0; + CHECK(p, waitpid(kid, &st, WNOHANG) == 0, st, 0); + struct flock q = span(F_RDLCK, 12, 1); + fcntl(fd, F_GETLK, &q); + CHECK(p, q.l_type == F_WRLCK && q.l_pid == kid, q.l_type, q.l_pid); + int again = open(DIR "/rl", O_RDONLY); + close(again); + write(go[1], "g", 1); + waitpid(kid, &st, 0); + CHECK(p, WIFEXITED(st) && WEXITSTATUS(st) == 0, st, 0); + q = span(F_WRLCK, 0, 0); + CHECK(p, fcntl(fd, F_SETLK, &q) == 0, errno, 0); + q = span(F_UNLCK, 0, 0); + CHECK(p, fcntl(fd, F_SETLK, &q) == 0, errno, 0); + int a = open(DIR "/rl", O_RDWR), b = open(DIR "/rl", O_RDWR); + struct flock o = span(F_WRLCK, 30, 5); + CHECK(p, fcntl(a, F_OFD_SETLK, &o) == 0, errno, 0); + o = span(F_WRLCK, 32, 1); + ERR(p, fcntl(b, F_OFD_SETLK, &o), EAGAIN); + close(a); + o = span(F_WRLCK, 32, 1); + CHECK(p, fcntl(b, F_OFD_SETLK, &o) == 0, errno, 0); + close(b); + close(fd); + done(p, "record locks conflict by range, wait, and go at close and at exit"); +} diff --git a/userland/linux_guests/c/cfiles/flock.c b/userland/linux_guests/c/cfiles/flock.c new file mode 100644 index 000000000..21f6c3892 --- /dev/null +++ b/userland/linux_guests/c/cfiles/flock.c @@ -0,0 +1,37 @@ +#include "cfiles.h" + +void part_flock(void) { + const char *p = "flock"; + int fd = mk("fl", "x"); + CHECK(p, flock(fd, LOCK_EX) == 0, 0, 0); + int dupd = dup(fd); + CHECK(p, flock(dupd, LOCK_EX | LOCK_NB) == 0, errno, 0); + int other = open(DIR "/fl", O_RDONLY); + ERR(p, flock(other, LOCK_SH | LOCK_NB), EWOULDBLOCK); + int ready[2]; + pipe(ready); + pid_t kid = fork(); + if (kid == 0) { + int mine = open(DIR "/fl", O_RDONLY); + int rc = flock(mine, LOCK_SH | LOCK_NB); + int nb = rc == -1 && errno == EWOULDBLOCK; + write(ready[1], "r", 1); + rc = flock(mine, LOCK_SH); + _exit(nb && rc == 0 ? 0 : 1); + } + char c; + read(ready[0], &c, 1); + nap_ms(200); + int st = 0; + CHECK(p, waitpid(kid, &st, WNOHANG) == 0, st, 0); + close(dupd); + CHECK(p, waitpid(kid, &st, WNOHANG) == 0, st, 0); + flock(fd, LOCK_UN); + waitpid(kid, &st, 0); + CHECK(p, WIFEXITED(st) && WEXITSTATUS(st) == 0, st, 0); + CHECK(p, flock(fd, LOCK_EX) == 0, 0, 0); + close(fd); + CHECK(p, flock(other, LOCK_EX | LOCK_NB) == 0, errno, 0); + close(other); + done(p, "shared by a dup, refused to another open, waited for, gone at close"); +} diff --git a/userland/linux_guests/c/cfiles/harness.c b/userland/linux_guests/c/cfiles/harness.c new file mode 100644 index 000000000..9c90eb281 --- /dev/null +++ b/userland/linux_guests/c/cfiles/harness.c @@ -0,0 +1,23 @@ +#include "cfiles.h" + +int parts, failed; + +char bad[512]; + +int check(const char *part, int line, int good, const char *what, long a, long b) { + if (good) { + return 1; + } + printf("[C] cfiles %s FAIL at line %d: %s (%ld, %ld)\n", part, line, what, a, b); + fflush(stdout); + failed++; + strncat(bad, " ", sizeof bad - strlen(bad) - 1); + strncat(bad, part, sizeof bad - strlen(bad) - 1); + return 0; +} + +void done(const char *part, const char *detail) { + parts++; + printf("[C] cfiles %s ok: %s\n", part, detail); + fflush(stdout); +} diff --git a/userland/linux_guests/c/cfiles/ids.c b/userland/linux_guests/c/cfiles/ids.c new file mode 100644 index 000000000..c0ca0e243 --- /dev/null +++ b/userland/linux_guests/c/cfiles/ids.c @@ -0,0 +1,26 @@ +#include "cfiles.h" + +void part_ids(void) { + const char *p = "ids"; + uid_t r = 9, e = 9, s = 9; + int rc = getresuid(&r, &e, &s); + CHECK(p, rc == 0 && r == 0 && e == 0 && s == 0, r, e); + gid_t gr = 9, ge = 9, gs = 9; + rc = getresgid(&gr, &ge, &gs); + CHECK(p, rc == 0 && gr == 0 && ge == 0 && gs == 0, gr, ge); + CHECK(p, setresuid(-1, 0, -1) == 0 && setresgid(0, -1, -1) == 0, errno, 0); + ERR(p, setresuid(1, 1, 1), EPERM); + ERR(p, setresgid(-1, 5, -1), EPERM); + gid_t g[4]; + CHECK(p, getgroups(4, g) == 0, 0, 0); + ERR(p, setgroups(0, g), EPERM); + errno = 0; + CHECK(p, getpriority(PRIO_PROCESS, 0) == 0 && errno == 0, errno, 0); + CHECK(p, setpriority(PRIO_PROCESS, 0, 5) == 0, errno, 0); + CHECK(p, getpriority(PRIO_PROCESS, 0) == 5, getpriority(PRIO_PROCESS, 0), 5); + ERR(p, setpriority(PRIO_PROCESS, 0, 2), EACCES); + CHECK(p, getpriority(PRIO_PROCESS, getpid()) == 5, 0, 0); + ERR(p, getpriority(9, 0), EINVAL); + CHECK(p, syscall(SYS_personality, 0xffffffff) == 0, 0, 0); + done(p, "root's ids with no capability to change them, groups, nice, personality"); +} diff --git a/userland/linux_guests/c/cfiles/main.c b/userland/linux_guests/c/cfiles/main.c new file mode 100644 index 000000000..020153c8a --- /dev/null +++ b/userland/linux_guests/c/cfiles/main.c @@ -0,0 +1,39 @@ +/* + * The file and system-information calls, each against what Linux answers: + * pwrite64, preadv and pwritev and their v2 forms, sendfile, + * copy_file_range, truncate and ftruncate, fallocate as tmpfs serves it, + * fadvise64, flock and fcntl record locks between processes (with a lock + * that waits and locks that go at close and at exit), close_range, openat2 + * with RESOLVE_ flags, sync, syncfs and fdatasync, the xattr calls, sysinfo, + * getrusage and times against a measured busy loop, the id and group calls, + * the priority calls and personality. Every part runs and prints; a failing + * part is named with the numbers it saw. Nothing printed depends on the + * machine, so the host's run prints the same lines. + */ + +#include "cfiles.h" + +int main(void) { + mkdir(DIR, 0755); + part_pwrite(); + part_vec(); + part_sendfile(); + part_cfr(); + part_trunc(); + part_falloc(); + part_flock(); + part_fcntl(); + part_close_range(); + part_openat2(); + part_sync(); + part_statfs(); + part_xattr(); + part_usage(); + part_ids(); + if (failed) { + printf("[C] cfiles FAIL: %d of %d parts:%s\n", failed, parts + failed, bad); + return 1; + } + printf("[C] cfiles PASS: %d parts\n", parts); + return 0; +} diff --git a/userland/linux_guests/c/cfiles/openat2.c b/userland/linux_guests/c/cfiles/openat2.c new file mode 100644 index 000000000..c2dfb5bf8 --- /dev/null +++ b/userland/linux_guests/c/cfiles/openat2.c @@ -0,0 +1,68 @@ +#include "cfiles.h" + +/* Linux's uapi, which musl's headers do not carry. */ +struct open_how { + uint64_t flags, mode, resolve; +}; + +#define RESOLVE_NO_MAGICLINKS 0x02 + +#define RESOLVE_NO_SYMLINKS 0x04 + +#define RESOLVE_BENEATH 0x08 + +#define RESOLVE_IN_ROOT 0x10 + +#ifndef SYS_openat2 +#define SYS_openat2 437 +#endif + +static long oa2(int dirfd, const char *path, uint64_t flags, uint64_t resolve) { + struct open_how how; + memset(&how, 0, sizeof how); + how.flags = flags; + how.resolve = resolve; + return syscall(SYS_openat2, dirfd, path, &how, sizeof how); +} + +void part_openat2(void) { + const char *p = "openat2"; + mkdir(DIR "/o2", 0755); + close(mk("o2/f", "x")); + symlink("f", DIR "/o2/ln"); + int d = open(DIR "/o2", O_RDONLY | O_DIRECTORY); + long fd = oa2(d, "f", O_RDONLY, 0); + CHECK(p, fd >= 0, fd, errno); + close(fd); + fd = oa2(d, "ln", O_RDONLY, RESOLVE_BENEATH); + CHECK(p, fd >= 0, fd, errno); + close(fd); + ERR(p, oa2(d, "../o2/f", O_RDONLY, RESOLVE_BENEATH), EXDEV); + ERR(p, oa2(d, "/tmp", O_RDONLY, RESOLVE_BENEATH), EXDEV); + ERR(p, oa2(d, "ln", O_RDONLY, RESOLVE_NO_SYMLINKS), ELOOP); + ERR(p, oa2(d, "/proc/self/cwd", O_RDONLY, RESOLVE_NO_MAGICLINKS), ELOOP); + fd = oa2(AT_FDCWD, "/proc/self/status", O_RDONLY, RESOLVE_NO_MAGICLINKS); + CHECK(p, fd >= 0, fd, errno); + close(fd); + /* + * IN_ROOT: the directory is /, for an absolute name, a .. past it and + * an absolute link alike. + */ + symlink("/f", DIR "/o2/abs"); + char c = 0; + fd = oa2(d, "/f", O_RDONLY, RESOLVE_IN_ROOT); + CHECK(p, fd >= 0 && read(fd, &c, 1) == 1 && c == 'x', fd, errno); + close(fd); + fd = oa2(d, "../../../f", O_RDONLY, RESOLVE_IN_ROOT); + CHECK(p, fd >= 0, fd, errno); + close(fd); + fd = oa2(d, "abs", O_RDONLY, RESOLVE_IN_ROOT); + CHECK(p, fd >= 0, fd, errno); + close(fd); + ERR(p, oa2(d, "abs", O_RDONLY, RESOLVE_IN_ROOT | RESOLVE_BENEATH), EINVAL); + struct open_how how = {.flags = O_RDONLY, .mode = 0644}; + ERR(p, syscall(SYS_openat2, d, "f", &how, sizeof how), EINVAL); + ERR(p, syscall(SYS_openat2, d, "f", &how, 8), EINVAL); + close(d); + done(p, "BENEATH, IN_ROOT, NO_SYMLINKS and NO_MAGICLINKS walk as Linux walks"); +} diff --git a/userland/linux_guests/c/cfiles/pwrite.c b/userland/linux_guests/c/cfiles/pwrite.c new file mode 100644 index 000000000..2e0485e08 --- /dev/null +++ b/userland/linux_guests/c/cfiles/pwrite.c @@ -0,0 +1,34 @@ +#include "cfiles.h" + +void part_pwrite(void) { + const char *p = "pwrite"; + int fd = mk("pw", "0123456789"); + CHECK(p, pwrite(fd, "AB", 2, 4) == 2, 0, 0); + CHECK(p, lseek(fd, 0, SEEK_CUR) == 10, lseek(fd, 0, SEEK_CUR), 10); + char b[16] = {0}; + CHECK(p, pread(fd, b, 10, 0) == 10 && memcmp(b, "0123AB6789", 10) == 0, b[4], b[5]); + CHECK(p, pwrite(fd, "Z", 1, 12) == 1, 0, 0); + struct stat st; + fstat(fd, &st); + CHECK(p, st.st_size == 13, st.st_size, 13); + CHECK(p, pread(fd, b, 3, 10) == 3 && b[0] == 0 && b[1] == 0 && b[2] == 'Z', b[0], b[2]); + ERR(p, pwrite(fd, "x", 1, -1), EINVAL); + int pp[2]; + pipe(pp); + ERR(p, pwrite(pp[1], "x", 1, 0), ESPIPE); + close(pp[0]); + close(pp[1]); + int ro = open(DIR "/pw", O_RDONLY); + ERR(p, pwrite(ro, "x", 1, 0), EBADF); + close(ro); + close(fd); + /* A file takes a whole write in one call, however large. */ + static char big[3 << 20]; + int w = mk("pwbig", 0); + CHECK(p, write(w, big, sizeof big) == sizeof big, errno, 0); + CHECK(p, pwrite(w, big, 2 << 20, 1 << 20) == 2 << 20, errno, 0); + CHECK(p, lseek(w, 0, SEEK_CUR) == sizeof big, lseek(w, 0, SEEK_CUR), 0); + close(w); + unlink(DIR "/pwbig"); + done(p, "writes at the offset, leaves the file offset, fills a gap, takes 3 MiB whole"); +} diff --git a/userland/linux_guests/c/cfiles/sendfile.c b/userland/linux_guests/c/cfiles/sendfile.c new file mode 100644 index 000000000..bec1f13cf --- /dev/null +++ b/userland/linux_guests/c/cfiles/sendfile.c @@ -0,0 +1,23 @@ +#include "cfiles.h" + +void part_sendfile(void) { + const char *p = "sendfile"; + int in = mk("sf-in", "hello sendfile world"); + int out = mk("sf-out", 0); + off_t off = 6; + CHECK(p, sendfile(out, in, &off, 8) == 8 && off == 14, off, 14); + CHECK(p, lseek(in, 0, SEEK_CUR) == 20, lseek(in, 0, SEEK_CUR), 20); + lseek(in, 0, SEEK_SET); + CHECK(p, sendfile(out, in, 0, 5) == 5 && lseek(in, 0, SEEK_CUR) == 5, lseek(in, 0, SEEK_CUR), 5); + char b[16] = {0}; + pread(out, b, 13, 0); + CHECK(p, memcmp(b, "sendfilehello", 13) == 0, b[0], b[8]); + off = 20; + CHECK(p, sendfile(out, in, &off, 5) == 0, 0, 0); + int ro = open(DIR "/sf-in", O_RDONLY); + ERR(p, sendfile(ro, in, 0, 1), EBADF); + close(ro); + close(in); + close(out); + done(p, "file to file, at an offset and at the file offset, and end of file"); +} diff --git a/userland/linux_guests/c/cfiles/shared.c b/userland/linux_guests/c/cfiles/shared.c new file mode 100644 index 000000000..f39256887 --- /dev/null +++ b/userland/linux_guests/c/cfiles/shared.c @@ -0,0 +1,16 @@ +#include "cfiles.h" + +void nap_ms(long ms) { + struct timespec ts = {ms / 1000, (ms % 1000) * 1000000}; + nanosleep(&ts, 0); +} + +int mk(const char *name, const char *text) { + char path[128]; + snprintf(path, sizeof path, DIR "/%s", name); + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd >= 0 && text) { + write(fd, text, strlen(text)); + } + return fd; +} diff --git a/userland/linux_guests/c/cfiles/statfs.c b/userland/linux_guests/c/cfiles/statfs.c new file mode 100644 index 000000000..91018fd27 --- /dev/null +++ b/userland/linux_guests/c/cfiles/statfs.c @@ -0,0 +1,56 @@ +#include "cfiles.h" + +/* f_flags bits for a mount's options, as /proc/self/mounts lists them. */ +#define ST_KNOWN (0x20 | 1 | 2 | 4 | 8 | 0x1000) + +static long mount_flags(const char *point) { + static const struct { const char *name; long bit; } opt[] = { + {"ro", 1}, {"nosuid", 2}, {"nodev", 4}, {"noexec", 8}, {"relatime", 0x1000}}; + FILE *m = fopen("/proc/self/mounts", "r"); + char line[512], at[128], opts[256]; + long f = -1; + while (m && fgets(line, sizeof line, m)) { + if (sscanf(line, "%*s %127s %*s %255s", at, opts) != 2 || strcmp(at, point) != 0) { + continue; + } + f = 0x20; + for (char *o = strtok(opts, ","); o; o = strtok(0, ",")) { + for (unsigned i = 0; i < sizeof opt / sizeof opt[0]; i++) { + f |= strcmp(o, opt[i].name) == 0 ? opt[i].bit : 0; + } + } + } + if (m) { + fclose(m); + } + return f; +} + +void part_statfs(void) { + const char *p = "statfs"; + struct statfs t, pr; + CHECK(p, statfs("/tmp", &t) == 0 && statfs("/proc", &pr) == 0, errno, 0); + CHECK(p, t.f_type == 0x01021994 && pr.f_type == 0x9fa0, t.f_type, pr.f_type); + CHECK(p, t.f_namelen == 255 && t.f_frsize == t.f_bsize, t.f_namelen, t.f_frsize); + CHECK(p, (t.f_flags & ST_KNOWN) == mount_flags("/tmp"), t.f_flags, mount_flags("/tmp")); + CHECK(p, (pr.f_flags & ST_KNOWN) == mount_flags("/proc"), pr.f_flags, mount_flags("/proc")); + int fd = mk("sf", "x"); + struct statfs f; + CHECK(p, fstatfs(fd, &f) == 0 && f.f_type == t.f_type, f.f_type, t.f_type); + close(fd); + CHECK(p, pr.f_blocks == 0 && t.f_bavail <= t.f_bfree && t.f_bfree <= t.f_blocks, 0, 0); + /* 256 KiB written takes 64 pages from the free count; unlinked, gives them back. */ + static char chunk[65536]; + memset(chunk, 'b', sizeof chunk); + int big = mk("big", 0); + CHECK(p, statfs("/tmp", &t) == 0, errno, 0); + for (int i = 0; i < 4; i++) { + CHECK(p, write(big, chunk, sizeof chunk) == sizeof chunk, errno, i); + } + CHECK(p, fsync(big) == 0 && statfs("/tmp", &f) == 0, errno, 0); + CHECK(p, t.f_bfree - f.f_bfree == 262144 / f.f_bsize, t.f_bfree, f.f_bfree); + close(big); + CHECK(p, unlink(DIR "/big") == 0 && statfs("/tmp", &f) == 0, errno, 0); + CHECK(p, f.f_bfree == t.f_bfree, f.f_bfree, t.f_bfree); + done(p, "statfs and fstatfs: type, name length, fragment size, mount flags and room"); +} diff --git a/userland/linux_guests/c/cfiles/sync.c b/userland/linux_guests/c/cfiles/sync.c new file mode 100644 index 000000000..452dc63bf --- /dev/null +++ b/userland/linux_guests/c/cfiles/sync.c @@ -0,0 +1,17 @@ +#include "cfiles.h" + +void part_sync(void) { + const char *p = "sync"; + int fd = mk("sy", "data"); + sync(); + CHECK(p, syncfs(fd) == 0, errno, 0); + CHECK(p, fdatasync(fd) == 0 && fsync(fd) == 0, errno, 0); + int pp[2]; + pipe(pp); + ERR(p, fdatasync(pp[0]), EINVAL); + ERR(p, syncfs(999), EBADF); + close(pp[0]); + close(pp[1]); + close(fd); + done(p, "sync, syncfs, fsync and fdatasync; a pipe cannot be synced"); +} diff --git a/userland/linux_guests/c/cfiles/trunc.c b/userland/linux_guests/c/cfiles/trunc.c new file mode 100644 index 000000000..85221093b --- /dev/null +++ b/userland/linux_guests/c/cfiles/trunc.c @@ -0,0 +1,24 @@ +#include "cfiles.h" + +void part_trunc(void) { + const char *p = "truncate"; + int fd = mk("tr", "abcdef"); + CHECK(p, ftruncate(fd, 3) == 0, 0, 0); + struct stat st; + fstat(fd, &st); + CHECK(p, st.st_size == 3, st.st_size, 3); + CHECK(p, ftruncate(fd, 8) == 0, 0, 0); + char b[8]; + CHECK(p, pread(fd, b, 8, 0) == 8 && b[2] == 'c' && b[3] == 0 && b[7] == 0, b[2], b[7]); + close(fd); + CHECK(p, truncate(DIR "/tr", 2) == 0, 0, 0); + stat(DIR "/tr", &st); + CHECK(p, st.st_size == 2, st.st_size, 2); + ERR(p, truncate(DIR "/nothere", 1), ENOENT); + ERR(p, truncate(DIR, 1), EISDIR); + ERR(p, truncate(DIR "/tr", -1), EINVAL); + int ro = open(DIR "/tr", O_RDONLY); + ERR(p, ftruncate(ro, 1), EINVAL); + close(ro); + done(p, "shrink and grow, by descriptor and by name"); +} diff --git a/userland/linux_guests/c/cfiles/usage.c b/userland/linux_guests/c/cfiles/usage.c new file mode 100644 index 000000000..9ce5f359c --- /dev/null +++ b/userland/linux_guests/c/cfiles/usage.c @@ -0,0 +1,60 @@ +#include "cfiles.h" + +static long spin(void) { + volatile long n = 0; + struct timespec a, z; + clock_gettime(CLOCK_MONOTONIC, &a); + do { + for (int i = 0; i < 100000; i++) { + n += i; + } + clock_gettime(CLOCK_MONOTONIC, &z); + } while ((z.tv_sec - a.tv_sec) * 1000 + (z.tv_nsec - a.tv_nsec) / 1000000 < 1500); + return n; +} + +static long ms(struct timeval t) { + return t.tv_sec * 1000 + t.tv_usec / 1000; +} + +void part_usage(void) { + const char *p = "usage"; + struct sysinfo si; + memset(&si, 0, sizeof si); + int rc = sysinfo(&si); + CHECK(p, rc == 0 && si.mem_unit == 1 && si.totalram > 0, rc, si.mem_unit); + CHECK(p, si.freeram <= si.totalram && si.procs >= 1, si.freeram, si.procs); + struct rusage before, after, kids; + getrusage(RUSAGE_SELF, &before); + spin(); + getrusage(RUSAGE_SELF, &after); + long used = ms(after.ru_utime) + ms(after.ru_stime) - ms(before.ru_utime) - ms(before.ru_stime); + CHECK(p, used >= 100, used, 100); + struct rusage th; + CHECK(p, getrusage(RUSAGE_THREAD, &th) == 0 && ms(th.ru_utime) + ms(th.ru_stime) >= 100, ms(th.ru_utime), 0); + getrusage(RUSAGE_CHILDREN, &kids); + long before_kid = ms(kids.ru_utime) + ms(kids.ru_stime); + int spun[2]; + pipe(spun); + pid_t kid = fork(); + if (kid == 0) { + spin(); + write(spun[1], "s", 1); + _exit(0); + } + char c; + read(spun[0], &c, 1); + nap_ms(300); + /* Exited but not yet waited for: Linux does not count it yet. */ + getrusage(RUSAGE_CHILDREN, &kids); + long unwaited = ms(kids.ru_utime) + ms(kids.ru_stime) - before_kid; + waitpid(kid, 0, 0); + getrusage(RUSAGE_CHILDREN, &kids); + long child = ms(kids.ru_utime) + ms(kids.ru_stime) - before_kid; + CHECK(p, unwaited == 0 && child >= 100, unwaited, child); + ERR(p, getrusage(7, &kids), EINVAL); + struct tms t; + clock_t now = times(&t); + CHECK(p, now > 0 && t.tms_utime + t.tms_stime >= 10 && t.tms_cutime + t.tms_cstime >= 10, t.tms_utime, t.tms_cutime); + done(p, "sysinfo, and getrusage and times measure a busy loop and a waited child"); +} diff --git a/userland/linux_guests/c/cfiles/vec.c b/userland/linux_guests/c/cfiles/vec.c new file mode 100644 index 000000000..d4f68e6ec --- /dev/null +++ b/userland/linux_guests/c/cfiles/vec.c @@ -0,0 +1,49 @@ +#include "cfiles.h" + +#ifndef RWF_DSYNC +#define RWF_DSYNC 0x02 +#endif + +#ifndef RWF_APPEND +#define RWF_APPEND 0x10 +#endif + +/* musl has no wrappers for the v2 forms; the offset goes as low and high words. */ +static long preadv2_(int fd, const struct iovec *v, int n, long off, int flags) { + return syscall(SYS_preadv2, fd, v, n, off, 0, flags); +} + +static long pwritev2_(int fd, const struct iovec *v, int n, long off, int flags) { + return syscall(SYS_pwritev2, fd, v, n, off, 0, flags); +} + +void part_vec(void) { + const char *p = "preadv"; + int fd = mk("vec", "abcdefghij"); + char x[3], y[4]; + struct iovec iv[2] = {{x, 3}, {y, 4}}; + CHECK(p, preadv(fd, iv, 2, 2) == 7 && memcmp(x, "cde", 3) == 0 && memcmp(y, "fghi", 4) == 0, x[0], y[0]); + CHECK(p, lseek(fd, 0, SEEK_CUR) == 10, lseek(fd, 0, SEEK_CUR), 10); + struct iovec ow[2] = {{"12", 2}, {"345", 3}}; + CHECK(p, pwritev(fd, ow, 2, 1) == 5, 0, 0); + char b[11] = {0}; + pread(fd, b, 10, 0); + CHECK(p, memcmp(b, "a12345ghij", 10) == 0, b[1], b[5]); + lseek(fd, 3, SEEK_SET); + CHECK(p, preadv2_(fd, iv, 1, -1, 0) == 3 && memcmp(x, "345", 3) == 0, x[0], 0); + CHECK(p, lseek(fd, 0, SEEK_CUR) == 6, lseek(fd, 0, SEEK_CUR), 6); + CHECK(p, pwritev2_(fd, ow, 1, 8, 0) == 2, 0, 0); + CHECK(p, lseek(fd, 0, SEEK_CUR) == 6, lseek(fd, 0, SEEK_CUR), 6); + ERR(p, preadv(fd, iv, 2, -2), EINVAL); + struct iovec tail[1] = {{"Z", 1}}; + CHECK(p, pwritev2_(fd, tail, 1, 0, RWF_APPEND | RWF_DSYNC) == 1, errno, 0); + struct stat st; + fstat(fd, &st); + CHECK(p, st.st_size == 11 && pread(fd, b, 1, 10) == 1 && b[0] == 'Z', st.st_size, b[0]); + lseek(fd, 2, SEEK_SET); + CHECK(p, pwritev2_(fd, tail, 1, -1, RWF_APPEND) == 1, errno, 0); + CHECK(p, lseek(fd, 0, SEEK_CUR) == 12, lseek(fd, 0, SEEK_CUR), 12); + ERR(p, pwritev2_(fd, tail, 1, 0, 0x10000), EOPNOTSUPP); + close(fd); + done(p, "preadv, pwritev and the v2 forms at an offset, at -1, and with RWF_ flags"); +} diff --git a/userland/linux_guests/c/cfiles/xattr.c b/userland/linux_guests/c/cfiles/xattr.c new file mode 100644 index 000000000..a1455f4b8 --- /dev/null +++ b/userland/linux_guests/c/cfiles/xattr.c @@ -0,0 +1,22 @@ +#include "cfiles.h" + +void part_xattr(void) { + const char *p = "xattr"; + close(mk("xa", "x")); + const char *f = DIR "/xa"; + char b[32]; + ERR(p, getxattr(f, "user.k", b, sizeof b), ENODATA); + CHECK(p, listxattr(f, b, sizeof b) == 0, 0, 0); + CHECK(p, setxattr(f, "user.k", "val", 3, 0) == 0, errno, 0); + CHECK(p, getxattr(f, "user.k", 0, 0) == 3, 0, 0); + CHECK(p, getxattr(f, "user.k", b, sizeof b) == 3 && memcmp(b, "val", 3) == 0, b[0], 0); + ERR(p, getxattr(f, "user.k", b, 1), ERANGE); + ERR(p, setxattr(f, "user.k", "v", 1, XATTR_CREATE), EEXIST); + ERR(p, setxattr(f, "user.none", "v", 1, XATTR_REPLACE), ENODATA); + CHECK(p, listxattr(f, b, sizeof b) == 7 && strcmp(b, "user.k") == 0, 0, 0); + ERR(p, setxattr(f, "nonamespace", "v", 1, 0), EOPNOTSUPP); + CHECK(p, removexattr(f, "user.k") == 0, errno, 0); + ERR(p, removexattr(f, "user.k"), ENODATA); + ERR(p, getxattr(DIR "/nothere", "user.k", b, sizeof b), ENOENT); + done(p, "set, get, list and remove, with Linux's flags and errors"); +} diff --git a/userland/linux_guests/c/cproc/cproc.h b/userland/linux_guests/c/cproc/cproc.h new file mode 100644 index 000000000..65351505d --- /dev/null +++ b/userland/linux_guests/c/cproc/cproc.h @@ -0,0 +1,51 @@ +/* What every part of cproc shares: the harness and each part. */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef CPROC_H +#define CPROC_H + +#define CHECK(p, cond, a, b) if (!check(p, __LINE__, cond, #cond, (long)(a), (long)(b))) return + +extern int parts, failed; +extern char bad[512]; +extern char **args; + +int check(const char *part, int line, int good, const char *what, long a, long b); +void done(const char *part, const char *detail); +char *slurp(const char *path, long *n); +long field_of(const char *text, const char *key); +void spin_ms(long ms); +int is_nonos(void); +void part_dev(void); +void part_self(void); +void part_maps(void); +void part_system(void); +void part_stat(void); +void part_load(void); +void part_memory(void); +void part_isolation(void); +void part_mem(void); +void part_facts(void); + +#endif diff --git a/userland/linux_guests/c/cproc/dev.c b/userland/linux_guests/c/cproc/dev.c new file mode 100644 index 000000000..66e083fed --- /dev/null +++ b/userland/linux_guests/c/cproc/dev.c @@ -0,0 +1,38 @@ +#include "cproc.h" + +void part_dev(void) { + const char *p = "dev"; + const struct { const char *name; int major, minor; } devs[] = { + {"/dev/null", 1, 3}, {"/dev/zero", 1, 5}, {"/dev/full", 1, 7}, + {"/dev/random", 1, 8}, {"/dev/urandom", 1, 9}, {"/dev/tty", 5, 0}, + }; + for (unsigned i = 0; i < sizeof devs / sizeof devs[0]; i++) { + struct stat st; + CHECK(p, stat(devs[i].name, &st) == 0 && S_ISCHR(st.st_mode), i, errno); + CHECK(p, major(st.st_rdev) == (unsigned)devs[i].major && minor(st.st_rdev) == (unsigned)devs[i].minor, i, st.st_rdev); + CHECK(p, (st.st_mode & 0777) == 0666, i, st.st_mode); + } + char b[32]; + int fd = open("/dev/null", O_RDWR); + CHECK(p, read(fd, b, sizeof b) == 0 && write(fd, "x", 1) == 1, fd, errno); + close(fd); + fd = open("/dev/zero", O_RDONLY); + memset(b, 7, sizeof b); + CHECK(p, read(fd, b, sizeof b) == 32 && b[0] == 0 && b[31] == 0, b[0], b[31]); + close(fd); + fd = open("/dev/full", O_WRONLY); + errno = 0; + CHECK(p, write(fd, "x", 1) == -1 && errno == ENOSPC, errno, ENOSPC); + close(fd); + char c[32]; + fd = open("/dev/urandom", O_RDONLY); + CHECK(p, read(fd, b, 32) == 32 && read(fd, c, 32) == 32 && memcmp(b, c, 32) != 0, 0, 0); + close(fd); + errno = 0; + CHECK(p, open("/dev/tty", O_RDWR) == -1 && errno == ENXIO, errno, ENXIO); + char to[64] = {0}; + CHECK(p, readlink("/dev/stdin", to, sizeof to) == 15 && strcmp(to, "/proc/self/fd/0") == 0, 0, 0); + memset(to, 0, sizeof to); + CHECK(p, readlink("/dev/fd", to, sizeof to) == 13 && strcmp(to, "/proc/self/fd") == 0, 0, 0); + done(p, "null zero full random urandom tty: numbers, modes, reads and writes"); +} diff --git a/userland/linux_guests/c/cproc/facts.c b/userland/linux_guests/c/cproc/facts.c new file mode 100644 index 000000000..ff88219dd --- /dev/null +++ b/userland/linux_guests/c/cproc/facts.c @@ -0,0 +1,67 @@ +#include "cproc.h" + +/* + * The strings no file may hold: the machine's own CPU brand, as CPUID gives + * it, and each line of /etc/cproc-host, the build host's facts. + */ +static int forbidden(char out[][128]) { + unsigned r[12]; + int n = 0; + if (__get_cpuid(0x80000002, &r[0], &r[1], &r[2], &r[3]) && + __get_cpuid(0x80000003, &r[4], &r[5], &r[6], &r[7]) && + __get_cpuid(0x80000004, &r[8], &r[9], &r[10], &r[11])) { + char brand[49] = {0}; + memcpy(brand, r, 48); + char *s = brand; + while (*s == ' ') s++; + for (char *e = s + strlen(s); e > s && e[-1] == ' '; *--e = 0) {} + if (strlen(s) > 4) { + strcpy(out[n++], s); + } + } + long len; + char *host = slurp("/etc/cproc-host", &len); + for (char *line = host; line && *line && n < 8;) { + char *end = strchr(line, '\n'); + int l = end ? end - line : (int)strlen(line); + if (l > 4 && l < 127) { + memcpy(out[n], line, l); + out[n++][l] = 0; + } + line = end ? end + 1 : 0; + } + return n; +} + +void part_facts(void) { + const char *p = "facts"; + const char *files[] = { + "/proc/cpuinfo", "/proc/meminfo", "/proc/stat", "/proc/uptime", "/proc/loadavg", + "/proc/version", "/proc/filesystems", "/proc/self/status", "/proc/self/stat", + "/proc/self/maps", "/proc/self/mounts", "/proc/self/mountinfo", "/proc/self/cgroup", + "/proc/self/limits", "/proc/sys/kernel/hostname", "/proc/sys/kernel/osrelease", + "/proc/sys/kernel/random/boot_id", "/proc/sys/kernel/ostype", + "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", + }; + char bad_strings[8][128]; + int nb = forbidden(bad_strings), found = 0, read_ok = 0; + for (unsigned i = 0; i < sizeof files / sizeof files[0]; i++) { + long n; + char *text = slurp(files[i], &n); + read_ok += n >= 0; + for (int j = 0; text && j < nb; j++) { + if (strstr(text, bad_strings[j])) { + printf("[C] cproc facts: %s names \"%s\"\n", files[i], bad_strings[j]); + found++; + } + } + } + CHECK(p, nb >= 1 && read_ok == (int)(sizeof files / sizeof files[0]), nb, read_ok); + if (is_nonos()) { + CHECK(p, found == 0, found, nb); + done(p, "no file names the machine's CPU or the build host"); + return; + } + CHECK(p, found > 0, found, nb); + done(p, "Linux's own files name its CPU and host, as they should"); +} diff --git a/userland/linux_guests/c/cproc/harness.c b/userland/linux_guests/c/cproc/harness.c new file mode 100644 index 000000000..0aa076e42 --- /dev/null +++ b/userland/linux_guests/c/cproc/harness.c @@ -0,0 +1,25 @@ +#include "cproc.h" + +int parts, failed; + +char bad[512]; + +char **args; + +int check(const char *part, int line, int good, const char *what, long a, long b) { + if (good) { + return 1; + } + printf("[C] cproc %s FAIL at line %d: %s (%ld, %ld)\n", part, line, what, a, b); + fflush(stdout); + failed++; + strncat(bad, " ", sizeof bad - strlen(bad) - 1); + strncat(bad, part, sizeof bad - strlen(bad) - 1); + return 0; +} + +void done(const char *part, const char *detail) { + parts++; + printf("[C] cproc %s ok: %s\n", part, detail); + fflush(stdout); +} diff --git a/userland/linux_guests/c/cproc/isolation.c b/userland/linux_guests/c/cproc/isolation.c new file mode 100644 index 000000000..029b70adf --- /dev/null +++ b/userland/linux_guests/c/cproc/isolation.c @@ -0,0 +1,61 @@ +#include "cproc.h" + +static int proc_pids(void) { + int n = 0; + struct stat st; + char path[32]; + for (int pid = 1; pid <= 4096; pid++) { + snprintf(path, sizeof path, "/proc/%d", pid); + n += stat(path, &st) == 0; + } + return n; +} + +void part_isolation(void) { + const char *p = "isolation"; + CHECK(p, proc_pids() == 1, proc_pids(), 1); + int go[2]; + pipe(go); + pid_t me = getpid(); + pid_t kid = fork(); + if (kid == 0) { + char c; + read(go[0], &c, 1); + _exit(getppid() == me ? 0 : 1); + } + int with_kid = proc_pids(); + char path[64]; + snprintf(path, sizeof path, "/proc/%d/stat", kid); + long n; + char *kst = slurp(path, &n); + int ppid = 0; + if (kst) { + sscanf(strrchr(kst, ')') + 4, "%d", &ppid); + } + write(go[1], "g", 1); + int status = -1; + waitpid(kid, &status, 0); + CHECK(p, with_kid == 2 && ppid == getpid(), with_kid, ppid); + CHECK(p, WIFEXITED(status) && WEXITSTATUS(status) == 0, status, 0); + errno = 0; + CHECK(p, open("/proc/99999/stat", O_RDONLY) == -1 && errno == ENOENT, errno, 0); + struct stat root, st; + stat("/", &root); + CHECK(p, stat("/proc/self/root/..", &st) == 0 && st.st_ino == root.st_ino, st.st_ino, root.st_ino); + symlink("/../../../..", "/tmp/cproc-esc"); + CHECK(p, stat("/tmp/cproc-esc", &st) == 0 && st.st_ino == root.st_ino, st.st_ino, root.st_ino); + CHECK(p, chdir("/../../../..") == 0, errno, 0); + char cwd[64]; + CHECK(p, getcwd(cwd, sizeof cwd) && strcmp(cwd, "/") == 0, 0, 0); + /* An absolute name is taken from the root, wherever the process is. */ + CHECK(p, chdir("/proc") == 0 && chdir("/tmp") == 0 && getcwd(cwd, sizeof cwd), errno, 0); + CHECK(p, strcmp(cwd, "/tmp") == 0, cwd[1], 0); + int d = open("/tmp", O_RDONLY | O_DIRECTORY); + chdir("/proc"); + int fd = openat(d, "cproc-at", O_WRONLY | O_CREAT, 0600); + CHECK(p, fd >= 0 && stat("/tmp/cproc-at", &st) == 0, fd, errno); + close(fd); + close(d); + chdir("/"); + done(p, "only the family's pids, a child's parent as getppid says, no way above the root"); +} diff --git a/userland/linux_guests/c/cproc/load.c b/userland/linux_guests/c/cproc/load.c new file mode 100644 index 000000000..562dfddd0 --- /dev/null +++ b/userland/linux_guests/c/cproc/load.c @@ -0,0 +1,14 @@ +#include "cproc.h" + +void part_load(void) { + const char *p = "load"; + spin_ms(11000); + long n; + double l1 = -1; + sscanf(slurp("/proc/loadavg", &n), "%lf", &l1); + struct sysinfo si; + sysinfo(&si); + long a = (long)(l1 * 100 + 0.5), b = (long)(si.loads[0] * 100 / 65536); + CHECK(p, a > 0 && b > 0 && a - b <= 5 && b - a <= 5, a, b); + done(p, "eleven seconds on the CPU show in loadavg and in sysinfo's loads alike"); +} diff --git a/userland/linux_guests/c/cproc/main.c b/userland/linux_guests/c/cproc/main.c new file mode 100644 index 000000000..2b8b3597b --- /dev/null +++ b/userland/linux_guests/c/cproc/main.c @@ -0,0 +1,38 @@ +/* + * /dev, /proc and /sys as a Linux program reads them: the devices and their + * numbers, the program's own /proc directory against what the calls say, + * its threads under task/, the system files against sysinfo and uname, the + * one /sys file Go reads, and isolation: only the family's own pids exist + * under /proc, no path climbs out of the root, a directory descriptor keeps + * its place across a chdir, /proc/self/mem is refused, and no file under + * /proc or /sys names the machine's CPU, or the build host's CPU, boot id + * or name (/etc/cproc-host). Run with the arguments "one two". Every part + * runs and prints; the two whose answers differ from Linux by design (mem, + * facts) say which answer they expected. + */ + +#include "cproc.h" + +int main(int argc, char **argv) { + args = argv; + if (argc != 3 || strcmp(argv[1], "one") || strcmp(argv[2], "two")) { + printf("[C] cproc FAIL: run as cproc one two\n"); + return 1; + } + part_dev(); + part_self(); + part_maps(); + part_system(); + part_stat(); + part_load(); + part_memory(); + part_isolation(); + part_mem(); + part_facts(); + if (failed) { + printf("[C] cproc FAIL: %d of %d parts:%s\n", failed, parts + failed, bad); + return 1; + } + printf("[C] cproc PASS: %d parts\n", parts); + return 0; +} diff --git a/userland/linux_guests/c/cproc/maps.c b/userland/linux_guests/c/cproc/maps.c new file mode 100644 index 000000000..20dd04f26 --- /dev/null +++ b/userland/linux_guests/c/cproc/maps.c @@ -0,0 +1,68 @@ +#include "cproc.h" + +static volatile int stop; + +static volatile pid_t helper_tid; + +static void *helper(void *arg) { + (void)arg; + helper_tid = syscall(SYS_gettid); + while (!stop) { + struct timespec ts = {0, 20000000}; + nanosleep(&ts, 0); + } + return 0; +} + +void part_maps(void) { + const char *p = "maps"; + long n; + char *maps = slurp("/proc/self/maps", &n); + uintptr_t local = (uintptr_t)&n; + int stack = 0, lines = 0; + for (char *line = maps; line && *line; line = strchr(line, '\n') ? strchr(line, '\n') + 1 : 0) { + unsigned long lo, hi; + char perms[5]; + if (sscanf(line, "%lx-%lx %4s", &lo, &hi, perms) != 3) { + break; + } + lines++; + stack |= lo <= local && local < hi && perms[0] == 'r' && perms[1] == 'w'; + } + CHECK(p, lines >= 3 && stack, lines, stack); + char *statm = slurp("/proc/self/statm", &n); + long f[7]; + CHECK(p, sscanf(statm, "%ld %ld %ld %ld %ld %ld %ld", &f[0], &f[1], &f[2], &f[3], &f[4], &f[5], &f[6]) == 7 && f[0] > 0, f[0], 0); + char *limits = slurp("/proc/self/limits", &n); + struct rlimit rl; + getrlimit(RLIMIT_NOFILE, &rl); + char want[128]; + snprintf(want, sizeof want, "Max open files %-20lu %-20lu files", (unsigned long)rl.rlim_cur, (unsigned long)rl.rlim_max); + CHECK(p, strstr(limits, want) != 0, rl.rlim_cur, 0); + CHECK(p, strstr(slurp("/proc/self/mounts", &n), " /proc proc ") != 0, n, 0); + CHECK(p, strstr(slurp("/proc/self/mountinfo", &n), " /proc ") != 0, n, 0); + /* v1 or v2, each line is id:controllers:/path. */ + char *cg = slurp("/proc/self/cgroup", &n); + char *c1 = strchr(cg, ':'), *c2 = c1 ? strchr(c1 + 1, ':') : 0; + CHECK(p, n > 0 && c2 && c2[1] == '/' && cg[n - 1] == '\n', n, 0); + pthread_t t; + pthread_create(&t, 0, helper, 0); + while (!helper_tid) { + sched_yield(); + } + int tasks = 0; + DIR *d = opendir("/proc/self/task"); + for (struct dirent *e; d && (e = readdir(d));) { + tasks += atoi(e->d_name) > 0; + } + closedir(d); + char path[96]; + snprintf(path, sizeof path, "/proc/self/task/%d/stat", (int)helper_tid); + char *tstat = slurp(path, &n); + int tid_ok = tstat && atoi(tstat) == helper_tid; + long threads = field_of(slurp("/proc/self/status", &n), "\nThreads:\t"); + stop = 1; + pthread_join(t, 0); + CHECK(p, tasks == 2 && tid_ok && threads == 2, tasks, threads); + done(p, "maps holds the stack, statm, limits, mounts, cgroup, and task/ has each thread"); +} diff --git a/userland/linux_guests/c/cproc/mem.c b/userland/linux_guests/c/cproc/mem.c new file mode 100644 index 000000000..6999f852e --- /dev/null +++ b/userland/linux_guests/c/cproc/mem.c @@ -0,0 +1,15 @@ +#include "cproc.h" + +void part_mem(void) { + const char *p = "mem"; + errno = 0; + int fd = open("/proc/self/mem", O_RDONLY); + if (is_nonos()) { + CHECK(p, fd == -1 && errno == EACCES, fd, errno); + done(p, "refused, as NONOS refuses a second way into memory"); + return; + } + CHECK(p, fd >= 0, fd, errno); + close(fd); + done(p, "opened, as Linux opens it for the process itself"); +} diff --git a/userland/linux_guests/c/cproc/memory.c b/userland/linux_guests/c/cproc/memory.c new file mode 100644 index 000000000..5d6866bf5 --- /dev/null +++ b/userland/linux_guests/c/cproc/memory.c @@ -0,0 +1,20 @@ +#include "cproc.h" + +void part_memory(void) { + const char *p = "memory"; + /* 4 MiB, so the host's own traffic in Shmem cannot hide it. */ + static char chunk[4 << 20]; + memset(chunk, 'm', sizeof chunk); + long n, before = field_of(slurp("/proc/meminfo", &n), "\nShmem:"); + int fd = open("/tmp/cproc-shm", O_RDWR | O_CREAT | O_TRUNC, 0600); + CHECK(p, write(fd, chunk, sizeof chunk) == sizeof chunk, errno, 0); + char *info = slurp("/proc/meminfo", &n); + long after = field_of(info, "\nShmem:"), cached = field_of(info, "\nCached:"); + CHECK(p, after - before >= 3072 && cached >= after, after - before, cached); + struct sysinfo si; + sysinfo(&si); + CHECK(p, (long)(si.sharedram * si.mem_unit / 1024) >= 3072, (long)si.sharedram, 0); + close(fd); + unlink("/tmp/cproc-shm"); + done(p, "a tmpfs file's bytes show as Shmem and Cached, and as sysinfo's sharedram"); +} diff --git a/userland/linux_guests/c/cproc/self.c b/userland/linux_guests/c/cproc/self.c new file mode 100644 index 000000000..334f90885 --- /dev/null +++ b/userland/linux_guests/c/cproc/self.c @@ -0,0 +1,57 @@ +#include "cproc.h" + +void part_self(void) { + const char *p = "self"; + char to[256] = {0}, path[128]; + long n; + CHECK(p, readlink("/proc/self", to, sizeof to) > 0 && atoi(to) == getpid(), atoi(to), getpid()); + char *stat = slurp("/proc/self/stat", &n); + CHECK(p, stat && atoi(stat) == getpid() && strstr(stat, "(cproc) R ") != 0, n, 0); + int ppid = 0, threads = 0; + char *after = strrchr(stat, ')'); + sscanf(after + 4, "%d", &ppid); + { int i = 0; char *q = after + 2; while (i < 17 && q) { q = strchr(q + 1, ' '); i++; } if (q) threads = atoi(q + 1); } + CHECK(p, ppid == getppid() && threads == 1, ppid, threads); + char *status = slurp("/proc/self/status", &n); + CHECK(p, strstr(status, "Name:\tcproc\n") && field_of(status, "\nPid:\t") == getpid(), 0, 0); + CHECK(p, field_of(status, "\nPPid:\t") == getppid() && field_of(status, "\nThreads:\t") == 1, 0, 0); + CHECK(p, strstr(status, "\nUid:\t0\t0\t0\t0\n") != 0, 0, 0); + char *cmd = slurp("/proc/self/cmdline", &n); + CHECK(p, n > 0 && strcmp(cmd + strlen(cmd) + 1, "one") == 0, n, 0); + char *env = slurp("/proc/self/environ", &n); + int home = 0; + for (long i = 0; i < n; i += strlen(env + i) + 1) { + home |= strcmp(env + i, "HOME=/root") == 0; + } + CHECK(p, home, n, 0); + CHECK(p, strcmp(slurp("/proc/self/comm", &n), "cproc\n") == 0, n, 0); + memset(to, 0, sizeof to); + CHECK(p, readlink("/proc/self/exe", to, sizeof to) > 0 && strcmp(strrchr(to, '/'), "/cproc") == 0, 0, 0); + char cwd[256]; + getcwd(cwd, sizeof cwd); + memset(to, 0, sizeof to); + CHECK(p, readlink("/proc/self/cwd", to, sizeof to) > 0 && strcmp(to, cwd) == 0, 0, 0); + int fd = open("/tmp/cproc-file", O_RDWR | O_CREAT | O_TRUNC, 0600); + write(fd, "abcdef", 6); + lseek(fd, 4, SEEK_SET); + snprintf(path, sizeof path, "/proc/self/fd/%d", fd); + memset(to, 0, sizeof to); + CHECK(p, readlink(path, to, sizeof to) > 0 && strcmp(to, "/tmp/cproc-file") == 0, fd, 0); + snprintf(path, sizeof path, "/proc/self/fdinfo/%d", fd); + char *info = slurp(path, &n); + CHECK(p, info && field_of(info, "pos:\t") == 4 && strstr(info, "flags:\t0") != 0, n, 0); + int seen = 0; + DIR *d = opendir("/proc/self/fd"); + for (struct dirent *e; d && (e = readdir(d));) { + seen += atoi(e->d_name) == fd || strcmp(e->d_name, "0") == 0; + } + closedir(d); + CHECK(p, seen == 2, seen, fd); + snprintf(path, sizeof path, "/proc/self/fd/%d", fd); + int again = open(path, O_RDONLY); + char b[4] = {0}; + CHECK(p, again >= 0 && read(again, b, 3) == 3 && memcmp(b, "abc", 3) == 0, again, errno); + close(again); + close(fd); + done(p, "stat, status, cmdline, environ, comm, exe, cwd, fd and fdinfo"); +} diff --git a/userland/linux_guests/c/cproc/shared.c b/userland/linux_guests/c/cproc/shared.c new file mode 100644 index 000000000..d009f6a26 --- /dev/null +++ b/userland/linux_guests/c/cproc/shared.c @@ -0,0 +1,42 @@ +#include "cproc.h" + +/* The whole file, NUL-terminated; its length in *n. */ +char *slurp(const char *path, long *n) { + static char buf[1 << 16]; + int fd = open(path, O_RDONLY); + *n = -1; + if (fd < 0) { + return 0; + } + long got = 0, r; + while ((r = read(fd, buf + got, sizeof buf - 1 - got)) > 0) { + got += r; + } + close(fd); + buf[got] = 0; + *n = got; + return buf; +} + +long field_of(const char *text, const char *key) { + const char *at = strstr(text, key); + return at ? strtol(at + strlen(key), 0, 10) : -1; +} + +void spin_ms(long ms) { + struct timespec t0, t; + clock_gettime(CLOCK_MONOTONIC, &t0); + volatile unsigned long x = 0; + do { + for (int i = 0; i < 100000; i++) { + x += i; + } + clock_gettime(CLOCK_MONOTONIC, &t); + } while ((t.tv_sec - t0.tv_sec) * 1000 + (t.tv_nsec - t0.tv_nsec) / 1000000 < ms); +} + +int is_nonos(void) { + struct utsname u; + uname(&u); + return strstr(u.version, "NONOS") != 0; +} diff --git a/userland/linux_guests/c/cproc/stat.c b/userland/linux_guests/c/cproc/stat.c new file mode 100644 index 000000000..ce0f35290 --- /dev/null +++ b/userland/linux_guests/c/cproc/stat.c @@ -0,0 +1,51 @@ +#include "cproc.h" + +static int inited = 7; + +/* The n-th field of /proc/self/stat, numbered as proc(5) numbers them. */ +static unsigned long long stat_field(int n) { + long len; + char *q = strrchr(slurp("/proc/self/stat", &len), ')') + 2; + for (int i = 3; i < n && q; i++) { + q = strchr(q, ' '); + q = q ? q + 1 : 0; + } + return q ? strtoull(q, 0, 10) : 0; +} + +void part_stat(void) { + const char *p = "stat"; + unsigned long long code0 = stat_field(26), code1 = stat_field(27); + unsigned long here = (unsigned long)&part_stat, data = (unsigned long)&inited; + CHECK(p, code0 <= here && here < code1, code0, code1); + CHECK(p, (unsigned long)args == stat_field(28) + 8, (long)args, stat_field(28)); + CHECK(p, stat_field(45) <= data && data < stat_field(46), stat_field(45), stat_field(46)); + int fds[2]; + CHECK(p, pipe(fds) == 0, errno, 0); + pid_t c = fork(); + if (c == 0) { + /* A child that waits for a grandchild: Linux counts both to us. */ + pid_t g = fork(); + if (g == 0) { + struct rusage r; + spin_ms(500); + getrusage(RUSAGE_SELF, &r); + unsigned long long mine[2] = {stat_field(10), r.ru_nvcsw + r.ru_nivcsw}; + _exit(write(fds[1], mine, sizeof mine) != sizeof mine); + } + _exit(g < 0 || waitpid(g, 0, 0) != g); + } + int st = -1; + unsigned long long mine[2] = {0, 0}; + CHECK(p, c > 0 && waitpid(c, &st, 0) == c && st == 0, c, st); + CHECK(p, read(fds[0], mine, sizeof mine) == sizeof mine, errno, 0); + close(fds[0]); + close(fds[1]); + struct rusage kids; + getrusage(RUSAGE_CHILDREN, &kids); + unsigned long long cut = stat_field(16) + stat_field(17), cmin = stat_field(11); + CHECK(p, cut >= 20 && cmin >= mine[0], cut, cmin); + unsigned long long switched = kids.ru_nvcsw + kids.ru_nivcsw; + CHECK(p, switched >= mine[1], switched, mine[1]); + done(p, "startcode, endcode, startstack, start_data, end_data, and the times, faults and switches of a waited child and of the grandchild it waited for"); +} diff --git a/userland/linux_guests/c/cproc/system.c b/userland/linux_guests/c/cproc/system.c new file mode 100644 index 000000000..a94283623 --- /dev/null +++ b/userland/linux_guests/c/cproc/system.c @@ -0,0 +1,51 @@ +#include "cproc.h" + +void part_system(void) { + const char *p = "system"; + long n; + struct sysinfo si; + sysinfo(&si); + long total = field_of(slurp("/proc/meminfo", &n), "MemTotal:"); + CHECK(p, total == (long)(si.totalram * si.mem_unit / 1024), total, si.totalram); + cpu_set_t set; + sched_getaffinity(0, sizeof set, &set); + int cpus = 0; + char *info = slurp("/proc/cpuinfo", &n); + for (char *at = info; (at = strstr(at, "processor\t:")); at++) { + cpus++; + } + CHECK(p, cpus == CPU_COUNT(&set) && cpus == get_nprocs(), cpus, CPU_COUNT(&set)); + double up = -1, idle = -1; + sscanf(slurp("/proc/uptime", &n), "%lf %lf", &up, &idle); + CHECK(p, up > 0 && idle >= 0, (long)up, (long)idle); + double l1, l5, l15; + int run, all, last; + CHECK(p, sscanf(slurp("/proc/loadavg", &n), "%lf %lf %lf %d/%d %d", &l1, &l5, &l15, &run, &all, &last) == 6 && run >= 1 && all >= run, run, all); + struct utsname u; + uname(&u); + char want[160]; + snprintf(want, sizeof want, "Linux version %s ", u.release); + CHECK(p, strncmp(slurp("/proc/version", &n), want, strlen(want)) == 0, n, 0); + CHECK(p, strstr(slurp("/proc/filesystems", &n), "\tproc\n") != 0, n, 0); + snprintf(want, sizeof want, "%s\n", u.sysname); + CHECK(p, strcmp(slurp("/proc/sys/kernel/ostype", &n), want) == 0, n, 0); + snprintf(want, sizeof want, "%s\n", u.release); + CHECK(p, strcmp(slurp("/proc/sys/kernel/osrelease", &n), want) == 0, n, 0); + snprintf(want, sizeof want, "%s\n", u.nodename); + CHECK(p, strcmp(slurp("/proc/sys/kernel/hostname", &n), want) == 0, n, 0); + CHECK(p, atol(slurp("/proc/sys/kernel/pid_max", &n)) >= getpid(), n, 0); + char boot[64], uuid[64]; + strcpy(boot, slurp("/proc/sys/kernel/random/boot_id", &n)); + CHECK(p, n == 37 && strcmp(boot, slurp("/proc/sys/kernel/random/boot_id", &n)) == 0, n, 0); + strcpy(uuid, slurp("/proc/sys/kernel/random/uuid", &n)); + CHECK(p, n == 37 && strcmp(uuid, slurp("/proc/sys/kernel/random/uuid", &n)) != 0, n, 0); + long over = atol(slurp("/proc/sys/vm/overcommit_memory", &n)); + CHECK(p, over >= 0 && over <= 2 && atol(slurp("/proc/sys/fs/pipe-max-size", &n)) > 0, over, 0); + CHECK(p, strcmp(slurp("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", &n), "2097152\n") == 0, n, 0); + struct stat st; + CHECK(p, stat("/proc", &st) == 0 && S_ISDIR(st.st_mode), errno, 0); + CHECK(p, stat("/proc/meminfo", &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 0, st.st_size, 0); + errno = 0; + CHECK(p, open("/sys/kernel/nothere", O_RDONLY) == -1 && errno == ENOENT, errno, 0); + done(p, "meminfo, cpuinfo, uptime, loadavg, version, sys/kernel, sys/vm, hugepage size"); +} diff --git a/userland/linux_guests/etc/group b/userland/linux_guests/etc/group new file mode 100644 index 000000000..18acc30a0 --- /dev/null +++ b/userland/linux_guests/etc/group @@ -0,0 +1 @@ +root:x:0:root diff --git a/userland/linux_guests/etc/passwd b/userland/linux_guests/etc/passwd new file mode 100644 index 000000000..eb85a552a --- /dev/null +++ b/userland/linux_guests/etc/passwd @@ -0,0 +1 @@ +root:x:0:0:root:/root:/bin/sh diff --git a/userland/linux_guests/go/goos/files.go b/userland/linux_guests/go/goos/files.go new file mode 100644 index 000000000..5ff1cf579 --- /dev/null +++ b/userland/linux_guests/go/goos/files.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "io" + "io/fs" + "os" + "time" +) + +func files() { + p := "files" + src := root + "/src" + data := bytes.Repeat([]byte("0123456789"), 1000) + os.WriteFile(src, data, 0o644) + in, _ := os.Open(src) + out, _ := os.Create(root + "/dst") + n, err := io.Copy(out, in) + in.Close() + out.Close() + got, _ := os.ReadFile(root + "/dst") + if !check(p, err == nil && n == 10000 && bytes.Equal(got, data), "io.Copy", n, err) { + return + } + if !check(p, os.Chmod(src, 0o600) == nil, "Chmod") { + return + } + st, _ := os.Stat(src) + if !check(p, st.Mode().Perm() == 0o600 && st.Size() == 10000, "Stat", st.Mode(), st.Size()) { + return + } + when := time.Date(2020, 5, 17, 10, 30, 0, 0, time.UTC) + os.Chtimes(src, when, when) + st, _ = os.Stat(src) + if !check(p, st.ModTime().Equal(when), "Chtimes", st.ModTime()) { + return + } + os.Truncate(src, 25) + st, _ = os.Stat(src) + if !check(p, st.Size() == 25, "Truncate", st.Size()) { + return + } + if !check(p, os.Rename(src, root+"/moved") == nil, "Rename") { + return + } + os.Symlink("moved", root+"/link") + to, err := os.Readlink(root + "/link") + lst, _ := os.Lstat(root + "/link") + fst, _ := os.Stat(root + "/link") + if !check(p, err == nil && to == "moved" && lst.Mode()&fs.ModeSymlink != 0 && fst.Size() == 25, "Symlink", to, err) { + return + } + done(p, "io.Copy, Chmod, Chtimes, Truncate, Rename, Symlink, Readlink, Lstat") +} diff --git a/userland/linux_guests/go/goos/go.mod b/userland/linux_guests/go/goos/go.mod new file mode 100644 index 000000000..95b5475f8 --- /dev/null +++ b/userland/linux_guests/go/goos/go.mod @@ -0,0 +1,3 @@ +module nonos/guest/goos + +go 1.24 diff --git a/userland/linux_guests/go/goos/locking.go b/userland/linux_guests/go/goos/locking.go new file mode 100644 index 000000000..59bc20b7d --- /dev/null +++ b/userland/linux_guests/go/goos/locking.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + "syscall" +) + +func locking() { + p := "flock" + path := root + "/lock" + os.WriteFile(path, nil, 0o644) + a, _ := os.Open(path) + b, _ := os.Open(path) + defer a.Close() + defer b.Close() + if !check(p, syscall.Flock(int(a.Fd()), syscall.LOCK_EX) == nil, "LOCK_EX") { + return + } + err := syscall.Flock(int(b.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if !check(p, err == syscall.EWOULDBLOCK, "second LOCK_NB", err) { + return + } + syscall.Flock(int(a.Fd()), syscall.LOCK_UN) + if !check(p, syscall.Flock(int(b.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) == nil, "after unlock") { + return + } + done(p, "an exclusive lock refuses a second open until it is released") +} diff --git a/userland/linux_guests/go/goos/main.go b/userland/linux_guests/go/goos/main.go new file mode 100644 index 000000000..ca18035e8 --- /dev/null +++ b/userland/linux_guests/go/goos/main.go @@ -0,0 +1,51 @@ +/* + * Go's os, io/fs and path/filepath as a Go program uses them: a tree made + * with MkdirAll and read back with ReadDir and WalkDir, CreateTemp, a copy + * through io.Copy (which Go does with copy_file_range), Chmod, Chtimes, + * Truncate, Rename, Symlink, Readlink and Lstat, Executable, Hostname + * against uname, NumCPU against the CPUs /proc/cpuinfo lists, and + * syscall.Flock between two opens. Every part prints; nothing printed + * depends on the machine, so the host prints the same lines. + */ +package main + +import ( + "fmt" + "os" + "strings" +) + +var parts, failed int + +var bad []string + +func check(part string, good bool, what string, a ...any) bool { + if !good { + fmt.Printf("[GO] goos %s FAIL: %s %v\n", part, what, a) + failed++ + bad = append(bad, part) + } + return good +} + +func done(part, detail string) { + parts++ + fmt.Printf("[GO] goos %s ok: %s\n", part, detail) +} + +const root = "/tmp/goos" + +func main() { + os.RemoveAll(root) + tree() + temp() + files() + system() + locking() + os.RemoveAll(root) + if failed > 0 { + fmt.Printf("[GO] goos FAIL: %d of %d parts: %s\n", failed, parts+failed, strings.Join(bad, " ")) + os.Exit(1) + } + fmt.Printf("[GO] goos PASS: %d parts\n", parts) +} diff --git a/userland/linux_guests/go/goos/system.go b/userland/linux_guests/go/goos/system.go new file mode 100644 index 000000000..e91ffc920 --- /dev/null +++ b/userland/linux_guests/go/goos/system.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "syscall" +) + +func system() { + p := "system" + exe, err := os.Executable() + if !check(p, err == nil && filepath.Base(exe) == "goos", "Executable", exe, err) { + return + } + host, err := os.Hostname() + var u syscall.Utsname + syscall.Uname(&u) + var node []byte + for _, c := range u.Nodename { + if c == 0 { + break + } + node = append(node, byte(c)) + } + if !check(p, err == nil && host == string(node), "Hostname", host, string(node)) { + return + } + info, _ := os.ReadFile("/proc/cpuinfo") + cpus := strings.Count(string(info), "processor\t:") + if !check(p, runtime.NumCPU() == cpus && cpus >= 1, "NumCPU", runtime.NumCPU(), cpus) { + return + } + wd, err := os.Getwd() + if !check(p, err == nil && wd == "/", "Getwd", wd, err) { + return + } + done(p, "Executable, Hostname as uname says it, NumCPU as cpuinfo lists them") +} diff --git a/userland/linux_guests/go/goos/temp.go b/userland/linux_guests/go/goos/temp.go new file mode 100644 index 000000000..b17b7f0ef --- /dev/null +++ b/userland/linux_guests/go/goos/temp.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + "strings" +) + +func temp() { + p := "temp" + f, err := os.CreateTemp("", "goos-*.txt") + if !check(p, err == nil && strings.HasPrefix(f.Name(), "/tmp/goos-"), "CreateTemp", err) { + return + } + f.WriteString("temporary") + f.Close() + got, err := os.ReadFile(f.Name()) + if !check(p, err == nil && string(got) == "temporary", "ReadFile", got, err) { + return + } + dir, err := os.MkdirTemp("", "goos-dir-*") + st, _ := os.Stat(dir) + if !check(p, err == nil && st != nil && st.IsDir() && st.Mode().Perm() == 0o700, "MkdirTemp", err) { + return + } + check(p, os.Remove(f.Name()) == nil && os.Remove(dir) == nil, "Remove") + _, err = os.Stat(f.Name()) + if !check(p, os.IsNotExist(err), "gone", err) { + return + } + done(p, "CreateTemp and MkdirTemp in /tmp, read back and removed") +} diff --git a/userland/linux_guests/go/goos/tree.go b/userland/linux_guests/go/goos/tree.go new file mode 100644 index 000000000..1e6a1dacd --- /dev/null +++ b/userland/linux_guests/go/goos/tree.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func tree() { + p := "tree" + if !check(p, os.MkdirAll(root+"/a/b/c", 0o755) == nil, "MkdirAll") { + return + } + for _, f := range []string{"a/one", "a/b/two", "a/b/c/three", "top"} { + os.WriteFile(root+"/"+f, []byte(f), 0o644) + } + entries, err := os.ReadDir(root + "/a") + var names []string + for _, e := range entries { + names = append(names, fmt.Sprintf("%s:%v", e.Name(), e.IsDir())) + } + if !check(p, err == nil && strings.Join(names, ",") == "b:true,one:false", "ReadDir", names, err) { + return + } + var walked []string + err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + walked = append(walked, strings.TrimPrefix(path, root)+map[bool]string{true: "/", false: ""}[d.IsDir()]) + return nil + }) + want := "/,/a/,/a/b/,/a/b/c/,/a/b/c/three,/a/b/two,/a/one,/top" + if !check(p, err == nil && strings.Join(walked, ",") == want, "WalkDir", walked, err) { + return + } + matches, _ := filepath.Glob(root + "/a/b/*") + if !check(p, len(matches) == 2, "Glob", matches) { + return + } + done(p, "MkdirAll, ReadDir, WalkDir and Glob see the tree as made") +} diff --git a/userland/linux_guests/sh/bbsuite-body.sh b/userland/linux_guests/sh/bbsuite-body.sh new file mode 100644 index 000000000..61ae8fde2 --- /dev/null +++ b/userland/linux_guests/sh/bbsuite-body.sh @@ -0,0 +1,38 @@ +# The applets bbsuite runs, one section a line of output or more. A line +# that names a time or a pid starts with @ and is left out of the compare. +W=${BBSUITE_DIR:-/tmp/bbsuite} +rm -rf "$W"; mkdir -p "$W/a/b/c" && cd "$W" || exit 1 +echo "mkdir=$?" +printf 'one\ntwo\nthree\ntwo\none\n' > words +printf 'alpha beta\ngamma delta\n' > a/b/text +echo nested > a/b/c/deep +echo "== cat"; cat words a/b/text +echo "== wc"; wc -l words; wc -w a/b/text; wc -c < words +echo "== head tail"; head -n 2 words; tail -n 1 words; tail -c 4 words +echo "== sort uniq"; sort words | uniq -c; sort -r words | head -n 1; sort -u words +echo "== grep"; grep -n two words; grep -rl nested . | sort; grep -c o words; grep -v o words; echo "grep-miss=$(grep -q zzz words; echo $?)" +echo "== sed"; sed 's/o/0/g' words; sed -n '2p' words; sed '/two/d' words +echo "== awk"; awk '{n += length($0)} END {print NR, n}' words; awk -F' ' '{print $2}' a/b/text +echo "== cp mv rm"; cp words copy; cp -r a acopy; mv copy moved; ls; rm moved; rm -r acopy; ls +echo "== find"; find . -type f | sort; find . -name deep; find . -type d | sort +echo "== ls"; ls -la a/b | awk 'NR>1 {print $1, $3, $4, $NF}'; ls -l words | awk '{print $1, $2, $3, $4, $5, $NF}' +echo "@ls-full $(ls -la | tr '\n' '|')" +echo "== touch stat"; touch new; stat -c '%s %F %a %n' new words; test -e new && echo touched +echo "== ln readlink"; ln -s words lnk; readlink lnk; cat lnk | wc -l; ls -l lnk | awk '{print $1, $(NF-2), $(NF-1), $NF}' +echo "== chmod"; chmod 600 words; stat -c '%a' words; chmod u+x,g+r words; stat -c '%a' words; test -x words && echo exec +echo "== test expr"; test 3 -gt 2 && echo gt; [ -d a ] && echo dir; [ -f a ] || echo notfile; expr 6 \* 7; expr length hello; expr 7 % 3 +echo "== env"; env -i A=1 B=two env | sort; X=inline sh -c 'echo $X' +echo "== xargs"; printf 'a/b/text\na/b/c/deep\n' | xargs cat; echo 1 2 3 | xargs -n 1 echo n +echo "== dd od"; dd if=/dev/zero bs=512 count=4 2>/dev/null | wc -c; printf 'abc\n' | od -An -tx1; dd if=words bs=1 skip=4 count=3 2>/dev/null; echo +echo "== sha256"; sha256sum words a/b/text +echo "== gzip"; gzip -c words > words.gz; gunzip -c words.gz | sha256sum; cp words w2; gzip w2; ls w2*; gunzip w2.gz; cmp w2 words && echo same +echo "== tar"; tar cf t.tar a; tar tf t.tar | sort; mkdir out; tar xf t.tar -C out; find out -type f | sort; cat out/a/b/c/deep +echo "== pipes redirects"; echo out1 > r; echo out2 >> r; cat < r; ls nothere 2> err; echo "rc=$?"; wc -l < err; { echo g1; echo g2; } | tail -n 1; echo e 2>&1 1>/dev/null | wc -c +echo "== subshell"; (cd a && pwd | sed "s|$W||"); pwd | sed "s|$W|W|"; v=outer; (v=inner; echo $v); echo $v; echo "$(echo sub $(echo nest))" +echo "== bg wait"; (echo bg1 > f1) & (echo bg2 > f2) & wait; cat f1 f2 +echo "== trap"; sh -c 'trap "echo exit-trap" EXIT; echo body'; sh -c 'trap "echo got-term" TERM; kill -TERM $$; echo after-term' +echo "== ps"; ps -o pid,comm | awk -v p=$$ '$1 == p {print "self", $2}'; echo "@ps $(ps | wc -l)" +echo "== df du"; df . > /dev/null; echo "df=$?"; echo "@df $(df . | tail -n 1)"; du -s a > /dev/null; echo "du=$?"; echo "@du $(du -s a)" +echo "== date"; echo "@date $(date)" +echo "== dev"; head -c 8 /dev/urandom | wc -c; cat /dev/null | wc -c; echo x > /dev/null; echo "null=$?" +cd / && rm -rf "$W"; echo "cleaned=$?" diff --git a/userland/linux_guests/sh/bbsuite-host.sh b/userland/linux_guests/sh/bbsuite-host.sh new file mode 100755 index 000000000..8588b7d0c --- /dev/null +++ b/userland/linux_guests/sh/bbsuite-host.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# The host oracle for bbsuite: the same busybox, reached through the same +# links the guest's tree has, with the guest's environment and nothing of +# the host's own tools on the path. Prints the output bbsuite must match. +# usage: bbsuite-host.sh +set -e +bb=$(realpath "$1"); body=$(realpath "$2") +h=$(mktemp -d /dev/shm/bbsuite-host.XXXXXX) +trap 'rm -rf "$h"' EXIT +mkdir -p "$h/bin" +cp "$bb" "$h/bin/busybox" +"$bb" --list-full | grep -v '^bin/busybox$' | while read -r p; do + mkdir -p "$h/$(dirname "$p")" + ln -s "$h/bin/busybox" "$h/$p" +done +cd / +env -i PATH="$h/usr/local/bin:$h/usr/bin:$h/bin:$h/usr/local/sbin:$h/usr/sbin:$h/sbin" \ + HOME=/root TERM=linux SHELL=/bin/sh LANG=C.UTF-8 BBSUITE_DIR="$h/work" \ + "$h/bin/sh" "$body" diff --git a/userland/linux_guests/sh/bbsuite.sh b/userland/linux_guests/sh/bbsuite.sh new file mode 100644 index 000000000..66042319d --- /dev/null +++ b/userland/linux_guests/sh/bbsuite.sh @@ -0,0 +1,21 @@ +# bbsuite: busybox runs the applets of bbsuite-body.sh on NONOS, and its +# output, less the lines that start with @ (they name a time or a pid), must +# equal what the same busybox printed on the host (bbsuite.expect). +# usage: sh /etc/bbsuite.sh +out=/tmp/bbsuite.out +sh /etc/bbsuite-body.sh > "$out" 2>&1 +grep -v '^@' "$out" > /tmp/bbsuite.got +grep -v '^@' /etc/bbsuite.expect > /tmp/bbsuite.want +lines=$(wc -l < /tmp/bbsuite.want) +sections=$(grep -c '^== ' /tmp/bbsuite.want) +got=$(sha256sum < /tmp/bbsuite.got | cut -c1-16) +want=$(sha256sum < /tmp/bbsuite.want | cut -c1-16) +if cmp -s /tmp/bbsuite.got /tmp/bbsuite.want; then + echo "[C] bbsuite PASS: $lines lines in $sections sections equal the host's, sha256 $got" + exit 0 +fi +# What differs, then everything this run printed, for a diff on the host. +differ=$(diff -U0 /tmp/bbsuite.want /tmp/bbsuite.got | grep -v '^---\|^+++' | grep -c '^[-+]') +sed 's/^/[C] bbsuite got: /' /tmp/bbsuite.got +echo "[C] bbsuite FAIL: $differ lines differ of $lines, sha256 $got, host $want" +exit 1 diff --git a/userland/linux_guests/sh/oracle.sh b/userland/linux_guests/sh/oracle.sh new file mode 100755 index 000000000..3deccdf92 --- /dev/null +++ b/userland/linux_guests/sh/oracle.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# The host oracle for a proof guest: the program run as the personality runs +# it. Root with no capability (capget is refused to a guest), in a pid +# namespace of its own with its own /proc (a guest sees only its family), +# a tmpfs at /tmp (the family's private /tmp), no terminal, and the +# guest's environment. usage: oracle.sh [args...] +set -e +prog=$(realpath "$1"); shift +h=$(mktemp -d /dev/shm/oracle.XXXXXX) +trap 'rm -rf "$h"' EXIT +mkdir -p "$h/bin" && cp "$prog" "$h/bin/" +name=$(basename "$prog") +exec setsid --wait unshare --pid --fork --mount-proc --mount sh -c ' + mount -t tmpfs tmpfs /tmp && cd / && + exec env -i PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin \ + HOME=/root TERM=linux PWD=/ SHELL=/bin/sh LANG=C.UTF-8 \ + setpriv --bounding-set=-all --inh-caps=-all --ambient-caps=-all \ + --securebits=+noroot,+noroot_locked,+no_setuid_fixup,+no_setuid_fixup_locked \ + "$0" "$@" < /dev/null' "$h/bin/$name" "$@"