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" "$@"