From 261cce548dd1317f2fcfc147a0dc7a427b23847b Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 22:52:16 +0000 Subject: [PATCH 01/34] linux: the calls that only look at a file, in a table of their own The file table held every call that names a file or a descriptor in one match of 77 lines, and the calls this lane adds would lengthen it further. Now stat and its forms, access, statfs, statx and readlink are answered from serve/table_meta.rs, which the file table falls back to when it has no arm. The table is the same; nothing is answered differently. --- userland/capsule_linux/src/linux/serve/mod.rs | 1 + .../src/linux/serve/table_file.rs | 14 ++----- .../src/linux/serve/table_meta.rs | 39 +++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 userland/capsule_linux/src/linux/serve/table_meta.rs diff --git a/userland/capsule_linux/src/linux/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index 917245ecf..7d2550f5a 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -52,6 +52,7 @@ mod table; mod table_file; mod table_link; mod table_mem; +mod table_meta; mod table_net; mod table_proc; mod table_sig; diff --git a/userland/capsule_linux/src/linux/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs index d0da0fe5e..e2983053b 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; @@ -34,12 +34,9 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option file::openat(guest, a[0], a[1], a[2]), nr::OPEN => file::openat(guest, flags::AT_FDCWD, a[0], a[1]), 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]), @@ -67,11 +64,6 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option 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, + _ => return super::table_meta::meta_ops(guest, nr, a), }) } 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..bf5c82256 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_meta.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 . + +/* + * 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; + +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 | nr::LSTAT => file::newfstatat(guest, flags::AT_FDCWD, a[0], a[1]), + nr::NEWFSTATAT => file::newfstatat(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, + }) +} From c7bfb97cff12810c9f56173c6aecc09208a1650b Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 18:37:41 +0000 Subject: [PATCH 02/34] linux-guests: bbsuite, busybox running forty applets against the host Nothing checked that a real Linux tool's output under the personality matches Linux's. bbsuite is a busybox sh script of 28 sections that runs more than forty applets: file and directory work, text tools, archives, pipes, redirects, subshells, background jobs, traps, ps, df and /dev. The same busybox runs it on the host at build time, through the same applet links and with the guest's environment and nothing of the host's own tools on the path; that output is the oracle. On NONOS the script compares its own output with the oracle's, less the lines that start with @ (they name a time or a pid), and prints PASS or FAIL with the number of lines that differ. /etc/passwd and /etc/group name root, as an Alpine tree does, so ls and ps print owners by name on both sides. --- userland/linux_guests/GuestFiles.mk | 17 +++++++++++ userland/linux_guests/etc/group | 1 + userland/linux_guests/etc/passwd | 1 + userland/linux_guests/sh/bbsuite-body.sh | 38 ++++++++++++++++++++++++ userland/linux_guests/sh/bbsuite-host.sh | 19 ++++++++++++ userland/linux_guests/sh/bbsuite.sh | 20 +++++++++++++ 6 files changed, 96 insertions(+) create mode 100644 userland/linux_guests/etc/group create mode 100644 userland/linux_guests/etc/passwd create mode 100644 userland/linux_guests/sh/bbsuite-body.sh create mode 100755 userland/linux_guests/sh/bbsuite-host.sh create mode 100644 userland/linux_guests/sh/bbsuite.sh diff --git a/userland/linux_guests/GuestFiles.mk b/userland/linux_guests/GuestFiles.mk index 75a418455..5d59bd70b 100644 --- a/userland/linux_guests/GuestFiles.mk +++ b/userland/linux_guests/GuestFiles.mk @@ -41,3 +41,20 @@ 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 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/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..034d72cf5 --- /dev/null +++ b/userland/linux_guests/sh/bbsuite.sh @@ -0,0 +1,20 @@ +# 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 +diff /tmp/bbsuite.want /tmp/bbsuite.got | head -n 80 +differ=$(diff /tmp/bbsuite.want /tmp/bbsuite.got | grep -c '^[<>]') +echo "[C] bbsuite FAIL: $differ lines differ of $lines, sha256 $got, host $want" +exit 1 From 0eba141ef38a657709b07467e82724866d1f822c Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:05:02 +0000 Subject: [PATCH 03/34] procstat: show a foreign supervisor the rows of the guests it hosts MkProcStat showed a caller its own row in full and every other row with its counters zeroed, unless it held AttestRead or ProcessControl. The Linux personality answers getrusage, times and /proc for the guests it hosts, and could read none of their ticks, faults or resident memory: every such figure would have had to read as zero. A supervisor now sees the full row of each process registered as its foreign guest, and still sees nothing more of anyone else's. The kernel reports; the personality decides what a guest is told. --- src/syscall/microkernel/procstat_redact.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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; From 67bf3f10e123ed8165963d58a1573506082b4213 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:06:34 +0000 Subject: [PATCH 04/34] linux: one copy of a file's bytes and offset for the whole family A written file lived in the descriptor that wrote it: its bytes in the descriptor's own buffer, flushed whole at close, and its offset in the descriptor alone. A dup or a fork copied the descriptor without either, so the copy read EBADF and a write through it replaced the file at close with only what it wrote. A descriptor opened O_APPEND wrote at offset 0, and a write-only open that did not truncate lost the file's old bytes. A shell's own output and the output of the commands it forks overwrote one another in a redirected file: busybox sh left 3 of 148 lines. Now the family keeps one copy of each file it writes, by path (held/cache/): every descriptor on it, in every process, reads and writes that copy, as Linux's page cache gives them, and stat's size is its size. Each open makes an open file description (held/desc/), which a dup and a fork share: its number, O_APPEND, whether it reads, and its offset, so a child's write moves the parent's offset. A dup or fork's descriptor opens its own stream from the store when it reads. The copy goes to the store at close and fsync. A write open of the read-only tree is EROFS at open, a directory opened to write is EISDIR, O_EXCL is honoured, and fdatasync is fsync; a pipe or a socket cannot be synced (EINVAL). The devices /dev opens as descriptors of their own take whether they write from the same access mode. --- userland/capsule_linux/src/linux/abi/errno.rs | 1 + .../capsule_linux/src/linux/abi/errno_io.rs | 22 +++++++ userland/capsule_linux/src/linux/abi/mod.rs | 2 + .../capsule_linux/src/linux/abi/nr_file.rs | 22 +++++++ .../capsule_linux/src/linux/abi/nr_path.rs | 1 + .../capsule_linux/src/linux/file/close.rs | 37 ++++++++---- userland/capsule_linux/src/linux/file/dev.rs | 4 +- .../capsule_linux/src/linux/file/flags.rs | 19 +++--- .../capsule_linux/src/linux/file/fsync.rs | 19 ++++-- .../src/linux/file/held/cache/change.rs | 45 ++++++++++++++ .../src/linux/file/held/cache/flush.rs | 41 +++++++++++++ .../src/linux/file/held/cache/mod.rs | 36 +++++++++++ .../src/linux/file/held/cache/table.rs | 51 ++++++++++++++++ .../src/linux/file/held/cache/take.rs | 44 ++++++++++++++ .../src/linux/file/held/desc/handle.rs | 52 ++++++++++++++++ .../src/linux/file/held/desc/mod.rs | 36 +++++++++++ .../src/linux/file/held/desc/offset.rs | 60 +++++++++++++++++++ .../src/linux/file/held/desc/shared.rs | 24 ++++++++ .../capsule_linux/src/linux/file/held/mod.rs | 25 ++++++++ .../src/linux/file/held/rw/mod.rs | 31 ++++++++++ .../src/linux/file/held/rw/read.rs | 53 ++++++++++++++++ .../src/linux/file/held/rw/write.rs | 46 ++++++++++++++ userland/capsule_linux/src/linux/file/mod.rs | 4 +- .../capsule_linux/src/linux/file/open/mark.rs | 52 ++++++++++++++++ .../capsule_linux/src/linux/file/open/mod.rs | 22 +++++++ .../linux/file/{open.rs => open/openat.rs} | 45 +++++--------- .../capsule_linux/src/linux/file/pread.rs | 41 ------------- .../capsule_linux/src/linux/file/pread/mod.rs | 25 ++++++++ .../src/linux/file/pread/plain.rs | 50 ++++++++++++++++ userland/capsule_linux/src/linux/file/read.rs | 38 +++--------- .../src/linux/file/regular/create.rs | 40 +++++++++++++ .../src/linux/file/regular/mod.rs | 29 +++++++++ .../file/{regular.rs => regular/open.rs} | 38 ++++++------ userland/capsule_linux/src/linux/file/seek.rs | 14 +++-- .../capsule_linux/src/linux/file/write.rs | 50 ++++++---------- .../src/linux/serve/table_file.rs | 2 +- 36 files changed, 936 insertions(+), 185 deletions(-) create mode 100644 userland/capsule_linux/src/linux/abi/errno_io.rs create mode 100644 userland/capsule_linux/src/linux/abi/nr_file.rs create mode 100644 userland/capsule_linux/src/linux/file/held/cache/change.rs create mode 100644 userland/capsule_linux/src/linux/file/held/cache/flush.rs create mode 100644 userland/capsule_linux/src/linux/file/held/cache/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/held/cache/table.rs create mode 100644 userland/capsule_linux/src/linux/file/held/cache/take.rs create mode 100644 userland/capsule_linux/src/linux/file/held/desc/handle.rs create mode 100644 userland/capsule_linux/src/linux/file/held/desc/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/held/desc/offset.rs create mode 100644 userland/capsule_linux/src/linux/file/held/desc/shared.rs create mode 100644 userland/capsule_linux/src/linux/file/held/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/held/rw/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/held/rw/read.rs create mode 100644 userland/capsule_linux/src/linux/file/held/rw/write.rs create mode 100644 userland/capsule_linux/src/linux/file/open/mark.rs create mode 100644 userland/capsule_linux/src/linux/file/open/mod.rs rename userland/capsule_linux/src/linux/file/{open.rs => open/openat.rs} (56%) delete mode 100644 userland/capsule_linux/src/linux/file/pread.rs create mode 100644 userland/capsule_linux/src/linux/file/pread/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/pread/plain.rs create mode 100644 userland/capsule_linux/src/linux/file/regular/create.rs create mode 100644 userland/capsule_linux/src/linux/file/regular/mod.rs rename userland/capsule_linux/src/linux/file/{regular.rs => regular/open.rs} (53%) 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..6f86c7f3c --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/errno_io.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 . + +/* + * The errnos the file, lock and xattr calls answer with, beyond the ones + * errno.rs has always held. + */ + +pub const EFBIG: i64 = 27; 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..28c365764 --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/nr_file.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 . + +/* + * Syscall numbers for the file, lock, xattr, id and usage calls, transcribed + * from the x86_64 table. + */ + +pub const FDATASYNC: u64 = 75; 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/file/close.rs b/userland/capsule_linux/src/linux/file/close.rs index 5b7d71049..004a9c4d8 100644 --- a/userland/capsule_linux/src/linux/file/close.rs +++ b/userland/capsule_linux/src/linux/file/close.rs @@ -14,35 +14,52 @@ // 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); } + if let Some(d) = super::desc::of(entry).filter(|d| !super::desc::held_elsewhere(guest, fd, *d)) + { + super::desc::gone(d); + } + 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 { +/* + * 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 { - return true; + 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/flags.rs b/userland/capsule_linux/src/linux/file/flags.rs index 36a29e9ef..2b4982b4b 100644 --- a/userland/capsule_linux/src/linux/file/flags.rs +++ b/userland/capsule_linux/src/linux/file/flags.rs @@ -14,25 +14,30 @@ // 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_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 +/* 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..526f2b1e0 100644 --- a/userland/capsule_linux/src/linux/file/fsync.rs +++ b/userland/capsule_linux/src/linux/file/fsync.rs @@ -16,14 +16,25 @@ //! 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 { + return errno::ok(0); + } + match super::cache::flush(&entry.path, true) { + 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..d7187d211 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/change.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 . + +/* Writing into a copy, and cutting or growing it. */ + +use crate::linux::abi::errno; + +use super::table::{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; + 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; + }) + .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..dc730f298 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.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 . + +/* A copy put in the store: at close, fsync and sync, and at exit. */ + +use crate::linux::abi::errno; + +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 { + store::write(&resolve::key(path), &all[i].data).map_err(|_| errno::EIO)?; + all[i].dirty = false; + } + if !keep { + all.remove(i); + } + Ok(()) +} 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..6e5e373bd --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/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 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 and at fsync. + */ + +mod change; +mod flush; +mod table; +mod take; + +pub use change::{resize, write}; +pub use flush::flush; +pub use table::{held, size}; +pub use take::{hold, read}; 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..d4f8063c3 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/table.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 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, +} + +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) +} 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..1a1055ad8 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/take.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 . + +/* 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, 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 data = match exists { + true => store::read(&resolve::key(path), MAX_FILE as u32).map_err(|_| errno::EIO)?, + false => Vec::new(), + }; + CACHE.0.borrow_mut().push(Entry { path: path.to_vec(), data, dirty: !exists }); + 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..3bed529e1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/desc/shared.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 . + +/* Whether another descriptor holds the same description. */ + +use super::handle::of; + +/* Whether a descriptor of this process other than `fd` holds its description. */ +pub fn held_elsewhere(guest: &crate::linux::guest::Guest, fd: u64, d: u32) -> bool { + guest.fds.iter().enumerate().any(|(i, o)| i as u64 != fd && o.is_open() && of(o) == 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..bf6366cbd --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/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 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 rw; 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..f5142ab77 --- /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: 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..fdd90b363 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/rw/read.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 . + +/* 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}; + +/* 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(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..0532a5c62 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/rw/write.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 . + +/* 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}; + +/* + * 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(); + 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/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index c9db777f0..ee0dc495b 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.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 . -//! The filesystem a guest sees. +/* The filesystem a guest sees. */ mod at; mod clamp; @@ -36,6 +36,7 @@ mod eventfd_io; pub mod family; pub mod flags; mod fsync; +mod held; mod link; mod memfd; mod memfd_map; @@ -71,6 +72,7 @@ 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; +use held::*; pub use link::{linkat, symlinkat}; pub use memfd::{ftruncate, is_memfd, memfd_create}; pub use memfd_map::{mapped_at, set_mapped, staged}; 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..bd7ab9630 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/mark.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 mark a descriptor carries of how it was opened, and the directory + * a relative name starts from. + */ + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::super::flags::AT_FDCWD; + +/* + * 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(super) 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. */ +pub(super) 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/mod.rs b/userland/capsule_linux/src/linux/file/open/mod.rs new file mode 100644 index 000000000..b99d1f7a2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/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 . + +/* `openat`. */ + +mod mark; +mod openat; + +pub use openat::openat; diff --git a/userland/capsule_linux/src/linux/file/open.rs b/userland/capsule_linux/src/linux/file/open/openat.rs similarity index 56% rename from userland/capsule_linux/src/linux/file/open.rs rename to userland/capsule_linux/src/linux/file/open/openat.rs index 5ef6051a9..b038f4966 100644 --- a/userland/capsule_linux/src/linux/file/open.rs +++ b/userland/capsule_linux/src/linux/file/open/openat.rs @@ -14,15 +14,14 @@ // 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; +/* openat. */ use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; +use crate::linux::guest::Guest; -use super::flags::{wants_write, AT_FDCWD, O_CLOEXEC, O_CREAT, O_DIRECTORY}; -use super::{dev, dir, path, regular, resolve, store}; +use super::super::flags::{writes, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL}; +use super::super::{cache, dev, dir, path, regular, resolve, store}; +use super::mark::{base_of, mark}; pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64) -> u64 { let Some(name) = path::read_path(guest, path_ptr) else { @@ -33,37 +32,21 @@ pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64) -> u64 { Err(e) => return e, }; let full = guest.links.follow(resolve::visible(&base, &name), true); - let got = match store::stat(&resolve::key(&full)).ok() { + /* A file the family is making is there before the store holds it. */ + let found = match cache::size(&full) { + Some(size) => Some((size, false)), + None => store::stat(&resolve::key(&full)).ok(), + }; + let got = match found { _ if dev::device_of(&full).is_some() => dev::open_path(guest, &full, flags), + 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 && wants_write(flags) => regular::create(guest, full), + None if flags & O_CREAT != 0 => regular::create(guest, full, flags), 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/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..7ea5fd7de --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/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 . + +/* + * pread64: a read at the offset it is given, which leaves the descriptor's + * own offset where it was; a pipe, a socket or a console has no offset, + * which Linux calls ESPIPE. + */ + +mod plain; + +pub use plain::pread64; 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..5fe081770 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/plain.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 . + +/* 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)) +} + +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. */ +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/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..4d0db26ea --- /dev/null +++ b/userland/capsule_linux/src/linux/file/regular/create.rs @@ -0,0 +1,40 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public 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, 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) -> 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); + } + 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/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/write.rs b/userland/capsule_linux/src/linux/file/write.rs index 2c29f6e55..92cacc7f0 100644 --- a/userland/capsule_linux/src/linux/file/write.rs +++ b/userland/capsule_linux/src/linux/file/write.rs @@ -14,45 +14,29 @@ // 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 { + let take = (len as usize).min(MAX_IO); + let Some(bytes) = guest.read(buf, take) else { return errno::fail(errno::EFAULT); }; - let Some(entry) = guest.fds.get_mut(fd as usize) else { - return errno::fail(errno::EBADF); - }; - if entry.kind != Kind::File || !entry.writable { - return errno::fail(errno::EBADF); - } - 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); + let at = guest.fds.get(fd as usize).map_or(0, super::desc::pos); + match write_at(guest, fd, at, &bytes) { + Ok((n, end)) => { + if let Some(entry) = guest.fds.get_mut(fd as usize) { + super::desc::set_pos(entry, end); + } + errno::ok(n as u64) + } + Err(e) => errno::fail(e), } - 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/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs index e2983053b..933ac46eb 100644 --- a/userland/capsule_linux/src/linux/serve/table_file.rs +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -59,7 +59,7 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option 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::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]), From 6f9a133d20722e62da064d27c132f6926d8ce3ed Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:06:54 +0000 Subject: [PATCH 05/34] linux: serve fcntl F_DUPFD and F_DUPFD_CLOEXEC F_DUPFD answered ENOSYS, because a duplicate needed a second handle on the store, and F_DUPFD_CLOEXEC was EINVAL. busybox sh moves the file it reads a script from, and the descriptors it saves around a builtin's redirection, above 10 with F_DUPFD_CLOEXEC, so it died on its first line: "sh: 3: Function not implemented". A file's description is the family's now and a duplicate reads its own stream when it needs one, so both commands make a second descriptor on the same open file at the lowest free number at or above the one asked for, EINVAL past the descriptor table and EMFILE when it is full. This is two arms in call/ctl.rs, a file of lane A's; the work is in file/calls/fdup.rs. --- userland/capsule_linux/src/linux/call/ctl.rs | 9 ++-- .../src/linux/file/calls/fdup.rs | 50 +++++++++++++++++++ .../capsule_linux/src/linux/file/calls/mod.rs | 23 +++++++++ userland/capsule_linux/src/linux/file/mod.rs | 2 + 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/calls/fdup.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/mod.rs diff --git a/userland/capsule_linux/src/linux/call/ctl.rs b/userland/capsule_linux/src/linux/call/ctl.rs index 4c3c129f6..267e72302 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,8 @@ 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), _ => errno::fail(errno::EINVAL), } } 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..7f5df9945 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/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 file calls beyond open, read and write. + */ + +pub(super) mod fdup; + +pub use fdup::dup_from; diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index ee0dc495b..ed9b9e91b 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -17,6 +17,7 @@ /* The filesystem a guest sees. */ mod at; +mod calls; mod clamp; pub(super) mod close; mod cstr; @@ -61,6 +62,7 @@ mod timerfd_read; mod timerfd_spec; mod write; +pub use calls::*; pub use close::close; pub use cstr::read_cstr; pub use dev_io::{read as dev_read, write as dev_write}; From 2682cebfbc9d7ac7c8973facae9636232d173b21 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:10:05 +0000 Subject: [PATCH 06/34] linux: stat, access and readlink say what Linux says about a file stat reported every file 0644 and every directory 0755 whatever chmod had said, one link, no times, and uid and gid 0 only by accident of a zeroed buffer; lstat followed links, so ls -l showed no link as a link; newfstatat refused a directory descriptor (ENOSYS) and ignored AT_SYMLINK_NOFOLLOW and AT_EMPTY_PATH; statx gave a third answer; access never said EROFS or EACCES; utimensat refused any time but "now for neither"; a listing had no "." or ".."; unlink could not remove a symbolic link; and a file the family had made but not yet written out did not exist to stat, ls or rmdir. Now one function answers for a path or a descriptor (meta/node/), and stat, lstat, fstat, fstatat and statx all fill from it. The store keeps no modes and one time, so the family keeps the rest for its own files: the mode each was made with less the umask, or chmod's (held/modes.rs), and the times utimensat, utimes and utime set, which stand until the next write (held/times.rs). The shared tree is read-only (EROFS for chmod, rename, unlink, mkdir, utimensat and access W_OK). A pipe or the console fstats as a FIFO, a socket as a socket. Directory descriptors keep their path across a chdir, a dirfd's high 32 bits are ignored as Linux ignores them, mkdir takes its mode, open takes O_CREAT's mode and O_NOFOLLOW, and rename and unlink carry the family's copy, mode and times along. The character devices /dev opens as descriptors of their own (dev.rs) answer through the same metadata: S_IFCHR, read and write for everyone, and their major and minor numbers in st_rdev. --- .../capsule_linux/src/linux/abi/nr_file.rs | 1 + userland/capsule_linux/src/linux/call/cwd.rs | 15 ++-- userland/capsule_linux/src/linux/file/at.rs | 33 ++++---- .../capsule_linux/src/linux/file/dev_stat.rs | 39 +++------- userland/capsule_linux/src/linux/file/dir.rs | 27 ++++--- .../src/linux/file/dirops/dirs.rs | 73 ++++++++++++++++++ .../src/linux/file/dirops/mod.rs | 23 ++++++ .../file/{dirops.rs => dirops/unlink.rs} | 62 +++++++-------- .../capsule_linux/src/linux/file/flags.rs | 10 +++ .../src/linux/file/held/cache/change.rs | 4 +- .../src/linux/file/held/cache/mod.rs | 4 +- .../src/linux/file/held/cache/names.rs | 48 ++++++++++++ .../src/linux/file/held/cache/table.rs | 10 +++ .../src/linux/file/held/cache/take.rs | 14 ++-- .../capsule_linux/src/linux/file/held/mod.rs | 2 + .../src/linux/file/held/modes.rs | 71 ++++++++++++++++++ .../src/linux/file/held/times.rs | 66 ++++++++++++++++ .../src/linux/file/{link.rs => link/calls.rs} | 31 +++----- .../capsule_linux/src/linux/file/link/free.rs | 34 +++++++++ .../capsule_linux/src/linux/file/link/mod.rs | 29 +++++++ .../capsule_linux/src/linux/file/meta/mod.rs | 8 +- .../src/linux/file/meta/node/device.rs | 28 +++++++ .../src/linux/file/meta/node/fd.rs | 55 ++++++++++++++ .../src/linux/file/meta/node/mod.rs | 28 +++++++ .../src/linux/file/meta/node/path.rs | 73 ++++++++++++++++++ .../src/linux/file/meta/perms.rs | 38 +++++----- .../src/linux/file/meta/query.rs | 54 ------------- .../src/linux/file/meta/query/access.rs | 65 ++++++++++++++++ .../src/linux/file/meta/query/link.rs | 49 ++++++++++++ .../src/linux/file/meta/query/mod.rs | 23 ++++++ .../capsule_linux/src/linux/file/meta/stat.rs | 75 ------------------- .../src/linux/file/meta/stat/at.rs | 55 ++++++++++++++ .../src/linux/file/meta/stat/calls.rs | 56 ++++++++++++++ .../src/linux/file/meta/stat/mod.rs | 23 ++++++ .../src/linux/file/meta/statbuf.rs | 60 --------------- .../src/linux/file/meta/statbuf/build.rs | 51 +++++++++++++ .../src/linux/file/meta/statbuf/mod.rs | 23 ++++++ .../src/linux/file/meta/statbuf/shape.rs | 54 +++++++++++++ .../src/linux/file/meta/statx.rs | 60 +++++++-------- .../capsule_linux/src/linux/file/mknod.rs | 9 ++- userland/capsule_linux/src/linux/file/mod.rs | 2 +- .../capsule_linux/src/linux/file/open/mark.rs | 20 +---- .../capsule_linux/src/linux/file/open/mod.rs | 1 + .../src/linux/file/open/named.rs | 51 +++++++++++++ .../src/linux/file/open/openat.rs | 32 +++----- .../linux/file/{owner.rs => owner/chown.rs} | 30 ++------ .../capsule_linux/src/linux/file/owner/mod.rs | 32 ++++++++ .../src/linux/file/owner/stamp.rs | 65 ++++++++++++++++ .../src/linux/file/owner/times.rs | 68 +++++++++++++++++ .../src/linux/file/regular/create.rs | 6 +- .../capsule_linux/src/linux/file/rename.rs | 52 ++++++++----- .../src/linux/file/store_name.rs | 7 +- .../capsule_linux/src/linux/guest/links.rs | 3 + .../src/linux/guest/links/edit.rs | 48 ++++++++++++ .../src/linux/serve/table_file.rs | 8 +- .../src/linux/serve/table_link.rs | 12 +-- .../src/linux/serve/table_meta.rs | 15 ++-- userland/capsule_linux_proofs/src/lib.rs | 2 +- .../src/tests/stat_tests.rs | 67 +++++++++-------- 59 files changed, 1497 insertions(+), 507 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/dirops/dirs.rs create mode 100644 userland/capsule_linux/src/linux/file/dirops/mod.rs rename userland/capsule_linux/src/linux/file/{dirops.rs => dirops/unlink.rs} (54%) create mode 100644 userland/capsule_linux/src/linux/file/held/cache/names.rs create mode 100644 userland/capsule_linux/src/linux/file/held/modes.rs create mode 100644 userland/capsule_linux/src/linux/file/held/times.rs rename userland/capsule_linux/src/linux/file/{link.rs => link/calls.rs} (66%) create mode 100644 userland/capsule_linux/src/linux/file/link/free.rs create mode 100644 userland/capsule_linux/src/linux/file/link/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/node/device.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/node/fd.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/node/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/node/path.rs delete mode 100644 userland/capsule_linux/src/linux/file/meta/query.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/query/access.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/query/link.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/query/mod.rs delete mode 100644 userland/capsule_linux/src/linux/file/meta/stat.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/stat/at.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/stat/calls.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/stat/mod.rs delete mode 100644 userland/capsule_linux/src/linux/file/meta/statbuf.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/statbuf/build.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/statbuf/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/statbuf/shape.rs create mode 100644 userland/capsule_linux/src/linux/file/open/named.rs rename userland/capsule_linux/src/linux/file/{owner.rs => owner/chown.rs} (58%) create mode 100644 userland/capsule_linux/src/linux/file/owner/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/owner/stamp.rs create mode 100644 userland/capsule_linux/src/linux/file/owner/times.rs create mode 100644 userland/capsule_linux/src/linux/guest/links/edit.rs diff --git a/userland/capsule_linux/src/linux/abi/nr_file.rs b/userland/capsule_linux/src/linux/abi/nr_file.rs index 28c365764..40dfa325e 100644 --- a/userland/capsule_linux/src/linux/abi/nr_file.rs +++ b/userland/capsule_linux/src/linux/abi/nr_file.rs @@ -20,3 +20,4 @@ */ pub const FDATASYNC: u64 = 75; +pub const TIMES: u64 = 100; diff --git a/userland/capsule_linux/src/linux/call/cwd.rs b/userland/capsule_linux/src/linux/call/cwd.rs index 474e0b54d..d66a8fa54 100644 --- a/userland/capsule_linux/src/linux/call/cwd.rs +++ b/userland/capsule_linux/src/linux/call/cwd.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 . -//! Moving the working directory. +/* Moving the working directory. */ use crate::linux::abi::errno; use crate::linux::file::{look, read_path, visible}; @@ -25,17 +25,18 @@ pub fn chdir(guest: &mut Guest, path: u64) -> u64 { return errno::fail(errno::EFAULT); }; let at = guest.links.follow(visible(&guest.cwd, &name), true); - // Checked before it is taken. + /* 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/file/at.rs b/userland/capsule_linux/src/linux/file/at.rs index 171bfd0d7..290845f74 100644 --- a/userland/capsule_linux/src/linux/file/at.rs +++ b/userland/capsule_linux/src/linux/file/at.rs @@ -14,35 +14,40 @@ // 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), - }; + let full = named_at(guest, dirfd, &name).ok()?; + /* The *at calls act on the name, so its own last component is not followed. */ Some(guest.links.follow(full, false)) } -fn base_of(guest: &Guest, dirfd: u64) -> Option> { +/* + * The absolute name `name` gives against `dirfd`, nothing followed yet. + * 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(visible(b"/", name)); + } if dirfd == AT_FDCWD { - return Some(guest.cwd.clone()); + return Ok(visible(&guest.cwd, name)); } - match guest.fds.get(dirfd as usize) { - Some(fd) if fd.kind == Kind::Dir => Some(fd.path.clone()), - _ => None, + match guest.fds.get(dirfd as usize).filter(|f| f.is_open()) { + Some(fd) if fd.kind == Kind::Dir => Ok(visible(&fd.path, name)), + Some(_) => Err(errno::ENOTDIR), + None => Err(errno::EBADF), } } 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..1fca201c6 100644 --- a/userland/capsule_linux/src/linux/file/dir.rs +++ b/userland/capsule_linux/src/linux/file/dir.rs @@ -14,30 +14,39 @@ // 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}; 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)); + for name in guest.links.names_in(&path).into_iter().chain(cache::names_in(&path)) { + 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..a02fa30f1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/dirops/dirs.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 . + +/* 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}; + +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 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 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 54% rename from userland/capsule_linux/src/linux/file/dirops.rs rename to userland/capsule_linux/src/linux/file/dirops/unlink.rs index 26eeb6cab..82ee812aa 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}; +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,29 @@ 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 key(&at).writable().is_err() => return errno::fail(errno::EROFS), + Some(_) => {} + } + cache::forget(&at); + modes::forget(&at); + super::super::times::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 2b4982b4b..dbda75f5c 100644 --- a/userland/capsule_linux/src/linux/file/flags.rs +++ b/userland/capsule_linux/src/linux/file/flags.rs @@ -27,6 +27,7 @@ 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; /* @@ -35,6 +36,15 @@ pub const O_CLOEXEC: u64 = 0o2000000; */ pub const AT_FDCWD: u64 = (-100i64) as u64; +/* + * 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 diff --git a/userland/capsule_linux/src/linux/file/held/cache/change.rs b/userland/capsule_linux/src/linux/file/held/cache/change.rs index d7187d211..db7662b3d 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/change.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/change.rs @@ -18,7 +18,7 @@ use crate::linux::abi::errno; -use super::table::{with, MAX_FILE}; +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 { @@ -30,6 +30,7 @@ pub fn write(path: &[u8], at: u64, bytes: &[u8]) -> Result { } e.data[at as usize..end].copy_from_slice(bytes); e.dirty = true; + e.mtime_ms = now(); bytes.len() }) .ok_or(errno::EBADF) @@ -40,6 +41,7 @@ pub fn resize(path: &[u8], len: u64) -> Result<(), i64> { 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/mod.rs b/userland/capsule_linux/src/linux/file/held/cache/mod.rs index 6e5e373bd..cdab12d6e 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -27,10 +27,12 @@ mod change; mod flush; +mod names; mod table; mod take; pub use change::{resize, write}; pub use flush::flush; -pub use table::{held, size}; +pub use names::{forget, 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..51254f552 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/held/cache/names.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 . + +/* Copies that follow their names, and what the copies add up to. */ + +use alloc::vec::Vec; + +use super::table::CACHE; + +/* 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 index d4f8063c3..603662d87 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/table.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/table.rs @@ -26,6 +26,12 @@ pub(super) struct Entry { pub(super) path: Vec, pub(super) data: Vec, pub(super) dirty: bool, + /* 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>); @@ -49,3 +55,7 @@ pub fn held(path: &[u8]) -> bool { 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 index 1a1055ad8..892240078 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/take.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/take.rs @@ -21,18 +21,22 @@ use alloc::vec::Vec; use crate::linux::abi::errno; use super::super::super::{resolve, store}; -use super::table::{held, with, Entry, CACHE, MAX_FILE}; +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 data = match exists { - true => store::read(&resolve::key(path), MAX_FILE as u32).map_err(|_| errno::EIO)?, - false => Vec::new(), + 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()), }; - CACHE.0.borrow_mut().push(Entry { path: path.to_vec(), data, dirty: !exists }); + CACHE.0.borrow_mut().push(Entry { path: path.to_vec(), data, dirty: !exists, mtime_ms }); Ok(()) } diff --git a/userland/capsule_linux/src/linux/file/held/mod.rs b/userland/capsule_linux/src/linux/file/held/mod.rs index bf6366cbd..eb41fcbe8 100644 --- a/userland/capsule_linux/src/linux/file/held/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/mod.rs @@ -22,4 +22,6 @@ pub(super) mod cache; pub(super) mod desc; +pub(super) mod modes; pub(super) mod rw; +pub(super) mod times; 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/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 66% rename from userland/capsule_linux/src/linux/file/link.rs rename to userland/capsule_linux/src/linux/file/link/calls.rs index 9dda297b2..feea123d4 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 { @@ -54,6 +50,10 @@ pub fn linkat(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64) -> u6 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 super::super::cache::flush(&from, true).is_err() { + return errno::fail(errno::EIO); + } let Ok(bytes) = store_read(&key(&from), MAX_LINKED) else { return errno::fail(errno::ENOENT); }; @@ -62,12 +62,3 @@ pub fn linkat(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64) -> u6 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); - } - 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/meta/mod.rs b/userland/capsule_linux/src/linux/file/meta/mod.rs index 6921433df..24a698f14 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 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..562fe44b1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/fd.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 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::statbuf::Meta; +use super::device::device; +use super::path::{at, of, S_IFIFO, S_IFREG, S_IFSOCK}; + +/* + * What has no path, a pipe, the console, a socket or an object with no + * file behind it, by its mode alone. + */ +pub(super) fn kind(mode: u32) -> Meta { + at(b"/", mode, 0, now()) +} + +/* fstat: a file by its path, and the rest by kind. */ +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()))) + } + /* The console is a stream with no terminal behind it, as a pipe is. */ + Kind::Stdin | Kind::Stdout | Kind::Stderr | Kind::Pipe => Ok(kind(S_IFIFO | 0o600)), + Kind::Socket | Kind::Unix | Kind::Resolver => Ok(kind(S_IFSOCK | 0o777)), + 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(kind(0o600)), + } +} + +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/mod.rs b/userland/capsule_linux/src/linux/file/meta/node/mod.rs new file mode 100644 index 000000000..9420b1c85 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/node/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 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 path; + +pub use super::statbuf::Meta; +pub use fd::{now, of_fd}; +pub use path::{of, S_IFDIR}; 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..a8ae3b712 --- /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::{cache, dev, modes, resolve, store, times}; +use super::super::statbuf::inode; +use super::super::statbuf::Meta; +use super::device::device; +use super::fd::now; + +pub const S_IFDIR: u32 = 0o040000; + +pub const S_IFREG: u32 = 0o100000; + +pub const S_IFLNK: u32 = 0o120000; + +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 (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: 0, 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 = guest.links.follow(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(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..bbd5220d5 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}; +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(&guest.links.follow(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 is read-only. + */ +fn change(full: &[u8], mode: u64) -> u64 { + if look(full).is_none() { + return errno::fail(errno::ENOENT); + } + if !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..6420eb4fc --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/query/access.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 . + +/* access and faccessat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{at, cache, path, resolve}; +use super::super::node::S_IFDIR; + +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 == 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 = guest.links.follow(named, true); + 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..18f469443 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/query/link.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 . + +/* Whether a name is a link, and readlinkat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::at; +use super::super::node::{self}; + +/* Whether the name is itself a link. */ +pub fn is_link(guest: &Guest, named: &[u8]) -> bool { + let full = guest.links.follow(named.to_vec(), false); + guest.links.target(&full).is_some() +} + +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 Some(to) = guest.links.target(&full) else { + 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..e4f044f55 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/stat/calls.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 . + +/* stat, fstat and newfstatat. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::super::super::{cache, dev, resolve, store}; +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(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/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..b2f3093aa 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"); @@ -42,7 +42,10 @@ pub fn mknodat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { return errno::fail(errno::EROFS); } match super::store_write(&key(&at), &[]) { - Ok(()) => errno::ok(0), + Ok(()) => { + super::modes::set(&at, mode as u32 & 0o7777 & !u32::from(guest.umask)); + errno::ok(0) + } Err(_) => errno::fail(errno::EIO), } } diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index ed9b9e91b..8e7fea926 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -83,7 +83,7 @@ pub use meta::{ }; 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 private::{allow_shared_writes, clear as clear_private, prepare as prepare_private}; diff --git a/userland/capsule_linux/src/linux/file/open/mark.rs b/userland/capsule_linux/src/linux/file/open/mark.rs index bd7ab9630..7d8e8eb44 100644 --- a/userland/capsule_linux/src/linux/file/open/mark.rs +++ b/userland/capsule_linux/src/linux/file/open/mark.rs @@ -19,34 +19,18 @@ * a relative name starts from. */ -use alloc::vec::Vec; - use crate::linux::abi::errno; -use crate::linux::guest::{Guest, Kind}; - -use super::super::flags::AT_FDCWD; +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(super) fn mark(guest: &mut Guest, got: u64, on: bool) { +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; } } } - -/* AT_FDCWD or a dirfd the guest itself opened. No other dirfd resolves. */ -pub(super) 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/mod.rs b/userland/capsule_linux/src/linux/file/open/mod.rs index b99d1f7a2..3ace2a966 100644 --- a/userland/capsule_linux/src/linux/file/open/mod.rs +++ b/userland/capsule_linux/src/linux/file/open/mod.rs @@ -17,6 +17,7 @@ /* `openat`. */ mod mark; +mod named; mod openat; 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..46df8317b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/open/named.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 . + +/* 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}; + +/* 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 = guest.links.follow(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); + } + 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 index b038f4966..ce57934e5 100644 --- a/userland/capsule_linux/src/linux/file/open/openat.rs +++ b/userland/capsule_linux/src/linux/file/open/openat.rs @@ -19,34 +19,20 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::flags::{writes, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL}; -use super::super::{cache, dev, dir, path, regular, resolve, store}; -use super::mark::{base_of, mark}; +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) -> u64 { +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 base = match base_of(guest, dirfd) { - Ok(base) => base, - Err(e) => return e, - }; - let full = guest.links.follow(resolve::visible(&base, &name), true); - /* A file the family is making is there before the store holds it. */ - let found = match cache::size(&full) { - Some(size) => Some((size, false)), - None => store::stat(&resolve::key(&full)).ok(), - }; - let got = match found { - _ if dev::device_of(&full).is_some() => dev::open_path(guest, &full, flags), - 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), - None => errno::fail(errno::ENOENT), + 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 58% rename from userland/capsule_linux/src/linux/file/owner.rs rename to userland/capsule_linux/src/linux/file/owner/chown.rs index e579fbcaa..7abdc217f 100644 --- a/userland/capsule_linux/src/linux/file/owner.rs +++ b/userland/capsule_linux/src/linux/file/owner/chown.rs @@ -14,25 +14,21 @@ // 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); @@ -43,7 +39,7 @@ pub fn fchownat(guest: &Guest, dirfd: u64, path: u64, uid: u64, gid: u64) -> u64 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..3567b6e55 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/owner/stamp.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 . + +/* 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) => guest.links.follow(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::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/regular/create.rs b/userland/capsule_linux/src/linux/file/regular/create.rs index 4d0db26ea..a5902177d 100644 --- a/userland/capsule_linux/src/linux/file/regular/create.rs +++ b/userland/capsule_linux/src/linux/file/regular/create.rs @@ -22,19 +22,21 @@ use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest}; use super::super::flags::writes; -use super::super::{cache, resolve}; +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) -> u64 { +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/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs index f97e27994..c74346d70 100644 --- a/userland/capsule_linux/src/linux/file/rename.rs +++ b/userland/capsule_linux/src/linux/file/rename.rs @@ -14,33 +14,22 @@ // 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; +use super::{cache, modes, 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), - } + renameat2(guest, super::flags::AT_FDCWD, old, super::flags::AT_FDCWD, new, 0) } 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 +38,38 @@ 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| 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); + errno::ok(0) + } + Err(_) => errno::fail(errno::EIO), } } 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/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/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs index 933ac46eb..b96349da1 100644 --- a/userland/capsule_linux/src/linux/serve/table_file.rs +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -31,8 +31,8 @@ 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::GETDENTS64 => file::getdents64(guest, a[0], a[1], a[2]), nr::EPOLL_CREATE1 => file::epoll_create(guest), @@ -53,8 +53,8 @@ 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]), 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 index bf5c82256..590b36733 100644 --- a/userland/capsule_linux/src/linux/serve/table_meta.rs +++ b/userland/capsule_linux/src/linux/serve/table_meta.rs @@ -24,15 +24,20 @@ 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 | nr::LSTAT => file::newfstatat(guest, flags::AT_FDCWD, a[0], a[1]), - nr::NEWFSTATAT => file::newfstatat(guest, a[0], a[1], a[2]), - np::FACCESSAT | np::FACCESSAT2 => file::faccessat(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 | 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]), + 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 None, }) 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/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); } From 350c2ea7a8878ec93c912932aa66f12047142707 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:11:32 +0000 Subject: [PATCH 07/34] linux: /dev, /proc and /sys, made from what NONOS declares A guest found no /dev, /proc or /sys. busybox ps listed nothing, df found no mounts, Go read /sys for its huge-page size and /proc/self/exe for os.Executable and got ENOENT, and a shell's 2>/dev/null had no file. Now the personality makes the three trees at every read. /dev has null, zero, full, random, urandom and tty with Linux's numbers and behaviour (full is ENOSPC, tty ENXIO with no terminal), and the stdin, stdout, stderr and fd links into /proc/self/fd. /proc has a directory for each process of the family and none other, under the number the family's pid namespace gives it: exe, cwd, root, fd, fdinfo, maps, status, stat, statm, cmdline, environ, comm, limits, mounts, mountinfo, cgroup and task/; mem is listed and refused at open. The system-wide files answer from one table of what NONOS declares (file/system/declared/, also now uname's source) and from the kernel's counts for the family's own threads (file/system/cpu/); nothing of the machine, another capsule or another family is in any of them. /sys has the one file Go reads. Only the family knows its processes and their numbers, so it lends /proc a view of itself for each call that may read /proc and takes it back after (serve/family_view/): two lines in Family::answer, a file of lane A's. Paths follow /proc/self, /dev/fd, /proc//cwd, root and exe as links, never out of the family's root. statfs and st_dev come from the family's mount table, and getppid now names a forked child's parent, as /proc//stat does. The five character devices themselves are the descriptors file/dev.rs answers; the tree made here lists them with their numbers, keeps /dev/tty's ENXIO and the links, and has no device I/O of its own. A process reads S in /proc while any of its threads is parked, a wait4 or signal wait included, as Guest::parked says. --- .../capsule_linux/src/linux/abi/errno_io.rs | 1 + userland/capsule_linux/src/linux/call/cwd.rs | 4 +- .../capsule_linux/src/linux/call/ident.rs | 22 ++++-- .../src/linux/call/limits_table.rs | 17 +++-- userland/capsule_linux/src/linux/call/mod.rs | 1 + .../capsule_linux/src/linux/call/uname.rs | 38 +++++----- userland/capsule_linux/src/linux/file/at.rs | 2 +- .../capsule_linux/src/linux/file/close.rs | 2 +- userland/capsule_linux/src/linux/file/dir.rs | 9 ++- .../src/linux/file/dirops/dirs.rs | 8 +- .../src/linux/file/dirops/unlink.rs | 6 +- .../capsule_linux/src/linux/file/fsync.rs | 2 +- .../src/linux/file/held/desc/shared.rs | 15 +++- .../src/linux/file/held/rw/mod.rs | 6 +- .../src/linux/file/held/rw/read.rs | 5 +- .../src/linux/file/held/rw/write.rs | 5 +- .../src/linux/file/link/calls.rs | 2 +- .../src/linux/file/made/boot_id.rs | 72 ++++++++++++++++++ .../src/linux/file/made/dev/mod.rs | 32 ++++++++ .../src/linux/file/made/dev/number.rs | 32 ++++++++ .../src/linux/file/made/dev/tree.rs | 66 +++++++++++++++++ .../src/linux/file/made/exe/mod.rs | 31 ++++++++ .../src/linux/file/made/exe/shape.rs | 39 ++++++++++ .../src/linux/file/made/exe/table.rs | 50 +++++++++++++ .../src/linux/file/made/exe_image.rs | 39 ++++++++++ .../src/linux/file/made/fdopen.rs | 61 ++++++++++++++++ .../capsule_linux/src/linux/file/made/mod.rs | 39 ++++++++++ .../capsule_linux/src/linux/file/made/need.rs | 46 ++++++++++++ .../src/linux/file/made/proc/fds/info.rs | 72 ++++++++++++++++++ .../src/linux/file/made/proc/fds/list.rs | 48 ++++++++++++ .../src/linux/file/made/proc/fds/mod.rs | 30 ++++++++ .../src/linux/file/made/proc/maps.rs | 65 +++++++++++++++++ .../src/linux/file/made/proc/mod.rs | 38 ++++++++++ .../src/linux/file/made/proc/mounts/files.rs | 55 ++++++++++++++ .../src/linux/file/made/proc/mounts/mod.rs | 31 ++++++++ .../src/linux/file/made/proc/mounts/table.rs | 49 +++++++++++++ .../src/linux/file/made/proc/names/at.rs | 37 ++++++++++ .../src/linux/file/made/proc/names/lists.rs | 41 +++++++++++ .../src/linux/file/made/proc/names/mod.rs | 26 +++++++ .../src/linux/file/made/proc/names/node.rs | 67 +++++++++++++++++ .../src/linux/file/made/proc/names/parse.rs | 61 ++++++++++++++++ .../linux/file/made/proc/pid_files/files.rs | 73 +++++++++++++++++++ .../src/linux/file/made/proc/pid_files/mod.rs | 30 ++++++++ .../linux/file/made/proc/pid_files/stat.rs | 52 +++++++++++++ .../linux/file/made/proc/pid_files/statm.rs | 33 +++++++++ .../linux/file/made/proc/pid_status/limits.rs | 64 ++++++++++++++++ .../linux/file/made/proc/pid_status/mod.rs | 23 ++++++ .../linux/file/made/proc/pid_status/status.rs | 67 +++++++++++++++++ .../src/linux/file/made/proc/sysctl.rs | 72 ++++++++++++++++++ .../src/linux/file/made/proc/system/files.rs | 50 +++++++++++++ .../src/linux/file/made/proc/system/memory.rs | 56 ++++++++++++++ .../src/linux/file/made/proc/system/mod.rs | 31 ++++++++ .../src/linux/file/made/proc/system/stat.rs | 54 ++++++++++++++ .../src/linux/file/made/proc/system/time.rs | 46 ++++++++++++ .../src/linux/file/made/proc/tree.rs | 49 +++++++++++++ .../src/linux/file/made/synth/mod.rs | 30 ++++++++ .../src/linux/file/made/synth/node.rs | 69 ++++++++++++++++++ .../src/linux/file/made/synth/roots.rs | 27 +++++++ .../src/linux/file/made/synth_ops/io.rs | 48 ++++++++++++ .../src/linux/file/made/synth_ops/made.rs | 34 +++++++++ .../src/linux/file/made/synth_ops/mod.rs | 24 ++++++ .../src/linux/file/made/synth_ops/open.rs | 65 +++++++++++++++++ .../capsule_linux/src/linux/file/made/sys.rs | 50 +++++++++++++ .../src/linux/file/made/view/lent.rs | 41 +++++++++++ .../src/linux/file/made/view/mod.rs | 32 ++++++++ .../src/linux/file/made/view/proc.rs | 47 ++++++++++++ .../src/linux/file/made/view/shape.rs | 49 +++++++++++++ .../capsule_linux/src/linux/file/meta/mod.rs | 2 +- .../src/linux/file/meta/node/fd.rs | 31 ++++---- .../src/linux/file/meta/node/made.rs | 58 +++++++++++++++ .../src/linux/file/meta/node/mod.rs | 3 +- .../src/linux/file/meta/node/path.rs | 18 ++--- .../src/linux/file/meta/perms.rs | 8 +- .../src/linux/file/meta/query/access.rs | 13 +++- .../src/linux/file/meta/query/link.rs | 28 ++++--- .../src/linux/file/meta/stat/calls.rs | 5 +- .../src/linux/file/meta/statfs/calls.rs | 51 +++++++++++++ .../file/meta/{statfs.rs => statfs/fill.rs} | 41 +++++------ .../src/linux/file/meta/statfs/mod.rs | 29 ++++++++ .../capsule_linux/src/linux/file/mknod.rs | 2 +- userland/capsule_linux/src/linux/file/mod.rs | 9 ++- .../src/linux/file/open/named.rs | 7 +- .../src/linux/file/owner/chown.rs | 2 +- .../src/linux/file/owner/stamp.rs | 8 +- .../capsule_linux/src/linux/file/rename.rs | 4 +- .../src/linux/file/system/cpu/ended.rs | 24 ++++++ .../src/linux/file/system/cpu/mod.rs | 31 ++++++++ .../src/linux/file/system/cpu/usage.rs | 67 +++++++++++++++++ .../src/linux/file/system/declared/mod.rs | 30 ++++++++ .../src/linux/file/system/declared/names.rs | 35 +++++++++ .../src/linux/file/system/declared/sizes.rs | 53 ++++++++++++++ .../src/linux/file/system/mod.rs | 23 ++++++ .../capsule_linux/src/linux/file/walk/mod.rs | 30 ++++++++ .../capsule_linux/src/linux/file/walk/path.rs | 49 +++++++++++++ .../capsule_linux/src/linux/file/walk/step.rs | 40 ++++++++++ .../capsule_linux/src/linux/image/stack.rs | 2 +- .../capsule_linux/src/linux/serve/family.rs | 2 + .../src/linux/serve/family_view/facts.rs | 58 +++++++++++++++ .../src/linux/serve/family_view/lend.rs | 44 +++++++++++ .../src/linux/serve/family_view/mod.rs | 28 +++++++ .../src/linux/serve/family_view/proc.rs | 57 +++++++++++++++ userland/capsule_linux/src/linux/serve/mod.rs | 5 +- .../src/linux/serve/table_meta.rs | 3 +- 103 files changed, 3257 insertions(+), 131 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/made/boot_id.rs create mode 100644 userland/capsule_linux/src/linux/file/made/dev/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/dev/number.rs create mode 100644 userland/capsule_linux/src/linux/file/made/dev/tree.rs create mode 100644 userland/capsule_linux/src/linux/file/made/exe/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/exe/shape.rs create mode 100644 userland/capsule_linux/src/linux/file/made/exe/table.rs create mode 100644 userland/capsule_linux/src/linux/file/made/exe_image.rs create mode 100644 userland/capsule_linux/src/linux/file/made/fdopen.rs create mode 100644 userland/capsule_linux/src/linux/file/made/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/need.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/fds/info.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/fds/list.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/fds/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/maps.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/mounts/files.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/mounts/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/mounts/table.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/names/at.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/names/lists.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/names/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/names/node.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/names/parse.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_files/files.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_files/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_files/statm.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_status/limits.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_status/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/pid_status/status.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/sysctl.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/system/files.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/system/memory.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/system/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/system/stat.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/system/time.rs create mode 100644 userland/capsule_linux/src/linux/file/made/proc/tree.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth/node.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth/roots.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth_ops/io.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth_ops/made.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth_ops/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/synth_ops/open.rs create mode 100644 userland/capsule_linux/src/linux/file/made/sys.rs create mode 100644 userland/capsule_linux/src/linux/file/made/view/lent.rs create mode 100644 userland/capsule_linux/src/linux/file/made/view/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/made/view/proc.rs create mode 100644 userland/capsule_linux/src/linux/file/made/view/shape.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/node/made.rs create mode 100644 userland/capsule_linux/src/linux/file/meta/statfs/calls.rs rename userland/capsule_linux/src/linux/file/meta/{statfs.rs => statfs/fill.rs} (52%) create mode 100644 userland/capsule_linux/src/linux/file/meta/statfs/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/system/cpu/ended.rs create mode 100644 userland/capsule_linux/src/linux/file/system/cpu/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/system/cpu/usage.rs create mode 100644 userland/capsule_linux/src/linux/file/system/declared/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/system/declared/names.rs create mode 100644 userland/capsule_linux/src/linux/file/system/declared/sizes.rs create mode 100644 userland/capsule_linux/src/linux/file/system/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/walk/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/walk/path.rs create mode 100644 userland/capsule_linux/src/linux/file/walk/step.rs create mode 100644 userland/capsule_linux/src/linux/serve/family_view/facts.rs create mode 100644 userland/capsule_linux/src/linux/serve/family_view/lend.rs create mode 100644 userland/capsule_linux/src/linux/serve/family_view/mod.rs create mode 100644 userland/capsule_linux/src/linux/serve/family_view/proc.rs diff --git a/userland/capsule_linux/src/linux/abi/errno_io.rs b/userland/capsule_linux/src/linux/abi/errno_io.rs index 6f86c7f3c..8b1e6a29d 100644 --- a/userland/capsule_linux/src/linux/abi/errno_io.rs +++ b/userland/capsule_linux/src/linux/abi/errno_io.rs @@ -19,4 +19,5 @@ * errno.rs has always held. */ +pub const ENXIO: i64 = 6; pub const EFBIG: i64 = 27; diff --git a/userland/capsule_linux/src/linux/call/cwd.rs b/userland/capsule_linux/src/linux/call/cwd.rs index d66a8fa54..8c598e400 100644 --- a/userland/capsule_linux/src/linux/call/cwd.rs +++ b/userland/capsule_linux/src/linux/call/cwd.rs @@ -17,14 +17,14 @@ /* Moving the working directory. */ use crate::linux::abi::errno; -use crate::linux::file::{look, read_path, visible}; +use crate::linux::file::{follow, look, read_path, visible}; 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); + let at = follow(guest, visible(&guest.cwd, &name), true); /* Checked before it is taken. */ match look(&at) { Some((_, true)) => { 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..3be3d6506 100644 --- a/userland/capsule_linux/src/linux/call/mod.rs +++ b/userland/capsule_linux/src/linux/call/mod.rs @@ -85,6 +85,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/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 290845f74..79c39c6f1 100644 --- a/userland/capsule_linux/src/linux/file/at.rs +++ b/userland/capsule_linux/src/linux/file/at.rs @@ -29,7 +29,7 @@ pub fn resolve_at(guest: &Guest, dirfd: u64, path: u64) -> Option> { let name = read_path(guest, path)?; let full = named_at(guest, dirfd, &name).ok()?; /* The *at calls act on the name, so its own last component is not followed. */ - Some(guest.links.follow(full, false)) + Some(super::walk::follow(guest, full, false)) } /* diff --git a/userland/capsule_linux/src/linux/file/close.rs b/userland/capsule_linux/src/linux/file/close.rs index 004a9c4d8..1f80c521d 100644 --- a/userland/capsule_linux/src/linux/file/close.rs +++ b/userland/capsule_linux/src/linux/file/close.rs @@ -52,7 +52,7 @@ 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 { + if entry.kind != Kind::File || !entry.writable || super::synth::owns(&entry.path) { return Ok(()); } let path = &entry.path; diff --git a/userland/capsule_linux/src/linux/file/dir.rs b/userland/capsule_linux/src/linux/file/dir.rs index 1fca201c6..d8504036f 100644 --- a/userland/capsule_linux/src/linux/file/dir.rs +++ b/userland/capsule_linux/src/linux/file/dir.rs @@ -26,7 +26,7 @@ use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest}; use super::dir_children::children; -use super::{cache, desc, 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); @@ -39,7 +39,12 @@ pub fn open(guest: &mut Guest, path: Vec) -> u64 { */ let mut names = alloc::vec![String::from("."), String::from("..")]; names.extend(children(at.as_bytes(), keys)); - for name in guest.links.names_in(&path).into_iter().chain(cache::names_in(&path)) { + 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); } diff --git a/userland/capsule_linux/src/linux/file/dirops/dirs.rs b/userland/capsule_linux/src/linux/file/dirops/dirs.rs index a02fa30f1..3d71f74c2 100644 --- a/userland/capsule_linux/src/linux/file/dirops/dirs.rs +++ b/userland/capsule_linux/src/linux/file/dirops/dirs.rs @@ -22,7 +22,7 @@ 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}; +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 { @@ -31,7 +31,7 @@ pub fn mkdirat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { if look(&at).is_some() || guest.links.target(&at).is_some() { return errno::fail(errno::EEXIST); } - if key(&at).writable().is_err() { + if synth::owns(&at) || key(&at).writable().is_err() { return errno::fail(errno::EROFS); } match store_name::mkdir(&key(&at)) { @@ -59,7 +59,9 @@ 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 key(at).writable().is_err() => return errno::fail(errno::EROFS), + 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(_) => {} } diff --git a/userland/capsule_linux/src/linux/file/dirops/unlink.rs b/userland/capsule_linux/src/linux/file/dirops/unlink.rs index 82ee812aa..8f3886b4e 100644 --- a/userland/capsule_linux/src/linux/file/dirops/unlink.rs +++ b/userland/capsule_linux/src/linux/file/dirops/unlink.rs @@ -22,7 +22,7 @@ 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}; +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 { @@ -47,7 +47,9 @@ pub fn unlinkat(guest: &Guest, dirfd: u64, path: u64, flags: u64) -> u64 { match look(&at) { None => return errno::fail(errno::ENOENT), Some((_, true)) => return errno::fail(errno::EISDIR), - Some(_) if key(&at).writable().is_err() => return errno::fail(errno::EROFS), + Some(_) if synth::owns(&at) || key(&at).writable().is_err() => { + return errno::fail(errno::EROFS) + } Some(_) => {} } cache::forget(&at); diff --git a/userland/capsule_linux/src/linux/file/fsync.rs b/userland/capsule_linux/src/linux/file/fsync.rs index 526f2b1e0..752054a8f 100644 --- a/userland/capsule_linux/src/linux/file/fsync.rs +++ b/userland/capsule_linux/src/linux/file/fsync.rs @@ -30,7 +30,7 @@ pub fn fsync(guest: &Guest, fd: u64) -> u64 { if !matches!(entry.kind, Kind::File | Kind::Dir) { return errno::fail(errno::EINVAL); } - if entry.kind == Kind::Dir { + if entry.kind == Kind::Dir || super::synth::owns(&entry.path) { return errno::ok(0); } match super::cache::flush(&entry.path, true) { diff --git a/userland/capsule_linux/src/linux/file/held/desc/shared.rs b/userland/capsule_linux/src/linux/file/held/desc/shared.rs index 3bed529e1..5be73aad7 100644 --- a/userland/capsule_linux/src/linux/file/held/desc/shared.rs +++ b/userland/capsule_linux/src/linux/file/held/desc/shared.rs @@ -18,7 +18,18 @@ use super::handle::of; -/* Whether a descriptor of this process other than `fd` holds its description. */ +/* + * 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 { - guest.fds.iter().enumerate().any(|(i, o)| i as u64 != fd && o.is_open() && of(o) == Some(d)) + 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/rw/mod.rs b/userland/capsule_linux/src/linux/file/held/rw/mod.rs index f5142ab77..40b6f7b5b 100644 --- a/userland/capsule_linux/src/linux/file/held/rw/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/rw/mod.rs @@ -19,9 +19,9 @@ * the p- and v- forms, sendfile and copy_file_range all come here, so a * file reads the same whichever call asks. * - * In order: 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. + * 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; diff --git a/userland/capsule_linux/src/linux/file/held/rw/read.rs b/userland/capsule_linux/src/linux/file/held/rw/read.rs index fdd90b363..35537cfb7 100644 --- a/userland/capsule_linux/src/linux/file/held/rw/read.rs +++ b/userland/capsule_linux/src/linux/file/held/rw/read.rs @@ -21,7 +21,7 @@ use alloc::vec::Vec; use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; -use super::super::super::{cache, desc, resolve, store}; +use super::super::super::{cache, desc, resolve, store, synth_ops}; /* The most one call moves. */ pub const MAX_IO: usize = 1 << 20; @@ -37,6 +37,9 @@ pub fn read_at(guest: &mut Guest, fd: u64, at: u64, len: usize) -> Result Result<(us 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 }; diff --git a/userland/capsule_linux/src/linux/file/link/calls.rs b/userland/capsule_linux/src/linux/file/link/calls.rs index feea123d4..e38b16df6 100644 --- a/userland/capsule_linux/src/linux/file/link/calls.rs +++ b/userland/capsule_linux/src/linux/file/link/calls.rs @@ -46,7 +46,7 @@ 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); } 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..c6de9e894 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe/shape.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 . + +/* 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, + /* 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..bfe653d64 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/exe_image.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 . + +/* + * 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`. + */ +pub fn record_image(pid: u32, argv: &[Vec], at: &[u64], top: 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(); + record(pid, Exe { path, comm: comm_of(name), args: *first, env, end: top, 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..bb30243ba --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/need.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 . + +/* + * 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, chdir, readlink, chmod, statfs, + * utime, getppid, the *at forms, and close and the calls that close. + */ +const ALWAYS: [u64; 23] = [ + 2, 3, 4, 6, 21, 33, 59, 80, 89, 90, 110, 132, 137, 235, 257, 262, 267, 268, 269, 280, 292, 332, + 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")) + }; + /* read, fstat, pread64 and readv */ + matches!(nr, 0 | 5 | 17 | 19) && proc_fd(a[0]) +} 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..4b72751f3 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.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 . + +/* /proc//stat. */ + +use alloc::vec::Vec; + +use super::super::super::view::Proc; +use super::files::{usage, vsize}; + +pub(super) fn stat(p: &Proc, tid: u32) -> Vec { + let u = usage(p, tid); + let state = if p.sleeping { 'S' } else { 'R' }; + let comm = core::str::from_utf8(&p.exe.comm).unwrap_or(""); + let start = p.exe.start_ms / (1000 / super::super::super::super::declared::HZ); + let (e, rss) = (&p.exe, u.resident_kb / 4); + let head = alloc::format!( + "{tid} ({comm}) {state} {} {} {} 0 -1 0 {} 0 0 0 {} {} 0 0 {} {} {} 0 {start} {} {rss} \ + 18446744073709551615 0 0 0 0 0 0 0 {} {} 0 0 0 17 0 0 0 0 0 0 0 0 {} {} {} {} {} 0\n", + p.ppid, + p.pgid, + p.sid, + u.faults, + u.user, + u.system, + 20, + 0, + p.tids.len(), + vsize(p), + p.ignored, + p.caught, + p.brk.0, + e.args, + e.env, + e.env, + e.end, + ); + head.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..b4a1b0e66 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/pid_files/statm.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 . + +/* /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; + +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..41a67d9de --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/memory.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 . + +/* /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() +} + +pub(super) fn meminfo(v: &View) -> Vec { + let total = d::MEMORY / 1024; + let free = total.saturating_sub(family(v).resident_kb); + let mut s = String::new(); + for (name, kb) in [ + ("MemTotal:", total), + ("MemFree:", free), + ("MemAvailable:", free), + ("Buffers:", 0), + ("Cached:", 0), + ("SwapCached:", 0), + ("SwapTotal:", 0), + ("SwapFree:", 0), + ("Shmem:", 0), + ] { + 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..3f0e94a0b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/proc/system/time.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 . + +/* /proc/uptime and /proc/loadavg. */ + +use alloc::vec::Vec; + +use super::super::super::super::declared::HZ; +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() +} + +/* Load is not measured for a family, so it reads as none. */ +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 mut s = alloc::format!("0.00 0.00 0.00 {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..2337fcd2b --- /dev/null +++ b/userland/capsule_linux/src/linux/file/made/view/proc.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 . + +/* 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, +} 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/meta/mod.rs b/userland/capsule_linux/src/linux/file/meta/mod.rs index 24a698f14..20e209bd2 100644 --- a/userland/capsule_linux/src/linux/file/meta/mod.rs +++ b/userland/capsule_linux/src/linux/file/meta/mod.rs @@ -28,5 +28,5 @@ 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/fd.rs b/userland/capsule_linux/src/linux/file/meta/node/fd.rs index 562fe44b1..c77469e8f 100644 --- a/userland/capsule_linux/src/linux/file/meta/node/fd.rs +++ b/userland/capsule_linux/src/linux/file/meta/node/fd.rs @@ -20,19 +20,14 @@ 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::path::{at, of, S_IFIFO, S_IFREG, S_IFSOCK}; +use super::made::named; +use super::path::{at, of}; -/* - * What has no path, a pipe, the console, a socket or an object with no - * file behind it, by its mode alone. - */ -pub(super) fn kind(mode: u32) -> Meta { - at(b"/", mode, 0, now()) -} - -/* fstat: a file by its path, and the rest by kind. */ +/* 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), @@ -41,12 +36,20 @@ pub fn of_fd(guest: &Guest, f: &Fd) -> Result { /* 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()))) } - /* The console is a stream with no terminal behind it, as a pipe is. */ - Kind::Stdin | Kind::Stdout | Kind::Stderr | Kind::Pipe => Ok(kind(S_IFIFO | 0o600)), - Kind::Socket | Kind::Unix | Kind::Resolver => Ok(kind(S_IFSOCK | 0o777)), + 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(kind(0o600)), + Kind::Epoll | Kind::Timer | Kind::Event | Kind::Signal => Ok(named(b"anon_inode:[]", b"/")), } } 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 index 9420b1c85..9ff2a0eb7 100644 --- a/userland/capsule_linux/src/linux/file/meta/node/mod.rs +++ b/userland/capsule_linux/src/linux/file/meta/node/mod.rs @@ -21,8 +21,9 @@ mod device; mod fd; +mod made; mod path; pub use super::statbuf::Meta; pub use fd::{now, of_fd}; -pub use path::{of, S_IFDIR}; +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 index a8ae3b712..75de5e9eb 100644 --- a/userland/capsule_linux/src/linux/file/meta/node/path.rs +++ b/userland/capsule_linux/src/linux/file/meta/node/path.rs @@ -22,33 +22,30 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::super::{cache, dev, modes, resolve, store, times}; +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; - -pub const S_IFDIR: u32 = 0o040000; - -pub const S_IFREG: u32 = 0o100000; - -pub const S_IFLNK: u32 = 0o120000; +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: 0, mtime_ms, atime_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 = guest.links.follow(named, follow); + 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())); @@ -57,6 +54,9 @@ pub fn of(guest: &Guest, named: alloc::vec::Vec, follow: bool) -> Result u64 { let Some(at) = resolve_at(guest, dirfd, path) else { return errno::fail(errno::EFAULT); }; - change(&guest.links.follow(at, true), mode) + change(&walk::follow(guest, at, true), mode) } pub fn fchmod(guest: &Guest, fd: u64, mode: u64) -> u64 { @@ -43,13 +43,13 @@ pub fn fchmod(guest: &Guest, fd: u64, mode: u64) -> u64 { /* * The store keeps no modes, so the family does (held/modes.rs). The shared - * tree is read-only. + * 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 !cache::held(full) && resolve::key(full).writable().is_err() { + if synth::owns(full) || (!cache::held(full) && resolve::key(full).writable().is_err()) { return errno::fail(errno::EROFS); } modes::set(full, mode as u32); diff --git a/userland/capsule_linux/src/linux/file/meta/query/access.rs b/userland/capsule_linux/src/linux/file/meta/query/access.rs index 6420eb4fc..40b0428de 100644 --- a/userland/capsule_linux/src/linux/file/meta/query/access.rs +++ b/userland/capsule_linux/src/linux/file/meta/query/access.rs @@ -19,8 +19,9 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::super::{at, cache, path, resolve}; -use super::super::node::S_IFDIR; +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; @@ -47,7 +48,7 @@ pub fn faccessat(guest: &Guest, dirfd: u64, path_ptr: u64, mode: u64, flags: u64 Ok(m) => m, Err(e) => return errno::fail(e), }; - let is_dir = m.mode & 0o170000 == S_IFDIR; + 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); } @@ -60,6 +61,10 @@ pub fn faccessat(guest: &Guest, dirfd: u64, path_ptr: u64, mode: u64, flags: u64 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 = guest.links.follow(named, true); + 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 index 18f469443..d737c07be 100644 --- a/userland/capsule_linux/src/linux/file/meta/query/link.rs +++ b/userland/capsule_linux/src/linux/file/meta/query/link.rs @@ -16,16 +16,19 @@ /* 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::at; -use super::super::node::{self}; +use super::super::super::synth::{self, Node}; +use super::super::super::{at, walk}; +use super::super::node; -/* Whether the name is itself a link. */ +/* 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 = guest.links.follow(named.to_vec(), false); - guest.links.target(&full).is_some() + 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 { @@ -35,11 +38,16 @@ pub fn readlinkat(guest: &Guest, dirfd: u64, path_ptr: u64, buf: u64, len: u64) let Some(full) = at::resolve_at(guest, dirfd, path_ptr) else { return errno::fail(errno::EFAULT); }; - let Some(to) = guest.links.target(&full) else { - return match node::of(guest, full, false) { - Ok(_) => errno::fail(errno::EINVAL), - Err(e) => errno::fail(e), - }; + 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 { diff --git a/userland/capsule_linux/src/linux/file/meta/stat/calls.rs b/userland/capsule_linux/src/linux/file/meta/stat/calls.rs index e4f044f55..0e1b52b29 100644 --- a/userland/capsule_linux/src/linux/file/meta/stat/calls.rs +++ b/userland/capsule_linux/src/linux/file/meta/stat/calls.rs @@ -19,7 +19,7 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::super::{cache, dev, resolve, store}; +use super::super::super::{cache, dev, resolve, store, synth}; use super::super::node::{self}; use super::at::{meta_at, write_out}; @@ -32,6 +32,9 @@ 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)); } 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..3d1669d24 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs/calls.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 . + +/* 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(super) const BSIZE: u64 = 1024; + +pub(super) const BLOCKS: u64 = 1 << 20; + +pub(super) const FREE: u64 = BLOCKS / 2; + +pub(super) const ST_RDONLY: u64 = 1; + +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.rs b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs similarity index 52% rename from userland/capsule_linux/src/linux/file/meta/statfs.rs rename to userland/capsule_linux/src/linux/file/meta/statfs/fill.rs index a4c9a04a4..afe4400ae 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs @@ -14,34 +14,33 @@ // 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. +/* struct statfs, from the mount table and the room on the mount. */ use crate::linux::abi::errno; use crate::linux::guest::Guest; -/// `struct statfs` on x86_64 is 120 bytes. +use super::super::super::mounts; +use super::calls::{BLOCKS, BSIZE, FREE, ST_RDONLY}; + 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 { +/* + * The mount's type and flags from the family's mount table; the sizes are + * the ones the personality declares for every mount. + */ +pub(super) fn fill(guest: &Guest, path: &[u8], out: u64) -> u64 { + let (id, _, magic) = mounts::of(path); + let ro = mounts::MOUNTS.iter().find(|m| m.0 == id).is_some_and(|m| m.4.starts_with("ro")); 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 + put(&mut buf, 0, magic); /* f_type */ + 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, 48, u64::from(id)); /* f_fsid */ + put(&mut buf, 56, 255); /* f_namelen, the vfs path limit */ + put(&mut buf, 64, BSIZE); /* f_frsize */ + put(&mut buf, 72, if ro { ST_RDONLY } else { 0 }); /* f_flags */ 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/meta/statfs/mod.rs b/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs new file mode 100644 index 000000000..c08b05d73 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs/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 . + +/* + * 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. + */ + +mod calls; +mod fill; + +pub use calls::{fstatfs, statfs}; diff --git a/userland/capsule_linux/src/linux/file/mknod.rs b/userland/capsule_linux/src/linux/file/mknod.rs index b2f3093aa..efb4f0426 100644 --- a/userland/capsule_linux/src/linux/file/mknod.rs +++ b/userland/capsule_linux/src/linux/file/mknod.rs @@ -38,7 +38,7 @@ 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), &[]) { diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index 8e7fea926..4a904e7c3 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -39,6 +39,7 @@ pub mod flags; mod fsync; mod held; mod link; +mod made; mod memfd; mod memfd_map; mod meta; @@ -57,12 +58,13 @@ mod seek; mod slot; mod store; mod store_name; +mod system; mod timerfd; mod timerfd_read; mod timerfd_spec; +mod walk; mod write; -pub use calls::*; pub use close::close; pub use cstr::read_cstr; pub use dev_io::{read as dev_read, write as dev_write}; @@ -79,7 +81,8 @@ pub use link::{linkat, symlinkat}; pub use memfd::{ftruncate, 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; @@ -95,4 +98,6 @@ 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::*, made::*, system::*}; diff --git a/userland/capsule_linux/src/linux/file/open/named.rs b/userland/capsule_linux/src/linux/file/open/named.rs index 46df8317b..07e47b5a1 100644 --- a/userland/capsule_linux/src/linux/file/open/named.rs +++ b/userland/capsule_linux/src/linux/file/open/named.rs @@ -22,7 +22,7 @@ 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}; +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 { @@ -30,11 +30,14 @@ pub fn open_named(guest: &mut Guest, named: Vec, flags: u64, mode: u64) -> u if flags & O_NOFOLLOW != 0 && super::super::meta::is_link(guest, &named) { return errno::fail(errno::ELOOP); } - let full = guest.links.follow(named, true); + 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(), diff --git a/userland/capsule_linux/src/linux/file/owner/chown.rs b/userland/capsule_linux/src/linux/file/owner/chown.rs index 7abdc217f..eba0313ee 100644 --- a/userland/capsule_linux/src/linux/file/owner/chown.rs +++ b/userland/capsule_linux/src/linux/file/owner/chown.rs @@ -33,7 +33,7 @@ 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) diff --git a/userland/capsule_linux/src/linux/file/owner/stamp.rs b/userland/capsule_linux/src/linux/file/owner/stamp.rs index 3567b6e55..9ff2408e2 100644 --- a/userland/capsule_linux/src/linux/file/owner/stamp.rs +++ b/userland/capsule_linux/src/linux/file/owner/stamp.rs @@ -41,7 +41,9 @@ pub(super) fn stamp( } } p => match resolve_at(guest, dirfd, p) { - Some(named) => guest.links.follow(named, flags & AT_SYMLINK_NOFOLLOW == 0), + Some(named) => { + super::super::walk::follow(guest, named, flags & AT_SYMLINK_NOFOLLOW == 0) + } None => return errno::fail(errno::EFAULT), }, }; @@ -49,7 +51,9 @@ pub(super) fn stamp( Ok(now) => now, Err(e) => return errno::fail(e), }; - if !super::super::cache::held(&full) && key(&full).writable().is_err() { + 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) diff --git a/userland/capsule_linux/src/linux/file/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs index c74346d70..14073c07e 100644 --- a/userland/capsule_linux/src/linux/file/rename.rs +++ b/userland/capsule_linux/src/linux/file/rename.rs @@ -22,7 +22,7 @@ use crate::linux::guest::Guest; use super::at::resolve_at; use super::meta::look; use super::resolve::key; -use super::{cache, modes, store_name}; +use super::{cache, modes, store_name, synth}; pub fn rename(guest: &Guest, old: u64, new: u64) -> u64 { renameat2(guest, super::flags::AT_FDCWD, old, super::flags::AT_FDCWD, new, 0) @@ -42,7 +42,7 @@ pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, fl if flags & RENAME_NOREPLACE != 0 && there { return errno::fail(errno::EEXIST); } - if [&from, &to].iter().any(|p| key(p).writable().is_err()) { + if [&from, &to].iter().any(|p| synth::owns(p) || key(p).writable().is_err()) { return errno::fail(errno::EROFS); } if from == to { 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..5e7636606 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/cpu/ended.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 . + +/* What each process that has exited used, kept for its parent. */ + +use alloc::vec::Vec; + +/* 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..09b1e3c8e --- /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::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..4dbbafc6c --- /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}; 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..0f42f8ca9 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/declared/sizes.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 . + +/* 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; 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..eb5412b7e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/system/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 . + +/* + * What the family is told about the system it runs on, and what it + * has used of it. + */ + +pub mod cpu; +pub mod declared; 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..67e76daf1 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/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 . + +/* + * 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). + * + * Every result is a path of the family's own tree. /proc//root is the + * family's root, so nothing reached through it, or through `..` after it, + * is outside that root; and a descriptor's link that names no path, a pipe + * or a socket, is not followed through, as Linux cannot follow it either. + */ + +mod path; +mod step; + +pub use step::follow; 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..029e59a6e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/path.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 . + +/* Following a path, a name at a time, under a root. */ + +use alloc::vec::Vec; + +use super::super::resolve::visible; +use super::super::synth::{self, Node}; + +/* The path with the first made link in it replaced by its target. */ +pub(super) fn made_link(path: &[u8], last: bool) -> Option> { + if !synth::owns(path) { + return None; + } + let ends = path.iter().enumerate().skip(1).filter(|(_, b)| **b == b'/').map(|(i, _)| i); + let whole = last.then_some(path.len()); + for end in ends.chain(whole) { + let Some(Ok(Node::Link(to))) = synth::node(&path[..end]) else { + continue; + }; + /* A pipe or a socket: there is nothing to walk through. */ + if to.first() != Some(&b'/') && to.contains(&b':') { + return None; + } + let dir = &path[..path[..end].iter().rposition(|b| *b == b'/').unwrap_or(0)]; + let mut joined = match to.first() == Some(&b'/') { + true => Vec::new(), + false => [dir, b"/"].concat(), + }; + joined.extend_from_slice(&to); + joined.extend_from_slice(&path[end..]); + return Some(visible(b"/", &joined)); + } + None +} 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..2c3fd934c --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/step.rs @@ -0,0 +1,40 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public 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::made_link; + +const MAX_HOPS: usize = 16; + +/* + * `path` with every link in it followed; its last component too when + * `last` is set. + */ +pub fn follow(guest: &Guest, path: Vec, last: bool) -> Vec { + let mut path = guest.links.follow(path, last); + for _ in 0..MAX_HOPS { + match made_link(&path, last) { + Some(next) => path = guest.links.follow(next, last), + None => break, + } + } + path +} diff --git a/userland/capsule_linux/src/linux/image/stack.rs b/userland/capsule_linux/src/linux/image/stack.rs index ddeac8e20..1fa5d62a1 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. @@ -38,6 +37,7 @@ pub fn build( let mut all = argv.to_vec(); all.extend_from_slice(envp); let placed = place(guest, top, &all)?; + crate::linux::file::record_image(guest.pid, argv, &placed.at, top); let random_at = (placed.floor - RANDOM_LEN) & !0x0F; let aux = pairs(image, interp_base, random_at, *placed.at.first()?); 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_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..65df6cefe --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/lend.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 . + +/* 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) { + 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..34d252a25 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_view/proc.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 . + +/* 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 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, + } +} diff --git a/userland/capsule_linux/src/linux/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index 7d2550f5a..a2359509c 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; @@ -37,6 +37,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,9 +45,9 @@ 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_file; diff --git a/userland/capsule_linux/src/linux/serve/table_meta.rs b/userland/capsule_linux/src/linux/serve/table_meta.rs index 590b36733..5542f2dff 100644 --- a/userland/capsule_linux/src/linux/serve/table_meta.rs +++ b/userland/capsule_linux/src/linux/serve/table_meta.rs @@ -35,7 +35,8 @@ pub fn meta_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { 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 | np::FSTATFS => file::statfs(guest, a[1]), + 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]), From 50e20d3d5d3a6d7e93ca48df5fe07eb78b6c6bc3 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:12:27 +0000 Subject: [PATCH 08/34] linux: serve the file calls a program meets beyond read and write pwrite64, preadv, pwritev, preadv2, pwritev2, sendfile, copy_file_range, truncate, fallocate, fadvise64, close_range, openat2, sync, syncfs, creat and the twelve xattr calls answered ENOSYS, and ftruncate refused a regular file. busybox cat copies with sendfile, Go's io.Copy between files uses copy_file_range, and os.Truncate and the xattr calls are os and x/sys routines. Each now answers as Linux does. The positional and vector forms read and write at an offset without moving the descriptor's (ESPIPE on a pipe), and the v2 forms take -1 for the descriptor's own offset and flags 0. sendfile copies a file to a file or to the console, and answers EINVAL for a pipe or a socket, as Linux does for an output it cannot splice into, so its callers fall back to read and write. copy_file_range copies between two files. truncate and ftruncate resize the family's copy. fallocate is tmpfs's, since the family's writable directories are its tmpfs mounts: mode 0 grows, KEEP_SIZE keeps, PUNCH_HOLE with KEEP_SIZE zeroes, and the rest is EOPNOTSUPP. close_range closes or marks a range close-on-exec. openat2 honours RESOLVE_BENEATH, NO_XDEV, NO_SYMLINKS and NO_MAGICLINKS, answers CACHED with EAGAIN as Linux may, and refuses IN_ROOT with EINVAL, as a kernel refuses a flag it does not know. sync and syncfs put every file the family is writing into the store. The family keeps its files' extended attributes as tmpfs keeps them, with Linux's names, flags and errors; the read-only tree has none (EROFS). Their table is reached from the look-only table's last arm, so the dispatch chain in table.rs is left as it was. --- .../capsule_linux/src/linux/abi/errno_io.rs | 3 + .../capsule_linux/src/linux/abi/nr_file.rs | 27 +++++++ .../src/linux/file/calls/falloc/calls.rs | 75 +++++++++++++++++++ .../src/linux/file/calls/falloc/mod.rs | 22 ++++++ .../src/linux/file/calls/falloc/zero.rs | 31 ++++++++ .../src/linux/file/calls/fdrange.rs | 50 +++++++++++++ .../capsule_linux/src/linux/file/calls/mod.rs | 10 +++ .../src/linux/file/calls/openat2/check.rs | 45 +++++++++++ .../src/linux/file/calls/openat2/how.rs | 57 ++++++++++++++ .../src/linux/file/calls/openat2/mod.rs | 33 ++++++++ .../src/linux/file/calls/openat2/open.rs | 69 +++++++++++++++++ .../src/linux/file/calls/openat2/walked.rs | 66 ++++++++++++++++ .../src/linux/file/calls/sendfile/bytes.rs | 65 ++++++++++++++++ .../src/linux/file/calls/sendfile/copy.rs | 64 ++++++++++++++++ .../src/linux/file/calls/sendfile/mod.rs | 34 +++++++++ .../src/linux/file/calls/sendfile/offset.rs | 32 ++++++++ .../src/linux/file/calls/sendfile/send.rs | 48 ++++++++++++ .../src/linux/file/calls/size/advice.rs | 51 +++++++++++++ .../src/linux/file/calls/size/mod.rs | 27 +++++++ .../src/linux/file/calls/size/truncate.rs | 73 ++++++++++++++++++ .../src/linux/file/dirops/unlink.rs | 1 + .../capsule_linux/src/linux/file/fsync.rs | 16 ++++ .../src/linux/file/held/cache/flush.rs | 9 +++ .../src/linux/file/held/cache/mod.rs | 5 +- .../capsule_linux/src/linux/file/made/need.rs | 20 +++-- .../capsule_linux/src/linux/file/memfd.rs | 15 +--- userland/capsule_linux/src/linux/file/mod.rs | 11 +-- .../capsule_linux/src/linux/file/open/mod.rs | 2 + .../capsule_linux/src/linux/file/pread/mod.rs | 12 ++- .../src/linux/file/pread/plain.rs | 25 ++++++- .../src/linux/file/pread/sync.rs | 30 ++++++++ .../src/linux/file/pread/vector.rs | 43 +++++++++++ .../capsule_linux/src/linux/file/rename.rs | 5 +- .../src/linux/file/xattrs/mod.rs | 22 ++++++ .../src/linux/file/xattrs/xattr/answer.rs | 65 ++++++++++++++++ .../src/linux/file/xattrs/xattr/calls.rs | 60 +++++++++++++++ .../src/linux/file/xattrs/xattr/give.rs | 39 ++++++++++ .../src/linux/file/xattrs/xattr/mod.rs | 26 +++++++ .../src/linux/file/xattrs/xattr_table/edit.rs | 55 ++++++++++++++ .../src/linux/file/xattrs/xattr_table/mod.rs | 33 ++++++++ .../src/linux/file/xattrs/xattr_table/set.rs | 46 ++++++++++++ .../linux/file/xattrs/xattr_table/table.rs | 58 ++++++++++++++ userland/capsule_linux/src/linux/serve/mod.rs | 1 + .../src/linux/serve/table_data.rs | 73 ++++++++++++++++++ .../src/linux/serve/table_file.rs | 2 +- .../src/linux/serve/table_meta.rs | 2 +- 46 files changed, 1519 insertions(+), 39 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/calls/falloc/calls.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/falloc/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/falloc/zero.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/fdrange.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/check.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/how.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/open.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/walked.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/sendfile/bytes.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/sendfile/copy.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/sendfile/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/sendfile/offset.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/sendfile/send.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/size/advice.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/size/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/calls/size/truncate.rs create mode 100644 userland/capsule_linux/src/linux/file/pread/sync.rs create mode 100644 userland/capsule_linux/src/linux/file/pread/vector.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr/answer.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr/calls.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr/give.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr_table/edit.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr_table/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr_table/set.rs create mode 100644 userland/capsule_linux/src/linux/file/xattrs/xattr_table/table.rs create mode 100644 userland/capsule_linux/src/linux/serve/table_data.rs diff --git a/userland/capsule_linux/src/linux/abi/errno_io.rs b/userland/capsule_linux/src/linux/abi/errno_io.rs index 8b1e6a29d..86c3ff558 100644 --- a/userland/capsule_linux/src/linux/abi/errno_io.rs +++ b/userland/capsule_linux/src/linux/abi/errno_io.rs @@ -20,4 +20,7 @@ */ pub const ENXIO: i64 = 6; +pub const EXDEV: i64 = 18; pub const EFBIG: i64 = 27; +pub const ENODATA: i64 = 61; +pub const EOPNOTSUPP: i64 = 95; diff --git a/userland/capsule_linux/src/linux/abi/nr_file.rs b/userland/capsule_linux/src/linux/abi/nr_file.rs index 40dfa325e..3c04ee2a1 100644 --- a/userland/capsule_linux/src/linux/abi/nr_file.rs +++ b/userland/capsule_linux/src/linux/abi/nr_file.rs @@ -19,5 +19,32 @@ * from the x86_64 table. */ +/* Files, their data and their locks; from syscall_64.tbl. */ +pub const SENDFILE: u64 = 40; 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; pub const TIMES: u64 = 100; 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/mod.rs b/userland/capsule_linux/src/linux/file/calls/mod.rs index 7f5df9945..404434cc0 100644 --- a/userland/capsule_linux/src/linux/file/calls/mod.rs +++ b/userland/capsule_linux/src/linux/file/calls/mod.rs @@ -18,6 +18,16 @@ * 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..ea1eea764 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/check.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 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, path}; +use super::how::open_how; +use super::open::{escapes, BENEATH}; +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 = at::named_at(guest, dirfd, b".")?; + if rules & BENEATH != 0 && (name.first() == Some(&b'/') || escapes(&name)) { + return Err(errno::EXDEV); + } + 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..8c32df5cc --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/how.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 . + +/* 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); + } + if 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..e4a34fd1c --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/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 . + +/* + * 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 and BENEATH, which a walk of + * the family's tree can check; CACHED, which Linux may always answer with + * EAGAIN and so does here. IN_ROOT would re-root every link's target at + * the directory, which this resolver does not do: it is refused with + * EINVAL, as a kernel refuses a flag it does not know. + */ + +mod check; +mod how; +mod open; +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..f7f322abc --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/open.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 . + +/* 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), + } +} + +/* Whether `..` in the name climbs above where it starts. */ +pub(super) fn escapes(name: &[u8]) -> bool { + let mut depth = 0i64; + for part in name.split(|b| *b == b'/') { + match part { + b"" | b"." => {} + b".." => depth -= 1, + _ => depth += 1, + } + if depth < 0 { + return true; + } + } + false +} 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..1397d6045 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/calls/openat2/walked.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 walk open makes, refused at the first step the rules forbid. */ + +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::{mounts, resolve, walk}; +use super::open::{BENEATH, NO_MAGICLINKS, NO_SYMLINKS, NO_XDEV}; + +/* + * Walk the name a component at a time from where it starts, and check each + * step against `rules`. + */ +pub(super) fn walked(guest: &Guest, base: &[u8], named: &[u8], rules: u64) -> Result<(), i64> { + let mount = mounts::of(base).0; + let below = base == b"/" + || named.starts_with(base) && matches!(named.get(base.len()), None | Some(b'/')); + let (mut at, rest) = match below && base != b"/" { + true => (base.to_vec(), &named[base.len()..]), + false => (Vec::new(), named), + }; + for part in rest.split(|b| *b == b'/').filter(|p| !p.is_empty()) { + at.push(b'/'); + at.extend_from_slice(part); + let link = guest.links.target(&at).is_some(); + let made = matches!(synth::node(&at), Some(Ok(Node::Link(_)))); + let magic = made + && at.starts_with(b"/proc/") + && !at.ends_with(b"/self") + && !at.ends_with(b"/thread-self"); + if (rules & NO_SYMLINKS != 0 && (link || made)) || (rules & NO_MAGICLINKS != 0 && magic) { + return Err(errno::ELOOP); + } + if link || made { + at = walk::follow(guest, core::mem::take(&mut at), true); + } + if rules & NO_XDEV != 0 && mounts::of(&at).0 != mount { + return Err(errno::EXDEV); + } + } + let end = resolve::visible(b"/", &at); + let inside = + base == b"/" || end.starts_with(base) && matches!(end.get(base.len()), None | Some(b'/')); + if rules & BENEATH != 0 && !inside { + return Err(errno::EXDEV); + } + Ok(()) +} 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/dirops/unlink.rs b/userland/capsule_linux/src/linux/file/dirops/unlink.rs index 8f3886b4e..c36353ce4 100644 --- a/userland/capsule_linux/src/linux/file/dirops/unlink.rs +++ b/userland/capsule_linux/src/linux/file/dirops/unlink.rs @@ -55,6 +55,7 @@ pub fn unlinkat(guest: &Guest, dirfd: u64, path: u64, flags: u64) -> u64 { 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), diff --git a/userland/capsule_linux/src/linux/file/fsync.rs b/userland/capsule_linux/src/linux/file/fsync.rs index 752054a8f..4a86051ee 100644 --- a/userland/capsule_linux/src/linux/file/fsync.rs +++ b/userland/capsule_linux/src/linux/file/fsync.rs @@ -38,3 +38,19 @@ pub fn fsync(guest: &Guest, fd: u64) -> u64 { 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/flush.rs b/userland/capsule_linux/src/linux/file/held/cache/flush.rs index dc730f298..132348a45 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/flush.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.rs @@ -16,6 +16,8 @@ /* A copy put in the store: at close, fsync and sync, and at exit. */ +use alloc::vec::Vec; + use crate::linux::abi::errno; use super::super::super::{resolve, store}; @@ -39,3 +41,10 @@ pub fn flush(path: &[u8], keep: bool) -> Result<(), i64> { } Ok(()) } + +/* Every changed file to the store: sync and syncfs. */ +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 index cdab12d6e..633495227 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -22,7 +22,8 @@ * 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 and at fsync. + * meet the same bytes. The copy goes to the store at close, at fsync and + * at sync. */ mod change; @@ -32,7 +33,7 @@ mod table; mod take; pub use change::{resize, write}; -pub use flush::flush; +pub use flush::{flush, flush_all}; pub use names::{forget, names_in, renamed}; pub use table::{held, mtime, size}; pub use take::{hold, read}; diff --git a/userland/capsule_linux/src/linux/file/made/need.rs b/userland/capsule_linux/src/linux/file/made/need.rs index bb30243ba..b94d359e2 100644 --- a/userland/capsule_linux/src/linux/file/made/need.rs +++ b/userland/capsule_linux/src/linux/file/made/need.rs @@ -23,12 +23,13 @@ use crate::linux::guest::{Guest, Kind}; /* - * open, stat, lstat, access, execve, chdir, readlink, chmod, statfs, - * utime, getppid, the *at forms, and close and the calls that close. + * open, stat, lstat, access, execve, truncate, chdir, readlink, chmod, + * statfs, utime, getppid, the xattr calls, the *at forms and openat2, and + * close and the calls that close. */ -const ALWAYS: [u64; 23] = [ - 2, 3, 4, 6, 21, 33, 59, 80, 89, 90, 110, 132, 137, 235, 257, 262, 267, 268, 269, 280, 292, 332, - 439, +const ALWAYS: [u64; 38] = [ + 2, 3, 4, 6, 21, 33, 59, 76, 80, 89, 90, 110, 132, 137, 188, 189, 190, 191, 192, 193, 194, 195, + 196, 197, 198, 199, 235, 257, 262, 267, 268, 269, 280, 292, 332, 436, 437, 439, ]; pub fn needs_view(guest: &Guest, nr: u64, a: [u64; 6]) -> bool { @@ -41,6 +42,11 @@ pub fn needs_view(guest: &Guest, nr: u64, a: [u64; 6]) -> bool { .get(fd as usize) .is_some_and(|f| f.kind == Kind::File && f.path.starts_with(b"/proc")) }; - /* read, fstat, pread64 and readv */ - matches!(nr, 0 | 5 | 17 | 19) && proc_fd(a[0]) + 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/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/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index 4a904e7c3..6e1ad046b 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -64,6 +64,7 @@ mod timerfd_read; mod timerfd_spec; mod walk; mod write; +mod xattrs; pub use close::close; pub use cstr::read_cstr; @@ -75,10 +76,10 @@ 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}; use held::*; 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, fstatfs, look, newfstatat, readlinkat, @@ -88,10 +89,10 @@ pub use mknod::mknodat; pub use open::openat; 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}; @@ -100,4 +101,4 @@ 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::*, made::*, system::*}; +pub use {calls::*, made::*, system::*, xattrs::*}; diff --git a/userland/capsule_linux/src/linux/file/open/mod.rs b/userland/capsule_linux/src/linux/file/open/mod.rs index 3ace2a966..a3081c7cf 100644 --- a/userland/capsule_linux/src/linux/file/open/mod.rs +++ b/userland/capsule_linux/src/linux/file/open/mod.rs @@ -20,4 +20,6 @@ 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/pread/mod.rs b/userland/capsule_linux/src/linux/file/pread/mod.rs index 7ea5fd7de..d593e66ec 100644 --- a/userland/capsule_linux/src/linux/file/pread/mod.rs +++ b/userland/capsule_linux/src/linux/file/pread/mod.rs @@ -15,11 +15,15 @@ // along with this program. If not, see . /* - * pread64: a read at the offset it is given, which leaves the descriptor's - * own offset where it was; a pipe, a socket or a console has no offset, - * which Linux calls ESPIPE. + * The positional forms: pread64, pwrite64, preadv, pwritev, and preadv2 and + * pwritev2 with no flags. 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 plain; +mod sync; +mod vector; -pub use plain::pread64; +pub use plain::{pread64, pwrite64}; +pub use sync::{preadv, pwritev}; diff --git a/userland/capsule_linux/src/linux/file/pread/plain.rs b/userland/capsule_linux/src/linux/file/pread/plain.rs index 5fe081770..f5ce3301a 100644 --- a/userland/capsule_linux/src/linux/file/pread/plain.rs +++ b/userland/capsule_linux/src/linux/file/pread/plain.rs @@ -19,10 +19,28 @@ use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; +use super::super::rw::{write_at, MAX_IO}; + 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; + } + let Some(bytes) = guest.read(buf, (len as usize).min(MAX_IO)) else { + return errno::fail(errno::EFAULT); + }; + match write_at(guest, fd, at, &bytes) { + Ok((n, _)) => errno::ok(n as u64), + Err(e) => errno::fail(e), + } +} + 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)), @@ -33,7 +51,12 @@ fn seekable(guest: &Guest, fd: u64) -> Result { } /* Run `go` with the descriptor's offset set to `at`, then put it back. */ -fn at_offset(guest: &mut Guest, fd: u64, at: u64, go: impl FnOnce(&mut Guest) -> u64) -> u64 { +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); } 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..7a527fef8 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/sync.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 . + +/* preadv and pwritev, and a write put in the store as RWF_DSYNC asks. */ + +use crate::linux::call; +use crate::linux::guest::Guest; + +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, |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, |g| call::writev(g, fd, iov, count)) +} 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..aede1fd94 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/pread/vector.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 . + +/* preadv2 and pwritev2 with their RWF_ flags. */ + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::plain::at_offset; + +/* + * The v2 forms: an offset of -1 means the descriptor's own, which then + * moves; any RWF_ flag is one this personality does not act on. + */ +pub(super) fn vectored( + guest: &mut Guest, + fd: u64, + at: u64, + flags: u64, + go: impl FnOnce(&mut Guest) -> u64, +) -> u64 { + if flags != 0 { + return errno::fail(errno::EOPNOTSUPP); + } + match at as i64 { + -1 => go(guest), + n if n < 0 => errno::fail(errno::EINVAL), + _ => at_offset(guest, fd, at, go), + } +} diff --git a/userland/capsule_linux/src/linux/file/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs index 14073c07e..dc0e96874 100644 --- a/userland/capsule_linux/src/linux/file/rename.rs +++ b/userland/capsule_linux/src/linux/file/rename.rs @@ -24,10 +24,6 @@ use super::meta::look; use super::resolve::key; use super::{cache, modes, store_name, synth}; -pub fn rename(guest: &Guest, old: u64, new: u64) -> u64 { - renameat2(guest, super::flags::AT_FDCWD, old, super::flags::AT_FDCWD, new, 0) -} - const RENAME_NOREPLACE: u64 = 1; pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, flags: u64) -> u64 { @@ -68,6 +64,7 @@ pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, fl cache::renamed(&from, &to); modes::renamed(&from, &to); super::times::renamed(&from, &to); + super::xattr_table::renamed(&from, &to); errno::ok(0) } Err(_) => errno::fail(errno::EIO), 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/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index a2359509c..c0affee18 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -50,6 +50,7 @@ mod pid_space; mod refused; mod route_life; mod table; +mod table_data; mod table_file; mod table_link; mod table_mem; 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..eed55b362 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_data.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 . + +/* + * 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)), + _ => 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 b96349da1..c5dc1533b 100644 --- a/userland/capsule_linux/src/linux/serve/table_file.rs +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -58,7 +58,7 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option 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::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]), diff --git a/userland/capsule_linux/src/linux/serve/table_meta.rs b/userland/capsule_linux/src/linux/serve/table_meta.rs index 5542f2dff..87fc12a7f 100644 --- a/userland/capsule_linux/src/linux/serve/table_meta.rs +++ b/userland/capsule_linux/src/linux/serve/table_meta.rs @@ -40,6 +40,6 @@ pub fn meta_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { 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 None, + _ => return super::table_data::data_ops(guest, nr, a), }) } From e1730d645740f929a0dd354dcb31918614feeb8c Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:12:36 +0000 Subject: [PATCH 09/34] linux: refuse inotify by name inotify_init, inotify_init1, inotify_add_watch and inotify_rm_watch answered as unserved numbers. None of the programs this lane proves asks for them: the host's strace of busybox running bbsuite, of Go's os, io/fs and path/filepath test suites, and of the regression guests shows no inotify call, and Go's runtime makes none. The store sends no change events a watch could be built on. So each is refused on purpose, with its reason on the console and ENOSYS, which a program that can do without it (tail -f, a file watcher's polling fallback) takes as "not here" and goes on. --- userland/capsule_linux/src/linux/serve/refused.rs | 4 ++++ 1 file changed, 4 insertions(+) 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. From 5943c360c666237e7cfe5799f3d246f6aa82e485 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:13:28 +0000 Subject: [PATCH 10/34] linux: serve flock and fcntl's record locks, with Linux's rules flock answered ENOSYS and fcntl's F_GETLK, F_SETLK, F_SETLKW and their OFD forms EINVAL, so Go's syscall.Flock and any program that locks a file failed. The family keeps one lock table (file/locks/lock/) with Linux's three kinds. A flock lock belongs to the open file description, so a dup or a fork shares it; it goes when the last descriptor on the description closes, anywhere in the family. A POSIX record lock belongs to the process and goes when it closes any descriptor on the file, or exits. An OFD lock is a record lock owned by a description. flock locks never meet record locks; POSIX and OFD locks meet each other, by overlapping byte ranges, a process's own POSIX locks replace and split each other, and F_GETLK reports the lock in the way with its owner's pid as the guest knows it. LOCK_NB and F_SETLK answer EAGAIN; flock without LOCK_NB and F_SETLKW wait, parked like a read on an empty pipe and tried again after every call, so the close, unlock or exit that frees the lock lets them in. The waiting route is one arm in serve/dispatch.rs, a file of lane A's, and one in waits::attempt; the fcntl commands are new arms in call/ctl.rs. --- .../capsule_linux/src/linux/abi/errno_io.rs | 1 + .../capsule_linux/src/linux/abi/nr_file.rs | 1 + userland/capsule_linux/src/linux/call/ctl.rs | 2 + .../capsule_linux/src/linux/file/close.rs | 5 +- .../src/linux/file/locks/lock/apply.rs | 53 ++++++++++++++ .../src/linux/file/locks/lock/mod.rs | 36 ++++++++++ .../src/linux/file/locks/lock/rules.rs | 47 ++++++++++++ .../src/linux/file/locks/lock/table.rs | 54 ++++++++++++++ .../linux/file/locks/lock_calls/closing.rs | 44 ++++++++++++ .../src/linux/file/locks/lock_calls/flock.rs | 70 ++++++++++++++++++ .../src/linux/file/locks/lock_calls/mod.rs | 31 ++++++++ .../src/linux/file/locks/lock_calls/purge.rs | 47 ++++++++++++ .../capsule_linux/src/linux/file/locks/mod.rs | 26 +++++++ .../src/linux/file/locks/record/cmds.rs | 41 +++++++++++ .../src/linux/file/locks/record/fcntl.rs | 54 ++++++++++++++ .../src/linux/file/locks/record/mod.rs | 30 ++++++++ .../src/linux/file/locks/record/range.rs | 58 +++++++++++++++ .../src/linux/file/locks/record/want.rs | 52 ++++++++++++++ .../capsule_linux/src/linux/file/made/need.rs | 10 +-- userland/capsule_linux/src/linux/file/mod.rs | 3 +- .../capsule_linux/src/linux/serve/dispatch.rs | 2 + userland/capsule_linux/src/linux/serve/mod.rs | 1 + .../src/linux/serve/table_data.rs | 1 + .../capsule_linux/src/linux/serve/waits.rs | 1 + .../src/linux/serve/waits_lock.rs | 71 +++++++++++++++++++ 25 files changed, 731 insertions(+), 10 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/locks/lock/apply.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock/rules.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock/table.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock_calls/flock.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/lock_calls/purge.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/record/cmds.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/record/fcntl.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/record/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/record/range.rs create mode 100644 userland/capsule_linux/src/linux/file/locks/record/want.rs create mode 100644 userland/capsule_linux/src/linux/serve/waits_lock.rs diff --git a/userland/capsule_linux/src/linux/abi/errno_io.rs b/userland/capsule_linux/src/linux/abi/errno_io.rs index 86c3ff558..0179faf66 100644 --- a/userland/capsule_linux/src/linux/abi/errno_io.rs +++ b/userland/capsule_linux/src/linux/abi/errno_io.rs @@ -22,5 +22,6 @@ 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/nr_file.rs b/userland/capsule_linux/src/linux/abi/nr_file.rs index 3c04ee2a1..ec77d2a9f 100644 --- a/userland/capsule_linux/src/linux/abi/nr_file.rs +++ b/userland/capsule_linux/src/linux/abi/nr_file.rs @@ -21,6 +21,7 @@ /* 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; diff --git a/userland/capsule_linux/src/linux/call/ctl.rs b/userland/capsule_linux/src/linux/call/ctl.rs index 267e72302..8bf8dc013 100644 --- a/userland/capsule_linux/src/linux/call/ctl.rs +++ b/userland/capsule_linux/src/linux/call/ctl.rs @@ -57,6 +57,8 @@ pub fn fcntl(guest: &mut Guest, fd: u64, cmd: u64, arg: u64) -> u64 { F_GETFL => errno::ok(status(entry)), /* 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/file/close.rs b/userland/capsule_linux/src/linux/file/close.rs index 1f80c521d..afafe8718 100644 --- a/userland/capsule_linux/src/linux/file/close.rs +++ b/userland/capsule_linux/src/linux/file/close.rs @@ -26,10 +26,7 @@ pub fn close(guest: &mut Guest, fd: u64) -> u64 { if !entry.is_open() { return errno::fail(errno::EBADF); } - if let Some(d) = super::desc::of(entry).filter(|d| !super::desc::held_elsewhere(guest, fd, *d)) - { - super::desc::gone(d); - } + super::lock_calls::closing(guest, fd); let flushed = flush(guest, fd); /* * The store handle is dropped with the descriptor, which closes it on the 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..65d246ab4 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.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 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); + } +} 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..3c1e1e388 --- /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; +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..f2adb15c5 --- /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::{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/need.rs b/userland/capsule_linux/src/linux/file/made/need.rs index b94d359e2..5319b919c 100644 --- a/userland/capsule_linux/src/linux/file/made/need.rs +++ b/userland/capsule_linux/src/linux/file/made/need.rs @@ -24,12 +24,12 @@ use crate::linux::guest::{Guest, Kind}; /* * open, stat, lstat, access, execve, truncate, chdir, readlink, chmod, - * statfs, utime, getppid, the xattr calls, the *at forms and openat2, and - * close and the calls that close. + * statfs, utime, getppid, the xattr calls, the *at forms and openat2, + * flock and fcntl, and close and the calls that close. */ -const ALWAYS: [u64; 38] = [ - 2, 3, 4, 6, 21, 33, 59, 76, 80, 89, 90, 110, 132, 137, 188, 189, 190, 191, 192, 193, 194, 195, - 196, 197, 198, 199, 235, 257, 262, 267, 268, 269, 280, 292, 332, 436, 437, 439, +const ALWAYS: [u64; 40] = [ + 2, 3, 4, 6, 21, 33, 59, 72, 73, 76, 80, 89, 90, 110, 132, 137, 188, 189, 190, 191, 192, 193, + 194, 195, 196, 197, 198, 199, 235, 257, 262, 267, 268, 269, 280, 292, 332, 436, 437, 439, ]; pub fn needs_view(guest: &Guest, nr: u64, a: [u64; 6]) -> bool { diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index 6e1ad046b..8b0fda818 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -39,6 +39,7 @@ pub mod flags; mod fsync; mod held; mod link; +mod locks; mod made; mod memfd; mod memfd_map; @@ -101,4 +102,4 @@ 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::*, made::*, system::*, xattrs::*}; +pub use {calls::*, locks::*, made::*, system::*, xattrs::*}; 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/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index c0affee18..929238df2 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -62,6 +62,7 @@ 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/table_data.rs b/userland/capsule_linux/src/linux/serve/table_data.rs index eed55b362..35ed80185 100644 --- a/userland/capsule_linux/src/linux/serve/table_data.rs +++ b/userland/capsule_linux/src/linux/serve/table_data.rs @@ -68,6 +68,7 @@ pub fn data_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { 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/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) +} From 4220b099a0fa95bedfc4bfffeeb24542e661307e Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:13:51 +0000 Subject: [PATCH 11/34] linux: sysinfo, getrusage, times, and the id, group and priority calls sysinfo, getrusage, times, getgroups, setgroups, setresuid, setresgid, getpriority, setpriority and personality answered ENOSYS, and getresuid and getresgid as unserved numbers although their constants were there. busybox free and uptime read sysinfo, time and Go's os.ProcessState read getrusage, and every shell asks for its groups. sysinfo answers from the declared system: the family's uptime, its memory limit as totalram and that less the family's resident memory as freeram, its thread count, and zero where the personality keeps no such thing (shared, buffer and swap memory, and the load, which is not measured). getrusage and times report what the kernel counted for the process's own threads, the calling thread's for RUSAGE_THREAD, and for RUSAGE_CHILDREN what each child used, recorded when it exits and counted once its parent has waited for it, as Linux counts it; a figure that is not measured is zero. A process's exit now also drops its POSIX locks at once and puts every file the family is writing in the store. Every id is root's and no guest holds a capability, so the rules are those of a process without CAP_SETUID, CAP_SETGID or CAP_SYS_NICE: the ids can be set only to 0, groups not at all (EPERM), and nice only raised (EACCES); the nice value is kept and shown in /proc, not acted on. personality answers PER_LINUX to a query and refuses any change by name. Their table is reached after the look-only table, from the file table's last arm, so the dispatch chain in table.rs is left as it was. --- .../capsule_linux/src/linux/abi/nr_file.rs | 11 +++ userland/capsule_linux/src/linux/call/mod.rs | 2 + .../src/linux/call/process/ids/creds.rs | 70 +++++++++++++++++++ .../src/linux/call/process/ids/mod.rs | 33 +++++++++ .../src/linux/call/process/ids/nice.rs | 68 ++++++++++++++++++ .../src/linux/call/process/ids/who.rs | 50 +++++++++++++ .../src/linux/call/process/mod.rs | 28 ++++++++ .../src/linux/call/process/usage/mod.rs | 34 +++++++++ .../src/linux/call/process/usage/rusage.rs | 57 +++++++++++++++ .../src/linux/call/process/usage/sysinfo.rs | 42 +++++++++++ .../src/linux/call/process/usage/times.rs | 56 +++++++++++++++ .../src/linux/file/held/cache/flush.rs | 2 +- .../src/linux/file/held/cache/mod.rs | 2 +- .../capsule_linux/src/linux/file/held/mod.rs | 2 + .../linux/file/locks/lock_calls/closing.rs | 5 ++ .../src/linux/file/locks/lock_calls/mod.rs | 2 +- .../capsule_linux/src/linux/file/locks/mod.rs | 2 +- .../capsule_linux/src/linux/file/made/need.rs | 11 +-- .../linux/file/made/proc/pid_files/stat.rs | 5 +- userland/capsule_linux/src/linux/file/mod.rs | 3 +- .../src/linux/file/system/cpu/ended.rs | 41 +++++++++++ .../src/linux/file/system/cpu/mod.rs | 2 +- .../src/linux/serve/family_exit.rs | 49 +++++++++++++ .../src/linux/serve/family_view/lend.rs | 1 + userland/capsule_linux/src/linux/serve/mod.rs | 2 + .../src/linux/serve/table_file.rs | 5 +- .../src/linux/serve/table_sys.rs | 43 ++++++++++++ 27 files changed, 613 insertions(+), 15 deletions(-) create mode 100644 userland/capsule_linux/src/linux/call/process/ids/creds.rs create mode 100644 userland/capsule_linux/src/linux/call/process/ids/mod.rs create mode 100644 userland/capsule_linux/src/linux/call/process/ids/nice.rs create mode 100644 userland/capsule_linux/src/linux/call/process/ids/who.rs create mode 100644 userland/capsule_linux/src/linux/call/process/mod.rs create mode 100644 userland/capsule_linux/src/linux/call/process/usage/mod.rs create mode 100644 userland/capsule_linux/src/linux/call/process/usage/rusage.rs create mode 100644 userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs create mode 100644 userland/capsule_linux/src/linux/call/process/usage/times.rs create mode 100644 userland/capsule_linux/src/linux/serve/family_exit.rs create mode 100644 userland/capsule_linux/src/linux/serve/table_sys.rs diff --git a/userland/capsule_linux/src/linux/abi/nr_file.rs b/userland/capsule_linux/src/linux/abi/nr_file.rs index ec77d2a9f..bc114485b 100644 --- a/userland/capsule_linux/src/linux/abi/nr_file.rs +++ b/userland/capsule_linux/src/linux/abi/nr_file.rs @@ -48,4 +48,15 @@ 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/call/mod.rs b/userland/capsule_linux/src/linux/call/mod.rs index 3be3d6506..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}; 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..3d57250b1 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.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 . + +/* 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) = file::view_with(|v| { + let leaders: alloc::vec::Vec = v.procs.iter().map(|p| p.kernel).collect(); + let n: usize = v.procs.iter().map(|p| p.tids.len()).sum(); + (n.max(1), file::cpu::usage(&leaders).resident_kb * 1024) + }); + 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 */ + put(32, declared::MEMORY); /* totalram */ + put(40, declared::MEMORY.saturating_sub(resident)); /* freeram */ + 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/file/held/cache/flush.rs b/userland/capsule_linux/src/linux/file/held/cache/flush.rs index 132348a45..276cc6be0 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/flush.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.rs @@ -42,7 +42,7 @@ pub fn flush(path: &[u8], keep: bool) -> Result<(), i64> { Ok(()) } -/* Every changed file to the store: sync and syncfs. */ +/* 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(); diff --git a/userland/capsule_linux/src/linux/file/held/cache/mod.rs b/userland/capsule_linux/src/linux/file/held/cache/mod.rs index 633495227..7457a1123 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -23,7 +23,7 @@ * 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 - * at sync. + * sync, and when a process exits. */ mod change; diff --git a/userland/capsule_linux/src/linux/file/held/mod.rs b/userland/capsule_linux/src/linux/file/held/mod.rs index eb41fcbe8..7f1acfc6f 100644 --- a/userland/capsule_linux/src/linux/file/held/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/mod.rs @@ -25,3 +25,5 @@ pub(super) mod desc; pub(super) mod modes; pub(super) mod rw; pub(super) mod times; + +pub use cache::flush_all; 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 index 65d246ab4..b20af76b5 100644 --- a/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/closing.rs @@ -42,3 +42,8 @@ pub fn closing(guest: &Guest, fd: u64) { 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/mod.rs b/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs index 3c1e1e388..5283506a6 100644 --- a/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs +++ b/userland/capsule_linux/src/linux/file/locks/lock_calls/mod.rs @@ -25,7 +25,7 @@ mod closing; mod flock; mod purge; -pub use closing::closing; +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/mod.rs b/userland/capsule_linux/src/linux/file/locks/mod.rs index f2adb15c5..1834bb49a 100644 --- a/userland/capsule_linux/src/linux/file/locks/mod.rs +++ b/userland/capsule_linux/src/linux/file/locks/mod.rs @@ -22,5 +22,5 @@ pub(super) mod lock; pub(super) mod lock_calls; pub(super) mod record; -pub use lock_calls::{flock, WAIT as LOCK_WAIT}; +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/made/need.rs b/userland/capsule_linux/src/linux/file/made/need.rs index 5319b919c..fc9f8a577 100644 --- a/userland/capsule_linux/src/linux/file/made/need.rs +++ b/userland/capsule_linux/src/linux/file/made/need.rs @@ -24,12 +24,13 @@ use crate::linux::guest::{Guest, Kind}; /* * open, stat, lstat, access, execve, truncate, chdir, readlink, chmod, - * statfs, utime, getppid, the xattr calls, the *at forms and openat2, - * flock and fcntl, and close and the calls that close. + * 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; 40] = [ - 2, 3, 4, 6, 21, 33, 59, 72, 73, 76, 80, 89, 90, 110, 132, 137, 188, 189, 190, 191, 192, 193, - 194, 195, 196, 197, 198, 199, 235, 257, 262, 267, 268, 269, 280, 292, 332, 436, 437, 439, +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 { 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 index 4b72751f3..555846c1c 100644 --- 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 @@ -27,6 +27,7 @@ pub(super) fn stat(p: &Proc, tid: u32) -> Vec { let comm = core::str::from_utf8(&p.exe.comm).unwrap_or(""); let start = p.exe.start_ms / (1000 / super::super::super::super::declared::HZ); let (e, rss) = (&p.exe, u.resident_kb / 4); + let nice = crate::linux::call::nice_of(p.kernel); let head = alloc::format!( "{tid} ({comm}) {state} {} {} {} 0 -1 0 {} 0 0 0 {} {} 0 0 {} {} {} 0 {start} {} {rss} \ 18446744073709551615 0 0 0 0 0 0 0 {} {} 0 0 0 17 0 0 0 0 0 0 0 0 {} {} {} {} {} 0\n", @@ -36,8 +37,8 @@ pub(super) fn stat(p: &Proc, tid: u32) -> Vec { u.faults, u.user, u.system, - 20, - 0, + 20 + nice, + nice, p.tids.len(), vsize(p), p.ignored, diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index 8b0fda818..a3f738bed 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -78,7 +78,6 @@ 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, sync, syncfs}; -use held::*; pub use link::{linkat, symlinkat}; pub use memfd::{is_memfd, memfd_create}; pub use memfd_map::{mapped_at, set_mapped, staged}; @@ -102,4 +101,4 @@ 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::*, locks::*, made::*, system::*, xattrs::*}; +pub use {calls::*, held::*, locks::*, made::*, system::*, xattrs::*}; diff --git a/userland/capsule_linux/src/linux/file/system/cpu/ended.rs b/userland/capsule_linux/src/linux/file/system/cpu/ended.rs index 5e7636606..03d67e8d5 100644 --- a/userland/capsule_linux/src/linux/file/system/cpu/ended.rs +++ b/userland/capsule_linux/src/linux/file/system/cpu/ended.rs @@ -17,6 +17,47 @@ /* 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 { diff --git a/userland/capsule_linux/src/linux/file/system/cpu/mod.rs b/userland/capsule_linux/src/linux/file/system/cpu/mod.rs index 09b1e3c8e..f244f3508 100644 --- a/userland/capsule_linux/src/linux/file/system/cpu/mod.rs +++ b/userland/capsule_linux/src/linux/file/system/cpu/mod.rs @@ -27,5 +27,5 @@ mod ended; mod usage; -pub use ended::threads_of; +pub use ended::{children, ended, threads_of}; pub use usage::{usage, Usage}; 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..c0d3dc243 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/family_exit.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 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 goes to its parent's + * RUSAGE_CHILDREN once waited for, 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); + let kids = file::cpu::children(g.pid, |c| !g.children.contains(&c)); + used.user += kids.user; + used.system += kids.system; + 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/lend.rs b/userland/capsule_linux/src/linux/serve/family_view/lend.rs index 65df6cefe..e8d461d54 100644 --- a/userland/capsule_linux/src/linux/serve/family_view/lend.rs +++ b/userland/capsule_linux/src/linux/serve/family_view/lend.rs @@ -28,6 +28,7 @@ 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; } diff --git a/userland/capsule_linux/src/linux/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index 929238df2..d329fbdf8 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -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; @@ -58,6 +59,7 @@ mod table_meta; mod table_net; mod table_proc; mod table_sig; +mod table_sys; mod tally; mod unserved; mod waits; diff --git a/userland/capsule_linux/src/linux/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs index c5dc1533b..ceca90bd4 100644 --- a/userland/capsule_linux/src/linux/serve/table_file.rs +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -64,6 +64,9 @@ pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option 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]), - _ => return super::table_meta::meta_ops(guest, nr, a), + _ => { + 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_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, + }) +} From ba00a7094af95b70586da9d18d206d340986b704 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:15:47 +0000 Subject: [PATCH 12/34] linux-guests: let an image carry only the guests it boots The store loads at most 16 MiB and each guest's proof is 327526 bytes, so the test image had room for about the guests it already had: adding three more made nonos-store-pack refuse it, 19031316 payload bytes against 16777216, and every lane adding guests will meet the same wall. LINUX_GUEST_ONLY now names the programs under /linux/bin an image carries; every other entry stays. Unset, the image is what it was. The guests left out are still built, signed and enrolled. --- userland/linux_guests/GuestOnly.mk | 13 +++++++++++++ userland/linux_guests/Guests.mk | 1 + 2 files changed, 14 insertions(+) create mode 100644 userland/linux_guests/GuestOnly.mk 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/Guests.mk b/userland/linux_guests/Guests.mk index 9499ab0b1..008baa3f2 100644 --- a/userland/linux_guests/Guests.mk +++ b/userland/linux_guests/Guests.mk @@ -130,3 +130,4 @@ override NONOS_STORE_MEDIA_ENTRIES := override NONOS_STORE_DEMO_ENTRIES := include $(LINUX_GUESTS_DIR)/GuestFiles.mk +include $(LINUX_GUESTS_DIR)/GuestOnly.mk From 1d73f4987cb129330441cf6ffec3621eb2a507ff Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:16:09 +0000 Subject: [PATCH 13/34] linux-guests: bbsuite prints its own output when it fails On a failure bbsuite ran diff and counted the lines starting with < or >, but busybox's diff prints the unified form, so it counted 0 of 107 differing lines, and only the diff's first line reached the console. It now counts the unified form's lines and prints every line the run made, each prefixed "[C] bbsuite got: ", so one boot's console holds what is needed to diff it against the host's on the host. --- userland/linux_guests/sh/bbsuite.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/userland/linux_guests/sh/bbsuite.sh b/userland/linux_guests/sh/bbsuite.sh index 034d72cf5..66042319d 100644 --- a/userland/linux_guests/sh/bbsuite.sh +++ b/userland/linux_guests/sh/bbsuite.sh @@ -14,7 +14,8 @@ 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 -diff /tmp/bbsuite.want /tmp/bbsuite.got | head -n 80 -differ=$(diff /tmp/bbsuite.want /tmp/bbsuite.got | grep -c '^[<>]') +# 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 From 75a6c39bb8cc19f36111eeb2899caf2970845d12 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:16:39 +0000 Subject: [PATCH 14/34] linux-guests: cfiles, the file and system calls against the host Nothing proved the file calls beyond read and write, the locks, or the system-information calls. cfiles runs fourteen parts, each printing only what must read the same on Linux: pwrite64; preadv, pwritev and the v2 forms; sendfile; copy_file_range; truncate and ftruncate; fallocate as tmpfs serves it, and fadvise64; flock across a dup, another open and a forked child that waits; fcntl record locks by range, with a waiting F_SETLKW, F_GETLK's pid, release at close and at exit, and OFD locks; close_range; openat2's RESOLVE_ flags; sync, syncfs, fsync and fdatasync; the xattr calls; sysinfo, and getrusage and times against a busy loop and a waited child; the ids, groups, nice and personality. Every part runs, and a failing one names its line and the numbers seen. sh/oracle.sh runs a program as the personality runs it: root with no capability, in its own pid namespace with its own /proc, a tmpfs at /tmp, no terminal, and the guest's environment. On the host it prints "[C] cfiles PASS: 14 parts". --- userland/linux_guests/GuestProofs.mk | 9 +++ userland/linux_guests/Guests.mk | 1 + userland/linux_guests/c/cfiles/cfiles.h | 57 ++++++++++++++++ userland/linux_guests/c/cfiles/cfr.c | 22 ++++++ userland/linux_guests/c/cfiles/close_range.c | 23 +++++++ userland/linux_guests/c/cfiles/falloc.c | 28 ++++++++ userland/linux_guests/c/cfiles/fcntl.c | 70 ++++++++++++++++++++ userland/linux_guests/c/cfiles/flock.c | 37 +++++++++++ userland/linux_guests/c/cfiles/harness.c | 23 +++++++ userland/linux_guests/c/cfiles/ids.c | 26 ++++++++ userland/linux_guests/c/cfiles/main.c | 38 +++++++++++ userland/linux_guests/c/cfiles/openat2.c | 50 ++++++++++++++ userland/linux_guests/c/cfiles/pwrite.c | 26 ++++++++ userland/linux_guests/c/cfiles/sendfile.c | 23 +++++++ userland/linux_guests/c/cfiles/shared.c | 16 +++++ userland/linux_guests/c/cfiles/sync.c | 17 +++++ userland/linux_guests/c/cfiles/trunc.c | 24 +++++++ userland/linux_guests/c/cfiles/usage.c | 60 +++++++++++++++++ userland/linux_guests/c/cfiles/vec.c | 32 +++++++++ userland/linux_guests/c/cfiles/xattr.c | 22 ++++++ userland/linux_guests/sh/oracle.sh | 19 ++++++ 21 files changed, 623 insertions(+) create mode 100644 userland/linux_guests/GuestProofs.mk create mode 100644 userland/linux_guests/c/cfiles/cfiles.h create mode 100644 userland/linux_guests/c/cfiles/cfr.c create mode 100644 userland/linux_guests/c/cfiles/close_range.c create mode 100644 userland/linux_guests/c/cfiles/falloc.c create mode 100644 userland/linux_guests/c/cfiles/fcntl.c create mode 100644 userland/linux_guests/c/cfiles/flock.c create mode 100644 userland/linux_guests/c/cfiles/harness.c create mode 100644 userland/linux_guests/c/cfiles/ids.c create mode 100644 userland/linux_guests/c/cfiles/main.c create mode 100644 userland/linux_guests/c/cfiles/openat2.c create mode 100644 userland/linux_guests/c/cfiles/pwrite.c create mode 100644 userland/linux_guests/c/cfiles/sendfile.c create mode 100644 userland/linux_guests/c/cfiles/shared.c create mode 100644 userland/linux_guests/c/cfiles/sync.c create mode 100644 userland/linux_guests/c/cfiles/trunc.c create mode 100644 userland/linux_guests/c/cfiles/usage.c create mode 100644 userland/linux_guests/c/cfiles/vec.c create mode 100644 userland/linux_guests/c/cfiles/xattr.c create mode 100755 userland/linux_guests/sh/oracle.sh diff --git a/userland/linux_guests/GuestProofs.mk b/userland/linux_guests/GuestProofs.mk new file mode 100644 index 000000000..e8901883c --- /dev/null +++ b/userland/linux_guests/GuestProofs.mk @@ -0,0 +1,9 @@ +# 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)) diff --git a/userland/linux_guests/Guests.mk b/userland/linux_guests/Guests.mk index 008baa3f2..bd5bf1555 100644 --- a/userland/linux_guests/Guests.mk +++ b/userland/linux_guests/Guests.mk @@ -130,4 +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..c60beb2f0 --- /dev/null +++ b/userland/linux_guests/c/cfiles/cfiles.h @@ -0,0 +1,57 @@ +/* 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 + +#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_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..3ddcdb08b --- /dev/null +++ b/userland/linux_guests/c/cfiles/main.c @@ -0,0 +1,38 @@ +/* + * 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_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..b9a4dd705 --- /dev/null +++ b/userland/linux_guests/c/cfiles/openat2.c @@ -0,0 +1,50 @@ +#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 + +#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); + 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, 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..41cac01d4 --- /dev/null +++ b/userland/linux_guests/c/cfiles/pwrite.c @@ -0,0 +1,26 @@ +#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); + done(p, "writes at the offset, leaves the file offset, fills a gap with zeros"); +} 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/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..93adf3410 --- /dev/null +++ b/userland/linux_guests/c/cfiles/vec.c @@ -0,0 +1,32 @@ +#include "cfiles.h" + +/* 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); + close(fd); + done(p, "preadv, pwritev and the v2 forms at an offset and at -1"); +} 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/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" "$@" From f856369701648dc6820f02d614a8dd421b1414c3 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:16:50 +0000 Subject: [PATCH 15/34] linux-guests: cproc, /dev, /proc and /sys against the host Nothing proved what a program finds in /dev, /proc and /sys, or that it finds nothing that is not its own. cproc runs seven parts. dev: each device's type, numbers and mode, and its reads and writes, /dev/tty's ENXIO with no terminal, and the /dev/stdin and /dev/fd links. self: its own stat, status, cmdline, environ, comm, exe, cwd, fd and fdinfo against what the calls say. maps: its stack in maps, statm, limits against getrlimit, mounts, mountinfo, cgroup, and a second thread under task/. system: meminfo against sysinfo, cpuinfo against the CPUs it may run on, uptime, loadavg, version and /proc/sys/kernel against uname, a boot id that holds and a uuid that does not, /sys's huge-page size. isolation: of pids 1 to 4096 only its own family's exist in /proc, a forked child's /proc//stat and getppid both name its parent, and no way through /proc/self/root, a link or ".." climbs above the root, and a directory descriptor keeps its place across a chdir. mem: NONOS refuses /proc/self/mem, which Linux opens for the process itself. facts: no file names the machine's CPU brand, which the program reads with CPUID, nor the build host's CPU model, boot id or name, which the build puts in /etc/cproc-host. On the host, through sh/oracle.sh, it prints "[C] cproc PASS: 7 parts"; there mem opens and facts finds the host's CPU in /proc/cpuinfo, as each part says it expects of Linux. --- userland/linux_guests/GuestFiles.mk | 10 ++++ userland/linux_guests/GuestProofs.mk | 6 ++ userland/linux_guests/c/cproc/cproc.h | 46 +++++++++++++++ userland/linux_guests/c/cproc/dev.c | 38 +++++++++++++ userland/linux_guests/c/cproc/facts.c | 67 ++++++++++++++++++++++ userland/linux_guests/c/cproc/harness.c | 25 +++++++++ userland/linux_guests/c/cproc/isolation.c | 61 ++++++++++++++++++++ userland/linux_guests/c/cproc/main.c | 35 ++++++++++++ userland/linux_guests/c/cproc/maps.c | 68 +++++++++++++++++++++++ userland/linux_guests/c/cproc/mem.c | 15 +++++ userland/linux_guests/c/cproc/self.c | 57 +++++++++++++++++++ userland/linux_guests/c/cproc/shared.c | 30 ++++++++++ userland/linux_guests/c/cproc/system.c | 51 +++++++++++++++++ 13 files changed, 509 insertions(+) create mode 100644 userland/linux_guests/c/cproc/cproc.h create mode 100644 userland/linux_guests/c/cproc/dev.c create mode 100644 userland/linux_guests/c/cproc/facts.c create mode 100644 userland/linux_guests/c/cproc/harness.c create mode 100644 userland/linux_guests/c/cproc/isolation.c create mode 100644 userland/linux_guests/c/cproc/main.c create mode 100644 userland/linux_guests/c/cproc/maps.c create mode 100644 userland/linux_guests/c/cproc/mem.c create mode 100644 userland/linux_guests/c/cproc/self.c create mode 100644 userland/linux_guests/c/cproc/shared.c create mode 100644 userland/linux_guests/c/cproc/system.c diff --git a/userland/linux_guests/GuestFiles.mk b/userland/linux_guests/GuestFiles.mk index 5d59bd70b..01163737e 100644 --- a/userland/linux_guests/GuestFiles.mk +++ b/userland/linux_guests/GuestFiles.mk @@ -58,3 +58,13 @@ LINUX_GUEST_STORE_ENTRIES += --entry /linux/etc/bbsuite.expect=$(LINUX_GUEST_BB) # 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/GuestProofs.mk b/userland/linux_guests/GuestProofs.mk index e8901883c..85f2355c7 100644 --- a/userland/linux_guests/GuestProofs.mk +++ b/userland/linux_guests/GuestProofs.mk @@ -7,3 +7,9 @@ $(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)) diff --git a/userland/linux_guests/c/cproc/cproc.h b/userland/linux_guests/c/cproc/cproc.h new file mode 100644 index 000000000..34bbe8d2a --- /dev/null +++ b/userland/linux_guests/c/cproc/cproc.h @@ -0,0 +1,46 @@ +/* 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 + +#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); +int is_nonos(void); +void part_dev(void); +void part_self(void); +void part_maps(void); +void part_system(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/main.c b/userland/linux_guests/c/cproc/main.c new file mode 100644 index 000000000..c475b2838 --- /dev/null +++ b/userland/linux_guests/c/cproc/main.c @@ -0,0 +1,35 @@ +/* + * /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_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/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..bec8a63f4 --- /dev/null +++ b/userland/linux_guests/c/cproc/shared.c @@ -0,0 +1,30 @@ +#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; +} + +int is_nonos(void) { + struct utsname u; + uname(&u); + return strstr(u.version, "NONOS") != 0; +} 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"); +} From 85ffec3dfae6be162a855d766790439dfe3a9f80 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:17:04 +0000 Subject: [PATCH 16/34] linux-guests: goos, Go's os, io/fs and path/filepath against the host Nothing proved that Go's file packages work under the personality. goos makes a tree with MkdirAll and reads it back with ReadDir, WalkDir and Glob; makes a file and a directory with CreateTemp and MkdirTemp and removes them; copies a file with io.Copy, which Go does with copy_file_range; and checks Chmod, Chtimes, Truncate, Rename, Symlink, Readlink and Lstat, Executable, Hostname against uname, NumCPU against the CPUs /proc/cpuinfo lists, Getwd, and an exclusive syscall.Flock refusing a second open until it is released. Nothing it prints depends on the machine. On the host, through sh/oracle.sh, it prints "[GO] goos PASS: 5 parts". --- userland/linux_guests/GuestProofs.mk | 4 ++ userland/linux_guests/go/goos/files.go | 54 ++++++++++++++++++++++++ userland/linux_guests/go/goos/go.mod | 3 ++ userland/linux_guests/go/goos/locking.go | 28 ++++++++++++ userland/linux_guests/go/goos/main.go | 51 ++++++++++++++++++++++ userland/linux_guests/go/goos/system.go | 40 ++++++++++++++++++ userland/linux_guests/go/goos/temp.go | 31 ++++++++++++++ userland/linux_guests/go/goos/tree.go | 44 +++++++++++++++++++ 8 files changed, 255 insertions(+) create mode 100644 userland/linux_guests/go/goos/files.go create mode 100644 userland/linux_guests/go/goos/go.mod create mode 100644 userland/linux_guests/go/goos/locking.go create mode 100644 userland/linux_guests/go/goos/main.go create mode 100644 userland/linux_guests/go/goos/system.go create mode 100644 userland/linux_guests/go/goos/temp.go create mode 100644 userland/linux_guests/go/goos/tree.go diff --git a/userland/linux_guests/GuestProofs.mk b/userland/linux_guests/GuestProofs.mk index 85f2355c7..7d9446cee 100644 --- a/userland/linux_guests/GuestProofs.mk +++ b/userland/linux_guests/GuestProofs.mk @@ -13,3 +13,7 @@ $(eval $(call LINUX_GUEST,cfiles,5020,5021,$(LINUX_GUESTS_C)/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/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") +} From acb4446c0b9fd18b1b8583553c22c57981656705 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 20:19:17 +0000 Subject: [PATCH 17/34] linux: walk a path a name at a time, so ".." follows the link before it A path was normalised as text before any link in it was followed, so a ".." removed the name before it whether or not that name was a link. cproc caught it: stat("/proc/self/root/..") answered /proc/, where Linux, which follows root to / first, answers /. A link /tmp/x to /etc made /tmp/x/.. /tmp, where Linux makes it /. No path left the family's root either way, but the answers were not Linux's. Now walk::walk takes a path a name at a time: it follows each link where it stands, the image's and the ones /dev and /proc make, then goes on from where the link led, and ".." goes to the parent of that; ".." at the root stays there. A trailing slash follows the last name, and 40 links in one walk is where Linux stops. named_at and chdir join a name to its directory, an absolute name standing as it is, and leave all of this to the walk. openat2 checks its RESOLVE_ rules against the same walk, step by step, so BENEATH now also refuses a step out through a link, and an absolute or magic link, as Linux does. --- userland/capsule_linux/src/linux/call/cwd.rs | 4 +- userland/capsule_linux/src/linux/file/at.rs | 22 ++++-- .../src/linux/file/calls/openat2/check.rs | 8 +-- .../src/linux/file/calls/openat2/open.rs | 16 ----- .../src/linux/file/calls/openat2/walked.rs | 72 +++++++++---------- userland/capsule_linux/src/linux/file/mod.rs | 1 + .../capsule_linux/src/linux/file/walk/link.rs | 35 +++++++++ .../capsule_linux/src/linux/file/walk/mod.rs | 15 ++-- .../capsule_linux/src/linux/file/walk/path.rs | 56 +++++++++------ .../src/linux/file/walk/state.rs | 66 +++++++++++++++++ .../capsule_linux/src/linux/file/walk/step.rs | 28 ++++---- 11 files changed, 218 insertions(+), 105 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/walk/link.rs create mode 100644 userland/capsule_linux/src/linux/file/walk/state.rs diff --git a/userland/capsule_linux/src/linux/call/cwd.rs b/userland/capsule_linux/src/linux/call/cwd.rs index 8c598e400..9bf285e66 100644 --- a/userland/capsule_linux/src/linux/call/cwd.rs +++ b/userland/capsule_linux/src/linux/call/cwd.rs @@ -17,14 +17,14 @@ /* Moving the working directory. */ use crate::linux::abi::errno; -use crate::linux::file::{follow, 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 = follow(guest, visible(&guest.cwd, &name), true); + let at = follow(guest, join(&guest.cwd, &name), true); /* Checked before it is taken. */ match look(&at) { Some((_, true)) => { diff --git a/userland/capsule_linux/src/linux/file/at.rs b/userland/capsule_linux/src/linux/file/at.rs index 79c39c6f1..af5825e3f 100644 --- a/userland/capsule_linux/src/linux/file/at.rs +++ b/userland/capsule_linux/src/linux/file/at.rs @@ -23,7 +23,6 @@ use crate::linux::guest::{Guest, Kind}; use super::flags::AT_FDCWD; use super::path::read_path; -use super::resolve::visible; pub fn resolve_at(guest: &Guest, dirfd: u64, path: u64) -> Option> { let name = read_path(guest, path)?; @@ -33,21 +32,30 @@ pub fn resolve_at(guest: &Guest, dirfd: u64, path: u64) -> Option> { } /* - * The absolute name `name` gives against `dirfd`, nothing followed yet. - * A directory descriptor keeps the path it was opened at, so a chdir made - * since does not move what it names. + * 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(visible(b"/", name)); + return Ok(name.to_vec()); } if dirfd == AT_FDCWD { - return Ok(visible(&guest.cwd, name)); + 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(visible(&fd.path, name)), + Some(fd) if fd.kind == Kind::Dir => Ok(join(&fd.path, name)), Some(_) => Err(errno::ENOTDIR), None => Err(errno::EBADF), } } + +/* `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/openat2/check.rs b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs index ea1eea764..edcb5cc0d 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/check.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs @@ -21,9 +21,9 @@ use alloc::vec::Vec; use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::super::{at, path}; +use super::super::super::{at, path, walk}; use super::how::open_how; -use super::open::{escapes, BENEATH}; +use super::open::BENEATH; use super::walked::walked; pub(super) fn check( @@ -35,8 +35,8 @@ pub(super) fn check( ) -> 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 = at::named_at(guest, dirfd, b".")?; - if rules & BENEATH != 0 && (name.first() == Some(&b'/') || escapes(&name)) { + let base = walk::follow(guest, at::named_at(guest, dirfd, b".")?, true); + if rules & BENEATH != 0 && name.first() == Some(&b'/') { return Err(errno::EXDEV); } let named = at::named_at(guest, dirfd, &name)?; diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/open.rs b/userland/capsule_linux/src/linux/file/calls/openat2/open.rs index f7f322abc..d332d8a4b 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/open.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/open.rs @@ -51,19 +51,3 @@ pub fn openat2(guest: &mut Guest, dirfd: u64, path_ptr: u64, how: u64, size: u64 Err(e) => errno::fail(e), } } - -/* Whether `..` in the name climbs above where it starts. */ -pub(super) fn escapes(name: &[u8]) -> bool { - let mut depth = 0i64; - for part in name.split(|b| *b == b'/') { - match part { - b"" | b"." => {} - b".." => depth -= 1, - _ => depth += 1, - } - if depth < 0 { - return true; - } - } - false -} diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs b/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs index 1397d6045..6ff8492fc 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs @@ -16,51 +16,51 @@ /* The walk open makes, refused at the first step the rules forbid. */ -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::{mounts, resolve, walk}; +use super::super::super::walk::Step; +use super::super::super::{mounts, walk}; use super::open::{BENEATH, NO_MAGICLINKS, NO_SYMLINKS, NO_XDEV}; /* - * Walk the name a component at a time from where it starts, and check each - * step against `rules`. + * 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 below = base == b"/" - || named.starts_with(base) && matches!(named.get(base.len()), None | Some(b'/')); - let (mut at, rest) = match below && base != b"/" { - true => (base.to_vec(), &named[base.len()..]), - false => (Vec::new(), named), + let under = |p: &[u8]| { + base == b"/" || p.starts_with(base) && matches!(p.get(base.len()), None | Some(b'/')) }; - for part in rest.split(|b| *b == b'/').filter(|p| !p.is_empty()) { - at.push(b'/'); - at.extend_from_slice(part); - let link = guest.links.target(&at).is_some(); - let made = matches!(synth::node(&at), Some(Ok(Node::Link(_)))); - let magic = made - && at.starts_with(b"/proc/") - && !at.ends_with(b"/self") - && !at.ends_with(b"/thread-self"); - if (rules & NO_SYMLINKS != 0 && (link || made)) || (rules & NO_MAGICLINKS != 0 && magic) { - return Err(errno::ELOOP); - } - if link || made { - at = walk::follow(guest, core::mem::take(&mut at), true); + /* 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 = at.starts_with(b"/proc/") + && !at.ends_with(b"/self") + && !at.ends_with(b"/thread-self") + && !at.ends_with(b"/mounts"); + 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); + } + } } - if rules & NO_XDEV != 0 && mounts::of(&at).0 != mount { - return Err(errno::EXDEV); - } - } - let end = resolve::visible(b"/", &at); - let inside = - base == b"/" || end.starts_with(base) && matches!(end.get(base.len()), None | Some(b'/')); - if rules & BENEATH != 0 && !inside { - return Err(errno::EXDEV); - } - Ok(()) + Ok(()) + }; + walk::walk(guest, named.to_vec(), true, step).map(|_| ()) } diff --git a/userland/capsule_linux/src/linux/file/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index a3f738bed..27039bada 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -67,6 +67,7 @@ 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}; 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 index 67e76daf1..fcf841b5d 100644 --- a/userland/capsule_linux/src/linux/file/walk/mod.rs +++ b/userland/capsule_linux/src/linux/file/walk/mod.rs @@ -18,13 +18,18 @@ * 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). * - * Every result is a path of the family's own tree. /proc//root is the - * family's root, so nothing reached through it, or through `..` after it, - * is outside that root; and a descriptor's link that names no path, a pipe - * or a socket, is not followed through, as Linux cannot follow it either. + * 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 step::follow; +pub use path::walk; +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 index 029e59a6e..acd57a238 100644 --- a/userland/capsule_linux/src/linux/file/walk/path.rs +++ b/userland/capsule_linux/src/linux/file/walk/path.rs @@ -18,32 +18,42 @@ use alloc::vec::Vec; -use super::super::resolve::visible; -use super::super::synth::{self, Node}; +use crate::linux::guest::Guest; -/* The path with the first made link in it replaced by its target. */ -pub(super) fn made_link(path: &[u8], last: bool) -> Option> { - if !synth::owns(path) { - return None; - } - let ends = path.iter().enumerate().skip(1).filter(|(_, b)| **b == b'/').map(|(i, _)| i); - let whole = last.then_some(path.len()); - for end in ends.chain(whole) { - let Some(Ok(Node::Link(to))) = synth::node(&path[..end]) else { +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, + mut check: impl FnMut(Step) -> Result<(), i64>, +) -> Result, i64> { + let last = last || path.last() == Some(&b'/'); + let mut w = Walk { todo: names(&path).collect(), done: Vec::new(), hops: 0 }; + while let Some(name) = w.todo.pop_front() { + if name == b".." { + w.up(&path); + check(Step::At(&joined(&w.done)))?; continue; - }; - /* A pipe or a socket: there is nothing to walk through. */ - if to.first() != Some(&b'/') && to.contains(&b':') { - return None; } - let dir = &path[..path[..end].iter().rposition(|b| *b == b'/').unwrap_or(0)]; - let mut joined = match to.first() == Some(&b'/') { - true => Vec::new(), - false => [dir, b"/"].concat(), + 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; }; - joined.extend_from_slice(&to); - joined.extend_from_slice(&path[end..]); - return Some(visible(b"/", &joined)); + check(Step::Link { at: &at, to: &to })?; + w.into_link(&to); } - None + 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..94615cae7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/walk/state.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 . + +/* 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, and + * how many links it has followed. + */ +pub(super) struct Walk { + pub(super) todo: VecDeque>, + pub(super) done: Vec>, + 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.pop().is_none() { + super::super::clamp::note(path); + } + } + + /* + * 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.clear(); + } + 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 index 2c3fd934c..164f84523 100644 --- a/userland/capsule_linux/src/linux/file/walk/step.rs +++ b/userland/capsule_linux/src/linux/file/walk/step.rs @@ -20,21 +20,25 @@ use alloc::vec::Vec; use crate::linux::guest::Guest; -use super::path::made_link; +use super::path::walk; -const MAX_HOPS: usize = 16; +/* 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 component too when - * `last` is set. + * `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 { - let mut path = guest.links.follow(path, last); - for _ in 0..MAX_HOPS { - match made_link(&path, last) { - Some(next) => path = guest.links.follow(next, last), - None => break, - } - } - path + /* With no check to refuse a step, the walk always ends somewhere. */ + walk(guest, path, last, |_| Ok(())).unwrap_or_else(|_| alloc::vec![b'/']) } From 9fa85946215b7e178f03202b61e28079e9bd690e Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:27:52 +0000 Subject: [PATCH 18/34] linux: serve openat2's RESOLVE_IN_ROOT openat2 refused RESOLVE_IN_ROOT with EINVAL, so a program that opens a name as if the directory were its root, as container tools do, could not open anything that way. The path walk now takes a root. An absolute name or link target starts from it and ".." never climbs above it, and openat2 walks an IN_ROOT name that way from the directory descriptor. As on Linux, IN_ROOT follows no /proc magic link, and IN_ROOT with RESOLVE_BENEATH is EINVAL. Every other walk passes "/" as the root and walks as before. --- .../src/linux/file/calls/openat2/check.rs | 10 +++- .../src/linux/file/calls/openat2/how.rs | 3 +- .../src/linux/file/calls/openat2/mod.rs | 9 ++-- .../src/linux/file/calls/openat2/rooted.rs | 47 +++++++++++++++++++ .../src/linux/file/calls/openat2/walked.rs | 6 +-- .../capsule_linux/src/linux/file/walk/mod.rs | 2 +- .../capsule_linux/src/linux/file/walk/path.rs | 18 ++++++- .../src/linux/file/walk/state.rs | 11 +++-- 8 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/calls/openat2/rooted.rs diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/check.rs b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs index edcb5cc0d..e13fc60c5 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/check.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/check.rs @@ -21,9 +21,10 @@ use alloc::vec::Vec; use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::super::super::{at, path, walk}; +use super::super::super::{at, mounts, path, walk}; use super::how::open_how; -use super::open::BENEATH; +use super::open::{BENEATH, IN_ROOT}; +use super::rooted::rooted; use super::walked::walked; pub(super) fn check( @@ -39,6 +40,11 @@ pub(super) fn check( 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 index 8c32df5cc..3b0e8bd29 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/how.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/how.rs @@ -47,7 +47,8 @@ pub(super) fn open_how(guest: &Guest, at: u64, size: u64) -> Result<(u64, u64, u if mode != 0 && flags & (O_CREAT | O_TMPFILE) == 0 || mode & !0o7777 != 0 { return Err(errno::EINVAL); } - if rules & IN_ROOT != 0 { + /* 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 { diff --git a/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs b/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs index e4a34fd1c..b127ce2a7 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/mod.rs @@ -18,16 +18,15 @@ * 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 and BENEATH, which a walk of - * the family's tree can check; CACHED, which Linux may always answer with - * EAGAIN and so does here. IN_ROOT would re-root every link's target at - * the directory, which this resolver does not do: it is refused with - * EINVAL, as a kernel refuses a flag it does not know. + * 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/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 index 6ff8492fc..71f90c5dc 100644 --- a/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs +++ b/userland/capsule_linux/src/linux/file/calls/openat2/walked.rs @@ -22,6 +22,7 @@ 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: @@ -38,10 +39,7 @@ pub(super) fn walked(guest: &Guest, base: &[u8], named: &[u8], rules: u64) -> Re let step = |s: Step| { match s { Step::Link { at, to } => { - let magic = at.starts_with(b"/proc/") - && !at.ends_with(b"/self") - && !at.ends_with(b"/thread-self") - && !at.ends_with(b"/mounts"); + let magic = magic(at); if rules & NO_SYMLINKS != 0 || (rules & NO_MAGICLINKS != 0 && magic) { return Err(errno::ELOOP); } diff --git a/userland/capsule_linux/src/linux/file/walk/mod.rs b/userland/capsule_linux/src/linux/file/walk/mod.rs index fcf841b5d..b3a95fc6a 100644 --- a/userland/capsule_linux/src/linux/file/walk/mod.rs +++ b/userland/capsule_linux/src/linux/file/walk/mod.rs @@ -31,5 +31,5 @@ mod path; mod state; mod step; -pub use path::walk; +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 index acd57a238..def8c7c04 100644 --- a/userland/capsule_linux/src/linux/file/walk/path.rs +++ b/userland/capsule_linux/src/linux/file/walk/path.rs @@ -32,10 +32,26 @@ 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 mut w = Walk { todo: names(&path).collect(), done: Vec::new(), hops: 0 }; + 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); diff --git a/userland/capsule_linux/src/linux/file/walk/state.rs b/userland/capsule_linux/src/linux/file/walk/state.rs index 94615cae7..1ce4169bf 100644 --- a/userland/capsule_linux/src/linux/file/walk/state.rs +++ b/userland/capsule_linux/src/linux/file/walk/state.rs @@ -20,20 +20,23 @@ use alloc::collections::VecDeque; use alloc::vec::Vec; /* - * Where a walk has got to: the names still to walk, the names walked, and - * how many links it has followed. + * 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.pop().is_none() { + if self.done.len() == self.floor { super::super::clamp::note(path); + } else { + self.done.pop(); } } @@ -45,7 +48,7 @@ impl Walk { self.hops += 1; self.done.pop(); if to.first() == Some(&b'/') { - self.done.clear(); + self.done.truncate(self.floor); } for (i, n) in names(to).enumerate() { self.todo.insert(i, n); From 0e7e4aef2403bcc0df5380fac1756cbce17e3b5d Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:28:06 +0000 Subject: [PATCH 19/34] linux: act on preadv2 and pwritev2's RWF_ flags as Linux does preadv2 and pwritev2 answered EOPNOTSUPP for any RWF_ flag, so a program that asked for RWF_APPEND or RWF_DSYNC, as databases and log writers do, failed where Linux serves it. Now the flags Linux 6.1 knows are served. RWF_APPEND writes at the end of the file whatever the offset, and at offset -1 moves the descriptor's offset to the end and past what it wrote. RWF_DSYNC and RWF_SYNC put the write in the store before answering, as fsync would. RWF_HIPRI and RWF_NOWAIT hold as they are, since a read or write here never blocks. Any other flag is still EOPNOTSUPP. --- .../capsule_linux/src/linux/file/pread/mod.rs | 9 ++-- .../src/linux/file/pread/place.rs | 51 +++++++++++++++++++ .../src/linux/file/pread/plain.rs | 2 +- .../src/linux/file/pread/sync.rs | 14 +++-- .../src/linux/file/pread/vector.rs | 34 +++++++++++-- 5 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/pread/place.rs diff --git a/userland/capsule_linux/src/linux/file/pread/mod.rs b/userland/capsule_linux/src/linux/file/pread/mod.rs index d593e66ec..cc0a66b51 100644 --- a/userland/capsule_linux/src/linux/file/pread/mod.rs +++ b/userland/capsule_linux/src/linux/file/pread/mod.rs @@ -15,12 +15,13 @@ // along with this program. If not, see . /* - * The positional forms: pread64, pwrite64, preadv, pwritev, and preadv2 and - * pwritev2 with no flags. 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. + * 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; 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 index f5ce3301a..abe624db8 100644 --- a/userland/capsule_linux/src/linux/file/pread/plain.rs +++ b/userland/capsule_linux/src/linux/file/pread/plain.rs @@ -41,7 +41,7 @@ pub fn pwrite64(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> u64 } } -fn seekable(guest: &Guest, fd: u64) -> Result { +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)), diff --git a/userland/capsule_linux/src/linux/file/pread/sync.rs b/userland/capsule_linux/src/linux/file/pread/sync.rs index 7a527fef8..d634eb81b 100644 --- a/userland/capsule_linux/src/linux/file/pread/sync.rs +++ b/userland/capsule_linux/src/linux/file/pread/sync.rs @@ -17,14 +17,22 @@ /* preadv and pwritev, and a write put in the store as RWF_DSYNC asks. */ use crate::linux::call; -use crate::linux::guest::Guest; +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, |g| call::readv(g, fd, iov, count)) + 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, |g| call::writev(g, fd, iov, count)) + 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 index aede1fd94..17e258e83 100644 --- a/userland/capsule_linux/src/linux/file/pread/vector.rs +++ b/userland/capsule_linux/src/linux/file/pread/vector.rs @@ -19,25 +19,49 @@ 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; any RWF_ flag is one this personality does not act on. + * 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 != 0 { + if flags & !RWF_KNOWN != 0 { return errno::fail(errno::EOPNOTSUPP); } - match at as i64 { - -1 => go(guest), - n if n < 0 => errno::fail(errno::EINVAL), + 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 } From 24d3332e751bd38e13c1bea2ab423bd0c6f18aa4 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:29:23 +0000 Subject: [PATCH 20/34] linux-guests: cfiles checks RWF_ flags and RESOLVE_IN_ROOT cfiles did not ask for preadv2 and pwritev2's RWF_ flags or for openat2's RESOLVE_IN_ROOT, so nothing showed whether either behaves as on Linux. Its vector part now writes with RWF_APPEND|RWF_DSYNC at offset 0 and with RWF_APPEND at -1, checks where the bytes and the offset land, and checks that an unknown flag is EOPNOTSUPP. Its openat2 part opens "/f", "../../../f" and an absolute link under IN_ROOT, all of which land inside the directory, and checks that IN_ROOT with BENEATH is EINVAL. It also includes grp.h for setgroups. The host oracle passes all 14 parts built with gcc and with musl-gcc. --- userland/linux_guests/c/cfiles/cfiles.h | 1 + userland/linux_guests/c/cfiles/openat2.c | 20 +++++++++++++++++++- userland/linux_guests/c/cfiles/vec.c | 19 ++++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/userland/linux_guests/c/cfiles/cfiles.h b/userland/linux_guests/c/cfiles/cfiles.h index c60beb2f0..7d6de300e 100644 --- a/userland/linux_guests/c/cfiles/cfiles.h +++ b/userland/linux_guests/c/cfiles/cfiles.h @@ -3,6 +3,7 @@ #define _GNU_SOURCE #include #include +#include #include #include #include diff --git a/userland/linux_guests/c/cfiles/openat2.c b/userland/linux_guests/c/cfiles/openat2.c index b9a4dd705..c2dfb5bf8 100644 --- a/userland/linux_guests/c/cfiles/openat2.c +++ b/userland/linux_guests/c/cfiles/openat2.c @@ -11,6 +11,8 @@ struct open_how { #define RESOLVE_BENEATH 0x08 +#define RESOLVE_IN_ROOT 0x10 + #ifndef SYS_openat2 #define SYS_openat2 437 #endif @@ -42,9 +44,25 @@ void part_openat2(void) { 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, NO_SYMLINKS and NO_MAGICLINKS walk as Linux walks"); + done(p, "BENEATH, IN_ROOT, NO_SYMLINKS and NO_MAGICLINKS walk as Linux walks"); } diff --git a/userland/linux_guests/c/cfiles/vec.c b/userland/linux_guests/c/cfiles/vec.c index 93adf3410..d4f68e6ec 100644 --- a/userland/linux_guests/c/cfiles/vec.c +++ b/userland/linux_guests/c/cfiles/vec.c @@ -1,5 +1,13 @@ #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); @@ -27,6 +35,15 @@ void part_vec(void) { 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 and at -1"); + done(p, "preadv, pwritev and the v2 forms at an offset, at -1, and with RWF_ flags"); } From 71a98722467c70385b445113b0d8ff1b1499b845 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:49:15 +0000 Subject: [PATCH 21/34] linux: put statfs's fields where Linux's struct statfs has them statfs wrote f_fsid at 48, f_namelen at 56, f_frsize at 64 and f_flags at 72. Linux's struct statfs has f_files and f_ffree at 40 and 48, so those four are at 56, 64, 72 and 80. A program read the fsid as the free inode count, the name length as the fsid, the fragment size as the name length and the flags as the fragment size. f_flags also carried only ST_RDONLY, where Linux always sets ST_VALID and adds the mount's nosuid, nodev, noexec and relatime. Now each field is at Linux's offset, and f_flags is ST_VALID plus the bits for the options the family's mount table gives the mount, the same options /proc/self/mounts lists. --- .../src/linux/file/meta/statfs/calls.rs | 2 -- .../src/linux/file/meta/statfs/fill.rs | 23 ++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs index 3d1669d24..b25752185 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs @@ -28,8 +28,6 @@ pub(super) const BLOCKS: u64 = 1 << 20; pub(super) const FREE: u64 = BLOCKS / 2; -pub(super) const ST_RDONLY: u64 = 1; - 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); diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs index afe4400ae..3bc84dfd9 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs @@ -20,27 +20,33 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; use super::super::super::mounts; -use super::calls::{BLOCKS, BSIZE, FREE, ST_RDONLY}; +use super::calls::{BLOCKS, BSIZE, FREE}; 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)]; + /* * The mount's type and flags from the family's mount table; the sizes are * the ones the personality declares for every mount. */ pub(super) fn fill(guest: &Guest, path: &[u8], out: u64) -> u64 { let (id, _, magic) = mounts::of(path); - let ro = mounts::MOUNTS.iter().find(|m| m.0 == id).is_some_and(|m| m.4.starts_with("ro")); + let opts = mounts::MOUNTS.iter().find(|m| m.0 == id).map_or("", |m| m.4); let mut buf = [0u8; STATFS]; put(&mut buf, 0, magic); /* f_type */ 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, 48, u64::from(id)); /* f_fsid */ - put(&mut buf, 56, 255); /* f_namelen, the vfs path limit */ - put(&mut buf, 64, BSIZE); /* f_frsize */ - put(&mut buf, 72, if ro { ST_RDONLY } else { 0 }); /* f_flags */ + 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), @@ -50,3 +56,8 @@ pub(super) fn fill(guest: &Guest, path: &[u8], out: u64) -> u64 { 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) +} From 8750b2f202366b7b502418e543408ce0d1d877be Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:49:15 +0000 Subject: [PATCH 22/34] linux-guests: cfiles checks statfs against the host Nothing checked statfs, so its fields could sit at the wrong offsets without a proof failing. cfiles now checks, for /tmp and /proc, the filesystem type, a name length of 255, a fragment size equal to the block size, and f_flags equal to ST_VALID plus the bits for the options /proc/self/mounts gives the same mount. It also checks that fstatfs on a file in /tmp agrees. The host oracle passes all 15 parts built with gcc and with musl-gcc. --- userland/linux_guests/c/cfiles/cfiles.h | 2 ++ userland/linux_guests/c/cfiles/main.c | 1 + userland/linux_guests/c/cfiles/statfs.c | 42 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 userland/linux_guests/c/cfiles/statfs.c diff --git a/userland/linux_guests/c/cfiles/cfiles.h b/userland/linux_guests/c/cfiles/cfiles.h index 7d6de300e..4020d520e 100644 --- a/userland/linux_guests/c/cfiles/cfiles.h +++ b/userland/linux_guests/c/cfiles/cfiles.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,7 @@ 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); diff --git a/userland/linux_guests/c/cfiles/main.c b/userland/linux_guests/c/cfiles/main.c index 3ddcdb08b..020153c8a 100644 --- a/userland/linux_guests/c/cfiles/main.c +++ b/userland/linux_guests/c/cfiles/main.c @@ -26,6 +26,7 @@ int main(void) { part_close_range(); part_openat2(); part_sync(); + part_statfs(); part_xattr(); part_usage(); part_ids(); diff --git a/userland/linux_guests/c/cfiles/statfs.c b/userland/linux_guests/c/cfiles/statfs.c new file mode 100644 index 000000000..1fba40303 --- /dev/null +++ b/userland/linux_guests/c/cfiles/statfs.c @@ -0,0 +1,42 @@ +#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); + done(p, "statfs and fstatfs: type, name length, fragment size and mount flags"); +} From 040f0ca82368072ceb5546f6f33e2daa826550a2 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:50:48 +0000 Subject: [PATCH 23/34] linux: say ENOSPC when the store is full, not EIO Every write that reached the store answered EIO when the store refused it: close, fsync and sync putting a file in the store, mknod, link and rename. The store names its reason, "no space left" for a full store, so a program told EIO could not tell a full disk from a broken one. Now the reason becomes the errno a Linux filesystem gives: ENOSPC for a full store, EFBIG for a file too large for it, and EROFS, ENOENT, EACCES, EEXIST, EISDIR or ENOTEMPTY where the store says so. A reason with no Linux name is still EIO. --- .../src/linux/file/held/cache/flush.rs | 5 +-- .../capsule_linux/src/linux/file/held/mod.rs | 1 + .../src/linux/file/held/store_err.rs | 37 +++++++++++++++++++ .../src/linux/file/link/calls.rs | 6 +-- .../capsule_linux/src/linux/file/mknod.rs | 2 +- .../capsule_linux/src/linux/file/rename.rs | 2 +- 6 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/held/store_err.rs diff --git a/userland/capsule_linux/src/linux/file/held/cache/flush.rs b/userland/capsule_linux/src/linux/file/held/cache/flush.rs index 276cc6be0..34e137a1b 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/flush.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.rs @@ -18,8 +18,6 @@ use alloc::vec::Vec; -use crate::linux::abi::errno; - use super::super::super::{resolve, store}; use super::table::CACHE; @@ -33,7 +31,8 @@ pub fn flush(path: &[u8], keep: bool) -> Result<(), i64> { return Ok(()); }; if all[i].dirty { - store::write(&resolve::key(path), &all[i].data).map_err(|_| errno::EIO)?; + store::write(&resolve::key(path), &all[i].data) + .map_err(super::super::store_err::errno_of)?; all[i].dirty = false; } if !keep { diff --git a/userland/capsule_linux/src/linux/file/held/mod.rs b/userland/capsule_linux/src/linux/file/held/mod.rs index 7f1acfc6f..2ad63c17a 100644 --- a/userland/capsule_linux/src/linux/file/held/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/mod.rs @@ -24,6 +24,7 @@ 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::flush_all; 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/link/calls.rs b/userland/capsule_linux/src/linux/file/link/calls.rs index e38b16df6..ec5fef1ab 100644 --- a/userland/capsule_linux/src/linux/file/link/calls.rs +++ b/userland/capsule_linux/src/linux/file/link/calls.rs @@ -51,14 +51,14 @@ pub fn linkat(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64) -> u6 return errno::fail(e); } /* The store copies what it has: the family's copy goes in first. */ - if super::super::cache::flush(&from, true).is_err() { - return errno::fail(errno::EIO); + 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), + Err(e) => errno::fail(super::super::store_err::errno_of(e)), } } diff --git a/userland/capsule_linux/src/linux/file/mknod.rs b/userland/capsule_linux/src/linux/file/mknod.rs index efb4f0426..8342ba890 100644 --- a/userland/capsule_linux/src/linux/file/mknod.rs +++ b/userland/capsule_linux/src/linux/file/mknod.rs @@ -46,6 +46,6 @@ pub fn mknodat(guest: &Guest, dirfd: u64, path: u64, mode: u64) -> u64 { super::modes::set(&at, mode as u32 & 0o7777 & !u32::from(guest.umask)); errno::ok(0) } - Err(_) => errno::fail(errno::EIO), + Err(e) => errno::fail(super::store_err::errno_of(e)), } } diff --git a/userland/capsule_linux/src/linux/file/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs index dc0e96874..14b508375 100644 --- a/userland/capsule_linux/src/linux/file/rename.rs +++ b/userland/capsule_linux/src/linux/file/rename.rs @@ -67,6 +67,6 @@ pub fn renameat2(guest: &Guest, olddir: u64, old: u64, newdir: u64, new: u64, fl super::xattr_table::renamed(&from, &to); errno::ok(0) } - Err(_) => errno::fail(errno::EIO), + Err(e) => errno::fail(super::store_err::errno_of(e)), } } From 08ce3494710098fe75ca25d383246f31b19fc001 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:55:01 +0000 Subject: [PATCH 24/34] linux: statfs reports the room the family has, not invented figures statfs gave every mount the same 1048576 blocks of 1024 bytes, half of them free, whatever was written. /proc and /sys seemed to have a gigabyte of room, the read-only root seemed half empty, and df could not show a file taking space. Now the sizes come from the family's own files and from nothing of the machine's. The read-only root is the bytes the store holds under the family's root, with none free. /dev, /proc and /sys have no blocks, as Linux reports for trees that hold no bytes. The private directories share one quota, half the family's memory, as Linux sizes a tmpfs it is given no size (declared::PRIVATE). Their free room is that quota less what the family keeps there, in the store and in copies not yet written. Putting a copy in the store that would go past the quota fails with ENOSPC. Blocks are 4096 bytes, as tmpfs counts them. --- .../src/linux/file/held/cache/flush.rs | 7 +++ .../src/linux/file/held/cache/mod.rs | 2 +- .../src/linux/file/held/cache/names.rs | 9 ++++ .../src/linux/file/held/cache/table.rs | 2 + .../src/linux/file/held/cache/take.rs | 9 +++- .../src/linux/file/meta/statfs/calls.rs | 6 --- .../src/linux/file/meta/statfs/fill.rs | 20 ++++---- .../src/linux/file/meta/statfs/mod.rs | 9 ++-- .../src/linux/file/system/declared/mod.rs | 2 +- .../src/linux/file/system/declared/sizes.rs | 6 +++ .../src/linux/file/system/mod.rs | 1 + .../src/linux/file/system/space/mod.rs | 33 ++++++++++++ .../src/linux/file/system/space/room.rs | 49 ++++++++++++++++++ .../src/linux/file/system/space/used.rs | 50 +++++++++++++++++++ 14 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/system/space/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/system/space/room.rs create mode 100644 userland/capsule_linux/src/linux/file/system/space/used.rs diff --git a/userland/capsule_linux/src/linux/file/held/cache/flush.rs b/userland/capsule_linux/src/linux/file/held/cache/flush.rs index 34e137a1b..7fb01a59a 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/flush.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/flush.rs @@ -31,9 +31,16 @@ pub fn flush(path: &[u8], keep: bool) -> Result<(), i64> { 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); diff --git a/userland/capsule_linux/src/linux/file/held/cache/mod.rs b/userland/capsule_linux/src/linux/file/held/cache/mod.rs index 7457a1123..19e705780 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -34,6 +34,6 @@ mod take; pub use change::{resize, write}; pub use flush::{flush, flush_all}; -pub use names::{forget, names_in, renamed}; +pub use names::{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 index 51254f552..426920edb 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/names.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/names.rs @@ -20,6 +20,15 @@ use alloc::vec::Vec; use super::table::CACHE; +/* + * 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); diff --git a/userland/capsule_linux/src/linux/file/held/cache/table.rs b/userland/capsule_linux/src/linux/file/held/cache/table.rs index 603662d87..3c0429422 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/table.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/table.rs @@ -26,6 +26,8 @@ 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, } diff --git a/userland/capsule_linux/src/linux/file/held/cache/take.rs b/userland/capsule_linux/src/linux/file/held/cache/take.rs index 892240078..8a3625adb 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/take.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/take.rs @@ -36,7 +36,14 @@ pub fn hold(path: &[u8], exists: bool) -> Result<(), i64> { } false => (Vec::new(), now()), }; - CACHE.0.borrow_mut().push(Entry { path: path.to_vec(), data, dirty: !exists, mtime_ms }); + let stored = data.len() as u64; + CACHE.0.borrow_mut().push(Entry { + path: path.to_vec(), + data, + dirty: !exists, + stored, + mtime_ms, + }); Ok(()) } diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs index b25752185..b74e88223 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/calls.rs @@ -22,12 +22,6 @@ use crate::linux::guest::{Guest, Kind}; use super::super::super::{at, walk}; use super::fill::fill; -pub(super) const BSIZE: u64 = 1024; - -pub(super) const BLOCKS: u64 = 1 << 20; - -pub(super) const FREE: u64 = BLOCKS / 2; - 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); diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs index 3bc84dfd9..9db15b701 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/fill.rs @@ -20,7 +20,7 @@ use crate::linux::abi::errno; use crate::linux::guest::Guest; use super::super::super::mounts; -use super::calls::{BLOCKS, BSIZE, FREE}; +use super::super::super::space::{self, BSIZE}; const STATFS: usize = 120; @@ -30,19 +30,21 @@ const ST_VALID: u64 = 0x20; const OPTS: [(&str, u64); 5] = [("ro", 1), ("nosuid", 2), ("nodev", 4), ("noexec", 8), ("relatime", 0x1000)]; -/* - * The mount's type and flags from the family's mount table; the sizes are - * the ones the personality declares for every mount. - */ pub(super) fn fill(guest: &Guest, path: &[u8], out: u64) -> u64 { let (id, _, magic) = mounts::of(path); - let opts = mounts::MOUNTS.iter().find(|m| m.0 == id).map_or("", |m| m.4); + 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, BLOCKS); /* f_blocks */ - put(&mut buf, 24, FREE); /* f_bfree */ - put(&mut buf, 32, FREE); /* f_bavail */ + 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 */ diff --git a/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs b/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs index c08b05d73..bb61ae8d1 100644 --- a/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs +++ b/userland/capsule_linux/src/linux/file/meta/statfs/mod.rs @@ -15,12 +15,9 @@ // 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. + * 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; diff --git a/userland/capsule_linux/src/linux/file/system/declared/mod.rs b/userland/capsule_linux/src/linux/file/system/declared/mod.rs index 4dbbafc6c..2cb0fdb5a 100644 --- a/userland/capsule_linux/src/linux/file/system/declared/mod.rs +++ b/userland/capsule_linux/src/linux/file/system/declared/mod.rs @@ -27,4 +27,4 @@ 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}; +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/sizes.rs b/userland/capsule_linux/src/linux/file/system/declared/sizes.rs index 0f42f8ca9..e4a30dc02 100644 --- a/userland/capsule_linux/src/linux/file/system/declared/sizes.rs +++ b/userland/capsule_linux/src/linux/file/system/declared/sizes.rs @@ -51,3 +51,9 @@ pub const OVERCOMMIT: u64 = 1; * 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/mod.rs b/userland/capsule_linux/src/linux/file/system/mod.rs index eb5412b7e..60c151de1 100644 --- a/userland/capsule_linux/src/linux/file/system/mod.rs +++ b/userland/capsule_linux/src/linux/file/system/mod.rs @@ -21,3 +21,4 @@ pub mod cpu; pub mod declared; +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))) +} From 12ae574325fd56d6bdbcb7a9f2ee15bdb1115d6d Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:55:01 +0000 Subject: [PATCH 25/34] linux-guests: cfiles checks that statfs counts what is written cfiles checked statfs's fields but not its sizes, so constant figures passed. Its statfs part now checks that /proc has no blocks and that free is at most the total, that writing 256 KiB to a file in /tmp takes exactly 64 blocks from the free count, and that unlinking the file gives them back. The host oracle passes all 15 parts built with gcc and with musl-gcc. --- userland/linux_guests/c/cfiles/statfs.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/userland/linux_guests/c/cfiles/statfs.c b/userland/linux_guests/c/cfiles/statfs.c index 1fba40303..91018fd27 100644 --- a/userland/linux_guests/c/cfiles/statfs.c +++ b/userland/linux_guests/c/cfiles/statfs.c @@ -38,5 +38,19 @@ void part_statfs(void) { struct statfs f; CHECK(p, fstatfs(fd, &f) == 0 && f.f_type == t.f_type, f.f_type, t.f_type); close(fd); - done(p, "statfs and fstatfs: type, name length, fragment size and mount flags"); + 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"); } From 993d9dcd5cce329c54252326ba6b3a38fe716af2 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:58:26 +0000 Subject: [PATCH 26/34] linux: measure the family's load average instead of printing zeros /proc/loadavg read "0.00 0.00 0.00" always, and sysinfo's loads were zero, because load was not measured. uptime, top and anything that throttles on load were told the family was idle while it was busy. Now both come from one measure (file/system/load/). Linux averages the tasks running or waiting to run, every five seconds, with fixed decay 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, counting processes that have exited. The factors, the fixed point and the rounding are Linux's, so a family that ran all of the last minute reads 0.63 as Linux does. A read gives the periods since the last read the share the family ran across all of them, since nothing samples in between. A host proof checks a busy minute, then an idle one, against values worked out with Linux's calc_load. --- .../src/linux/call/process/usage/sysinfo.rs | 10 ++- .../src/linux/file/made/proc/system/time.rs | 9 ++- .../src/linux/file/system/load/average.rs | 67 +++++++++++++++++++ .../src/linux/file/system/load/mod.rs | 34 ++++++++++ .../src/linux/file/system/load/state.rs | 41 ++++++++++++ .../src/linux/file/system/mod.rs | 1 + .../src/linux/serve/family_exit.rs | 1 + userland/capsule_linux_proofs/src/calls.rs | 4 ++ userland/capsule_linux_proofs/src/load/mod.rs | 26 +++++++ userland/capsule_linux_proofs/src/tests.rs | 1 + .../src/tests/load_tests.rs | 38 +++++++++++ 11 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 userland/capsule_linux/src/linux/file/system/load/average.rs create mode 100644 userland/capsule_linux/src/linux/file/system/load/mod.rs create mode 100644 userland/capsule_linux/src/linux/file/system/load/state.rs create mode 100644 userland/capsule_linux_proofs/src/load/mod.rs create mode 100644 userland/capsule_linux_proofs/src/tests/load_tests.rs diff --git a/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs index 3d57250b1..5975c9b15 100644 --- a/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs +++ b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs @@ -23,14 +23,20 @@ use crate::linux::guest::Guest; use super::super::super::family_ms; pub fn sysinfo(guest: &Guest, out: u64) -> u64 { - let (threads, resident) = file::view_with(|v| { + 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) + (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 */ put(40, declared::MEMORY.saturating_sub(resident)); /* freeram */ b[80..82].copy_from_slice(&(threads.min(u16::MAX as usize) as u16).to_le_bytes()); /* procs */ 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 index 3f0e94a0b..f61fee7d2 100644 --- a/userland/capsule_linux/src/linux/file/made/proc/system/time.rs +++ b/userland/capsule_linux/src/linux/file/made/proc/system/time.rs @@ -18,7 +18,10 @@ 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; @@ -30,11 +33,13 @@ pub(super) fn uptime(v: &View) -> Vec { alloc::format!("{} {}\n", two(up), two(idle)).into_bytes() } -/* Load is not measured for a family, so it reads as none. */ +/* 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 mut s = alloc::format!("0.00 0.00 0.00 {running}/{threads} ").into_bytes(); + 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 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 index 60c151de1..61913ca39 100644 --- a/userland/capsule_linux/src/linux/file/system/mod.rs +++ b/userland/capsule_linux/src/linux/file/system/mod.rs @@ -21,4 +21,5 @@ pub mod cpu; pub mod declared; +pub mod load; pub(super) mod space; diff --git a/userland/capsule_linux/src/linux/serve/family_exit.rs b/userland/capsule_linux/src/linux/serve/family_exit.rs index c0d3dc243..08dd39659 100644 --- a/userland/capsule_linux/src/linux/serve/family_exit.rs +++ b/userland/capsule_linux/src/linux/serve/family_exit.rs @@ -39,6 +39,7 @@ impl Family { } 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; 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/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); +} From 71d16146ab37ec15fa410f430d1c62db2654997b Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 21:59:12 +0000 Subject: [PATCH 27/34] linux: count the family's file copies as its cache and shared memory /proc/meminfo said Cached and Shmem were 0, and sysinfo's sharedram was 0, while the family held in memory the copy of every file it was writing. free(1) could not see those bytes, and MemFree counted them as free. Now the bytes the family's copies hold are Cached and Shmem in meminfo, as a tmpfs file's pages are on Linux, and sysinfo's sharedram. MemFree and freeram leave them out, and MemAvailable counts them back in, since a copy can be put in the store. Buffers, SwapCached, SwapTotal and SwapFree stay 0, because there are no block-device buffers and no swap. --- .../src/linux/call/process/usage/sysinfo.rs | 4 +++- .../src/linux/file/held/cache/mod.rs | 2 +- .../src/linux/file/held/cache/names.rs | 8 ++++++++ .../capsule_linux/src/linux/file/held/mod.rs | 2 +- .../src/linux/file/made/proc/system/memory.rs | 16 ++++++++++++---- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs index 5975c9b15..b20107b85 100644 --- a/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs +++ b/userland/capsule_linux/src/linux/call/process/usage/sysinfo.rs @@ -38,7 +38,9 @@ pub fn sysinfo(guest: &Guest, out: u64) -> u64 { put(8 + i * 8, avg << 5); /* loads, from Linux's 11 bits to sysinfo's 16 */ } put(32, declared::MEMORY); /* totalram */ - put(40, declared::MEMORY.saturating_sub(resident)); /* freeram */ + 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) { diff --git a/userland/capsule_linux/src/linux/file/held/cache/mod.rs b/userland/capsule_linux/src/linux/file/held/cache/mod.rs index 19e705780..dcfeac4d6 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/mod.rs @@ -34,6 +34,6 @@ mod take; pub use change::{resize, write}; pub use flush::{flush, flush_all}; -pub use names::{forget, growth, names_in, renamed}; +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 index 426920edb..79d62299b 100644 --- a/userland/capsule_linux/src/linux/file/held/cache/names.rs +++ b/userland/capsule_linux/src/linux/file/held/cache/names.rs @@ -20,6 +20,14 @@ 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. diff --git a/userland/capsule_linux/src/linux/file/held/mod.rs b/userland/capsule_linux/src/linux/file/held/mod.rs index 2ad63c17a..e0df7c25d 100644 --- a/userland/capsule_linux/src/linux/file/held/mod.rs +++ b/userland/capsule_linux/src/linux/file/held/mod.rs @@ -27,4 +27,4 @@ pub(super) mod rw; pub(super) mod store_err; pub(super) mod times; -pub use cache::flush_all; +pub use cache::{bytes as cache_bytes, flush_all}; 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 index 41a67d9de..0c054ad3b 100644 --- a/userland/capsule_linux/src/linux/file/made/proc/system/memory.rs +++ b/userland/capsule_linux/src/linux/file/made/proc/system/memory.rs @@ -35,20 +35,28 @@ pub(super) fn cpuinfo() -> Vec { 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 free = total.saturating_sub(family(v).resident_kb); + 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), + ("MemAvailable:", free + cached), ("Buffers:", 0), - ("Cached:", 0), + ("Cached:", cached), ("SwapCached:", 0), ("SwapTotal:", 0), ("SwapFree:", 0), - ("Shmem:", 0), + ("Shmem:", cached), ] { s += &alloc::format!("{name:<16}{kb:>8} kB\n"); } From 745b049bea9d8bdb44ed5042958e876196f93cce Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 22:05:23 +0000 Subject: [PATCH 28/34] linux: fill /proc//stat's fields that are known, not zeros /proc//stat gave 0 for a process's children's CPU times and minor faults, though the family keeps what each waited-for child used. It also gave 0 for where the program's code and data start and end, and for the stack it started on, though the family placed all three. ps and top showed no child time, and a debugger found no code range. Now cutime, cstime and cminflt are what the children the process has waited for used. startcode and endcode bound the program's executable regions, and start_data and end_data its writable ones, leaving out the interpreter's. startstack is the stack pointer it started with, where argc is, as Linux's start_stack is. The fields are grouped as proc(5) groups them. The zeros left are Linux's own, and two are this view's limits, as the code says: no kernel flags are kept, and pending signals are not in the family's view. --- .../src/linux/file/made/exe/shape.rs | 5 ++ .../src/linux/file/made/exe_image.rs | 8 +-- .../linux/file/made/proc/pid_files/stat.rs | 53 ++++++++++--------- .../linux/file/made/proc/pid_files/statm.rs | 15 ++++++ .../src/linux/file/made/view/proc.rs | 2 + .../capsule_linux/src/linux/image/stack.rs | 6 ++- .../src/linux/serve/family_view/proc.rs | 2 + 7 files changed, 60 insertions(+), 31 deletions(-) diff --git a/userland/capsule_linux/src/linux/file/made/exe/shape.rs b/userland/capsule_linux/src/linux/file/made/exe/shape.rs index c6de9e894..c0a5fc427 100644 --- a/userland/capsule_linux/src/linux/file/made/exe/shape.rs +++ b/userland/capsule_linux/src/linux/file/made/exe/shape.rs @@ -34,6 +34,11 @@ pub struct Exe { 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_image.rs b/userland/capsule_linux/src/linux/file/made/exe_image.rs index bfe653d64..99a4ed786 100644 --- a/userland/capsule_linux/src/linux/file/made/exe_image.rs +++ b/userland/capsule_linux/src/linux/file/made/exe_image.rs @@ -25,9 +25,10 @@ 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`. + * `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) { +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; }; @@ -35,5 +36,6 @@ pub fn record_image(pid: u32, argv: &[Vec], at: &[u64], top: u64) { /* 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(); - record(pid, Exe { path, comm: comm_of(name), args: *first, env, end: top, start_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/proc/pid_files/stat.rs b/userland/capsule_linux/src/linux/file/made/proc/pid_files/stat.rs index 555846c1c..ff4c99587 100644 --- 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 @@ -20,34 +20,35 @@ 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 = usage(p, tid); + 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(&p.exe.comm).unwrap_or(""); - let start = p.exe.start_ms / (1000 / super::super::super::super::declared::HZ); - let (e, rss) = (&p.exe, u.resident_kb / 4); + let comm = core::str::from_utf8(&e.comm).unwrap_or(""); let nice = crate::linux::call::nice_of(p.kernel); - let head = alloc::format!( - "{tid} ({comm}) {state} {} {} {} 0 -1 0 {} 0 0 0 {} {} 0 0 {} {} {} 0 {start} {} {rss} \ - 18446744073709551615 0 0 0 0 0 0 0 {} {} 0 0 0 17 0 0 0 0 0 0 0 0 {} {} {} {} {} 0\n", - p.ppid, - p.pgid, - p.sid, - u.faults, - u.user, - u.system, - 20 + nice, - nice, - p.tids.len(), - vsize(p), - p.ignored, - p.caught, - p.brk.0, - e.args, - e.env, - e.env, - e.end, - ); - head.into_bytes() + 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 index b4a1b0e66..4262d251d 100644 --- 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 @@ -21,6 +21,21 @@ 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::() diff --git a/userland/capsule_linux/src/linux/file/made/view/proc.rs b/userland/capsule_linux/src/linux/file/made/view/proc.rs index 2337fcd2b..e2d27d9d6 100644 --- a/userland/capsule_linux/src/linux/file/made/view/proc.rs +++ b/userland/capsule_linux/src/linux/file/made/view/proc.rs @@ -44,4 +44,6 @@ pub struct Proc { /* 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/image/stack.rs b/userland/capsule_linux/src/linux/image/stack.rs index 1fa5d62a1..746d47684 100644 --- a/userland/capsule_linux/src/linux/image/stack.rs +++ b/userland/capsule_linux/src/linux/image/stack.rs @@ -37,7 +37,6 @@ pub fn build( let mut all = argv.to_vec(); all.extend_from_slice(envp); let placed = place(guest, top, &all)?; - crate::linux::file::record_image(guest.pid, argv, &placed.at, top); let random_at = (placed.floor - RANDOM_LEN) & !0x0F; let aux = pairs(image, interp_base, random_at, *placed.at.first()?); @@ -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/family_view/proc.rs b/userland/capsule_linux/src/linux/serve/family_view/proc.rs index 34d252a25..8736931f6 100644 --- a/userland/capsule_linux/src/linux/serve/family_view/proc.rs +++ b/userland/capsule_linux/src/linux/serve/family_view/proc.rs @@ -35,6 +35,7 @@ pub(super) fn proc_of(guests: &[Guest], ns: &mut PidNs, j: usize, asking: bool) 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 { @@ -53,5 +54,6 @@ pub(super) fn proc_of(guests: &[Guest], ns: &mut PidNs, j: usize, asking: bool) umask, caught, ignored, + reaped, } } From c4813ade60c594d230b4063a980d9724ba3dc4d7 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 22:05:24 +0000 Subject: [PATCH 29/34] linux: take a whole write to a file in one call, as Linux does write and pwrite64 on a file took at most 1 MiB and returned that count for anything larger. Linux writes a regular file whole, and code that checks write(fd, buf, n) == n without a retry loop, which is most code, failed on a 4 MiB write. Now a write to a file goes on in 1 MiB copies out of the guest until all of it is written. A failure after some bytes have landed returns the count so far, as on Linux. Pipes and sockets are unchanged. --- .../src/linux/file/pread/plain.rs | 9 +--- .../capsule_linux/src/linux/file/write.rs | 43 ++++++++++++++----- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/userland/capsule_linux/src/linux/file/pread/plain.rs b/userland/capsule_linux/src/linux/file/pread/plain.rs index abe624db8..7c4d00f63 100644 --- a/userland/capsule_linux/src/linux/file/pread/plain.rs +++ b/userland/capsule_linux/src/linux/file/pread/plain.rs @@ -19,8 +19,6 @@ use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; -use super::super::rw::{write_at, MAX_IO}; - 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)) } @@ -32,11 +30,8 @@ pub fn pwrite64(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64) -> u64 if let Err(e) = seekable(guest, fd) { return e; } - let Some(bytes) = guest.read(buf, (len as usize).min(MAX_IO)) else { - return errno::fail(errno::EFAULT); - }; - match write_at(guest, fd, at, &bytes) { - Ok((n, _)) => errno::ok(n as u64), + match super::super::write::whole(guest, fd, buf, len, at) { + Ok((n, _)) => errno::ok(n), Err(e) => errno::fail(e), } } diff --git a/userland/capsule_linux/src/linux/file/write.rs b/userland/capsule_linux/src/linux/file/write.rs index 92cacc7f0..992fe7dd1 100644 --- a/userland/capsule_linux/src/linux/file/write.rs +++ b/userland/capsule_linux/src/linux/file/write.rs @@ -25,18 +25,39 @@ use crate::linux::guest::Guest; use super::rw::{write_at, MAX_IO}; pub fn write(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { - let take = (len as usize).min(MAX_IO); - let Some(bytes) = guest.read(buf, take) else { - return errno::fail(errno::EFAULT); - }; let at = guest.fds.get(fd as usize).map_or(0, super::desc::pos); - match write_at(guest, fd, at, &bytes) { - Ok((n, end)) => { - if let Some(entry) = guest.fds.get_mut(fd as usize) { - super::desc::set_pos(entry, end); - } - errno::ok(n as u64) + let (n, end) = match whole(guest, fd, buf, len, at) { + Ok(done) => done, + Err(e) => return errno::fail(e), + }; + if let Some(entry) = guest.fds.get_mut(fd as usize) { + super::desc::set_pos(entry, end); + } + 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)); } - Err(e) => errno::fail(e), } } From 04b8e788fdb85e5371ae9810dad2686f1ff76fc8 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 22:05:24 +0000 Subject: [PATCH 30/34] linux-guests: cproc checks stat's fields, the load and shared memory cproc did not look at /proc/self/stat's code, data and stack bounds or its children's fields, at the load average, or at Shmem, so zeros there passed. Three parts now check them against facts the program sees itself. stat: main lies between startcode and endcode, an initialised global between start_data and end_data, and argv sits right after argc at startstack. After a child that touched 1 MiB of fresh memory and ran half a second is waited for, cutime and cstime are at least 20 ticks and cminflt at least 64. load: after eleven seconds on the CPU, the one-minute average in /proc/loadavg is above zero and within 0.05 of sysinfo's. memory: 4 MiB written to an open file in /tmp raise Shmem by at least 3 MiB, the host's own traffic allowed for, with Cached at least Shmem and sysinfo's sharedram at least 3 MiB. The host oracle passes all 10 parts built with gcc and with musl-gcc. --- userland/linux_guests/c/cproc/cproc.h | 5 ++++ userland/linux_guests/c/cproc/load.c | 14 +++++++++++ userland/linux_guests/c/cproc/main.c | 3 +++ userland/linux_guests/c/cproc/memory.c | 20 +++++++++++++++ userland/linux_guests/c/cproc/shared.c | 12 +++++++++ userland/linux_guests/c/cproc/stat.c | 35 ++++++++++++++++++++++++++ 6 files changed, 89 insertions(+) create mode 100644 userland/linux_guests/c/cproc/load.c create mode 100644 userland/linux_guests/c/cproc/memory.c create mode 100644 userland/linux_guests/c/cproc/stat.c diff --git a/userland/linux_guests/c/cproc/cproc.h b/userland/linux_guests/c/cproc/cproc.h index 34bbe8d2a..65351505d 100644 --- a/userland/linux_guests/c/cproc/cproc.h +++ b/userland/linux_guests/c/cproc/cproc.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -34,11 +35,15 @@ 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); 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 index c475b2838..2b8b3597b 100644 --- a/userland/linux_guests/c/cproc/main.c +++ b/userland/linux_guests/c/cproc/main.c @@ -23,6 +23,9 @@ int main(int argc, char **argv) { part_self(); part_maps(); part_system(); + part_stat(); + part_load(); + part_memory(); part_isolation(); part_mem(); part_facts(); 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/shared.c b/userland/linux_guests/c/cproc/shared.c index bec8a63f4..d009f6a26 100644 --- a/userland/linux_guests/c/cproc/shared.c +++ b/userland/linux_guests/c/cproc/shared.c @@ -23,6 +23,18 @@ long field_of(const char *text, const char *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); diff --git a/userland/linux_guests/c/cproc/stat.c b/userland/linux_guests/c/cproc/stat.c new file mode 100644 index 000000000..dd3063765 --- /dev/null +++ b/userland/linux_guests/c/cproc/stat.c @@ -0,0 +1,35 @@ +#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)); + pid_t c = fork(); + if (c == 0) { + /* Fresh memory faults on first touch; a fork's copy need not. */ + char *pages = mmap(0, 1 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memset(pages, 1, 1 << 20); + spin_ms(500); + _exit(0); + } + CHECK(p, c > 0 && waitpid(c, 0, 0) == c, c, errno); + unsigned long long cut = stat_field(16) + stat_field(17), cmin = stat_field(11); + CHECK(p, cut >= 20 && cmin >= 64, cut, cmin); + done(p, "startcode, endcode, startstack, start_data, end_data, and a waited child's times and faults"); +} From 7938c20bc09199123d42f6f4fee73cba238b291a Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Mon, 28 Sep 2026 22:05:24 +0000 Subject: [PATCH 31/34] linux-guests: cfiles checks that a file takes a 3 MiB write whole Nothing wrote more than 1 MiB in one call, so a write cut short passed. cfiles's pwrite part now writes 3 MiB with one write and 2 MiB with one pwrite, and checks that each returns its whole length and that the offset ends at 3 MiB. The host oracle passes all 15 parts built with gcc and with musl-gcc. --- userland/linux_guests/c/cfiles/pwrite.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/userland/linux_guests/c/cfiles/pwrite.c b/userland/linux_guests/c/cfiles/pwrite.c index 41cac01d4..2e0485e08 100644 --- a/userland/linux_guests/c/cfiles/pwrite.c +++ b/userland/linux_guests/c/cfiles/pwrite.c @@ -22,5 +22,13 @@ void part_pwrite(void) { ERR(p, pwrite(ro, "x", 1, 0), EBADF); close(ro); close(fd); - done(p, "writes at the offset, leaves the file offset, fills a gap with zeros"); + /* 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"); } From 824a3c4874543fe431466153a184774a745fd7ee Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Tue, 29 Sep 2026 01:06:01 +0000 Subject: [PATCH 32/34] linux: count what a waited child waited for into its parent's children A process that exited passed its parent its own counts and the times of the children it had waited for, but not those children's page faults or context switches. A parent whose child ran a grandchild saw the grandchild's time in RUSAGE_CHILDREN and /proc//stat's cutime, and not its switches or faults: cproc's parent saw 10 switches where the grandchild alone had 459. Now the waited children's faults and switches go up with their times, as Linux adds a child's cmin_flt and cnvcsw into its parent's. --- userland/capsule_linux/src/linux/serve/family_exit.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/userland/capsule_linux/src/linux/serve/family_exit.rs b/userland/capsule_linux/src/linux/serve/family_exit.rs index 08dd39659..7622c1b41 100644 --- a/userland/capsule_linux/src/linux/serve/family_exit.rs +++ b/userland/capsule_linux/src/linux/serve/family_exit.rs @@ -27,8 +27,9 @@ use crate::linux::file; impl Family { /* - * A process about to exit: what it used goes to its parent's - * RUSAGE_CHILDREN once waited for, its POSIX locks go, and every file + * 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) { @@ -43,6 +44,8 @@ impl Family { 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(); From 38d5dec2336109e64d47325170cb3c74e9af52e5 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Tue, 29 Sep 2026 01:06:01 +0000 Subject: [PATCH 33/34] linux-guests: cproc checks a grandchild's counts, not a fault count cproc's stat part wanted at least 64 minor faults from a waited child that touched a fresh megabyte. NONOS backs a readable and writable anonymous mapping when it is made and a fork copies eagerly, so such a child takes no page fault and its true count is 0. The part failed on NONOS with a correct cminflt, and it passed on the host only because Linux maps on first touch. Now the child waits for a grandchild that spins and reports its own minflt and switches before it exits. The parent's cminflt and its RUSAGE_CHILDREN switches must be at least those, and its cutime must hold the grandchild's run. Without the previous commit the switch check fails on NONOS (10 against 459); the host passes it as before. --- userland/linux_guests/c/cproc/stat.c | 32 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/userland/linux_guests/c/cproc/stat.c b/userland/linux_guests/c/cproc/stat.c index dd3063765..ce0f35290 100644 --- a/userland/linux_guests/c/cproc/stat.c +++ b/userland/linux_guests/c/cproc/stat.c @@ -20,16 +20,32 @@ void part_stat(void) { 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) { - /* Fresh memory faults on first touch; a fork's copy need not. */ - char *pages = mmap(0, 1 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - memset(pages, 1, 1 << 20); - spin_ms(500); - _exit(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); } - CHECK(p, c > 0 && waitpid(c, 0, 0) == c, c, errno); + 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 >= 64, cut, cmin); - done(p, "startcode, endcode, startstack, start_data, end_data, and a waited child's times and faults"); + 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"); } From abaaf55bd6d3dd5408e3bde5619f852052cbaf89 Mon Sep 17 00:00:00 2001 From: eKisNonos Date: Wed, 30 Sep 2026 09:38:06 +0000 Subject: [PATCH 34/34] linux: say what each call this branch serves discloses abi/disclosure.txt holds one line per served Linux call, and the disclosure ratchet in CI fails for a served call without one. The 41 calls this branch serves had none: the file calls, the xattr calls, flock, sync, sysinfo, getrusage, times, the id and group calls, priority and personality, so the check listed each of them. Now each has its line: what the call tells a guest, and why that is acceptable. Every answer is about the guest's own files, the family's own processes or a fixed identity; sysinfo gives declared figures and the family's own measurements, nothing of the machine. --- userland/capsule_linux/abi/disclosure.txt | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) 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