diff --git a/.keys/install-cli_publisher_ed25519.pub b/.keys/install-cli_publisher_ed25519.pub new file mode 100644 index 000000000..795753524 Binary files /dev/null and b/.keys/install-cli_publisher_ed25519.pub differ diff --git a/.keys/install-cli_publisher_mldsa65.pub b/.keys/install-cli_publisher_mldsa65.pub new file mode 100644 index 000000000..af04d893c Binary files /dev/null and b/.keys/install-cli_publisher_mldsa65.pub differ diff --git a/.keys/install_publisher_ed25519.pub b/.keys/install_publisher_ed25519.pub new file mode 100644 index 000000000..dff768db3 Binary files /dev/null and b/.keys/install_publisher_ed25519.pub differ diff --git a/.keys/install_publisher_mldsa65.pub b/.keys/install_publisher_mldsa65.pub new file mode 100644 index 000000000..b7a2c3398 Binary files /dev/null and b/.keys/install_publisher_mldsa65.pub differ diff --git a/abi/syscalls.toml b/abi/syscalls.toml index 868127f00..41f46c9a9 100644 --- a/abi/syscalls.toml +++ b/abi/syscalls.toml @@ -53,10 +53,13 @@ MDRQ = 0x5152444D APPS = 0x53505041 CDAD = 0x44414443 CEAD = 0x44414543 +CEDP = 0x50444543 +CEDS = 0x53444543 CHKF = 0x464B4843 -CMKY = 0x594B4D43 CHMC = 0x434D4843 CKEC = 0x43454B43 +CSKS = 0x534B5343 +CSPB = 0x42505343 CXPK = 0x4B505843 CXSH = 0x48535843 MAST = 0x5453414D @@ -75,6 +78,9 @@ MPMP = 0x504D504D MPCP = 0x5043504D MPPT = 0x5450504D MFTH = 0x4854464D +MPTL = 0x4C54504D +MFFK = 0x4B46464D +MPUN = 0x4E55504D MIRW = 0x5752494D MIRY = 0x5952494D MKAR = 0x52414B4D @@ -101,6 +107,7 @@ CRND = 0x444E5243 CHSH = 0x48534843 CENC = 0x434E4543 CDEC = 0x43454443 +CEDV = 0x56444543 ARBT = 0x54425241 ASDN = 0x4E445341 @@ -147,21 +154,15 @@ MDVW = 0x5756444D MIEP = 0x5045494D MIED = 0x4445494D -[desc.CMKY] -nr = 0x594B4D43 -caps = ["Crypto"] -args = [{name="label_ptr",type="u64",dir="in"},{name="label_len",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"}] -ret = {type="i64"} - [desc.CRND] nr = 0x444E5243 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="buf",type="u8*",dir="out"},{name="len",type="usize",dir="in"}] ret = {type="i64"} [desc.CHSH] nr = 0x48534843 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="alg",type="u32",dir="in"},{name="data",type="u8*",dir="in"},{name="len",type="usize",dir="in"},{name="out",type="u8*",dir="out"},{name="out_len",type="usize",dir="in"}] ret = {type="i64"} @@ -177,6 +178,12 @@ caps = ["Crypto"] args = [{name="key_id",type="u64",dir="in"},{name="ct",type="u8*",dir="in"},{name="ct_len",type="usize",dir="in"},{name="pt_out",type="u8*",dir="out"},{name="pt_cap",type="usize",dir="in"}] ret = {type="i64"} +[desc.CEDV] +nr = 0x56444543 +caps = ["Crypto"] +args = [{name="pubkey",type="[u8;32]",dir="in"},{name="msg",type="u8*",dir="in"},{name="msg_len",type="usize",dir="in"},{name="sig",type="[u8;64]",dir="in"}] +ret = {type="i64"} + [desc.ARBT] nr = 0x54425241 caps = ["Admin"] @@ -233,13 +240,13 @@ ret = {type="i64"} [desc.MMAP] nr = 0x50414D4D -caps = ["Memory"] +caps = ["valid_token"] args = [{name="hint",type="u64",dir="in"},{name="len",type="usize",dir="in",max="@limits.max_mmap_bytes"},{name="flags",type="u32",dir="in"},{name="addr_out",type="u64*",dir="out"}] ret = {type="i64"} [desc.MUMP] nr = 0x504D554D -caps = ["Memory"] +caps = ["valid_token"] args = [{name="addr",type="u64",dir="in"},{name="len",type="usize",dir="in"}] ret = {type="i64"} @@ -263,13 +270,13 @@ ret = {type="i64"} [desc.MCGT] nr = 0x5447434D -caps = ["Admin"] +caps = ["IPC"] args = [{name="target_pid",type="u32",dir="in"},{name="cap_mask",type="u64",dir="in"}] ret = {type="i64"} [desc.MCRV] nr = 0x5652434D -caps = ["Admin"] +caps = ["IPC"] args = [{name="target_pid",type="u32",dir="in"},{name="cap_mask",type="u64",dir="in"}] ret = {type="i64"} @@ -281,91 +288,91 @@ ret = {type="i64"} [desc.MDLS] nr = 0x534C444D -caps = ["valid_token"] +caps = ["DeviceEnum"] args = [{name="class",type="u32",dir="in"},{name="buf",type="u8*",dir="out"},{name="count",type="usize",dir="in",max="@limits.max_devices"}] ret = {type="i64"} [desc.MDCL] nr = 0x4C43444D -caps = ["valid_token"] +caps = ["Driver"] args = [{name="device_id",type="u64",dir="in"}] ret = {type="i64"} [desc.MDRL] nr = 0x4C52444D -caps = ["valid_token"] +caps = ["Driver"] args = [{name="device_id",type="u64",dir="in"}] ret = {type="i64"} [desc.MMMP] nr = 0x504D4D4D -caps = ["valid_token"] +caps = ["Mmio"] args = [{name="device_id",type="u64",dir="in"},{name="bar_index",type="u32",dir="in"},{name="length",type="usize",dir="in"},{name="flags",type="u32",dir="in"},{name="vaddr_out",type="u64*",dir="out"}] ret = {type="i64"} [desc.MMUM] nr = 0x4D554D4D -caps = ["valid_token"] +caps = ["Mmio"] args = [{name="device_id",type="u64",dir="in"},{name="vaddr",type="u64",dir="in"}] ret = {type="i64"} [desc.MIRB] nr = 0x4252494D -caps = ["valid_token"] +caps = ["Irq"] args = [{name="device_id",type="u64",dir="in"},{name="claim_epoch",type="u64",dir="in"},{name="vector_count",type="u32",dir="in"},{name="flags",type="u32",dir="in"},{name="slot_base_out",type="u32*",dir="out"}] ret = {type="i64"} [desc.MIRU] nr = 0x5552494D -caps = ["valid_token"] +caps = ["Irq"] args = [{name="device_id",type="u64",dir="in"},{name="slot_base",type="u32",dir="in"},{name="vector_count",type="u32",dir="in"}] ret = {type="i64"} [desc.MIRA] nr = 0x4152494D -caps = ["valid_token"] +caps = ["Irq"] args = [{name="slot",type="u32",dir="in"}] ret = {type="i64"} [desc.MIRP] nr = 0x5052494D -caps = ["valid_token"] +caps = ["Irq"] args = [{name="slot",type="u32",dir="in"},{name="seq_out",type="u64*",dir="out"}] ret = {type="i64"} [desc.MDMM] nr = 0x4D4D444D -caps = ["valid_token"] +caps = ["Dma"] args = [{name="device_id",type="u64",dir="in"},{name="claim_epoch",type="u64",dir="in"},{name="length",type="usize",dir="in"},{name="flags",type="u32",dir="in"},{name="grant_out",type="u8*",dir="out"}] ret = {type="i64"} [desc.MDMU] nr = 0x554D444D -caps = ["valid_token"] +caps = ["Dma"] args = [{name="device_id",type="u64",dir="in"},{name="grant_id",type="u64",dir="in"}] ret = {type="i64"} [desc.MPGT] nr = 0x5447504D -caps = ["valid_token"] +caps = ["Pio"] args = [{name="device_id",type="u64",dir="in"},{name="claim_epoch",type="u64",dir="in"},{name="port_base",type="u16",dir="in"},{name="port_count",type="u16",dir="in"},{name="grant_out",type="u32*",dir="out"}] ret = {type="i64"} [desc.MPRD] nr = 0x4452504D -caps = ["valid_token"] +caps = ["Pio"] args = [{name="grant_id",type="u32",dir="in"},{name="offset",type="u16",dir="in"},{name="width",type="u8",dir="in"},{name="value_out",type="u32*",dir="out"}] ret = {type="i64"} [desc.MPWR] nr = 0x5257504D -caps = ["valid_token"] +caps = ["Pio"] args = [{name="grant_id",type="u32",dir="in"},{name="offset",type="u16",dir="in"},{name="width",type="u8",dir="in"},{name="value",type="u32",dir="in"}] ret = {type="i64"} [desc.MPRL] nr = 0x4C52504D -caps = ["valid_token"] +caps = ["Pio"] args = [{name="grant_id",type="u32",dir="in"}] ret = {type="i64"} @@ -413,61 +420,85 @@ ret = {type="i64"} [desc.MIEP] nr = 0x5045494D -caps = ["valid_token"] +caps = ["Irq"] args = [{name="event_kind",type="u32",dir="in"},{name="payload",type="u8*",dir="in"},{name="len",type="usize",dir="in"}] ret = {type="i64"} [desc.MIED] nr = 0x4445494D -caps = ["valid_token"] +caps = ["IPC"] args = [{name="buf",type="u8*",dir="out"},{name="cap",type="usize",dir="in"},{name="count_out",type="u32*",dir="out"}] ret = {type="i64"} [desc.APPS] nr = 0x53505041 -caps = ["Admin"] +caps = ["valid_token"] args = [{name="field_id",type="u64",dir="in"},{name="kind",type="u64",dir="in"},{name="value_ptr",type="u64",dir="in"},{name="value_len",type="u64",dir="in"}] ret = {type="i64"} [desc.CDAD] nr = 0x44414443 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="algo",type="u64",dir="in"},{name="key_ptr",type="u64",dir="in"},{name="nonce_ptr",type="u64",dir="in"},{name="frame_ptr",type="u64",dir="in"},{name="frame_len",type="u64",dir="in"},{name="plaintext_ptr",type="u64",dir="out"}] ret = {type="i64"} [desc.CEAD] nr = 0x44414543 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="algo",type="u64",dir="in"},{name="key_ptr",type="u64",dir="in"},{name="nonce_ptr",type="u64",dir="in"},{name="frame_ptr",type="u64",dir="in"},{name="frame_len",type="u64",dir="in"},{name="ciphertext_ptr",type="u64",dir="out"}] ret = {type="i64"} +[desc.CEDP] +nr = 0x50444543 +caps = ["valid_token"] +args = [{name="seed_ptr",type="u64",dir="in"},{name="out",type="u64",dir="out"}] +ret = {type="i64"} + +[desc.CEDS] +nr = 0x53444543 +caps = ["valid_token"] +args = [{name="seed_ptr",type="u64",dir="in"},{name="msg_ptr",type="u64",dir="in"},{name="msg_len",type="u64",dir="in"},{name="out",type="u64",dir="out"}] +ret = {type="i64"} + [desc.CHKF] nr = 0x464B4843 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="frame_ptr",type="u64",dir="in"},{name="frame_len",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"},{name="out_len",type="u64",dir="in"}] ret = {type="i64"} [desc.CHMC] nr = 0x434D4843 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="key_ptr",type="u64",dir="in"},{name="key_len",type="u64",dir="in"},{name="data_ptr",type="u64",dir="in"},{name="data_len",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"}] ret = {type="i64"} [desc.CKEC] nr = 0x43454B43 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="data",type="u64",dir="in"},{name="len",type="u64",dir="in"},{name="out",type="u64",dir="out"},{name="out_len",type="u64",dir="in"}] ret = {type="i64"} +[desc.CSKS] +nr = 0x534B5343 +caps = ["valid_token"] +args = [{name="sk_ptr",type="u64",dir="in"},{name="digest_ptr",type="u64",dir="in"},{name="out",type="u64",dir="out"}] +ret = {type="i64"} + +[desc.CSPB] +nr = 0x42505343 +caps = ["valid_token"] +args = [{name="sk_ptr",type="u64",dir="in"},{name="out",type="u64",dir="out"}] +ret = {type="i64"} + [desc.CXPK] nr = 0x4B505843 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="private_ptr",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"}] ret = {type="i64"} [desc.CXSH] nr = 0x48535843 -caps = ["Crypto"] +caps = ["valid_token"] args = [{name="private_ptr",type="u64",dir="in"},{name="public_ptr",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"}] ret = {type="i64"} [desc.MAST] @@ -484,13 +515,13 @@ ret = {type="i64"} [desc.MCLD] nr = 0x444C434D -caps = ["CoreExec", "IPC", "Memory"] +caps = ["valid_token"] args = [{name="req_ptr",type="u64",dir="in"}] ret = {type="i64"} [desc.MCVF] nr = 0x4656434D -caps = ["CoreExec", "IPC", "Memory"] +caps = ["valid_token"] args = [{name="req_ptr",type="u64",dir="in"},{name="out_ptr",type="u64",dir="out"}] ret = {type="i64"} [desc.MFTK] @@ -507,7 +538,7 @@ ret = {type="i64"} [desc.MGPD] nr = 0x4450474D -caps = ["CoreExec"] +caps = ["valid_token"] args = [] ret = {type="i64"} @@ -525,19 +556,19 @@ ret = {type="i64"} [desc.MIRY] nr = 0x5952494D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="dest_pid",type="u64",dir="in"},{name="buf",type="u64",dir="out"},{name="len",type="usize",dir="in"}] ret = {type="i64"} [desc.MKAR] nr = 0x52414B4D -caps = ["CoreExec"] +caps = ["valid_token"] args = [{name="buf",type="u64",dir="out"},{name="len",type="usize",dir="in"}] ret = {type="i64"} [desc.MKIL] nr = 0x4C494B4D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="pid",type="u64",dir="in"},{name="sig",type="u64",dir="in"}] ret = {type="i64"} @@ -549,7 +580,7 @@ ret = {type="i64"} [desc.MOUT] nr = 0x54554F4D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="pid",type="u64",dir="in"},{name="buf_ptr",type="u64",dir="out"},{name="buf_len",type="usize",dir="in"}] ret = {type="i64"} @@ -567,7 +598,7 @@ ret = {type="i64"} [desc.MPIN] nr = 0x4E49504D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="pid",type="u64",dir="in"},{name="buf_ptr",type="u64",dir="out"},{name="buf_len",type="usize",dir="in"}] ret = {type="i64"} @@ -579,7 +610,7 @@ ret = {type="i64"} [desc.MSOW] nr = 0x574F534D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="user_ptr",type="u64",dir="in"},{name="len",type="u64",dir="in"}] ret = {type="i64"} @@ -625,6 +656,24 @@ caps = ["ForeignExec"] args = [{name="pid",type="u32",dir="in"},{name="entry",type="u64",dir="in"},{name="rsp",type="u64",dir="in"},{name="tls",type="u64",dir="in"}] ret = {type="i64"} +[desc.MPUN] +nr = 0x4E55504D +caps = ["ForeignExec"] +args = [{name="pid",type="u32",dir="in"},{name="addr",type="u64",dir="in"},{name="len",type="u64",dir="in"}] +ret = {type="i64"} + +[desc.MFFK] +nr = 0x4B46464D +caps = ["ForeignExec"] +args = [{name="pid",type="u32",dir="in"}] +ret = {type="i64"} + +[desc.MPTL] +nr = 0x4C54504D +caps = ["ForeignExec"] +args = [{name="pid",type="u32",dir="in"},{name="base",type="u64",dir="in"}] +ret = {type="i64"} + [desc.MPPT] nr = 0x5450504D caps = ["ForeignExec"] @@ -639,7 +688,7 @@ ret = {type="i64"} [desc.MSRD] nr = 0x4452534D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="buf_ptr",type="u64",dir="out"},{name="buf_len",type="usize",dir="in"}] ret = {type="i64"} @@ -651,13 +700,13 @@ ret = {type="i64"} [desc.MSVR] nr = 0x5256534D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="name_ptr",type="u64",dir="in"},{name="name_len",type="usize",dir="in"},{name="port",type="u32",dir="in"}] ret = {type="i64"} [desc.MSWR] nr = 0x5257534D -caps = ["StoreWrite"] +caps = ["valid_token"] args = [{name="lba",type="u64",dir="in"},{name="user_ptr",type="u64",dir="in"},{name="len",type="u64",dir="in"}] ret = {type="i64"} @@ -675,7 +724,7 @@ ret = {type="i64"} [desc.MTRN] nr = 0x4E52544D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="name_ptr",type="u64",dir="in"},{name="name_len",type="u64",dir="in"},{name="argv_ptr",type="u64",dir="in"},{name="argv_len",type="u64",dir="in"}] ret = {type="i64"} @@ -687,13 +736,13 @@ ret = {type="i64"} [desc.MTSP] nr = 0x5053544D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="entry",type="u64",dir="in"},{name="stack",type="u64",dir="in"}] ret = {type="i64"} [desc.MWAT] nr = 0x5441574D -caps = ["IPC"] +caps = ["valid_token"] args = [{name="pid",type="u64",dir="in"},{name="timeout_ms",type="u64",dir="in"}] ret = {type="i64"} diff --git a/src/arch/x86_64/asm/syscall.S b/src/arch/x86_64/asm/syscall.S index a2a180bd8..b9623994d 100644 --- a/src/arch/x86_64/asm/syscall.S +++ b/src/arch/x86_64/asm/syscall.S @@ -27,6 +27,20 @@ syscall_entry_asm: below destroys them, so stash the user copies under the saved frame and restore them on exit. One pad slot keeps 16-alignment since three arg-saves are odd. Eleven slots -> rsp = 8 (mod 16). */ + /* The callee-saved five, plus a pad to keep the parity the comment + above depends on. Nothing else on this path writes them to memory, + and Rust cannot read them either: by the time a handler runs, its + own prologue may already be using them. A forked child has to + resume with the parent's whole register state, so the capture has + to happen here or not at all. Six slots is 0 (mod 16), so every + offset below stays exactly where it was. */ + push r15 + push r14 + push r13 + push r12 + push rbx + sub rsp, 8 + sub rsp, 8 push rdx push rsi @@ -50,10 +64,23 @@ syscall_entry_asm: mov r9, [rsp + 0x08] mov r11, [rsp + 0x10] - /* Spill a6 as 7th arg, realigns to 16. */ + /* a6 goes on the stack as the seventh argument; the eighth is a + pointer to the frame just saved. A pointer rather than a single + register because a forked child resumes with the parent's whole + state, and the frame holds all of it: the return address is the + saved rcx at offset 0x20 and the rest follows it. Taken before + the two pushes, so it points at the saved rax. + + One push left rsp 16-aligned for the call. Two do not, so a pad goes + underneath them: the arguments themselves must sit at [rsp] and + [rsp+8] when the call executes. Cleanup grows from 0x10 to 0x20 for + the pad and the extra argument. */ + mov rax, rsp + sub rsp, 8 + push rax push r11 call syscall_handler - add rsp, 0x10 + add rsp, 0x20 /* SyscallSavedFrame{rax, r8, r9, r10, rcx, r11, rbp} at rsp. */ push rax @@ -80,6 +107,17 @@ syscall_entry_asm: pop rdx add rsp, 8 + /* The callee-saved five come back with their pad, in reverse. They + still hold the user's values, so this restores rather than + changes them; the point of saving was to give a supervisor a + complete frame to fork from. */ + add rsp, 8 + pop rbx + pop r12 + pop r13 + pop r14 + pop r15 + push rax movabs rax, 0xffffffffffe08aff and r11, rax diff --git a/src/arch/x86_64/syscall/manager/entry.rs b/src/arch/x86_64/syscall/manager/entry.rs index f53d12a6c..4566f3ded 100644 --- a/src/arch/x86_64/syscall/manager/entry.rs +++ b/src/arch/x86_64/syscall/manager/entry.rs @@ -17,6 +17,7 @@ use crate::security::hardening::speculation::kernel_entry; use crate::syscall::contract::{dispatch as contract_dispatch, SyscallArgs}; use crate::syscall::numbers::SyscallNumber; +use crate::process::foreign::FRAME_WORDS; use crate::syscall::types::errnos; #[no_mangle] @@ -28,28 +29,27 @@ pub(super) extern "C" fn syscall_handler( arg4: u64, arg5: u64, arg6: u64, + frame: *const u64, ) -> u64 { // A capsule reaching this point last controlled the branch predictors and - // the return stack. Refilling the RSB and re-asserting IBRS before any - // kernel branch runs is the whole point of the entry side, and it was the - // side with no caller: `kernel_exit` was wired on the return path, so - // mitigations were being applied leaving the kernel but not entering it. + // the return stack. kernel_entry(); let Some(sc) = SyscallNumber::from_u64(number) else { - /* - * A number this kernel does not know. NONOS numbers are four - * character tags, so nothing legitimate lands here; a foreign - * binary's own numbering does. When the caller is a guest, its - * supervisor answers and the kernel stays ignorant of what was - * asked. Everyone else still gets ENOSYS. - */ + // A number this kernel does not know. let args = [arg1, arg2, arg3, arg4, arg5, arg6]; - return match crate::process::foreign::redirect(number, args, 0) { + // SAFETY: eK@nonos.systems - `frame` is the pointer the entry + // stub in syscall.S passed, naming the sixteen words it pushed + // on this kernel stack, which outlive this call. This is the + // one place that pointer is turned into a reference; everything + // downstream of it is safe code. + let saved = unsafe { &*(frame as *const [u64; FRAME_WORDS]) }; + return match crate::process::foreign::redirect(number, args, saved) { Some(value) => value, None => (-(errnos::ENOSYS as i64)) as u64, }; }; + let _ = frame; let result = contract_dispatch(sc, SyscallArgs::new([arg1, arg2, arg3, arg4, arg5, arg6])); result.value as u64 } diff --git a/src/process/foreign/exec.rs b/src/process/foreign/exec.rs new file mode 100644 index 000000000..2d2e9bb0b --- /dev/null +++ b/src/process/foreign/exec.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 . + +//! `MkForeignExec`: the same guest, a different program. + +use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_PERM}; + +use super::exec_context::fresh; +use super::peer_guard::in_user_half; + +type Saved = Option; + +/// What a parked guest receives when its supervisor has replaced the program +/// under it. +pub(super) const EXECED: u64 = u64::MAX; + +pub fn sys_foreign_exec(pid: u64, entry: u64, rsp: u64) -> i64 { + let Some(caller) = crate::process::current_pid() else { + return ERRNO_INVAL; + }; + let pid = pid as u32; + if super::registry::supervisor_of(pid) != Some(caller) { + return ERRNO_PERM; + } + if rsp == 0 || !in_user_half(entry, 1) || !in_user_half(rsp, 1) { + return ERRNO_INVAL; + } + let Some(previous) = swap(pid, Some(fresh(entry, rsp))) else { + return ERRNO_INVAL; + }; + drop_tls(pid); + // Answering is what releases the guest. + match super::trap_reply::answer_raw(pid, EXECED) { + 0 => 0, + err => { + swap(pid, previous); + err + } + } +} + +/// Put a context in place and hand back the one it displaced. +fn swap(pid: u32, ctx: Saved) -> Option { + crate::process::with_process(pid, |p| { + core::mem::replace(&mut *p.saved_user_context.lock(), ctx) + }) +} + +/// Forget the thread pointer the replaced runtime set: the scheduler writes +/// the control block's base on every switch, so leaving it would put the new +/// image back on the old TLS the first time it is preempted. +fn drop_tls(pid: u32) { + crate::process::with_process(pid, |pcb| pcb.set_tls_base(0)); +} diff --git a/src/process/foreign/exec_context.rs b/src/process/foreign/exec_context.rs new file mode 100644 index 000000000..77e85afb1 --- /dev/null +++ b/src/process/foreign/exec_context.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 registers a program starts on. + +use crate::arch::context::SavedUser; +use crate::process::userspace::{USER_CS, USER_DS, USER_RFLAGS}; + +/// Everything zero but the entry point, the stack and the constants the ABI +/// fixes. +pub(super) fn fresh(entry: u64, rsp: u64) -> SavedUser { + SavedUser { + rax: 0, + rbx: 0, + rcx: 0, + rdx: 0, + rsi: 0, + rdi: 0, + rbp: 0, + r8: 0, + r9: 0, + r10: 0, + r11: 0, + r12: 0, + r13: 0, + r14: 0, + r15: 0, + rip: entry, + rsp, + rflags: USER_RFLAGS, + cs: USER_CS as u64, + ss: USER_DS as u64, + // The thread pointer belongs to the runtime that is being replaced. + fs_base: 0, + gs_base: 0, + } +} diff --git a/src/process/foreign/exec_enter.rs b/src/process/foreign/exec_enter.rs new file mode 100644 index 000000000..0b7aed287 --- /dev/null +++ b/src/process/foreign/exec_enter.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 . + +//! Leaving the kernel on a program the thread was not running when it entered. + +use crate::arch::context::SavedUser; +use crate::process::signal::SIGSEGV; +use crate::process::userspace::{restore_user_context_iretq, USER_CS, USER_DS}; + +const USER_VA_MAX: u64 = 0x0000_7FFF_FFFF_FFFF; + +pub(super) fn enter(pid: u32) -> ! { + let ctx = crate::process::with_process(pid, |pcb| pcb.saved_user_context.lock().take()); + match ctx.flatten() { + Some(c) if sane(&c) => resume(c), + // Nothing to run. + _ => crate::process::terminate_current_with_signal(SIGSEGV), + } +} + +fn resume(ctx: SavedUser) -> ! { + /* + * The scheduler installs the control block's base on every switch, and + * this path deliberately does not go through the scheduler, so the + * register is written here as well. + */ + crate::arch::context::set_user_tls(ctx.fs_base); + /* + * SAFETY: eK@nonos.systems - `ctx` is a local, so it outlives the + * five pushes the restore makes below rsp. Its selectors are the + * user pair and rip/rsp are in the low half, checked above; the + * address space is this pid's own, which is already on cr3 because + * this thread is the one running. + */ + unsafe { restore_user_context_iretq(&ctx) } +} + +fn sane(c: &SavedUser) -> bool { + c.cs == USER_CS as u64 + && c.ss == USER_DS as u64 + && c.rip <= USER_VA_MAX + && c.rsp <= USER_VA_MAX + && c.rsp != 0 +} diff --git a/src/process/foreign/fork.rs b/src/process/foreign/fork.rs new file mode 100644 index 000000000..9590ba9b9 --- /dev/null +++ b/src/process/foreign/fork.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 . + +//! Duplicating a guest. + +use crate::process::core::ProcessState; +use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOENT, ERRNO_PERM}; + +/// `MkForeignFork`: a second process holding the first one's register state, +/// with zero in its return register so the two can tell each other apart, +/// which is the whole of fork's contract to the program. +pub fn sys_foreign_fork(pid: u64) -> i64 { + let Some(caller) = crate::process::current_pid() else { + return ERRNO_INVAL; + }; + let parent = pid as u32; + if super::registry::supervisor_of(parent) != Some(caller) { + return ERRNO_PERM; + } + let Some(state) = saved_state(parent) else { + // A guest that is not parked inside a syscall has no frame to copy. + return ERRNO_NOENT; + }; + let child = match super::spawn::empty_guest(caller, b"fork") { + Ok(pid) => pid, + Err(e) => return e, + }; + let mut frame = state; + frame.rax = 0; + crate::process::with_process(child, |pcb| { + *pcb.saved_user_context.lock() = Some(frame); + *pcb.state.lock() = ProcessState::New; + }); + child as i64 +} + +fn saved_state(pid: u32) -> Option { + crate::process::with_process(pid, |pcb| *pcb.saved_user_context.lock()).flatten() +} diff --git a/src/process/foreign/frame_cpu.rs b/src/process/foreign/frame_cpu.rs new file mode 100644 index 000000000..762297f51 --- /dev/null +++ b/src/process/foreign/frame_cpu.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 . + +//! The one word of a guest's state that is not in the saved frame. + +/// The user stack pointer. +#[inline] +pub fn user_rsp() -> u64 { + let rsp: u64; + /* + * SAFETY: eK@nonos.systems - reads `user_stack_saved` in PerCpuData + * at a compile-time offset. Kernel GS is still active on this path: + * neither sysret nor iretq has run, which is the same condition the + * sigreturn path relies on for the same read. + */ + unsafe { + core::arch::asm!( + "mov {0}, gs:[{off}]", + out(reg) rsp, + off = const crate::smp::percpu::layout::USER_STACK_SAVED, + options(nomem, nostack, preserves_flags), + ); + } + rsp +} diff --git a/src/process/foreign/frame_snapshot.rs b/src/process/foreign/frame_snapshot.rs new file mode 100644 index 000000000..f93e6fd69 --- /dev/null +++ b/src/process/foreign/frame_snapshot.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 register state a parked guest is holding, kept where a fork can reach +//! it. + +use crate::arch::context::SavedUser; +use crate::process::userspace::{USER_CS, USER_DS}; + +/// Words the entry stub pushes before it calls the handler. +pub const FRAME_WORDS: usize = 16; + +/// Offsets into that frame. rax is pushed last so it sits at zero and +/// the rest count upward from it. +const RAX: usize = 0; +const R8: usize = 1; +const R9: usize = 2; +const R10: usize = 3; +const RCX: usize = 4; +const R11: usize = 5; +const RBP: usize = 6; +const RDI: usize = 7; +const RSI: usize = 8; +const RDX: usize = 9; +const RBX: usize = 11; +const R12: usize = 12; +const R13: usize = 13; +const R14: usize = 14; +const R15: usize = 15; + +pub fn capture(frame: &[u64; FRAME_WORDS], user_rsp: u64) -> SavedUser { + SavedUser { + rax: frame[RAX], + rbx: frame[RBX], + rcx: frame[RCX], + rdx: frame[RDX], + rsi: frame[RSI], + rdi: frame[RDI], + rbp: frame[RBP], + r8: frame[R8], + r9: frame[R9], + r10: frame[R10], + r11: frame[R11], + r12: frame[R12], + r13: frame[R13], + r14: frame[R14], + r15: frame[R15], + rsp: user_rsp, + /* + * SYSCALL leaves the resume point in rcx and the flags in r11, so the + * frame carries both under those names. + */ + rip: frame[RCX], + rflags: frame[R11], + cs: USER_CS as u64, + ss: USER_DS as u64, + fs_base: 0, + gs_base: 0, + } +} diff --git a/src/process/foreign/mod.rs b/src/process/foreign/mod.rs index 7a965e401..93713448e 100644 --- a/src/process/foreign/mod.rs +++ b/src/process/foreign/mod.rs @@ -15,39 +15,44 @@ // along with this program. If not, see . //! Hosting code the kernel does not trust and does not understand. -//! -//! A foreign process holds no capabilities, so every NONOS syscall it makes -//! is refused at the contract gate. What it can do is issue a syscall -//! number this kernel has never heard of, which is exactly what a binary -//! built for another system does. Rather than answering `ENOSYS`, the -//! kernel hands that register frame to the userspace supervisor that -//! created the process and parks the caller until an answer comes back. -//! -//! The kernel copies six registers out and one value in. It does not read -//! them, does not know what they mean, and holds no table that could tell -//! it. Every syscall number, struct, path and errno belongs to the -//! supervisor, an ordinary attested capsule that can be replaced or -//! revoked without touching ring 0. +mod exec; +mod exec_context; +mod exec_enter; +mod fork; mod frame; +mod frame_cpu; +mod frame_snapshot; mod peer_chunk; mod peer_copy; mod peer_guard; +mod peer_lock; mod peer_map; mod peer_protect; +mod peer_tls; +mod peer_unmap; mod registry; +mod resume; mod spawn; mod spawn_start; +mod start_context; mod thread; mod trap; +mod trap_claim; mod trap_reply; mod trap_table; +mod trap_wait; mod wait; +pub use exec::sys_foreign_exec; +pub use fork::sys_foreign_fork; pub use frame::ForeignFrame; +pub use frame_snapshot::FRAME_WORDS; pub use peer_copy::sys_peer_copy; pub use peer_map::sys_peer_map; pub use peer_protect::sys_peer_protect; +pub use peer_tls::sys_peer_tls; +pub use peer_unmap::sys_peer_unmap; pub use registry::{clear, is_foreign, supervisor_of}; pub use spawn::sys_foreign_spawn; pub use spawn_start::sys_foreign_start; diff --git a/src/process/foreign/peer_copy.rs b/src/process/foreign/peer_copy.rs index 078645e7f..a4bdadcc2 100644 --- a/src/process/foreign/peer_copy.rs +++ b/src/process/foreign/peer_copy.rs @@ -15,28 +15,24 @@ // along with this program. If not, see . //! Moving bytes between a supervisor and a guest it created. -//! -//! The guest address is translated page by page through the guest's own -//! tables, so an unmapped byte is a refusal and never a read of somebody -//! else's memory. This is the shape of ptrace and none of its semantics. use crate::memory::addr::VirtAddr; use crate::memory::paging::manager::translate_in_asid; use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_INVAL}; use super::peer_chunk::{chunk_copy, validate}; -use super::peer_guard::{supervised_asid, MAX_SPAN, PAGE}; +use super::peer_guard::{in_user_half, supervised_asid, MAX_SPAN, PAGE}; /// `MkPeerCopy`: `to_guest` chooses the direction; returns bytes moved. pub fn sys_peer_copy(pid: u64, guest_addr: u64, buf: u64, len: u64, to_guest: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; }; - let asid = match supervised_asid(caller, pid as u32) { - Ok(a) => a, + let (asid, _held) = match supervised_asid(caller, pid as u32) { + Ok(pair) => pair, Err(e) => return e, }; - if len == 0 || len > MAX_SPAN || guest_addr.checked_add(len).is_none() { + if len == 0 || len > MAX_SPAN || !in_user_half(guest_addr, len) { return ERRNO_INVAL; } let writing = to_guest != 0; diff --git a/src/process/foreign/peer_guard.rs b/src/process/foreign/peer_guard.rs index 2295233d0..4a4f12d84 100644 --- a/src/process/foreign/peer_guard.rs +++ b/src/process/foreign/peer_guard.rs @@ -15,28 +15,38 @@ // along with this program. If not, see . //! The one check every peer call makes before it touches a guest. -//! -//! Knowing a pid buys nothing: the target must be foreign and must name -//! the caller as its supervisor. Both conditions are set once at creation -//! and never move. +use super::peer_lock::Held; use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_PERM}; pub(super) const PAGE: u64 = 4096; -/* - * One call maps or copies at most this much, so a guest image crosses in - * bounded pieces and no single call holds the processor. - */ +// One call maps or copies at most this much, so a guest image crosses in +// bounded pieces and no single call holds the processor. pub(super) const MAX_SPAN: u64 = 1 << 20; +// The first address of the kernel half. +pub(super) const USER_VA_END: u64 = 0x0000_8000_0000_0000; + +/// True when `[addr, addr + len)` lies wholly in the guest's own half. +pub(super) fn in_user_half(addr: u64, len: u64) -> bool { + match addr.checked_add(len) { + Some(end) => end <= USER_VA_END, + None => false, + } +} + pub const PROT_WRITE: u64 = 1 << 0; pub const PROT_EXEC: u64 = 1 << 1; -/// The guest's address space, or the errno the caller gets instead. -pub(super) fn supervised_asid(caller: u32, pid: u32) -> Result { + +/// The guest's address space and the lock over it, or the errno the +/// caller gets instead. +pub(super) fn supervised_asid(caller: u32, pid: u32) -> Result<(u32, Held), i64> { if super::registry::supervisor_of(pid) != Some(caller) { return Err(ERRNO_PERM); } - crate::memory::paging::manager::lookup_asid_for_process(pid).ok_or(ERRNO_INVAL) + let held = super::peer_lock::take(); + let asid = crate::memory::paging::manager::lookup_asid_for_process(pid).ok_or(ERRNO_INVAL)?; + Ok((asid, held)) } diff --git a/src/process/foreign/peer_lock.rs b/src/process/foreign/peer_lock.rs new file mode 100644 index 000000000..b287496a8 --- /dev/null +++ b/src/process/foreign/peer_lock.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 . + +//! Exclusion over a guest's address space. + +use spin::{Mutex, MutexGuard}; + +// Every peer call that reads or reshapes a guest's address space runs under +// this. +static ADDRESS_SPACE: Mutex<()> = Mutex::new(()); + +/// Proof that the caller holds the peer lock. +pub(super) type Held = MutexGuard<'static, ()>; + +/// Take it. Only `peer_guard::supervised_asid` calls this, and it hands +/// the result back beside the asid so the two cannot be separated. +pub(super) fn take() -> Held { + ADDRESS_SPACE.lock() +} diff --git a/src/process/foreign/peer_map.rs b/src/process/foreign/peer_map.rs index eab0b2a6c..699aeac97 100644 --- a/src/process/foreign/peer_map.rs +++ b/src/process/foreign/peer_map.rs @@ -15,20 +15,16 @@ // along with this program. If not, see . //! Backing a span of a guest's address space with fresh frames. -//! -//! The supervisor builds the guest's image itself, page by page, because -//! the kernel parses no foreign format. Pages are private to the guest: -//! the supervisor reaches them only through `MkPeerCopy`. use crate::memory::addr::VirtAddr; use crate::memory::paging::manager::{map_page_in_asid, translate_in_asid}; use crate::memory::paging::types::PagePermissions; use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOMEM}; -use super::peer_guard::{supervised_asid, MAX_SPAN, PAGE, PROT_EXEC, PROT_WRITE}; +use super::peer_guard::{in_user_half, supervised_asid, MAX_SPAN, PAGE, PROT_EXEC, PROT_WRITE}; fn span_ok(addr: u64, len: u64) -> bool { - len != 0 && len <= MAX_SPAN && addr % PAGE == 0 && addr.checked_add(len).is_some() + len != 0 && len <= MAX_SPAN && addr % PAGE == 0 && in_user_half(addr, len) } pub(super) fn perms_of(prot: u64) -> PagePermissions { @@ -43,14 +39,12 @@ pub(super) fn perms_of(prot: u64) -> PagePermissions { } /// `MkPeerMap`: map `[addr, addr + len)` in a guest the caller supervises. -/// A page that is already mapped is left alone, so a supervisor may lay -/// down overlapping segments the way an ELF does. pub fn sys_peer_map(pid: u64, addr: u64, len: u64, prot: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; }; - let asid = match supervised_asid(caller, pid as u32) { - Ok(a) => a, + let (asid, _held) = match supervised_asid(caller, pid as u32) { + Ok(pair) => pair, Err(e) => return e, }; if !span_ok(addr, len) { diff --git a/src/process/foreign/peer_protect.rs b/src/process/foreign/peer_protect.rs index 30511e07c..0f5e63648 100644 --- a/src/process/foreign/peer_protect.rs +++ b/src/process/foreign/peer_protect.rs @@ -16,36 +16,26 @@ //! Changing the protection of pages a guest already has. -//! -//! A supervisor that loads code cannot know the final protection when it -//! lays the pages down. A dynamic linker maps a library writable, applies -//! relocations to it, and only then asks for it to be executable. Without -//! this the pages would stay writable and the program would fault on its -//! first call into the library, or the supervisor would have to map the -//! code writable and executable at once, which the mapping layer refuses -//! and should go on refusing. use crate::memory::addr::VirtAddr; use crate::memory::paging::manager::{map_page_in_asid, translate_in_asid}; use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_INVAL}; -use super::peer_guard::{supervised_asid, MAX_SPAN, PAGE}; +use super::peer_guard::{in_user_half, supervised_asid, MAX_SPAN, PAGE}; use super::peer_map::perms_of; fn span_ok(addr: u64, len: u64) -> bool { - len != 0 && len <= MAX_SPAN && addr % PAGE == 0 && addr.checked_add(len).is_some() + len != 0 && len <= MAX_SPAN && addr % PAGE == 0 && in_user_half(addr, len) } -/// `MkPeerProtect`: set the protection of `[addr, addr + len)` in a guest -/// the caller supervises. Every page must already be mapped; a hole is an -/// error rather than a silent gap, because a caller asking for execute on -/// a range it has not filled is not asking for what it thinks. +/// `MkPeerProtect`: set the protection of `[addr, addr + len)` in a guest the +/// caller supervises. pub fn sys_peer_protect(pid: u64, addr: u64, len: u64, prot: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; }; - let asid = match supervised_asid(caller, pid as u32) { - Ok(a) => a, + let (asid, _held) = match supervised_asid(caller, pid as u32) { + Ok(pair) => pair, Err(e) => return e, }; if !span_ok(addr, len) { diff --git a/src/process/foreign/peer_tls.rs b/src/process/foreign/peer_tls.rs new file mode 100644 index 000000000..ec6f208af --- /dev/null +++ b/src/process/foreign/peer_tls.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 . + + +//! Setting a guest thread's thread pointer. + +use super::peer_guard::in_user_half; +use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOENT, ERRNO_PERM}; + +/// `MkPeerTls`: the FS base `pid` wakes with from now on. +pub fn sys_peer_tls(pid: u64, base: u64) -> i64 { + let Some(caller) = crate::process::current_pid() else { + return ERRNO_INVAL; + }; + let pid = pid as u32; + if super::registry::supervisor_of(pid) != Some(caller) { + return ERRNO_PERM; + } + /* + * The context switch writes this straight to MSR_FS_BASE, and that + * instruction faults in ring zero for a non-canonical value. + */ + if !in_user_half(base, 1) { + return ERRNO_INVAL; + } + match crate::process::with_process(pid, |pcb| pcb.set_tls_base(base)) { + Some(()) => 0, + None => ERRNO_NOENT, + } +} diff --git a/src/process/foreign/peer_unmap.rs b/src/process/foreign/peer_unmap.rs new file mode 100644 index 000000000..38906f894 --- /dev/null +++ b/src/process/foreign/peer_unmap.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 . + +//! Taking pages away from a guest. + +use crate::memory::addr::VirtAddr; +use crate::memory::paging::manager::{translate_in_asid, unmap_page_in_asid}; +use crate::memory::paging::types::PagePermissions; +use crate::syscall::microkernel::errnos::ERRNO_INVAL; + +use super::peer_guard::{in_user_half, supervised_asid, MAX_SPAN, PAGE}; + +fn span_ok(addr: u64, len: u64) -> bool { + len != 0 && len <= MAX_SPAN && addr % PAGE == 0 && in_user_half(addr, len) +} + +/// `MkPeerUnmap`: drop `[addr, addr + len)` from a guest the caller +/// supervises. +pub fn sys_peer_unmap(pid: u64, addr: u64, len: u64) -> i64 { + let Some(caller) = crate::process::current_pid() else { + return ERRNO_INVAL; + }; + let (asid, _held) = match supervised_asid(caller, pid as u32) { + Ok(pair) => pair, + Err(e) => return e, + }; + if !span_ok(addr, len) { + return ERRNO_INVAL; + } + let perms = PagePermissions::READ | PagePermissions::USER; + for i in 0..len.div_ceil(PAGE) { + let va = VirtAddr::new(addr + i * PAGE); + if translate_in_asid(asid, va).is_none() { + continue; + } + if let Ok(frame) = unmap_page_in_asid(asid, va, perms) { + let _ = crate::memory::frame_alloc::deallocate_frame(frame); + } + } + 0 +} diff --git a/src/process/foreign/registry.rs b/src/process/foreign/registry.rs index 1bd2219b1..1248b7f74 100644 --- a/src/process/foreign/registry.rs +++ b/src/process/foreign/registry.rs @@ -15,12 +15,6 @@ // along with this program. If not, see . //! Which processes are foreign, and who supervises each one. -//! -//! The pairing is set once at creation and never changes: a supervisor -//! cannot be handed a guest it did not create, and a guest cannot be moved -//! between supervisors. Both directions of every peer operation check this -//! table, so an ordinary capsule that learns a foreign pid can still do -//! nothing with it. use alloc::vec::Vec; @@ -60,8 +54,15 @@ pub(super) fn guests_of(supervisor: u32) -> Vec { /// Called from process teardown; a supervisor leaving takes its guests. pub fn clear(pid: u32) { let orphans = guests_of(pid); - FOREIGN.write().retain(|e| e.pid != pid); + // Both directions go, not just this process's own row. + FOREIGN.write().retain(|e| e.pid != pid && e.supervisor != pid); for guest in orphans { super::trap_reply::abandon(guest); } + /* + * A guest that died while parked leaves its frame behind, and + * `take_answer` finds a frame by pid alone, so a reused pid would collect + * an answer meant for a process that no longer exists. + */ + super::trap_reply::forget(pid); } diff --git a/src/process/foreign/resume.rs b/src/process/foreign/resume.rs new file mode 100644 index 000000000..b03c8b1f1 --- /dev/null +++ b/src/process/foreign/resume.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 . + +//! Waking a guest on state it already holds. + +use crate::process::core::claim_new; +use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_PERM}; + +/// Make a guest runnable on the state it already carries. Used by a +/// fork, whose child was born holding its parent's registers. +pub(super) fn resume(pid: u32) -> i64 { + let has_state = + crate::process::with_process(pid, |pcb| pcb.saved_user_context.lock().is_some()); + if has_state != Some(true) { + return ERRNO_INVAL; + } + /* + * The claim is the check. A fork started twice would otherwise put + * the child on a run queue twice. + */ + if !claim_new(pid) { + return ERRNO_PERM; + } + crate::sched::add_to_run_queue(pid); + 0 +} diff --git a/src/process/foreign/spawn.rs b/src/process/foreign/spawn.rs index cffb85ec8..aa9295006 100644 --- a/src/process/foreign/spawn.rs +++ b/src/process/foreign/spawn.rs @@ -15,25 +15,21 @@ // along with this program. If not, see . //! Creating a guest: a process, a kernel stack, and nothing else. -//! -//! The kernel parses no image here. It hands back an empty process with no -//! capabilities, and the supervisor fills the address space itself through -//! the peer calls. Two steps rather than one, so a half-built guest never -//! becomes runnable: this file makes it, `spawn_start` runs it. use alloc::format; use crate::kernel_core::process_spawn::allocate_kernel_stack; use crate::process::core::types::Priority; use crate::process::core::{create_process_with_parent, ProcessState}; -use crate::syscall::microkernel::errnos::{ERRNO_EXIST, ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOMEM}; +use crate::syscall::microkernel::errnos::{ + ERRNO_EXIST, ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOMEM, ERRNO_PERM, +}; use crate::usercopy::read_user_bytes; const MAX_NAME: usize = 24; /// `MkForeignSpawn`: an empty, capability-free process supervised by the -/// caller. Returns its pid. The name is for the process table and the logs -/// only; it grants nothing and is not a service name. +/// caller. pub fn sys_foreign_spawn(name_ptr: u64, name_len: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; @@ -47,22 +43,30 @@ pub fn sys_foreign_spawn(name_ptr: u64, name_len: u64) -> i64 { let Ok(name) = core::str::from_utf8(&bytes) else { return ERRNO_INVAL; }; - let tag = format!("foreign:{name}"); - let Ok(pid) = create_process_with_parent(&tag, ProcessState::New, Priority::Normal, 0, None) - else { - return ERRNO_NOMEM; - }; + match empty_guest(caller, name.as_bytes()) { + Ok(pid) => pid as i64, + Err(e) => e, + } +} + +/// The one place a guest comes into being. +pub(super) fn empty_guest(supervisor: u32, name: &[u8]) -> Result { + let tag = format!("foreign:{}", core::str::from_utf8(name).unwrap_or("guest")); + let pid = create_process_with_parent(&tag, ProcessState::New, Priority::Normal, 0, None) + .map_err(|_| ERRNO_NOMEM)?; if allocate_kernel_stack(pid).is_err() { - return ERRNO_NOMEM; + return Err(ERRNO_NOMEM); } /* - * No capabilities are installed. The contract gate then refuses every - * NONOS syscall this process can name, which is the confinement - * itself: the supervisor's own policy is a second layer, not the - * only one. + * Every process is born with its parent's capabilities bounded by the + * ambient set, which for a guest of this capsule means core exec, IPC and + * memory. */ - if !super::registry::insert(pid, caller) { - return ERRNO_EXIST; + if crate::process::caps::install_spawn(pid, 0).is_none() { + return Err(ERRNO_PERM); + } + if !super::registry::insert(pid, supervisor) { + return Err(ERRNO_EXIST); } - pid as i64 + Ok(pid) } diff --git a/src/process/foreign/spawn_start.rs b/src/process/foreign/spawn_start.rs index 7dd806813..60506409c 100644 --- a/src/process/foreign/spawn_start.rs +++ b/src/process/foreign/spawn_start.rs @@ -16,17 +16,10 @@ //! Making a built guest runnable. -use crate::kernel_core::process_spawn::{allocate_user_stack, setup_initial_user_context}; -use crate::process::core::ProcessState; -use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOMEM, ERRNO_PERM}; +use super::peer_guard::in_user_half; +use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_PERM}; -/* - * `rsp` of zero asks for the kernel's own user stack. Any other value is - * a stack the supervisor built inside the guest, which is what a program - * that reads its arguments needs: the kernel knows nothing about argument - * vectors, and the supervisor that does cannot install a stack pointer - * without this call. - */ +// `rsp` of zero asks for the kernel's own user stack. pub fn sys_foreign_start(pid: u64, entry: u64, rsp: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; @@ -35,19 +28,19 @@ pub fn sys_foreign_start(pid: u64, entry: u64, rsp: u64) -> i64 { if super::registry::supervisor_of(pid) != Some(caller) { return ERRNO_PERM; } - let stack = match rsp { - 0 => match allocate_user_stack(pid) { - Ok(top) => top, - Err(_) => return ERRNO_NOMEM, - }, - given => given, - }; - if setup_initial_user_context(pid, entry, stack).is_err() { - return ERRNO_FAULT; + /* + * An entry of zero means the guest already holds the state it should wake + * in, which is what a fork leaves behind. + */ + if entry == 0 && rsp == 0 { + return super::resume::resume(pid); + } + /* + * A guest runs in ring three, so a kernel entry or stack would fault on + * its first instruction rather than escalate. + */ + if !in_user_half(entry, 1) || (rsp != 0 && !in_user_half(rsp, 0)) { + return ERRNO_INVAL; } - crate::process::with_process(pid, |pcb| { - *pcb.state.lock() = ProcessState::Ready; - }); - crate::sched::add_to_run_queue(pid); - 0 + super::start_context::install(pid, entry, rsp) } diff --git a/src/process/foreign/start_context.rs b/src/process/foreign/start_context.rs new file mode 100644 index 000000000..d21a17e11 --- /dev/null +++ b/src/process/foreign/start_context.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 . + +//! Giving a fresh guest its first user context. + +use crate::kernel_core::process_spawn::{allocate_user_stack, setup_initial_user_context}; +use crate::process::core::{claim_new, release_new}; +use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_NOMEM, ERRNO_PERM}; + +pub(super) fn install(pid: u32, entry: u64, rsp: u64) -> i64 { + // Claimed before the context is built, not after. + if !claim_new(pid) { + return ERRNO_PERM; + } + let stack = match rsp { + 0 => match allocate_user_stack(pid) { + Ok(top) => top, + Err(_) => { + release_new(pid); + return ERRNO_NOMEM; + } + }, + given => given, + }; + if setup_initial_user_context(pid, entry, stack).is_err() { + release_new(pid); + return ERRNO_FAULT; + } + crate::sched::add_to_run_queue(pid); + 0 +} diff --git a/src/process/foreign/thread.rs b/src/process/foreign/thread.rs index fa300d5d5..d86fb3885 100644 --- a/src/process/foreign/thread.rs +++ b/src/process/foreign/thread.rs @@ -14,18 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! A second thread inside a guest. -use crate::process::core::spawn_thread_in; +use super::peer_guard::in_user_half; +use crate::process::core::{admit_thread, spawn_thread_parked}; use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOMEM, ERRNO_PERM}; /// `MkForeignThread`: a thread in `pid`, sharing its address space and -/// supervised by the same caller. `tls` is stored rather than applied: -/// the context switch loads FS base from the control block, so a thread -/// that has never run still wakes with it set, which it must, because a -/// C runtime touches thread-local storage before its first instruction -/// of program code. +/// supervised by the same caller. pub fn sys_foreign_thread(pid: u64, entry: u64, rsp: u64, tls: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; @@ -34,19 +30,28 @@ pub fn sys_foreign_thread(pid: u64, entry: u64, rsp: u64, tls: u64) -> i64 { if super::registry::supervisor_of(pid) != Some(caller) { return ERRNO_PERM; } - if entry == 0 || rsp == 0 { + /* + * Ring three addresses only, and the thread pointer among them: the switch + * writes that one to an MSR that faults in ring zero on a non-canonical + * value. + */ + if !in_user_half(entry, 1) || !in_user_half(rsp, 0) { + return ERRNO_INVAL; + } + if tls != 0 && !in_user_half(tls, 1) { return ERRNO_INVAL; } - let Ok(tid) = spawn_thread_in(pid, entry, rsp) else { + let Ok(tid) = spawn_thread_parked(pid, entry, rsp) else { return ERRNO_NOMEM; }; if tls != 0 { crate::process::with_process(tid, |pcb| pcb.set_tls_base(tls)); } - // Its unknown syscalls have to reach the same supervisor, or the - // thread traps into ENOSYS while its siblings are being served. + // Everything true before anything can run it. if !super::registry::insert(tid, caller) { + crate::process::exit::teardown(tid, 0, false); return ERRNO_NOMEM; } + admit_thread(tid); tid as i64 } diff --git a/src/process/foreign/trap.rs b/src/process/foreign/trap.rs index 4ea31c5a7..11e59836e 100644 --- a/src/process/foreign/trap.rs +++ b/src/process/foreign/trap.rs @@ -15,44 +15,29 @@ // along with this program. If not, see . //! A refused syscall, parked until its supervisor answers. -//! -//! The guest's thread sleeps inside the syscall it made, so from the -//! guest's side nothing happened except that its `syscall` took a while. -//! The supervisor is a separate process with its own capabilities; the -//! only thing crossing between them is a register frame out and one value -//! back. use super::frame::ForeignFrame; +use super::frame_snapshot::{capture, FRAME_WORDS}; use super::registry; -use super::trap_table::{park, take_answer}; +use super::trap_table::park; +use super::trap_wait::wait_for_answer; /// The kernel's answer to a syscall number it does not know, made by the -/// supervisor rather than by the kernel. `None` when the caller has no -/// supervisor, which leaves the refusal the entry shim would have given. -pub fn redirect(nr: u64, args: [u64; 6], rip: u64) -> Option { +/// supervisor rather than by the kernel. +pub fn redirect(nr: u64, args: [u64; 6], frame: &[u64; FRAME_WORDS]) -> Option { let pid = crate::process::current_pid()?; let supervisor = registry::supervisor_of(pid)?; - park(ForeignFrame::new(pid, nr, args, rip)); + /* + * The frame is reachable only while this call is on the stack, and a fork + * asks for it long afterwards, so it is copied into the control block now. + */ + let saved = capture(frame, super::frame_cpu::user_rsp()); + crate::process::with_process(pid, |pcb| { + *pcb.saved_user_context.lock() = Some(saved); + }); + if !park(ForeignFrame::new(pid, nr, args, saved.rip)) { + return Some(super::trap_reply::ABANDONED); + } crate::sched::wake_process(supervisor); Some(wait_for_answer(pid)) } - -/* - * The guest holds no locks here and owns nothing the supervisor needs, so - * a supervisor that never answers costs exactly one parked thread. The - * answer is re-checked after taking the wake token and again after the - * sleep, so a reply landing in either window is not slept through. - */ -fn wait_for_answer(pid: u32) -> u64 { - loop { - if let Some(value) = take_answer(pid) { - return value; - } - let token = crate::sched::wake_token(pid); - if let Some(value) = take_answer(pid) { - return value; - } - crate::sched::sleep_until_unless_woken(pid, u64::MAX, token); - crate::sched::yield_now(); - } -} diff --git a/src/process/foreign/trap_claim.rs b/src/process/foreign/trap_claim.rs new file mode 100644 index 000000000..2d299a479 --- /dev/null +++ b/src/process/foreign/trap_claim.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 . + +//! Handing a waiting supervisor its next piece of work. + +use super::frame::ForeignFrame; +use super::registry; +use super::trap_table::PARKED; + +/// The next unclaimed frame for `supervisor`, if one is waiting. +pub(super) fn claim_next(supervisor: u32) -> Option { + let mut parked = PARKED.lock(); + for entry in parked.iter_mut() { + if entry.claimed || entry.answer.is_some() { + continue; + } + if registry::supervisor_of(entry.frame.pid) == Some(supervisor) { + entry.claimed = true; + return Some(entry.frame); + } + } + None +} + +/// Give a frame back after a delivery that did not happen. +pub(super) fn unclaim(pid: u32) { + let mut parked = PARKED.lock(); + if let Some(entry) = parked.iter_mut().find(|p| p.frame.pid == pid) { + entry.claimed = false; + } +} diff --git a/src/process/foreign/trap_reply.rs b/src/process/foreign/trap_reply.rs index 6cb4e772d..c5968cb8a 100644 --- a/src/process/foreign/trap_reply.rs +++ b/src/process/foreign/trap_reply.rs @@ -20,11 +20,9 @@ use super::registry; use super::trap_table::PARKED; use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOENT, ERRNO_PERM}; -/* - * A guest whose supervisor died is not left asleep forever and is not - * told its call succeeded. It gets a refusal it can act on. - */ -const ABANDONED: u64 = ERRNO_NOENT as u64; +// A guest whose supervisor died is not left asleep forever and is not told its +// call succeeded. +pub(super) const ABANDONED: u64 = ERRNO_NOENT as u64; /// `MkForeignReply`: answer one parked guest. Refused unless the caller is /// that guest's recorded supervisor, so a pid alone buys nothing. @@ -36,6 +34,12 @@ pub fn sys_foreign_reply(pid: u64, value: u64) -> i64 { if registry::supervisor_of(pid) != Some(caller) { return ERRNO_PERM; } + answer_raw(pid, value) +} + +/// Hand a parked guest its value and wake it. The permission check is +/// the caller's: `exec` has made it already, on the same terms. +pub(super) fn answer_raw(pid: u32, value: u64) -> i64 { let mut parked = PARKED.lock(); let Some(entry) = parked.iter_mut().find(|p| p.frame.pid == pid && p.answer.is_none()) else { return ERRNO_NOENT; @@ -46,6 +50,12 @@ pub fn sys_foreign_reply(pid: u64, value: u64) -> i64 { 0 } +/// Drop every frame belonging to a process that is gone, so a reused +/// pid cannot collect an answer left behind by its predecessor. +pub(super) fn forget(pid: u32) { + PARKED.lock().retain(|p| p.frame.pid != pid); +} + /// Release every frame belonging to a guest whose supervisor has gone. pub(super) fn abandon(pid: u32) { let mut parked = PARKED.lock(); diff --git a/src/process/foreign/trap_table.rs b/src/process/foreign/trap_table.rs index df6dd809c..887ad6c2e 100644 --- a/src/process/foreign/trap_table.rs +++ b/src/process/foreign/trap_table.rs @@ -34,8 +34,18 @@ pub(super) struct Parked { pub(super) static PARKED: Mutex> = Mutex::new(Vec::new()); -pub(super) fn park(frame: ForeignFrame) { - PARKED.lock().push(Parked { frame, answer: None, claimed: false }); +/// Park `frame`, unless its supervisor has gone in the meantime. False +/// says nothing was parked and the guest must be refused instead. +pub(super) fn park(frame: ForeignFrame) -> bool { + let pid = frame.pid; + let mut parked = PARKED.lock(); + parked.push(Parked { frame, answer: None, claimed: false }); + // Checked with the table held, and after the push rather than before. + if registry::supervisor_of(pid).is_some() { + return true; + } + parked.pop(); + false } /// The answer for `pid`, removing the entry once it is taken. @@ -46,18 +56,3 @@ pub(super) fn take_answer(pid: u32) -> Option { parked.remove(at); Some(value) } - -/// The next unclaimed frame for `supervisor`, if one is waiting. -pub(super) fn claim_next(supervisor: u32) -> Option { - let mut parked = PARKED.lock(); - for entry in parked.iter_mut() { - if entry.claimed || entry.answer.is_some() { - continue; - } - if registry::supervisor_of(entry.frame.pid) == Some(supervisor) { - entry.claimed = true; - return Some(entry.frame); - } - } - None -} diff --git a/src/process/foreign/trap_wait.rs b/src/process/foreign/trap_wait.rs new file mode 100644 index 000000000..8beb8726a --- /dev/null +++ b/src/process/foreign/trap_wait.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 guest asleep inside the syscall it made. + +use super::trap_table::take_answer; + +pub(super) fn wait_for_answer(pid: u32) -> u64 { + loop { + if let Some(value) = take_answer(pid) { + return settle(pid, value); + } + let token = crate::sched::wake_token(pid); + if let Some(value) = take_answer(pid) { + return settle(pid, value); + } + crate::sched::sleep_until_unless_woken(pid, u64::MAX, token); + crate::sched::yield_now(); + } +} + +/// Every answer but one is a return value. +fn settle(pid: u32, value: u64) -> u64 { + if value == super::exec::EXECED { + super::exec_enter::enter(pid) + } + value +} diff --git a/src/process/foreign/wait.rs b/src/process/foreign/wait.rs index d8785e7a6..8b6861823 100644 --- a/src/process/foreign/wait.rs +++ b/src/process/foreign/wait.rs @@ -20,14 +20,12 @@ use core::mem::size_of; use super::frame::ForeignFrame; -use super::trap_table; +use super::trap_claim; use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_INVAL, ERRNO_TIMEDOUT}; use crate::usercopy::{validate_user_write, write_user_value}; -/// `MkForeignWait`: wait for a guest of the calling process to issue a -/// syscall this kernel refuses, and copy its frame out. Returns the size -/// written, or `ERRNO_TIMEDOUT` when the deadline passes with nothing -/// waiting. A caller that supervises nothing waits like any other. +/// `MkForeignWait`: wait for a guest of the calling process to issue a syscall +/// this kernel refuses, and copy its frame out. pub fn sys_foreign_wait(out_ptr: u64, out_len: u64, timeout_ms: u64) -> i64 { let Some(caller) = crate::process::current_pid() else { return ERRNO_INVAL; @@ -41,11 +39,8 @@ pub fn sys_foreign_wait(out_ptr: u64, out_len: u64, timeout_ms: u64) -> i64 { } let start = crate::time::timestamp_millis(); loop { - if let Some(frame) = trap_table::claim_next(caller) { - if write_user_value(out_ptr, &frame).is_err() { - return ERRNO_FAULT; - } - return size as i64; + if let Some(frame) = trap_claim::claim_next(caller) { + return deliver(out_ptr, frame, size); } let waited = crate::time::timestamp_millis().saturating_sub(start); if timeout_ms > 0 && waited >= timeout_ms { @@ -53,13 +48,20 @@ pub fn sys_foreign_wait(out_ptr: u64, out_len: u64, timeout_ms: u64) -> i64 { } let deadline = if timeout_ms == 0 { u64::MAX } else { start.saturating_add(timeout_ms) }; let token = crate::sched::wake_token(caller); - if let Some(frame) = trap_table::claim_next(caller) { - if write_user_value(out_ptr, &frame).is_err() { - return ERRNO_FAULT; - } - return size as i64; + if let Some(frame) = trap_claim::claim_next(caller) { + return deliver(out_ptr, frame, size); } crate::sched::sleep_until_unless_woken(caller, deadline, token); crate::sched::yield_now(); } } + +/// Hand one claimed frame over, or give it back when the supervisor's buffer +/// will not take it. +fn deliver(out_ptr: u64, frame: ForeignFrame, size: usize) -> i64 { + if write_user_value(out_ptr, &frame).is_err() { + trap_claim::unclaim(frame.pid); + return ERRNO_FAULT; + } + size as i64 +} diff --git a/src/userspace/capsule_linux/install.rs b/src/userspace/capsule_linux/install.rs new file mode 100644 index 000000000..829f803dc --- /dev/null +++ b/src/userspace/capsule_linux/install.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! The personality, spawned to install a package rather than host one. + +use alloc::string::String; +use alloc::vec; + +use super::embed::{ + LINUX_ATTESTATION_BYTES, LINUX_ELF, LINUX_MANIFEST_BYTES, LINUX_NONOS_ID_CERT_BYTES, +}; +use super::spawn::LINUX_CAPS; +use crate::kernel_core::process_spawn::capsule_spawn::{self, CapsuleSpecVerified, SpawnError}; +use crate::security::nonos_id_cert::IdCertVerifyError; +use crate::security::nonos_trust_anchor::{ + decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY, +}; + +// A second service name, because the installer is a second live process and +// two of them announcing one endpoint is a race over which answers. +const SERVICE_NAME: &str = "app.linux.install"; +const SERVICE_PORT: u32 = 4938; +const REPLY_INBOX: &str = "endpoint.app.linux.install.reply"; +const REPLY_PORT: u32 = 4939; + +pub fn spawn_install(package: &str) -> Result { + let trust_anchor = decode_trust_anchor(BAKED_TRUST_ANCHOR_POLICY) + .map_err(|_| SpawnError::NonosIdCertRejected(IdCertVerifyError::TrustAnchorPolicy))?; + let spec = CapsuleSpecVerified { + name: SERVICE_NAME, + service_port: SERVICE_PORT, + reply_inbox: REPLY_INBOX, + reply_port: REPLY_PORT, + elf: LINUX_ELF, + nonos_id_cert_bytes: LINUX_NONOS_ID_CERT_BYTES, + manifest_bytes: LINUX_MANIFEST_BYTES, + attestation_trailer: LINUX_ATTESTATION_BYTES, + target_triple: env!("NONOS_USER_TARGET"), + requested_caps: LINUX_CAPS, + debug_tag: b"[LINUX-INSTALL] elf error:", + }; + let pid = capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + let argv = vec![String::from("install"), String::from(package)]; + crate::process::with_process(pid, |pcb| *pcb.argv.lock() = argv); + Ok(pid) +} diff --git a/src/userspace/capsule_linux/mod.rs b/src/userspace/capsule_linux/mod.rs index fe7590c8e..a4f0158a5 100644 --- a/src/userspace/capsule_linux/mod.rs +++ b/src/userspace/capsule_linux/mod.rs @@ -18,8 +18,10 @@ //! kernel, and the spawn that admits them. mod embed; +mod install; mod spawn; mod state; +pub use install::spawn_install; pub use spawn::{spawn_linux_capsule, LINUX_CAPS}; pub use state::shared_state; diff --git a/src/userspace/capsule_linux/spawn.rs b/src/userspace/capsule_linux/spawn.rs index f0a6bbd70..06493bee2 100644 --- a/src/userspace/capsule_linux/spawn.rs +++ b/src/userspace/capsule_linux/spawn.rs @@ -15,9 +15,7 @@ // along with this program. If not, see . //! Admitting the Linux personality through the same verified path every -//! capsule takes. ForeignExec is the only capability here that no other -//! capsule holds, and it is what lets this one host code the kernel has -//! not verified. Its guests hold nothing at all. +//! capsule takes. use super::embed::{ LINUX_ATTESTATION_BYTES, LINUX_ELF, LINUX_MANIFEST_BYTES, LINUX_NONOS_ID_CERT_BYTES, @@ -36,14 +34,15 @@ const REPLY_INBOX: &str = "endpoint.app.linux.reply"; const REPLY_PORT: u32 = 4937; const TARGET_TRIPLE: &str = env!("NONOS_USER_TARGET"); -/// Declared once here and mirrored by the capsule's manifest, which is -/// what the gate actually enforces. +/// Declared once here and mirrored by the capsule's manifest, which is what +/// the gate actually enforces. pub const LINUX_CAPS: u64 = Capability::CoreExec.bit() | Capability::IPC.bit() | Capability::Memory.bit() | Capability::Crypto.bit() | Capability::Debug.bit() - | Capability::ForeignExec.bit(); + | Capability::ForeignExec.bit() + | Capability::LocalSign.bit(); pub fn spawn_linux_capsule() -> Result<(), SpawnError> { let trust_anchor = decode_trust_anchor(BAKED_TRUST_ANCHOR_POLICY) diff --git a/tools/nonos-linux-coverage b/tools/nonos-linux-coverage new file mode 100755 index 000000000..7781badba --- /dev/null +++ b/tools/nonos-linux-coverage @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public 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 packages the personality can actually run, and what stops the rest. + +A listing that installs and then dies on its first unimplemented syscall +is worse than one that was never offered, so this answers the question +before anyone clicks: disassemble every executable in every fetched +package, find the immediate loaded into eax ahead of each `syscall`, and +compare that set against what the personality's dispatch tables answer. + +The analysis is deliberately static and deliberately pessimistic. A +number reached only on a path the program never takes still counts as +required here, because nothing in the binary says which paths run. So a +package this reports as covered is covered; one it reports as blocked +may still work, and the named syscall is where to look first. +""" + +import argparse +import io +import re +import subprocess +import sys +import tarfile +import zlib +from collections import Counter +from pathlib import Path + +# `mov $N, %eax` close enough before a syscall to be its number. objdump +# writes the immediate in hex with a $ prefix. +MOV_EAX = re.compile(r"mov\s+\$0x([0-9a-f]+),%eax") +SYSCALL = re.compile(r"\bsyscall\b") + +# How far back to look. A compiler may schedule a few instructions +# between loading the number and making the call. +WINDOW = 12 + + +def payload(apk: Path) -> bytes: + """The tar stream inside an apk. + + An apk is several gzip members end to end: a signature, a control + segment, then the data. `tarfile.open(r:gz)` stops after the first, + which holds no files at all, so reading one that way finds nothing + and looks exactly like a package with no binaries in it. Every + member is inflated and concatenated instead. + """ + raw = apk.read_bytes() + out, at = bytearray(), 0 + while at < len(raw): + d = zlib.decompressobj(47) + try: + out += d.decompress(raw[at:]) + except zlib.error: + break + if d.unused_data == raw[at:]: + break + at = len(raw) - len(d.unused_data) + return bytes(out) + + +def executables(apk: Path, into: Path) -> list: + """Every ELF in the package, unpacked to a scratch directory.""" + out = [] + try: + with tarfile.open(fileobj=io.BytesIO(payload(apk))) as t: + for member in t.getmembers(): + if not member.isfile() or member.size < 128: + continue + f = t.extractfile(member) + if f is None: + continue + head = f.read(4) + if head != b"\x7fELF": + continue + f.seek(0) + at = into / member.name.replace("/", "_") + at.write_bytes(f.read()) + out.append(at) + except (tarfile.TarError, OSError, EOFError): + # Alpine's apk is a concatenated stream; a truncated tail after + # the payload is normal and not a reason to discard what parsed. + pass + return out + + +def numbers_used(elf: Path) -> set: + try: + text = subprocess.run( + ["objdump", "-d", "--no-show-raw-insn", str(elf)], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=120, + ).stdout.decode(errors="replace") + except (OSError, subprocess.SubprocessError): + return set() + lines = text.splitlines() + used, recent = set(), [] + for line in lines: + m = MOV_EAX.search(line) + if m: + recent.append(int(m.group(1), 16)) + recent = recent[-WINDOW:] + continue + if SYSCALL.search(line) and recent: + used.add(recent[-1]) + return used + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--cache", type=Path, default=Path("target/market-cache")) + ap.add_argument("--served", type=Path, required=True, + help="file of syscall numbers the personality answers") + ap.add_argument("--scratch", type=Path, default=Path("target/coverage-scratch")) + ap.add_argument("--names", type=Path, + help="capsule_linux abi/nr.rs, to name the gaps") + args = ap.parse_args() + + served = {int(x) for x in args.served.read_text().split()} + naming = {} + if args.names and args.names.exists(): + for name, num in re.findall(r"pub const ([A-Z0-9_]+): u64 = (\d+);", + args.names.read_text()): + naming[int(num)] = name.lower() + + args.scratch.mkdir(parents=True, exist_ok=True) + covered, blocked, missing = [], [], Counter() + for apk in sorted(args.cache.glob("*.apk")): + used = set() + for elf in executables(apk, args.scratch): + used |= numbers_used(elf) + elf.unlink(missing_ok=True) + if not used: + continue + gap = used - served + name = apk.name.rsplit("-", 2)[0] + if gap: + blocked.append((name, sorted(gap))) + missing.update(gap) + else: + covered.append(name) + + total = len(covered) + len(blocked) + if total == 0: + # Finding no syscalls in 76 packages means the reader is broken, + # not that the packages are empty. Saying "0 of 0 covered" here + # reads as a clean pass, which is the worst possible answer. + print("no binaries were read; the extractor or objdump is not working", + file=sys.stderr) + return 2 + print(f"{len(covered)} of {total} packages need nothing the personality lacks\n") + if covered: + print("runs today:", ", ".join(sorted(covered))) + print(f"\nmost common gaps across {len(blocked)} blocked packages:") + for num, count in missing.most_common(20): + print(f" {count:3} packages need {num:4} {naming.get(num, '(unnamed)')}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/nonos-linux-fetch b/tools/nonos-linux-fetch new file mode 100755 index 000000000..3954f5030 --- /dev/null +++ b/tools/nonos-linux-fetch @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +"""Install a Linux program and everything it needs, from a distribution. + +`nonos-linux-pack` says which shared libraries a program needs and refuses to +pack one whose closure is incomplete. This closes that loop: it reads the +distribution's package index, finds the package providing each missing +library, unpacks it into the sysroot, and repeats until the closure is whole +or nothing can supply the rest. + +The index is the distribution's own, fetched over TLS and used as data: a +package is only ever unpacked into the sysroot directory, never executed, and +nothing here runs a package's scripts. +""" + +import argparse +import subprocess +import sys +import tarfile +import urllib.request +from pathlib import Path + +MIRROR = "https://dl-cdn.alpinelinux.org/alpine" +BRANCHES = ("main", "community") + + +def fetch(url: str, into: Path) -> Path: + if into.exists(): + return into + into.parent.mkdir(parents=True, exist_ok=True) + with urllib.request.urlopen(url, timeout=120) as r: + into.write_bytes(r.read()) + return into + + +def index(cache: Path, release: str, arch: str, branch: str): + """(soname -> package, name -> package), from the branch's APKINDEX.""" + base = f"{MIRROR}/{release}/{branch}/{arch}" + tgz = fetch(f"{base}/APKINDEX.tar.gz", cache / f"{branch}-APKINDEX.tar.gz") + with tarfile.open(tgz) as t: + raw = t.extractfile("APKINDEX").read().decode(errors="replace") + libs, pkgs = {}, {} + name = version = None + for line in raw.split("\n"): + if line.startswith("P:"): + name = line[2:] + elif line.startswith("V:"): + version = line[2:] + if name: + pkgs.setdefault(name, (name, version, base)) + elif line.startswith("p:") and name and version: + for token in line[2:].split(): + if token.startswith("so:"): + libs.setdefault(token[3:].split("=")[0], (name, version, base)) + return libs, pkgs + + +def missing(pack: Path, sysroot: Path, programs) -> list: + """What the closure tool says it cannot resolve.""" + cmd = [sys.executable, str(pack), "--sysroot", str(sysroot)] + for p in programs: + cmd += ["--program", p] + done = subprocess.run(cmd, capture_output=True, text=True) + if done.returncode == 0: + return [] + out = [] + for line in done.stderr.split("\n"): + line = line.strip() + if line and not line.startswith("linux-pack:"): + out.append(line) + return out + + +def safe(member, dest: str): + """Reject anything that would write outside the sysroot, and rewrite a + link whose target is absolute so it points inside it instead. + + A distribution is full of absolute symlinks: every coreutils applet + points at /bin/busybox. Following one during extraction would write to + the real root, so the target is made relative to the sysroot rather + than the archive being trusted or the link being dropped.""" + name = member.name.lstrip("./") + if name.startswith("/") or ".." in Path(name).parts: + return None + member = member.replace(name=name, deep=False) + if member.issym() or member.islnk(): + target = member.linkname + if target.startswith("/"): + up = "../" * (len(Path(name).parts) - 1) + member = member.replace(linkname=up + target.lstrip("/"), deep=False) + return member + + +def install(sysroot: Path, cache: Path, package: str, version: str, base: str) -> bool: + apk = fetch(f"{base}/{package}-{version}.apk", cache / f"{package}-{version}.apk") + with tarfile.open(apk) as t: + members = [m for m in t.getmembers() if not m.name.startswith(".")] + for m in members: + # A package may replace a file an earlier one installed, and + # the earlier one is often read-only. Clear the way rather + # than stopping halfway through a package. + at = sysroot / m.name.lstrip("./") + if at.is_symlink() or at.is_file(): + at.unlink() + t.extractall(sysroot, members=members, filter=safe) + return True + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--sysroot", type=Path, required=True) + ap.add_argument("--program", action="append", default=[], + help="path inside the sysroot; repeatable") + ap.add_argument("--package", action="append", default=[], + help="a package to install by name before resolving; " + "repeatable. Use it to bring in the programs " + "themselves, not only the libraries under them.") + ap.add_argument("--release", default="v3.20") + ap.add_argument("--arch", default="x86_64") + ap.add_argument("--cache", type=Path, default=Path(".apk-cache")) + ap.add_argument("--pack", type=Path, default=Path("tools/nonos-linux-pack")) + ap.add_argument("--rounds", type=int, default=16, + help="give up after this many, rather than looping on a " + "library no package provides") + args = ap.parse_args() + + provides, names = {}, {} + for branch in BRANCHES: + libs, pkgs = index(args.cache, args.release, args.arch, branch) + provides.update(libs) + names.update(pkgs) + print(f"linux-fetch: {len(provides)} libraries, {len(names)} packages in the index") + + for want in args.package: + found = names.get(want) + if not found: + print(f"linux-fetch: no package named {want}", file=sys.stderr) + return 1 + package, version, base = found + print(f" package {package} {version}") + install(args.sysroot, args.cache, package, version, base) + if not args.program: + return 0 + + for round_no in range(args.rounds): + gaps = missing(args.pack, args.sysroot, args.program) + if not gaps: + print(f"linux-fetch: closure complete after {round_no} rounds") + return 0 + added = 0 + for soname in gaps: + found = provides.get(soname) + if not found: + continue + package, version, base = found + print(f" {soname} -> {package} {version}") + install(args.sysroot, args.cache, package, version, base) + added += 1 + if added == 0: + print("linux-fetch: nothing in the index provides:", file=sys.stderr) + for soname in gaps: + print(f" {soname}", file=sys.stderr) + return 1 + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/nonos-linux-pack b/tools/nonos-linux-pack new file mode 100755 index 000000000..da88dfe0f --- /dev/null +++ b/tools/nonos-linux-pack @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public 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 a Linux program's shared library closure and place it in the store. + +The Linux personality reads a program and its libraries out of the NONOS store +at the paths the program itself names: the interpreter from PT_INTERP, then +whatever that interpreter opens. So installing an application is placing the +real files at the real paths, and the only hard part is knowing which files. + +This walks the ELF dynamic section for DT_NEEDED, resolves each name against a +sysroot using DT_RUNPATH and then the default library directories, and repeats +until nothing new appears. It emits the entry list that tools/nonos-store-pack +takes, or writes them into an image directly. + +Nothing here guesses. A name that cannot be resolved inside the sysroot is +reported and the run fails, because a missing library at boot is a dynamic +linker abort with no message worth reading. +""" + +import argparse +import struct +import subprocess +import sys +from pathlib import Path + +PT_INTERP = 3 +PT_DYNAMIC = 2 +DT_NEEDED = 1 +DT_RUNPATH = 29 +DT_RPATH = 15 +DT_STRTAB = 5 +DT_STRSZ = 10 + +DEFAULT_LIBDIRS = ("/lib", "/usr/lib", "/lib64", "/usr/lib64") + + +class NotElf(Exception): + pass + + +def phdrs(data: bytes): + """(type, offset, vaddr, filesz) for each program header.""" + if len(data) < 64 or data[:4] != b"\x7fELF": + raise NotElf("not an ELF") + if data[4] != 2 or data[5] != 1: + raise NotElf("not 64-bit little-endian") + e_phoff, = struct.unpack_from(" str: + for p_type, p_offset, _, p_filesz in headers: + if p_type == PT_INTERP: + raw = data[p_offset:p_offset + p_filesz] + return raw.split(b"\0", 1)[0].decode() + return "" + + +def dynamic(data: bytes, headers): + """(needed names, runpath entries) from the dynamic section.""" + seg = next((p for p in headers if p[0] == PT_DYNAMIC), None) + if seg is None: + return [], [] + _, off, _, size = seg + tags = [] + at = off + while at + 16 <= off + size: + tag, val = struct.unpack_from(" str: + end = table.find(b"\0", index) + return table[index:end if end >= 0 else None].decode(errors="replace") + + needed = [name(v) for t, v in tags if t == DT_NEEDED] + paths = [] + for t, v in tags: + if t in (DT_RUNPATH, DT_RPATH): + paths.extend(p for p in name(v).split(":") if p) + return needed, paths + + +def find(sysroot: Path, name: str, extra: list) -> Path: + """A library by soname, inside the sysroot and nowhere else.""" + if name.startswith("/"): + candidate = sysroot / name.lstrip("/") + return candidate if candidate.is_file() else None + for directory in list(extra) + list(DEFAULT_LIBDIRS): + candidate = sysroot / directory.lstrip("/") / name + if candidate.is_file(): + return candidate + return None + + +def closure(sysroot: Path, program: Path): + """Every file the program needs, as store path -> file on disk.""" + out = {} + missing = [] + queue = [("/" + str(program.relative_to(sysroot)), program)] + seen = set() + while queue: + store_path, disk = queue.pop() + if store_path in seen: + continue + seen.add(store_path) + data = disk.read_bytes() + out[store_path] = disk + headers = phdrs(data) + ld = interp(data, headers) + if ld: + target = sysroot / ld.lstrip("/") + if target.is_file(): + queue.append((ld, target)) + else: + missing.append(ld) + needed, runpath = dynamic(data, headers) + for name in needed: + found = find(sysroot, name, runpath) + if found is None: + missing.append(name) + continue + queue.append(("/" + str(found.relative_to(sysroot)), found)) + return out, missing + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--sysroot", type=Path, required=True, + help="root of the Linux distribution the program came from") + ap.add_argument("--program", type=Path, action="append", required=True, + help="program to install, inside the sysroot; repeatable") + ap.add_argument("--data", action="append", default=[], + help="a data file as STOREPATH=DISKPATH, or a path inside " + "the sysroot to install where it already sits; " + "repeatable. The keymap goes in this way.") + ap.add_argument("--sign", type=Path, + help="capsule-sign binary. With it, each program is given " + "a certificate, manifest and trailer beside it, over " + "the closure computed here, and the personality " + "refuses anything unproven.") + ap.add_argument("--publisher", type=Path, + help="publisher seed prefix for --sign") + ap.add_argument("--image", type=Path, + help="store image to write into; without it the entries are printed") + ap.add_argument("--lba", type=int, default=256, help="store LBA in the image") + ap.add_argument("--pack", type=Path, default=Path("tools/nonos-store-pack"), + help="the packer to hand the entries to") + args = ap.parse_args() + + if not args.sysroot.is_dir(): + print(f"linux-pack: no sysroot at {args.sysroot}", file=sys.stderr) + return 2 + + entries, missing = {}, [] + for program in args.program: + path = program if program.is_absolute() else args.sysroot / program + if not path.is_file(): + print(f"linux-pack: no program at {path}", file=sys.stderr) + return 2 + found, gone = closure(args.sysroot, path.resolve()) + entries.update(found) + missing.extend(gone) + + for item in args.data: + store, _, disk = item.partition("=") + path = Path(disk) if disk else (args.sysroot / store.lstrip("/")) + if not path.is_file(): + print(f"linux-pack: no data file at {path}", file=sys.stderr) + return 2 + entries["/" + store.lstrip("/")] = path + + if missing: + print("linux-pack: unresolved inside the sysroot, refusing to pack a " + "program that cannot start:", file=sys.stderr) + for name in sorted(set(missing)): + print(f" {name}", file=sys.stderr) + return 1 + + pairs = [f"{store}={disk}" for store, disk in sorted(entries.items())] + total = sum(disk.stat().st_size for disk in entries.values()) + print(f"linux-pack: {len(pairs)} files, {total // 1024} KiB") + if args.image is None: + for pair in pairs: + print(f" --entry {pair}") + return 0 + + cmd = [sys.executable, str(args.pack), "--image", str(args.image), + "--lba", str(args.lba)] + for pair in pairs: + cmd += ["--entry", pair] + return subprocess.call(cmd) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/userland/capsule_linux/Capsule.mk b/userland/capsule_linux/Capsule.mk index ccdfeca26..f5fe14738 100644 --- a/userland/capsule_linux/Capsule.mk +++ b/userland/capsule_linux/Capsule.mk @@ -8,8 +8,16 @@ # until the file layer carries it, and the rest is the ordinary capsule # floor. # +# LocalSign is what lets this capsule vouch for a package it installed. +# Without it MkLocalSign is refused, every package goes into the store +# with no trailer, and the exec gate then refuses all of them: the +# marketplace installs software that can never run. It is a narrow +# right. The trailer is made against the machine's own root, and that +# root verifies nothing until a human confirms a code on the console, +# which lapses at the next boot. +# # = CoreExec 0x1 | IPC 0x8 | Memory 0x10 | Crypto 0x20 | Debug 0x100 -# | ForeignExec 0x100000000 = 0x100000139 +# | ForeignExec 0x100000000 | LocalSign 0x200000000 = 0x300000139 CAPSULE_SLUG := linux CAPSULE_HANDLE := app.linux @@ -20,7 +28,7 @@ CAPSULE_FEATURE := nonos-capsule-linux CAPSULE_NAMESPACE := systems.nonos.app.linux CAPSULE_SERVICE_ENDPOINT := service:4936:app.linux CAPSULE_REPLY_ENDPOINT := reply:4937:endpoint.app.linux.reply -CAPSULE_REQUIRED_CAPS := 0x100000139 +CAPSULE_REQUIRED_CAPS := 0x300000139 CAPSULE_KERNEL_MIRROR := src/userspace/capsule_linux include nonos-mk/capsule.mk diff --git a/userland/capsule_linux/Cargo.lock b/userland/capsule_linux/Cargo.lock index e2c8f62a4..5d8a4577d 100644 --- a/userland/capsule_linux/Cargo.lock +++ b/userland/capsule_linux/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "libm", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +dependencies = [ + "libm", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linked_list_allocator" version = "0.10.6" @@ -20,13 +55,36 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "nonos_app_skeleton" +version = "0.3.0" +dependencies = [ + "nonos_toolkit", + "nonos_userland_libc", +] + [[package]] name = "nonos_capsule_linux" version = "0.1.0" dependencies = [ + "nonos_app_skeleton", + "nonos_inflate", "nonos_userland_libc", ] +[[package]] +name = "nonos_inflate" +version = "0.3.0" + +[[package]] +name = "nonos_toolkit" +version = "0.3.0" +dependencies = [ + "ab_glyph", + "nonos_userland_libc", + "spin", +] + [[package]] name = "nonos_userland_libc" version = "0.3.0" @@ -34,12 +92,30 @@ dependencies = [ "linked_list_allocator", ] +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + [[package]] name = "spinning_top" version = "0.2.5" @@ -48,3 +124,12 @@ checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" dependencies = [ "lock_api", ] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] diff --git a/userland/capsule_linux/Cargo.toml b/userland/capsule_linux/Cargo.toml index 3298e255b..1a57cc5ab 100644 --- a/userland/capsule_linux/Cargo.toml +++ b/userland/capsule_linux/Cargo.toml @@ -23,6 +23,7 @@ path = "src/main.rs" [dependencies] nonos_libc = { package = "nonos_userland_libc", path = "../libc" } nonos_app_skeleton = { path = "../app_skeleton", default-features = false } +nonos_inflate = { path = "../inflate" } [profile.release] panic = "abort" diff --git a/userland/capsule_linux/guests/busybox.elf b/userland/capsule_linux/guests/busybox.elf new file mode 100755 index 000000000..6e660eb31 Binary files /dev/null and b/userland/capsule_linux/guests/busybox.elf differ diff --git a/userland/capsule_linux/guests/hello.elf b/userland/capsule_linux/guests/hello.elf deleted file mode 100644 index a4c732d15..000000000 Binary files a/userland/capsule_linux/guests/hello.elf and /dev/null differ diff --git a/userland/capsule_linux/src/linux/abi/errno.rs b/userland/capsule_linux/src/linux/abi/errno.rs index 0c53b7f80..7cbcaebde 100644 --- a/userland/capsule_linux/src/linux/abi/errno.rs +++ b/userland/capsule_linux/src/linux/abi/errno.rs @@ -15,22 +15,21 @@ // along with this program. If not, see . //! Linux errno values, and the convention for returning them. -//! -//! A Linux syscall reports failure as a small negative number in `rax`, -//! not as an errno variable. These are the values a guest expects to see, -//! which are Linux's and not this system's. pub const EPERM: i64 = 1; pub const ENOENT: i64 = 2; +pub const EINTR: i64 = 4; pub const EIO: i64 = 5; pub const EBADF: i64 = 9; pub const ECHILD: i64 = 10; pub const EAGAIN: i64 = 11; pub const ENOMEM: i64 = 12; +pub const ESRCH: i64 = 3; pub const EACCES: i64 = 13; pub const EFAULT: i64 = 14; pub const EBUSY: i64 = 16; pub const EEXIST: i64 = 17; +pub const ENOTEMPTY: i64 = 39; pub const ENODEV: i64 = 19; pub const ENOTDIR: i64 = 20; pub const ENOSPC: i64 = 28; @@ -42,19 +41,29 @@ pub const ENOTTY: i64 = 25; pub const ESPIPE: i64 = 29; pub const EPIPE: i64 = 32; pub const ERANGE: i64 = 34; +pub const ELOOP: i64 = 40; +pub const ENOEXEC: i64 = 8; pub const ENOSYS: i64 = 38; +pub const ECONNRESET: i64 = 104; +pub const ENOTCONN: i64 = 107; +pub const ENOTSOCK: i64 = 88; pub const ENOTSUP: i64 = 95; pub const EAFNOSUPPORT: i64 = 97; pub const ECONNREFUSED: i64 = 111; pub const EINPROGRESS: i64 = 115; -/// The value a guest's `rax` receives for a failure. pub fn fail(errno: i64) -> u64 { (-errno) as u64 } -/// The value a guest's `rax` receives for a success carrying a count or a -/// descriptor. pub fn ok(value: u64) -> u64 { value } + +/// A returned descriptor, or `None` if the call failed. +pub fn slot(value: u64) -> Option { + match value { + v if v > u64::MAX - 4096 => None, + v => Some(v as usize), + } +} diff --git a/userland/capsule_linux/src/linux/abi/mod.rs b/userland/capsule_linux/src/linux/abi/mod.rs index a528c3f6c..fe250836a 100644 --- a/userland/capsule_linux/src/linux/abi/mod.rs +++ b/userland/capsule_linux/src/linux/abi/mod.rs @@ -14,14 +14,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! The Linux contract a compiled binary was built against: its numbers, -//! its errnos, and the names it knows them by. -//! -//! The tables are the whole contract and not only the part served today, -//! so a call that arrives before its handler is named rather than guessed -//! at, and coverage is a list anyone can read. +//! The Linux contract a compiled binary was built against: its numbers, its +//! errnos, and the names it knows them by. #![allow(dead_code)] pub mod errno; pub mod name; pub mod nr; +pub mod nr_path; +pub mod nr_high; diff --git a/userland/capsule_linux/src/linux/abi/name.rs b/userland/capsule_linux/src/linux/abi/name.rs index edcc49a64..875b825ac 100644 --- a/userland/capsule_linux/src/linux/abi/name.rs +++ b/userland/capsule_linux/src/linux/abi/name.rs @@ -14,9 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Names for the numbers, so a guest that asks for something not yet -//! served is reported by name rather than by integer. Coverage is then a -//! measured list and never a claim. +//! Names for the numbers, so a guest that asks for something not yet served is +//! reported by name rather than by integer. use super::nr; @@ -57,6 +56,11 @@ pub fn of(number: u64) -> &'static [u8] { nr::CLOCK_GETTIME => b"clock_gettime", nr::EXIT_GROUP => b"exit_group", nr::CLONE => b"clone", + nr::SOCKET => b"socket", + nr::SENDMSG => b"sendmsg", + nr::RECVMSG => b"recvmsg", + nr::MEMFD_CREATE => b"memfd_create", + nr::CONNECT => b"connect", nr::GETDENTS64 => b"getdents64", nr::OPENAT => b"openat", nr::NEWFSTATAT => b"newfstatat", diff --git a/userland/capsule_linux/src/linux/abi/nr.rs b/userland/capsule_linux/src/linux/abi/nr.rs index 61b68eb7a..ee370fdb3 100644 --- a/userland/capsule_linux/src/linux/abi/nr.rs +++ b/userland/capsule_linux/src/linux/abi/nr.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Linux x86_64 syscall numbers, by family. These are Linux's numbers and -//! never this system's: they are the contract a compiled binary was built -//! against, so they are transcribed rather than chosen. + +//! Linux x86_64 syscall numbers, by family. + +pub use super::nr_high::*; pub const READ: u64 = 0; pub const WRITE: u64 = 1; @@ -40,29 +41,25 @@ pub const READV: u64 = 19; pub const WRITEV: u64 = 20; pub const ACCESS: u64 = 21; pub const MADVISE: u64 = 28; +pub const DUP: u64 = 32; +pub const DUP2: u64 = 33; +pub const PIPE: u64 = 22; pub const NANOSLEEP: u64 = 35; pub const GETPID: u64 = 39; +pub const SOCKET: u64 = 41; +pub const CONNECT: u64 = 42; +pub const SENDTO: u64 = 44; +pub const RECVFROM: u64 = 45; +pub const SENDMSG: u64 = 46; +pub const RECVMSG: u64 = 47; +pub const SHUTDOWN: u64 = 48; pub const CLONE: u64 = 56; +pub const FORK: u64 = 57; +pub const VFORK: u64 = 58; +pub const EXECVE: u64 = 59; pub const EXIT: u64 = 60; pub const UNAME: u64 = 63; pub const FCNTL: u64 = 72; +pub const FTRUNCATE: u64 = 77; pub const GETCWD: u64 = 79; pub const READLINK: u64 = 89; -pub const GETUID: u64 = 102; -pub const GETGID: u64 = 104; -pub const GETEUID: u64 = 107; -pub const GETEGID: u64 = 108; -pub const SIGALTSTACK: u64 = 131; -pub const ARCH_PRCTL: u64 = 158; -pub const GETTID: u64 = 186; -pub const FUTEX: u64 = 202; -pub const GETDENTS64: u64 = 217; -pub const SET_TID_ADDRESS: u64 = 218; -pub const CLOCK_GETTIME: u64 = 228; -pub const EXIT_GROUP: u64 = 231; -pub const OPENAT: u64 = 257; -pub const NEWFSTATAT: u64 = 262; -pub const SET_ROBUST_LIST: u64 = 273; -pub const PRLIMIT64: u64 = 302; -pub const GETRANDOM: u64 = 318; -pub const RSEQ: u64 = 334; diff --git a/userland/capsule_linux/src/linux/abi/nr_high.rs b/userland/capsule_linux/src/linux/abi/nr_high.rs new file mode 100644 index 000000000..4652e125c --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/nr_high.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 x86_64 syscall numbers from one hundred up. Same contract +//! as `nr`, split only because a file here stays under seventy-five lines. + +pub const GETUID: u64 = 102; +pub const GETGID: u64 = 104; +pub const GETEUID: u64 = 107; +pub const GETEGID: u64 = 108; +pub const SIGALTSTACK: u64 = 131; +pub const ARCH_PRCTL: u64 = 158; +pub const GETTID: u64 = 186; +pub const FUTEX: u64 = 202; +pub const GETDENTS64: u64 = 217; +pub const WAIT4: u64 = 61; +pub const EPOLL_CTL: u64 = 233; +pub const DUP3: u64 = 292; +pub const PIPE2: u64 = 293; +pub const TIMERFD_CREATE: u64 = 283; +pub const TIMERFD_SETTIME: u64 = 286; +pub const EPOLL_CREATE1: u64 = 291; +pub const EPOLL_PWAIT: u64 = 281; +pub const SET_TID_ADDRESS: u64 = 218; +pub const CLOCK_GETTIME: u64 = 228; +pub const EXIT_GROUP: u64 = 231; +pub const OPENAT: u64 = 257; +pub const NEWFSTATAT: u64 = 262; +pub const SET_ROBUST_LIST: u64 = 273; +pub const PRLIMIT64: u64 = 302; +pub const GETRANDOM: u64 = 318; +pub const MEMFD_CREATE: u64 = 319; +pub const RSEQ: u64 = 334; diff --git a/userland/capsule_linux/src/linux/abi/nr_path.rs b/userland/capsule_linux/src/linux/abi/nr_path.rs new file mode 100644 index 000000000..733c624bb --- /dev/null +++ b/userland/capsule_linux/src/linux/abi/nr_path.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Syscall numbers for the path, time and process calls, transcribed from the +//! x86_64 table. + +pub const CHDIR: u64 = 80; +pub const FCHDIR: u64 = 81; +pub const RENAME: u64 = 82; +pub const MKDIR: u64 = 83; +pub const RMDIR: u64 = 84; +pub const UNLINK: u64 = 87; +pub const MKDIRAT: u64 = 258; +pub const UNLINKAT: u64 = 263; +pub const TIME: u64 = 201; +pub const GETTIMEOFDAY: u64 = 96; +pub const NANOSLEEP: u64 = 35; +pub const CLOCK_NANOSLEEP: u64 = 230; +pub const GETPPID: u64 = 110; +pub const SCHED_YIELD: u64 = 24; +pub const FSYNC: u64 = 74; +pub const UMASK: u64 = 95; +pub const SETPGID: u64 = 109; +pub const GETPGRP: u64 = 111; +pub const SETSID: u64 = 112; +pub const GETPGID: u64 = 121; +pub const GETSID: u64 = 124; +pub const SETUID: u64 = 105; +pub const SETGID: u64 = 106; +pub const READV: u64 = 19; +pub const GETRLIMIT: u64 = 97; +pub const SETRLIMIT: u64 = 160; +pub const PRLIMIT64: u64 = 302; +pub const SELECT: u64 = 23; +pub const PSELECT6: u64 = 270; +pub const CHMOD: u64 = 90; +pub const FCHMOD: u64 = 91; +pub const FCHMODAT: u64 = 268; +pub const FACCESSAT: u64 = 269; +pub const FACCESSAT2: u64 = 439; +pub const STATFS: u64 = 137; +pub const FSTATFS: u64 = 138; +pub const STATX: u64 = 332; +pub const KILL: u64 = 62; +pub const TKILL: u64 = 200; +pub const GETRESUID: u64 = 118; +pub const GETRESGID: u64 = 120; +pub const PPOLL: u64 = 271; +pub const EPOLL_WAIT: u64 = 232; diff --git a/userland/capsule_linux/src/linux/attest.rs b/userland/capsule_linux/src/linux/attest.rs new file mode 100644 index 000000000..f25addabc --- /dev/null +++ b/userland/capsule_linux/src/linux/attest.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 . + +//! Proving a program before running it. + +use alloc::vec::Vec; + +use nonos_libc::CapsuleVerifySummary; + +use crate::linux::file::{key, store_read}; + +use super::attest_local; +use super::attest_paths::beside; +use super::attest_publisher; + +/// A certificate, manifest and trailer are small; a refusal to read one +/// of this size is a malformed artifact rather than a large one. +const MAX_ARTIFACT: u32 = 1 << 22; + +pub fn verify(path: &[u8], image: &[u8]) -> Result { + let trailer = fetch(path, b".zk_trailer.bin")?; + let Ok(cert) = fetch(path, b".nonos_id_cert.bin") else { + return attest_local::verify(image, &trailer); + }; + let manifest = fetch(path, b".manifest.bin")?; + attest_publisher::verify(image, &cert, &manifest, &trailer) +} + +/// The proof sits beside the program, inside the same root, so the suffix is +/// appended to the guest-visible path and the whole thing is confined once. +fn fetch(path: &[u8], suffix: &[u8]) -> Result, &'static str> { + let at = beside(path, suffix); + store_read(&key(&at), MAX_ARTIFACT).map_err(|_| "no proof beside the program") +} diff --git a/userland/capsule_linux/src/linux/attest_local.rs b/userland/capsule_linux/src/linux/attest_local.rs new file mode 100644 index 000000000..b6349829e --- /dev/null +++ b/userland/capsule_linux/src/linux/attest_local.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 . + +//! A program the machine installed for itself. + +use nonos_libc::{mk_local_verify, CapsuleVerifySummary}; + +/// A guest is spawned holding nothing, so the proof that admits it is a proof +/// for nothing. +const GUEST_CAPS: u64 = 0; + +pub fn verify(image: &[u8], trailer: &[u8]) -> Result { + if !mk_local_verify(image, GUEST_CAPS, trailer) { + return Err("no publisher, and the machine did not vouch for it"); + } + Ok(CapsuleVerifySummary::zeroed()) +} diff --git a/userland/capsule_linux/src/linux/attest_paths.rs b/userland/capsule_linux/src/linux/attest_paths.rs new file mode 100644 index 000000000..453826238 --- /dev/null +++ b/userland/capsule_linux/src/linux/attest_paths.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 . + + +//! Where a program's proof lives: beside it, under its own name. + +use alloc::vec::Vec; + +pub fn beside(path: &[u8], suffix: &[u8]) -> Vec { + let mut out = Vec::with_capacity(path.len() + suffix.len()); + out.extend_from_slice(path); + out.extend_from_slice(suffix); + out +} diff --git a/userland/capsule_linux/src/linux/attest_publisher.rs b/userland/capsule_linux/src/linux/attest_publisher.rs new file mode 100644 index 000000000..a94bf24b0 --- /dev/null +++ b/userland/capsule_linux/src/linux/attest_publisher.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 program with a publisher behind it, checked exactly as a capsule is. + +use nonos_libc::{mk_capsule_verify, CapsuleVerifyRequest, CapsuleVerifySummary}; + +pub fn verify( + image: &[u8], + cert: &[u8], + manifest: &[u8], + trailer: &[u8], +) -> Result { + let req = CapsuleVerifyRequest { + elf_ptr: image.as_ptr() as u64, + cert_ptr: cert.as_ptr() as u64, + manifest_ptr: manifest.as_ptr() as u64, + trailer_ptr: trailer.as_ptr() as u64, + elf_len: image.len() as u32, + cert_len: cert.len() as u32, + manifest_len: manifest.len() as u32, + trailer_len: trailer.len() as u32, + }; + let mut out = CapsuleVerifySummary::zeroed(); + match mk_capsule_verify(&req, &mut out) { + 0 => Ok(out), + -13 => Err("rejected by verification"), + -22 => Err("malformed artifact or manifest"), + _ => Err("verification could not run"), + } +} diff --git a/userland/capsule_linux/src/linux/call/console.rs b/userland/capsule_linux/src/linux/call/console.rs new file mode 100644 index 000000000..f70884604 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/console.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 . + + +//! A guest's console output, carried to the host's log. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// Cap on one transfer, matching the kernel's own peer-copy ceiling. +const MAX_IO: u64 = 1 << 20; + +/// A guest's console output, carried to the host's log. The bytes are the +/// guest's and are never interpreted, only forwarded. +pub(super) fn console(guest: &Guest, buf: u64, len: u64) -> u64 { + if len == 0 { + return errno::ok(0); + } + let take = len.min(MAX_IO); + let Some(bytes) = guest.read(buf, take as usize) else { + return errno::fail(errno::EFAULT); + }; + let _ = nonos_libc::mk_debug(bytes.as_ptr(), bytes.len()); + errno::ok(take) +} + diff --git a/userland/capsule_linux/src/linux/call/ctl.rs b/userland/capsule_linux/src/linux/call/ctl.rs index e5ef9b2ba..fc5fbc668 100644 --- a/userland/capsule_linux/src/linux/call/ctl.rs +++ b/userland/capsule_linux/src/linux/call/ctl.rs @@ -14,14 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! `ioctl` and `fcntl`. -//! -//! There is no terminal behind any descriptor here, so every ioctl is -//! refused with ENOTTY. That is not a gap: a libc asks TCGETS to find out -//! whether stdout is a terminal, and ENOTTY is the true answer. Claiming -//! otherwise would put the program into line buffering on something that -//! is not a line. use crate::linux::abi::errno; use crate::linux::guest::Guest; @@ -39,24 +32,33 @@ pub fn ioctl(guest: &Guest, fd: u64, _request: u64) -> u64 { } } -pub fn fcntl(guest: &Guest, fd: u64, cmd: u64) -> u64 { - let open = matches!(guest.fds.get(fd as usize), Some(e) if e.is_open()); - if !open { +/// The only descriptor flag there is. +const FD_CLOEXEC: u64 = 1; + +pub fn fcntl(guest: &mut Guest, fd: u64, cmd: u64, arg: u64) -> u64 { + let Some(entry) = guest.fds.get_mut(fd as usize).filter(|e| e.is_open()) else { return errno::fail(errno::EBADF); - } + }; match cmd { /* - * Nothing here is ever handed to an exec, so the close-on-exec - * flag is genuinely clear and setting it changes nothing. The - * status flags are reported as the read-write the descriptor - * already has, and a request to change them is accepted because - * none of the flags a program sets here has an effect. + * A shell sets close-on-exec on the descriptors it keeps for itself, + * then execs, and expects the command not to see them. + */ + F_GETFD => errno::ok(u64::from(entry.cloexec)), + F_SETFD => { + entry.cloexec = arg & FD_CLOEXEC != 0; + errno::ok(0) + } + /* + * Reported as the read-write the descriptor already has; a request to + * change them is accepted because none of the flags a program sets + * here has an effect. */ - F_GETFD | F_SETFD | F_SETFL => errno::ok(0), + F_SETFL => errno::ok(0), F_GETFL => errno::ok(2), /* - * Duplication needs a second handle on the server, which the - * store does not offer yet. Refused rather than aliased. + * Duplication needs a second handle on the server, which the store + * does not offer yet. */ F_DUPFD => errno::fail(errno::ENOSYS), _ => errno::fail(errno::EINVAL), diff --git a/userland/capsule_linux/src/linux/call/cwd.rs b/userland/capsule_linux/src/linux/call/cwd.rs new file mode 100644 index 000000000..90a55a008 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/cwd.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Moving the working directory. + +use crate::linux::abi::errno; +use crate::linux::file::{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 = visible(&guest.cwd, &name); + // Checked before it is taken. + match look(&at) { + Some(_) => { + guest.cwd = at; + errno::ok(0) + } + None => errno::fail(errno::ENOENT), + } +} + +/// `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); + }; + guest.cwd = entry.path.clone(); + 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. +pub fn getcwd(guest: &Guest, buf: u64, len: u64) -> u64 { + let mut out = guest.cwd.clone(); + out.push(0); + if (len as usize) < out.len() { + return errno::fail(errno::ERANGE); + } + match guest.write(buf, &out) { + n if n < 0 => errno::fail(errno::EFAULT), + _ => errno::ok(out.len() as u64), + } +} diff --git a/userland/capsule_linux/src/linux/call/ident.rs b/userland/capsule_linux/src/linux/call/ident.rs new file mode 100644 index 000000000..eca7a107b --- /dev/null +++ b/userland/capsule_linux/src/linux/call/ident.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 . +//! Who the guest is, and which process group it belongs to. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// The identity every guest runs as. +const GUEST_UID: u64 = 0; + +pub fn getppid(guest: &Guest) -> u64 { + // The personality is the parent of every guest it hosts. + errno::ok(u64::from(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. +pub fn setuid(want: u64) -> u64 { + match want { + GUEST_UID => errno::ok(0), + _ => errno::fail(errno::EPERM), + } +} diff --git a/userland/capsule_linux/src/linux/call/io.rs b/userland/capsule_linux/src/linux/call/io.rs index c046eb9fa..c49c170d6 100644 --- a/userland/capsule_linux/src/linux/call/io.rs +++ b/userland/capsule_linux/src/linux/call/io.rs @@ -14,51 +14,51 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! `read`, `write` and `close`, routed by what the descriptor is. A -//! guest's descriptor is an index into a table this capsule owns, so the -//! number it passes can only ever reach something this capsule gave it. +//! `read`, `write` and `close`, routed by what the descriptor is. use crate::linux::abi::errno; use crate::linux::file; -use crate::linux::guest::{Guest, Kind}; +use crate::linux::net; + +use super::console::console; -/// Cap on one transfer, matching the kernel's own peer-copy ceiling. -const MAX_IO: u64 = 1 << 20; +/// Port 53 on the loopback address, in network order. What a program +/// that wrote to its nameserver without naming one was talking to. +const LOOPBACK_53: [u8; 6] = [0, 53, 127, 0, 0, 1]; + +use super::io_socket::{socket_read, socket_write}; +use crate::linux::guest::{Guest, Kind}; pub fn write(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { match guest.fds.get(fd as usize).map(|f| &f.kind) { Some(Kind::Stdout) | Some(Kind::Stderr) => console(guest, buf, len), Some(Kind::File) => file::write(guest, fd, buf, len), + Some(Kind::Socket) => socket_write(guest, fd, buf, len), + Some(Kind::Unix) => crate::linux::unix::send(guest, fd, buf, len), + Some(Kind::Pipe) => super::pipe_write(guest, fd, buf, len), + Some(Kind::Resolver) => net::dns::query(guest, fd, buf, len, LOOPBACK_53), Some(Kind::Dir) => errno::fail(errno::EISDIR), _ => errno::fail(errno::EBADF), } } - -/// A guest's console output, carried to the host's log. The bytes are the -/// guest's and are never interpreted, only forwarded. -fn console(guest: &Guest, buf: u64, len: u64) -> u64 { - if len == 0 { - return errno::ok(0); - } - let take = len.min(MAX_IO); - let Some(bytes) = guest.read(buf, take as usize) else { - return errno::fail(errno::EFAULT); - }; - let _ = nonos_libc::mk_debug(bytes.as_ptr(), bytes.len()); - errno::ok(take) -} - pub fn read(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { match guest.fds.get(fd as usize).map(|f| &f.kind) { // Nothing is typed at a guest yet, and end of file is the truth. Some(Kind::Stdin) => errno::ok(0), Some(Kind::File) => file::read(guest, fd, buf, len), + Some(Kind::Timer) => file::timerfd_read(guest, fd, buf), + Some(Kind::Socket) => socket_read(guest, fd, buf, len), + Some(Kind::Unix) => crate::linux::unix::recv(guest, fd, buf, len), + Some(Kind::Pipe) => super::pipe_read(guest, fd, buf, len), + Some(Kind::Resolver) => net::dns::answer_out(guest, fd, buf, len).0, Some(Kind::Dir) => errno::fail(errno::EISDIR), _ => errno::fail(errno::EBADF), } } pub fn close(guest: &mut Guest, fd: u64) -> u64 { + if let Some(h) = guest.socket_handle(fd) { + net::close(h); + } file::close(guest, fd) } diff --git a/userland/capsule_linux/src/linux/call/io_socket.rs b/userland/capsule_linux/src/linux/call/io_socket.rs new file mode 100644 index 000000000..e55b9ebf7 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/io_socket.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 . + + +//! Reads and writes that land on a socket rather than a file. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::net; + +pub(super) fn socket_write(guest: &Guest, fd: u64, buf: u64, len: u64) -> u64 { + match guest.socket_handle(fd) { + Some(h) => net::send(guest, h, buf, len), + None => errno::fail(errno::EBADF), + } +} + +pub(super) fn socket_read(guest: &Guest, fd: u64, buf: u64, len: u64) -> u64 { + match guest.socket_handle(fd) { + Some(h) => net::recv(guest, h, buf, len), + None => errno::fail(errno::EBADF), + } +} + diff --git a/userland/capsule_linux/src/linux/call/limits.rs b/userland/capsule_linux/src/linux/call/limits.rs new file mode 100644 index 000000000..e0525a8e9 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/limits.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 limits this personality actually enforces. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::limits_table::{limit_for, RLIMIT}; + +pub fn getrlimit(guest: &Guest, resource: u64, out: u64) -> u64 { + let Some((soft, hard)) = limit_for(resource) else { + return errno::fail(errno::EINVAL); + }; + let mut buf = [0u8; RLIMIT]; + buf[..8].copy_from_slice(&soft.to_le_bytes()); + buf[8..].copy_from_slice(&hard.to_le_bytes()); + match guest.write(out, &buf) { + n if n < 0 => errno::fail(errno::EFAULT), + _ => errno::ok(0), + } +} + +/// `prlimit64` reads and writes in one call. +pub fn prlimit64(guest: &Guest, resource: u64, new: u64, old: u64) -> u64 { + if new != 0 { + return errno::fail(errno::EPERM); + } + if old == 0 { + return errno::ok(0); + } + getrlimit(guest, resource, old) +} diff --git a/userland/capsule_linux/src/linux/call/limits_table.rs b/userland/capsule_linux/src/linux/call/limits_table.rs new file mode 100644 index 000000000..cc1785184 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/limits_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 . + +//! 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. +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. +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. +const ADDRESS_SPACE: u64 = 0x0000_7FFF_F000; + +/// Unlimited, as Linux spells it. +const INFINITY: u64 = u64::MAX; + +pub(super) 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)), + RLIMIT_AS => Some((ADDRESS_SPACE, ADDRESS_SPACE)), + /* + * Everything else this personality does not bound at all, and saying + * so is truthful: there is no ceiling to report. + */ + _ => Some((INFINITY, INFINITY)), + } +} + diff --git a/userland/capsule_linux/src/linux/call/map.rs b/userland/capsule_linux/src/linux/call/mem/map.rs similarity index 68% rename from userland/capsule_linux/src/linux/call/map.rs rename to userland/capsule_linux/src/linux/call/mem/map.rs index f0b800f88..68c0fb11e 100644 --- a/userland/capsule_linux/src/linux/call/map.rs +++ b/userland/capsule_linux/src/linux/call/mem/map.rs @@ -14,15 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! `mmap`: anonymous pages, or a private mapping of a file. use crate::linux::abi::errno; -use crate::linux::guest::{page_up, Guest}; +use crate::linux::guest::{span_within, Guest, MMAP_LIMIT, STACK_TOP}; +use super::map_anon::{anonymous, memfd}; use super::map_file::file; use super::map_req::MapReq; -use super::prot::{wx_refused, PROT_EXEC, PROT_WRITE}; +use super::prot::wx_refused; const MAP_SHARED: u64 = 0x01; const MAP_ANONYMOUS: u64 = 0x20; @@ -34,31 +34,26 @@ pub fn mmap(guest: &mut Guest, req: MapReq) -> u64 { if wx_refused(req.prot) { return errno::fail(errno::EPERM); } - let span = page_up(req.len); - let at = req.fixed().unwrap_or(guest.mmap_next); + // The ceiling differs by who chose the address. + let (at, limit) = match req.fixed() { + Some(addr) => (addr, STACK_TOP), + None => (guest.mmap_next, MMAP_LIMIT), + }; + let Some((at, span)) = span_within(at, req.len, limit) else { + return errno::fail(errno::ENOMEM); + }; if req.flags & MAP_ANONYMOUS != 0 { return anonymous(guest, &req, at, span); } + if crate::linux::file::is_memfd(guest, req.fd) { + return memfd(guest, &req, at, span); + } if req.flags & MAP_SHARED != 0 { /* * Sharing a file between processes needs frames that two address - * spaces both point at, which no peer call offers. Refused rather - * than quietly downgraded to a private copy, which would lose a - * writer's changes with nothing to show that it had. + * spaces both point at, which no peer call offers. */ return errno::fail(errno::ENOSYS); } file(guest, &req, at, span) } - -fn anonymous(guest: &mut Guest, req: &MapReq, at: u64, span: u64) -> u64 { - let write = req.prot & PROT_WRITE != 0; - let exec = req.prot & PROT_EXEC != 0; - if guest.map(at, span, write, exec) < 0 { - return errno::fail(errno::ENOMEM); - } - if req.fixed().is_none() { - guest.mmap_next += span; - } - errno::ok(at) -} diff --git a/userland/capsule_linux/src/linux/call/mem/map_anon.rs b/userland/capsule_linux/src/linux/call/mem/map_anon.rs new file mode 100644 index 000000000..00410f806 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/mem/map_anon.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 . + +//! Mappings with no file behind them. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::map_req::MapReq; +use super::prot::{PROT_EXEC, PROT_WRITE}; + +/// A memfd has nothing to read in: it is pages, and the client is about to +/// draw into them. +pub fn memfd(guest: &mut Guest, req: &MapReq, at: u64, span: u64) -> u64 { + let out = anonymous(guest, req, at, span); + if (out as i64) < 0 { + return out; + } + crate::linux::file::set_mapped(guest, req.fd, at); + /* + * A descriptor this capsule staged content on, the keymap being the one + * that matters, has to arrive with those bytes already in it: the client + * maps it and reads it without ever issuing a read. + */ + if let Some(bytes) = crate::linux::file::staged(guest, req.fd) { + if guest.write(at, &bytes) < bytes.len() as i64 { + return errno::fail(errno::EFAULT); + } + } + out +} + +pub fn anonymous(guest: &mut Guest, req: &MapReq, at: u64, span: u64) -> u64 { + let write = req.prot & PROT_WRITE != 0; + let exec = req.prot & PROT_EXEC != 0; + if guest.map(at, span, write, exec) < 0 { + return errno::fail(errno::ENOMEM); + } + if req.fixed().is_none() { + guest.mmap_next += span; + } + errno::ok(at) +} diff --git a/userland/capsule_linux/src/linux/call/mem/map_exec.rs b/userland/capsule_linux/src/linux/call/mem/map_exec.rs new file mode 100644 index 000000000..fe62d05d6 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/mem/map_exec.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 . + +//! Proving a file before any of its pages become executable. + +use crate::linux::file::{key, store_read}; +use crate::linux::guest::{Guest, Kind}; + +/// The same ceiling the exec path reads an image under. +const MAX_IMAGE: u32 = 64 << 20; + +/// Whether `fd` names a file this machine has agreed to execute. +pub fn proven(guest: &Guest, fd: u64) -> bool { + let Some(entry) = guest.fds.get(fd as usize).filter(|f| f.kind == Kind::File) else { + return false; + }; + /* + * A descriptor's path was normalised when it was opened, so it + * needs no resolving here, only confining. + */ + let at = &entry.path; + let Ok(bytes) = store_read(&key(at), MAX_IMAGE) else { + return false; + }; + crate::linux::attest::verify(at, &bytes).is_ok() +} diff --git a/userland/capsule_linux/src/linux/call/map_file.rs b/userland/capsule_linux/src/linux/call/mem/map_file.rs similarity index 56% rename from userland/capsule_linux/src/linux/call/map_file.rs rename to userland/capsule_linux/src/linux/call/mem/map_file.rs index 0d16549dd..1bfd0302d 100644 --- a/userland/capsule_linux/src/linux/call/map_file.rs +++ b/userland/capsule_linux/src/linux/call/mem/map_file.rs @@ -14,28 +14,45 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! A private file mapping. -//! -//! The pages are filled here rather than paged in on first touch: they -//! are mapped writable, the bytes are read into them, and the protection -//! the caller asked for is set afterwards. A dynamic linker needs exactly -//! that order, and the cost is honest, which is that the whole span is -//! read at once rather than on demand. use crate::linux::abi::errno; use crate::linux::file::pread64; use crate::linux::guest::Guest; +use super::map_exec::proven; use super::map_req::MapReq; -use super::prot::protect_span; +use super::prot::PROT_EXEC; +use super::prot_span::protect_span; pub fn file(guest: &mut Guest, req: &MapReq, at: u64, span: u64) -> u64 { + /* + * Asked before a page is allocated, not after the bytes are in place: a + * mapping that would be refused should cost the guest nothing, and a + * half-filled span left behind by a late refusal is memory the guest still + * holds and did not ask to keep. + */ + if req.prot & PROT_EXEC != 0 && !proven(guest, req.fd) { + return errno::fail(errno::EPERM); + } if guest.map(at, span, true, false) < 0 { return errno::fail(errno::ENOMEM); } - if (pread64(guest, req.fd, at, req.len, req.off) as i64) < 0 { - return errno::fail(errno::EACCES); + let mut done = 0u64; + while done < req.len { + let n = pread64(guest, req.fd, at + done, req.len - done, req.off + done) as i64; + if n < 0 { + return errno::fail(errno::EACCES); + } + if n == 0 { + /* + * Short of the requested span: the rest of the mapping is the + * zeroes the fresh frames already hold, which is what a segment's + * bss is. + */ + break; + } + done += n as u64; } if protect_span(guest, at, span, req.prot) < 0 { return errno::fail(errno::EACCES); diff --git a/userland/capsule_linux/src/linux/call/map_req.rs b/userland/capsule_linux/src/linux/call/mem/map_req.rs similarity index 87% rename from userland/capsule_linux/src/linux/call/map_req.rs rename to userland/capsule_linux/src/linux/call/mem/map_req.rs index 25f2cbe2a..c426dcb11 100644 --- a/userland/capsule_linux/src/linux/call/map_req.rs +++ b/userland/capsule_linux/src/linux/call/mem/map_req.rs @@ -16,10 +16,6 @@ //! What a guest asked `mmap` for, in one value. -//! -//! The Linux call takes six arguments and passing them onward one at a -//! time makes every function that touches them wider than it should be. -//! They travel together because they are one request. pub struct MapReq { pub addr: u64, diff --git a/userland/capsule_linux/src/linux/call/memory.rs b/userland/capsule_linux/src/linux/call/mem/memory.rs similarity index 67% rename from userland/capsule_linux/src/linux/call/memory.rs rename to userland/capsule_linux/src/linux/call/mem/memory.rs index cfb424583..28a2cfb6a 100644 --- a/userland/capsule_linux/src/linux/call/memory.rs +++ b/userland/capsule_linux/src/linux/call/mem/memory.rs @@ -14,17 +14,19 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! `brk` and `munmap`. The addresses are chosen here because a -//! Linux program expects a Linux address space, and the kernel only ever -//! maps the pages it is told to. +//! `brk` and `munmap`. use crate::linux::abi::errno; -use crate::linux::guest::{page_up, Guest}; +use crate::linux::guest::{page_up, Guest, BRK_BASE, BRK_LIMIT}; /// `brk(0)` reports the break; any other value moves it and reports where /// it landed, which is Linux's contract and not an error channel. pub fn brk(guest: &mut Guest, want: u64) -> u64 { - if want == 0 || want < crate::linux::guest::BRK_BASE { + /* + * A break outside the heap area is reported as no move, which is what a + * Linux program reads as the request being refused. + */ + if want == 0 || want < BRK_BASE || want > BRK_LIMIT { return errno::ok(guest.brk); } let top = page_up(want); @@ -38,9 +40,13 @@ pub fn brk(guest: &mut Guest, want: u64) -> u64 { errno::ok(guest.brk) } -/// Accepted and remembered as unmapped only in the sense that the guest -/// may map over it again. Returning the pages needs an unmap peer call, -/// which is the next primitive; until then the memory stays the guest's. -pub fn munmap(_guest: &mut Guest, _addr: u64, _len: u64) -> u64 { - errno::ok(0) +/// The pages go back to the kernel and leave the guest's region list. +pub fn munmap(guest: &mut Guest, addr: u64, len: u64) -> u64 { + if len == 0 { + return errno::fail(errno::EINVAL); + } + match guest.unmap(addr, len) { + rc if rc < 0 => errno::fail(errno::EINVAL), + _ => errno::ok(0), + } } diff --git a/userland/capsule_linux/src/linux/call/mem/mod.rs b/userland/capsule_linux/src/linux/call/mem/mod.rs new file mode 100644 index 000000000..9aae9430c --- /dev/null +++ b/userland/capsule_linux/src/linux/call/mem/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 calls that shape a guest's address space: `mmap`, `munmap`, `brk` and +//! `mprotect`. + +mod map; +mod map_anon; +mod map_exec; +mod map_file; +mod map_req; +mod memory; +mod prot; +mod prot_span; + +pub use map::mmap; +pub use map_req::MapReq; +pub use memory::{brk, munmap}; +pub use prot::mprotect; diff --git a/userland/capsule_linux/src/linux/call/prot.rs b/userland/capsule_linux/src/linux/call/mem/prot.rs similarity index 53% rename from userland/capsule_linux/src/linux/call/prot.rs rename to userland/capsule_linux/src/linux/call/mem/prot.rs index 1fbca70f0..5136faba6 100644 --- a/userland/capsule_linux/src/linux/call/prot.rs +++ b/userland/capsule_linux/src/linux/call/mem/prot.rs @@ -14,42 +14,21 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! `mprotect`, and the rule that makes it necessary. -//! -//! NONOS refuses to map a page writable and executable at once, so a -//! loader cannot ask for the end state up front: it writes the pages, -//! then asks for them to be executable. This is the call that makes the -//! second half possible, and it is the reason the kernel gained a -//! protection primitive rather than the loader gaining an exception. - -use nonos_libc::peer::{mk_peer_protect, PEER_PROT_EXEC, PEER_PROT_WRITE}; use crate::linux::abi::errno; -use crate::linux::guest::{page_down, page_up, Guest}; +use crate::linux::guest::{span_within, Guest, STACK_TOP}; + +use super::prot_span::protect_span; pub const PROT_WRITE: u64 = 2; pub const PROT_EXEC: u64 = 4; -/// A request for both at once. Linux allows it and this system does not, -/// so it is refused here with the errno Linux uses for a protection the -/// policy forbids, rather than silently granted as one or the other. +/// A request for both at once. pub fn wx_refused(prot: u64) -> bool { prot & PROT_WRITE != 0 && prot & PROT_EXEC != 0 } -/// Set the protection of a span already mapped in the guest. -pub fn protect_span(guest: &Guest, addr: u64, span: u64, prot: u64) -> i64 { - let mut bits = 0; - if prot & PROT_WRITE != 0 { - bits |= PEER_PROT_WRITE; - } - if prot & PROT_EXEC != 0 { - bits |= PEER_PROT_EXEC; - } - mk_peer_protect(guest.pid, addr, span, bits) -} - pub fn mprotect(guest: &mut Guest, addr: u64, len: u64, prot: u64) -> u64 { if len == 0 { return errno::ok(0); @@ -57,8 +36,13 @@ pub fn mprotect(guest: &mut Guest, addr: u64, len: u64, prot: u64) -> u64 { if wx_refused(prot) { return errno::fail(errno::EPERM); } - let start = page_down(addr); - let span = page_up(addr + len) - start; + /* + * Checked, because `len` is the guest's: `addr + len` wraps and the + * span computed from the wrapped value comes out enormous. + */ + let Some((start, span)) = span_within(addr, len, STACK_TOP) else { + return errno::fail(errno::EINVAL); + }; if protect_span(guest, start, span, prot) < 0 { return errno::fail(errno::EACCES); } diff --git a/userland/capsule_linux/src/linux/call/mem/prot_span.rs b/userland/capsule_linux/src/linux/call/mem/prot_span.rs new file mode 100644 index 000000000..aac1b7043 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/mem/prot_span.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 . + +//! Reprotecting a span, a peer call at a time. + +use nonos_libc::peer::{mk_peer_protect, PEER_PROT_EXEC, PEER_PROT_WRITE}; + +use crate::linux::guest::{Guest, MAX_SPAN}; + +use super::prot::{PROT_EXEC, PROT_WRITE}; + +/// Set the protection of a span already mapped in the guest. +pub fn protect_span(guest: &Guest, addr: u64, span: u64, prot: u64) -> i64 { + let mut bits = 0; + if prot & PROT_WRITE != 0 { + bits |= PEER_PROT_WRITE; + } + if prot & PROT_EXEC != 0 { + bits |= PEER_PROT_EXEC; + } + let mut done = 0; + while done < span { + let take = (span - done).min(MAX_SPAN); + let rc = mk_peer_protect(guest.pid, addr + done, take, bits); + if rc < 0 { + return rc; + } + done += take; + } + 0 +} diff --git a/userland/capsule_linux/src/linux/call/mod.rs b/userland/capsule_linux/src/linux/call/mod.rs index 385a49baa..23894c0e0 100644 --- a/userland/capsule_linux/src/linux/call/mod.rs +++ b/userland/capsule_linux/src/linux/call/mod.rs @@ -14,33 +14,56 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! One file per family of Linux calls. A family gets its own file the -//! moment it has more than a handful, so the table stays readable as the -//! surface grows toward the whole of it. +//! One file per family of Linux calls; declarations and re-exports only. -mod io; -mod life; -mod clone; +mod console; mod ctl; -mod map; -mod map_file; -mod map_req; -mod memory; +mod cwd; mod futex; -mod prot; +mod ident; +mod io; +mod io_socket; +mod life; +mod limits; +mod limits_table; +mod mem; +mod pipe; +mod pipe_dup; +mod pipe_end; +mod pipe_io; +mod pipe_read; +mod session; +mod signal; +mod signal_send; +mod sleep; +mod spawn; mod thread; +mod timeops; +mod umask; mod uname; mod vector; +mod vector_read; -pub use io::{close, read, write}; -pub use clone::clone; pub use ctl::{fcntl, ioctl}; +pub use cwd::{chdir, fchdir, getcwd}; pub use futex::futex; +pub use ident::{getppid, setuid}; +pub use io::{close, read, write}; pub use life::{exit, exit_thread}; -pub use map::mmap; -pub use map_req::MapReq; -pub use memory::{brk, munmap}; -pub use prot::mprotect; +pub use limits::{getrlimit, prlimit64}; +pub use mem::{brk, mmap, mprotect, munmap, MapReq}; +pub use pipe::pipe2; +pub use pipe_dup::{dup, dup2}; +pub use pipe_io::write as pipe_write; +pub use pipe_read::read as pipe_read; +pub use session::{getpgid, getsid, setpgid, setsid}; +pub use signal::{rt_sigaction, rt_sigprocmask, sigaltstack}; +pub use signal_send::kill; +pub use sleep::nanosleep; +pub use spawn::{clone, execve, fork, wait4}; pub use thread::{arch_prctl, clock_gettime, getrandom}; +pub use timeops::{gettimeofday, time}; +pub use umask::{umask, DEFAULT_UMASK}; pub use uname::uname; pub use vector::writev; +pub use vector_read::readv; diff --git a/userland/capsule_linux/src/linux/call/pipe.rs b/userland/capsule_linux/src/linux/call/pipe.rs new file mode 100644 index 000000000..61a1debe7 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/pipe.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 . + +//! `pipe2`. + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use crate::linux::file::flags::O_CLOEXEC; +use crate::linux::file::install; + +pub fn pipe2(guest: &mut Guest, out: u64, flags: u64) -> u64 { + let buffer = guest.pipes.len() as u32; + guest.pipes.push(Vec::new()); + let Some(read_end) = install(guest, Fd::pipe(buffer, false)) else { + return errno::fail(errno::EMFILE); + }; + let Some(write_end) = install(guest, Fd::pipe(buffer, true)) else { + return errno::fail(errno::EMFILE); + }; + if flags & O_CLOEXEC != 0 { + for end in [read_end, write_end] { + if let Some(fd) = guest.fds.get_mut(end as usize) { + fd.cloexec = true; + } + } + } + let mut pair = [0u8; 8]; + pair[..4].copy_from_slice(&(read_end as u32).to_le_bytes()); + pair[4..].copy_from_slice(&(write_end as u32).to_le_bytes()); + if guest.write(out, &pair) < 8 { + return errno::fail(errno::EFAULT); + } + errno::ok(0) +} + diff --git a/userland/capsule_linux/src/linux/call/pipe_dup.rs b/userland/capsule_linux/src/linux/call/pipe_dup.rs new file mode 100644 index 000000000..ca2273a1d --- /dev/null +++ b/userland/capsule_linux/src/linux/call/pipe_dup.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 . + +//! `dup` and `dup2`: a second descriptor onto the same thing. + +use crate::linux::abi::errno; +use crate::linux::file::install; +use crate::linux::guest::{Fd, Guest, Kind}; + +/// `dup2` puts the copy at a number the caller chose, which is how a +/// shell wires a pipe onto stdout before it runs a command. +pub fn dup2(guest: &mut Guest, from: u64, to: u64) -> u64 { + let Some(source) = guest.fds.get(from as usize).filter(|f| f.is_open()).map(Fd::clone_of) + else { + return errno::fail(errno::EBADF); + }; + if from == to { + return errno::ok(to); + } + while guest.fds.len() <= to as usize { + guest.fds.push(Fd::empty(Kind::Free)); + } + guest.fds[to as usize] = source; + errno::ok(to) +} + +pub fn dup(guest: &mut Guest, from: u64) -> u64 { + let Some(source) = guest.fds.get(from as usize).filter(|f| f.is_open()).map(Fd::clone_of) + else { + return errno::fail(errno::EBADF); + }; + match install(guest, source) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} diff --git a/userland/capsule_linux/src/linux/call/pipe_end.rs b/userland/capsule_linux/src/linux/call/pipe_end.rs new file mode 100644 index 000000000..f1980ded6 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/pipe_end.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 . + +//! Which buffer a pipe descriptor names, and which end of it it is. + +use crate::linux::guest::{Guest, Kind}; + +pub fn end_of(guest: &Guest, fd: u64) -> Option<(usize, bool)> { + let entry = guest.fds.get(fd as usize)?; + if entry.kind != Kind::Pipe { + return None; + } + let slot = entry.handle as usize; + match slot < guest.pipes.len() { + true => Some((slot, entry.writable)), + false => None, + } +} diff --git a/userland/capsule_linux/src/linux/call/pipe_io.rs b/userland/capsule_linux/src/linux/call/pipe_io.rs new file mode 100644 index 000000000..896ff0d4e --- /dev/null +++ b/userland/capsule_linux/src/linux/call/pipe_io.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 . + +//! Bytes through a pipe. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::pipe_end::end_of; + +/// What one pipe will hold before a writer is told to wait. Linux uses +/// sixty-four kilobytes and programs are written around that number. +const CAPACITY: usize = 64 << 10; + +pub fn write(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { + let Some((slot, writable)) = end_of(guest, fd) else { + return errno::fail(errno::EBADF); + }; + if !writable { + return errno::fail(errno::EBADF); + } + let room = CAPACITY.saturating_sub(guest.pipes[slot].len()); + if room == 0 { + // A full pipe blocks on Linux until a reader drains it. + return errno::fail(errno::EAGAIN); + } + let take = (len as usize).min(room); + let Some(bytes) = guest.read(buf, take) else { + return errno::fail(errno::EFAULT); + }; + guest.pipes[slot].extend_from_slice(&bytes); + errno::ok(take as u64) +} diff --git a/userland/capsule_linux/src/linux/call/pipe_read.rs b/userland/capsule_linux/src/linux/call/pipe_read.rs new file mode 100644 index 000000000..fc588e415 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/pipe_read.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 . + + +//! Draining a pipe. + + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::pipe_end::end_of; + +pub fn read(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { + let Some((slot, writable)) = end_of(guest, fd) else { + return errno::fail(errno::EBADF); + }; + if writable { + return errno::fail(errno::EBADF); + } + let have = guest.pipes[slot].len(); + if have == 0 { + return errno::fail(errno::EAGAIN); + } + let take = (len as usize).min(have); + let bytes: alloc::vec::Vec = guest.pipes[slot].drain(..take).collect(); + if guest.write(buf, &bytes) < take as i64 { + return errno::fail(errno::EFAULT); + } + errno::ok(take as u64) +} diff --git a/userland/capsule_linux/src/linux/call/session.rs b/userland/capsule_linux/src/linux/call/session.rs new file mode 100644 index 000000000..89b99af50 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/session.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 . + +//! Process groups and sessions. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +pub fn setpgid(guest: &mut Guest, pid: u64, pgid: u64) -> u64 { + let target = match pid { + 0 => guest.pid, + n => n as u32, + }; + if !guest.owns(target) { + return errno::fail(errno::ESRCH); + } + guest.pgid = match pgid { + 0 => target, + n => n as u32, + }; + errno::ok(0) +} + +pub fn getpgid(guest: &Guest) -> u64 { + errno::ok(u64::from(guest.pgid)) +} + +/// `setsid` makes the caller a session leader, which means a new group with +/// its own id. +pub fn setsid(guest: &mut Guest) -> u64 { + if guest.sid == guest.pid { + return errno::fail(errno::EPERM); + } + guest.sid = guest.pid; + guest.pgid = guest.pid; + errno::ok(u64::from(guest.sid)) +} + +pub fn getsid(guest: &Guest) -> u64 { + errno::ok(u64::from(guest.sid)) +} diff --git a/userland/capsule_linux/src/linux/call/signal.rs b/userland/capsule_linux/src/linux/call/signal.rs new file mode 100644 index 000000000..9ad517d36 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/signal.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 . + + +//! Signal dispositions, recorded and never delivered. +//! +//! Delivery means pushing a frame onto a guest thread's stack and +//! redirecting it, which needs the guest's register state, and the trap +//! mechanism hands out a frame but no way to rewrite one. So the +//! handlers a program installs are remembered and nothing is ever +//! raised. That is a real limit and it is recorded here rather than +//! hidden behind a success: a program whose correctness depends on +//! SIGALRM firing will hang, not misbehave quietly. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// Linux refuses to let these two be caught, and so does this. +const SIGKILL: u64 = 9; +const SIGSTOP: u64 = 19; + +/// The largest signal number Linux defines. +const NSIG: u64 = 64; + +pub fn rt_sigaction(guest: &mut Guest, signum: u64, act: u64, old: u64) -> u64 { + if signum == 0 || signum > NSIG || signum == SIGKILL || signum == SIGSTOP { + return errno::fail(errno::EINVAL); + } + if old != 0 && guest.write(old, &[0u8; SIGACTION_LEN]) < SIGACTION_LEN as i64 { + return errno::fail(errno::EFAULT); + } + if act != 0 { + guest.handlers[signum as usize - 1] = true; + } + errno::ok(0) +} + +/// `struct sigaction` on x86_64: handler, flags, restorer, mask. +const SIGACTION_LEN: usize = 32; + +/// The mask is recorded nowhere because nothing is ever raised against +/// it. Reporting an empty old mask is true: no signal is pending. +pub fn rt_sigprocmask(guest: &Guest, old: u64) -> u64 { + if old != 0 && guest.write(old, &[0u8; 8]) < 8 { + return errno::fail(errno::EFAULT); + } + errno::ok(0) +} + +/// An alternate stack for a handler that will never run. +pub fn sigaltstack(guest: &Guest, old: u64) -> u64 { + if old != 0 && guest.write(old, &[0u8; 24]) < 24 { + return errno::fail(errno::EFAULT); + } + errno::ok(0) +} diff --git a/userland/capsule_linux/src/linux/call/signal_send.rs b/userland/capsule_linux/src/linux/call/signal_send.rs new file mode 100644 index 000000000..e023636db --- /dev/null +++ b/userland/capsule_linux/src/linux/call/signal_send.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 . + +//! `kill` and `tkill`, for the guest's own threads and children. + +use nonos_libc::mk_kill; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// The only signals the kernel can carry. Anything else is accepted as +/// a request it cannot honour rather than silently dropped. +const SIGKILL: u64 = 9; +const SIGTERM: u64 = 15; + +pub fn kill(guest: &mut Guest, pid: u64, signo: u64) -> u64 { + let target = pid as u32; + /* + * A guest may signal itself, its threads and its children, and nothing + * else. + */ + if !guest.owns(target) && !guest.children.contains(&target) { + return errno::fail(errno::ESRCH); + } + if signo == 0 { + return errno::ok(0); + } + if signo != SIGKILL && signo != SIGTERM { + return errno::fail(errno::EINVAL); + } + /* + * A thread that was parked in a futex has to be let out before it + * can be collected; the reply is the wake. + */ + guest.waits.retain(|(w, _)| *w != target); + guest.threads.retain(|t| *t != target); + match mk_kill(target as u64, signo) { + n if n < 0 => errno::fail(errno::EPERM), + _ => errno::ok(0), + } +} diff --git a/userland/capsule_linux/src/linux/call/sleep.rs b/userland/capsule_linux/src/linux/call/sleep.rs new file mode 100644 index 000000000..1d29ecab3 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/sleep.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 . + +//! Waiting. + +use nonos_libc::{mk_uptime_ms, mk_yield}; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// `timespec` is two 64-bit words: seconds then nanoseconds. +const PAIR: usize = 16; + +/// `nanosleep`: yield until the deadline passes. +pub fn nanosleep(guest: &Guest, req: u64) -> u64 { + let Some(spec) = guest.read(req, PAIR) else { + return errno::fail(errno::EFAULT); + }; + let secs = u64::from_le_bytes(spec[..8].try_into().unwrap_or([0; 8])); + let nanos = u64::from_le_bytes(spec[8..16].try_into().unwrap_or([0; 8])); + let until = uptime().saturating_add(secs * 1000 + nanos / 1_000_000); + while uptime() < until { + mk_yield(); + } + errno::ok(0) +} + +fn uptime() -> u64 { + u64::try_from(mk_uptime_ms()).unwrap_or(0) +} diff --git a/userland/capsule_linux/src/linux/call/spawn/clone.rs b/userland/capsule_linux/src/linux/call/spawn/clone.rs new file mode 100644 index 000000000..3ecdb0cae --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/clone.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 . + +//! `clone`, for threads only. + +use nonos_libc::{mk_foreign_thread, ForeignFrame}; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::serve::Answer; + +const CLONE_VM: u64 = 0x100; +const CLONE_THREAD: u64 = 0x10000; + +/// musl's `__clone` resumes the child at the instruction after its own +/// `syscall`, with rax zero and rsp pointing at the function and argument it +/// pushed. +pub fn clone(guest: &mut Guest, frame: &ForeignFrame) -> Answer { + let a = frame.args(); + let (flags, stack, tls) = (a[0], a[1], a[4]); + if flags & (CLONE_VM | CLONE_THREAD) != CLONE_VM | CLONE_THREAD { + /* + * A new process, not a thread. That is fork, and fork needs an + * address space copy no peer call offers. + */ + return Answer::value(errno::fail(errno::ENOSYS)); + } + if frame.rip == 0 { + /* + * The kernel is not yet passing the guest's return address, so there + * is nowhere correct to start the child. + */ + return Answer::value(errno::fail(errno::ENOSYS)); + } + if stack == 0 { + return Answer::value(errno::fail(errno::EINVAL)); + } + let tid = mk_foreign_thread(guest.pid, frame.rip, stack, tls); + if tid < 0 { + return Answer::value(errno::fail(errno::ENOMEM)); + } + guest.threads.push(tid as u32); + Answer::value(errno::ok(tid as u64)) +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec.rs b/userland/capsule_linux/src/linux/call/spawn/exec.rs new file mode 100644 index 000000000..e45cb5311 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec.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 . + +//! `execve`: the same process, a different program. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::serve::Answer; + +use super::exec_args::vector; +use super::exec_clear::clear; +use super::exec_load::load_over; +use super::exec_resolve::resolve; + +pub fn execve(guest: &mut Guest, pid: u32, path: u64, argv: u64, envp: u64) -> Answer { + let Some(name) = crate::linux::file::read_path(guest, path) else { + return Answer::value(errno::fail(errno::EFAULT)); + }; + /* + * argv and envp live in the memory that is about to be unmapped, so they + * are copied out here and not one step later. + */ + let (Some(args), Some(env)) = (vector(guest, argv), vector(guest, envp)) else { + return Answer::value(errno::fail(errno::EFAULT)); + }; + /* + * Found, followed through any `#!` line, and proved at every step, all + * while the caller still has an address space to be told no in. + */ + let program = match resolve(&guest.cwd, &name, &args) { + Ok(p) => p, + Err(e) => return Answer::value(e), + }; + super::exec_threads::reap(guest, pid); + clear(guest); + match load_over(guest, pid, &program, &env) { + Some(()) => Answer::Park, + None => Answer::value(errno::fail(errno::ENOEXEC)), + } +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_args.rs b/userland/capsule_linux/src/linux/call/spawn/exec_args.rs new file mode 100644 index 000000000..46237997b --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_args.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Reading a guest's argv or envp. + +use alloc::vec::Vec; + +use crate::linux::file::read_cstr; +use crate::linux::guest::{Guest, STACK_SIZE}; + +/// Enough for any real command line. A caller handing over more than +/// this is not going to be helped by us trying. +const MAX_ENTRIES: usize = 4096; + +/// Linux's own ceiling on one argument, thirty-two pages. +const MAX_ARG: usize = 32 * 4096; + +/// The whole vector, a quarter of the stack the guest wakes on. +const MAX_TOTAL: usize = STACK_SIZE as usize / 4; + +pub fn vector(guest: &Guest, mut array: u64) -> Option>> { + let mut out = Vec::new(); + if array == 0 { + return Some(out); + } + let mut total = 0usize; + while out.len() < MAX_ENTRIES { + let slot = guest.read(array, 8)?; + let ptr = u64::from_le_bytes(slot.as_slice().try_into().ok()?); + if ptr == 0 { + return Some(out); + } + let arg = read_cstr(guest, ptr, MAX_ARG)?; + /* + * Each string costs its bytes and the terminator the stack + * block will need for it. + */ + total = total.checked_add(arg.len() + 1)?; + if total > MAX_TOTAL { + return None; + } + out.push(arg); + array = array.checked_add(8)?; + } + Some(out) +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_clear.rs b/userland/capsule_linux/src/linux/call/spawn/exec_clear.rs new file mode 100644 index 000000000..f8565bb32 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_clear.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 . + +//! Emptying a guest's address space. + +use nonos_libc::peer::mk_peer_unmap; + +use crate::linux::guest::{Guest, MAX_SPAN}; + +/// Every span this capsule gave the guest, taken back. +pub fn clear(guest: &mut Guest) { + for span in core::mem::take(&mut guest.regions) { + let mut done = 0; + /* + * One peer call per megabyte: the kernel refuses a longer span rather + * than sitting in a loop with the page tables locked. + */ + while done < span.len { + let take = (span.len - done).min(MAX_SPAN); + let _ = mk_peer_unmap(guest.pid, span.at + done, take); + done += take; + } + } +} + +/// Every descriptor the guest marked close-on-exec. +pub fn shed(guest: &mut Guest) { + for fd in 0..guest.fds.len() as u64 { + if guest.fds.get(fd as usize).is_some_and(|f| f.cloexec && f.is_open()) { + let _ = crate::linux::file::close(guest, fd); + } + } +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_load.rs b/userland/capsule_linux/src/linux/call/spawn/exec_load.rs new file mode 100644 index 000000000..11df1e292 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_load.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 . + +//! Putting the replacing image into a guest that has just been emptied. + +use alloc::vec::Vec; + +use nonos_libc::mk_foreign_exec; + +use super::exec_resolve::Program; +use crate::linux::guest::{Guest, STACK_SIZE, STACK_TOP}; +use crate::linux::image; + +/// The stack top a guest wakes on. The same one a fresh guest gets, +/// because after exec it is a fresh guest in every way but its pid. + +pub fn load_over(guest: &mut Guest, pid: u32, program: &Program, envp: &[Vec]) -> Option<()> { + let (loaded, entry, interp_base) = image::program(guest, &program.bytes).ok()?; + guest.map(STACK_TOP - STACK_SIZE, STACK_SIZE, true, false); + /* + * A program invoked with no argv still gets one entry: argv[0] is what a C + * runtime prints in its own error messages, and a shell that execs without + * it reports failures against an empty name. + */ + let args = match program.argv.is_empty() { + true => alloc::vec![program.path.clone()], + false => program.argv.clone(), + }; + let rsp = image::build(guest, STACK_TOP, &loaded, interp_base, &args, envp)?; + /* + * The descriptor table survives, which is what makes a shell's redirection + * work: it wires the pipe, then executes. + */ + super::exec_clear::shed(guest); + reset(guest); + /* + * Not a start: the caller is parked inside the `execve` it made and is + * already runnable. + */ + match mk_foreign_exec(pid, entry, rsp) { + n if n < 0 => None, + _ => Some(()), + } +} + +/// State that belongs to the program rather than the process. +fn reset(guest: &mut Guest) { + guest.brk = crate::linux::guest::BRK_BASE; + guest.mmap_next = crate::linux::guest::MMAP_BASE; + guest.fs_base = 0; +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_resolve.rs b/userland/capsule_linux/src/linux/call/spawn/exec_resolve.rs new file mode 100644 index 000000000..3c782a18e --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_resolve.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 . + +//! Which image actually runs, once `#!` has had its say. + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::file::{key, store_read, visible}; + +use super::exec_shebang::parse; + +/// The same ceiling the first image is read under. +pub const MAX_IMAGE: u32 = 64 << 20; + +/// Linux's own limit on how deep `#!` may point at another script. +const MAX_DEPTH: usize = 4; + +pub struct Program { + pub path: Vec, + pub bytes: Vec, + pub argv: Vec>, +} + +pub fn resolve(cwd: &[u8], name: &[u8], argv: &[Vec]) -> Result { + let mut path = visible(cwd, name); + let mut args = argv.to_vec(); + for _ in 0..MAX_DEPTH { + let Ok(bytes) = store_read(&key(&path), MAX_IMAGE) else { + return Err(errno::fail(errno::ENOENT)); + }; + if crate::linux::attest::verify(&path, &bytes).is_err() { + return Err(errno::fail(errno::EPERM)); + } + let Some(interp) = parse(&bytes) else { + return Ok(Program { path, bytes, argv: args }); + }; + args = rewrite(&interp, &path, &args); + path = visible(cwd, &interp.path); + } + Err(errno::fail(errno::ELOOP)) +} + +/// The interpreter, its one optional argument, the script, then whatever the +/// caller passed after argv[0]. +fn rewrite(interp: &super::exec_shebang::Interp, script: &[u8], args: &[Vec]) -> Vec> { + let mut out = alloc::vec![interp.path.clone()]; + out.extend(interp.arg.clone()); + out.push(script.to_vec()); + out.extend(args.iter().skip(1).cloned()); + out +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_shebang.rs b/userland/capsule_linux/src/linux/call/spawn/exec_shebang.rs new file mode 100644 index 000000000..7263ac2f7 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_shebang.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 `#!` line. + +use alloc::vec::Vec; + +/// Linux's BINPRM_BUF_SIZE. A line longer than this is truncated at the +/// limit rather than refused, which is also what Linux does. +const MAX_LINE: usize = 127; + +pub struct Interp { + pub path: Vec, + pub arg: Option>, +} + +pub fn parse(image: &[u8]) -> Option { + let head = image.get(..MAX_LINE.min(image.len()))?; + let body = head.strip_prefix(b"#!")?; + let line = &body[..body.iter().position(|b| *b == b'\n').unwrap_or(body.len())]; + let line = trim(line); + let cut = line.iter().position(|b| *b == b' ' || *b == b'\t').unwrap_or(line.len()); + let path = &line[..cut]; + if path.is_empty() { + return None; + } + let rest = trim(&line[cut..]); + Some(Interp { + path: path.to_vec(), + arg: (!rest.is_empty()).then(|| rest.to_vec()), + }) +} + +fn trim(s: &[u8]) -> &[u8] { + let start = s.iter().position(|b| *b != b' ' && *b != b'\t').unwrap_or(s.len()); + let end = s.iter().rposition(|b| *b != b' ' && *b != b'\t' && *b != b'\r'); + match end { + Some(e) => &s[start..=e.max(start)], + None => &s[..0], + } +} diff --git a/userland/capsule_linux/src/linux/call/spawn/exec_threads.rs b/userland/capsule_linux/src/linux/call/spawn/exec_threads.rs new file mode 100644 index 000000000..c2a423772 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/exec_threads.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! What happens to the other threads when one of them calls `execve`. + +use nonos_libc::{mk_foreign_reply, mk_kill}; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +const SIGKILL: u64 = 9; + +pub fn reap(guest: &mut Guest, caller: u32) { + for tid in core::mem::take(&mut guest.threads) { + if tid == caller { + guest.threads.push(tid); + continue; + } + /* + * A thread parked in a futex has to be let out of the kernel before it + * can be ended: the kill marks it, but nothing collects a thread that + * is still waiting for an answer. + */ + guest.waits.retain(|(w, _)| *w != tid); + let _ = mk_foreign_reply(tid, errno::fail(errno::EINTR)); + let _ = mk_kill(tid as u64, SIGKILL); + } +} diff --git a/userland/capsule_linux/src/linux/call/spawn/fork.rs b/userland/capsule_linux/src/linux/call/spawn/fork.rs new file mode 100644 index 000000000..e2b8efeaa --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/fork.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 . + +//! `fork`. + +use nonos_libc::{mk_foreign_fork, mk_foreign_resume}; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::serve::Answer; + +use super::fork_copy::copy_spans; + +pub fn fork(guest: &mut Guest) -> Answer { + let child = mk_foreign_fork(guest.pid); + if child < 0 { + return Answer::value(errno::fail(errno::ENOMEM)); + } + let child = child as u32; + if !copy_spans(guest, child) { + return Answer::value(errno::fail(errno::ENOMEM)); + } + if mk_foreign_resume(child) < 0 { + return Answer::value(errno::fail(errno::ENOMEM)); + } + guest.children.push(child); + Answer::value(errno::ok(child as u64)) +} diff --git a/userland/capsule_linux/src/linux/call/spawn/fork_copy.rs b/userland/capsule_linux/src/linux/call/spawn/fork_copy.rs new file mode 100644 index 000000000..5a165b451 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/fork_copy.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 . + + +//! Copying a parent's spans into the child it just made. + +use crate::linux::guest::{Guest, Region}; +use nonos_libc::peer::{mk_peer_map, mk_peer_write, PEER_PROT_EXEC, PEER_PROT_WRITE}; + +/// Every span, mapped into the child and then filled from the parent. +pub(super) fn copy_spans(guest: &mut Guest, child: u32) -> bool { + let spans = guest.regions.clone(); + for span in spans { + if mk_peer_map(child, span.at, span.len, prot_of(&span)) < 0 { + return false; + } + if !copy_one(guest, child, span.at, span.len) { + return false; + } + } + true +} + +fn prot_of(span: &Region) -> u64 { + let mut prot = 0; + if span.write { + prot |= PEER_PROT_WRITE; + } + if span.exec { + prot |= PEER_PROT_EXEC; + } + prot +} + +fn copy_one(guest: &Guest, child: u32, at: u64, len: u64) -> bool { + let Some(bytes) = guest.read(at, len as usize) else { + return false; + }; + mk_peer_write(child, at, &bytes) >= 0 +} diff --git a/userland/capsule_linux/src/linux/call/spawn/mod.rs b/userland/capsule_linux/src/linux/call/spawn/mod.rs new file mode 100644 index 000000000..7946a83e8 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/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 . + +//! Making and replacing processes: clone, fork, exec, wait. + +mod clone; +mod exec; +mod exec_args; +mod exec_clear; +mod exec_load; +mod exec_resolve; +mod exec_shebang; +mod exec_threads; +mod fork; +mod fork_copy; +mod wait; + +pub use clone::clone; +pub use exec::execve; +pub use fork::fork; +pub use wait::wait4; diff --git a/userland/capsule_linux/src/linux/call/spawn/wait.rs b/userland/capsule_linux/src/linux/call/spawn/wait.rs new file mode 100644 index 000000000..7b87d4480 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/spawn/wait.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 . + +//! `wait4`: which of this guest's children has ended. + +use nonos_libc::mk_pid_alive; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::serve::Answer; + +/// Set by a caller that will not wait. +const WNOHANG: u64 = 1; + +pub fn wait4(guest: &mut Guest, want: u64, status: u64, flags: u64) -> Answer { + if guest.children.is_empty() { + return Answer::value(errno::fail(errno::ECHILD)); + } + let gone = guest.children.iter().copied().find(|pid| { + (want as i64) <= 0 || want as u32 == *pid + }).filter(|pid| !mk_pid_alive(*pid)); + let Some(pid) = gone else { + let _ = flags & WNOHANG; + return Answer::value(errno::fail(errno::EAGAIN)); + }; + guest.children.retain(|p| *p != pid); + /* + * The exit code a guest passed to exit is not readable from here: the + * kernel records it and nothing hands it back. + */ + if status != 0 && guest.write(status, &0u32.to_le_bytes()) < 4 { + return Answer::value(errno::fail(errno::EFAULT)); + } + Answer::value(errno::ok(pid as u64)) +} diff --git a/userland/capsule_linux/src/linux/call/thread.rs b/userland/capsule_linux/src/linux/call/thread.rs index 150c40404..dd74708a3 100644 --- a/userland/capsule_linux/src/linux/call/thread.rs +++ b/userland/capsule_linux/src/linux/call/thread.rs @@ -23,12 +23,14 @@ use crate::linux::guest::Guest; const ARCH_SET_FS: u64 = 0x1002; const ARCH_GET_FS: u64 = 0x1003; -/// A guest setting `fs` is setting its thread pointer. It is recorded here -/// and applied when the guest next runs; a guest cannot set a segment base -/// itself because it holds no capability that would let it. -pub fn arch_prctl(guest: &mut Guest, code: u64, addr: u64) -> u64 { +/// The thread pointer belongs to the calling thread, not the process, so the +/// tid is the one that traps and not the guest's own pid. +pub fn arch_prctl(guest: &mut Guest, tid: u32, code: u64, addr: u64) -> u64 { match code { ARCH_SET_FS => { + if nonos_libc::peer::mk_peer_tls(tid, addr) < 0 { + return errno::fail(errno::EPERM); + } guest.fs_base = addr; errno::ok(0) } @@ -40,10 +42,7 @@ pub fn arch_prctl(guest: &mut Guest, code: u64, addr: u64) -> u64 { } } -/// Seconds and nanoseconds, from the host's own monotonic millisecond -/// clock. Every clock a guest can name reads from the one clock this -/// system has, which is honest and is not the same as pretending to have -/// several. +/// Seconds and nanoseconds, from the host's own monotonic millisecond clock. pub fn clock_gettime(guest: &mut Guest, _clock: u64, out: u64) -> u64 { let ms = nonos_libc::mk_uptime_ms().max(0) as u64; let mut buf = [0u8; 16]; diff --git a/userland/capsule_linux/src/linux/call/timeops.rs b/userland/capsule_linux/src/linux/call/timeops.rs new file mode 100644 index 000000000..d35c4f46c --- /dev/null +++ b/userland/capsule_linux/src/linux/call/timeops.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 clock, as a Linux program asks for it. + +use nonos_libc::mk_time_millis; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// `timespec` and `timeval` are both two 64-bit words; they differ only +/// in whether the second is nanoseconds or microseconds. +const PAIR: usize = 16; + +fn now_ms() -> u64 { + u64::try_from(mk_time_millis()).unwrap_or(0) +} + +/// `time`: whole seconds since the epoch, returned and optionally stored. +pub fn time(guest: &Guest, out: u64) -> u64 { + let secs = now_ms() / 1000; + if out != 0 && guest.write(out, &secs.to_le_bytes()) < 0 { + return errno::fail(errno::EFAULT); + } + errno::ok(secs) +} + +pub fn gettimeofday(guest: &Guest, tv: u64) -> u64 { + if tv == 0 { + return errno::ok(0); + } + let ms = now_ms(); + let mut buf = [0u8; PAIR]; + buf[..8].copy_from_slice(&(ms / 1000).to_le_bytes()); + buf[8..].copy_from_slice(&((ms % 1000) * 1000).to_le_bytes()); + match guest.write(tv, &buf) { + n if n < 0 => errno::fail(errno::EFAULT), + _ => errno::ok(0), + } +} + diff --git a/userland/capsule_linux/src/linux/call/umask.rs b/userland/capsule_linux/src/linux/call/umask.rs new file mode 100644 index 000000000..5494a1247 --- /dev/null +++ b/userland/capsule_linux/src/linux/call/umask.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 file-creation mask. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// What a shell starts with, and what Linux gives a fresh process. +pub const DEFAULT_UMASK: u16 = 0o022; + +pub fn umask(guest: &mut Guest, want: u64) -> u64 { + let previous = guest.umask; + /* + * Only the nine permission bits are a mask; the rest are not the + * caller's to set and Linux discards them too. + */ + guest.umask = (want as u16) & 0o777; + errno::ok(u64::from(previous)) +} diff --git a/userland/capsule_linux/src/linux/call/vector_read.rs b/userland/capsule_linux/src/linux/call/vector_read.rs new file mode 100644 index 000000000..8b97f907d --- /dev/null +++ b/userland/capsule_linux/src/linux/call/vector_read.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 . +//! `readv`, the reading half of the scatter-gather pair. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// One `struct iovec`: a pointer and a length, both eight bytes. +const IOVEC: usize = 16; +/// Linux refuses a longer vector, so a guest cannot ask this capsule to +/// walk an unbounded list. +const IOV_MAX: u64 = 1024; + +pub fn readv(guest: &mut Guest, fd: u64, iov: u64, count: u64) -> u64 { + if count > IOV_MAX { + return errno::fail(errno::EINVAL); + } + let Some(table) = guest.read(iov, count as usize * IOVEC) else { + return errno::fail(errno::EFAULT); + }; + let mut got = 0u64; + for i in 0..count as usize { + let at = i * IOVEC; + let (base, len) = (word(&table, at), word(&table, at + 8)); + if len == 0 { + continue; + } + let result = super::io::read(guest, fd, base, len); + if (result as i64) < 0 { + /* + * A failure after a partial read is that partial count: the bytes + * already in the guest's buffers are real and a caller told + * otherwise would read them twice. + */ + return if got == 0 { result } else { errno::ok(got) }; + } + got += result; + // A short read ends the vector. + if result < len { + break; + } + } + errno::ok(got) +} + +fn word(bytes: &[u8], at: usize) -> u64 { + match bytes.get(at..at + 8).and_then(|s| s.try_into().ok()) { + Some(eight) => u64::from_le_bytes(eight), + None => 0, + } +} diff --git a/userland/capsule_linux/src/linux/env.rs b/userland/capsule_linux/src/linux/env.rs new file mode 100644 index 000000000..1face4d2e --- /dev/null +++ b/userland/capsule_linux/src/linux/env.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 environment the first program of a guest starts with. + +use alloc::vec::Vec; + +/// Deliberately short. +pub fn default() -> Vec> { + alloc::vec![ + b"PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin".to_vec(), + b"HOME=/root".to_vec(), + b"TERM=linux".to_vec(), + b"PWD=/".to_vec(), + b"SHELL=/bin/sh".to_vec(), + b"LANG=C.UTF-8".to_vec(), + ] +} diff --git a/userland/capsule_linux/src/linux/file/at.rs b/userland/capsule_linux/src/linux/file/at.rs new file mode 100644 index 000000000..81cc736c2 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/at.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 . + +//! A `dirfd` and a path, resolved to one absolute name. + +use alloc::vec::Vec; + +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)?; + if name.first() == Some(&b'/') { + return Some(visible(b"/", &name)); + } + let base = base_of(guest, dirfd)?; + Some(visible(&base, &name)) +} + +fn base_of(guest: &Guest, dirfd: u64) -> Option> { + if dirfd == AT_FDCWD { + return Some(guest.cwd.clone()); + } + match guest.fds.get(dirfd as usize) { + Some(fd) if fd.kind == Kind::Dir => Some(fd.path.clone()), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/file/close.rs b/userland/capsule_linux/src/linux/file/close.rs index 28968be97..50fb92284 100644 --- a/userland/capsule_linux/src/linux/file/close.rs +++ b/userland/capsule_linux/src/linux/file/close.rs @@ -14,12 +14,8 @@ // 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. -use nonos_app_skeleton::clients::vfs::write_file; -use nonos_libc::mk_getpid; - use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest, Kind}; @@ -31,9 +27,8 @@ pub fn close(guest: &mut Guest, fd: u64) -> u64 { return errno::fail(errno::EBADF); } /* - * The store handle is dropped with the descriptor, which closes it on - * the server. A failed flush is reported here because close is the - * last chance a program has to learn that its output never landed. + * The store handle is dropped with the descriptor, which closes it on the + * server. */ let flushed = flush(entry); *entry = Fd::empty(Kind::Free); @@ -43,9 +38,10 @@ pub fn close(guest: &mut Guest, fd: u64) -> u64 { } } -fn flush(entry: &Fd) -> bool { +/// Write a descriptor's buffered bytes out. +pub(super) fn flush(entry: &Fd) -> bool { if entry.kind != Kind::File || !entry.writable { return true; } - write_file(mk_getpid() as u32, &entry.path, &entry.pending).is_ok() + super::store::write(&super::resolve::key(&entry.path), &entry.pending).is_ok() } diff --git a/userland/capsule_linux/src/linux/file/cstr.rs b/userland/capsule_linux/src/linux/file/cstr.rs new file mode 100644 index 000000000..61dc6e6d0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/cstr.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 NUL-terminated string out of a guest, a page at a time. + +use alloc::vec::Vec; + +use crate::linux::guest::{page_down, Guest, PAGE}; + +/// Bytes up to the terminator, or nothing when the string is not +/// terminated inside `max` or reaches memory the guest does not hold. +pub fn read_cstr(guest: &Guest, addr: u64, max: usize) -> Option> { + if addr == 0 { + return None; + } + let mut out: Vec = Vec::new(); + let mut at = addr; + while out.len() <= max { + let page_end = page_down(at).checked_add(PAGE)?; + let room = (max + 1 - out.len()) as u64; + let take = (page_end.saturating_sub(at)).min(room); + let chunk = guest.read(at, take as usize)?; + if let Some(i) = chunk.iter().position(|b| *b == 0) { + out.extend_from_slice(&chunk[..i]); + return Some(out); + } + out.extend_from_slice(&chunk); + at = page_end; + } + None +} diff --git a/userland/capsule_linux/src/linux/file/dir.rs b/userland/capsule_linux/src/linux/file/dir.rs index 335e954c2..9dfe5053c 100644 --- a/userland/capsule_linux/src/linux/file/dir.rs +++ b/userland/capsule_linux/src/linux/file/dir.rs @@ -14,25 +14,24 @@ // 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. use alloc::string::String; use alloc::vec::Vec; -use nonos_app_skeleton::clients::vfs::list_paths; - use crate::linux::abi::errno; use crate::linux::guest::{Fd, Guest}; -use super::slot; +use super::{resolve, slot, store}; -pub fn open(guest: &mut Guest, owner: u32, path: Vec) -> u64 { - let Ok(keys) = list_paths(owner, &path) else { +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); }; - let names = children(&path, keys); + // Cut against the store key, not against the path the guest named. + let names = children(at.as_bytes(), keys); match slot::install(guest, Fd::dir(path, names)) { Some(n) => errno::ok(n), None => errno::fail(errno::EMFILE), @@ -41,8 +40,8 @@ pub fn open(guest: &mut Guest, owner: u32, path: Vec) -> u64 { // OP_LIST returns whole keys at any depth. Cut at the first separator // past the prefix and dedupe, or every file below shows up as a sibling. -fn children(path: &[u8], keys: Vec) -> Vec { - let cut = if path == b"/" { 1 } else { path.len() + 1 }; +fn children(at: &[u8], keys: Vec) -> Vec { + let cut = at.len() + 1; let mut out: Vec = Vec::new(); for key in keys { let bytes = key.as_bytes(); diff --git a/userland/capsule_linux/src/linux/file/dirops.rs b/userland/capsule_linux/src/linux/file/dirops.rs new file mode 100644 index 000000000..26eeb6cab --- /dev/null +++ b/userland/capsule_linux/src/linux/file/dirops.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 . + +//! Making, removing and moving names in the store. + +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), + } +} + +pub fn unlinkat(guest: &Guest, dirfd: u64, path: u64, flags: u64) -> u64 { + let Some(at) = resolve_at(guest, dirfd, path) else { + return errno::fail(errno::EFAULT); + }; + /* + * AT_REMOVEDIR turns unlinkat into rmdir, which is how a libc implements + * 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 { + Ok(()) => errno::ok(0), + Err(_) => errno::fail(errno::ENOENT), + } +} diff --git a/userland/capsule_linux/src/linux/file/epoll.rs b/userland/capsule_linux/src/linux/file/epoll.rs new file mode 100644 index 000000000..9e30e4b80 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/epoll.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 . + +//! `epoll_create1` and `epoll_ctl`: the interest list a program keeps. + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::slot::install; + +const EPOLL_CTL_ADD: u64 = 1; +const EPOLL_CTL_DEL: u64 = 2; +const EPOLL_CTL_MOD: u64 = 3; + +/// `struct epoll_event` is packed on x86_64: a u32 of events then a u64 +/// of caller data, twelve bytes and not sixteen. +pub const EVENT_LEN: usize = 12; + +pub fn epoll_create(guest: &mut Guest) -> u64 { + match install(guest, Fd::empty(Kind::Epoll)) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} + +pub fn epoll_ctl(guest: &mut Guest, ep: u64, op: u64, fd: u64, event: u64) -> u64 { + let entry = match op { + EPOLL_CTL_DEL => None, + EPOLL_CTL_ADD | EPOLL_CTL_MOD => match read_event(guest, event) { + Some(pair) => Some(pair), + None => return errno::fail(errno::EFAULT), + }, + _ => return errno::fail(errno::EINVAL), + }; + let Some(list) = guest.fds.get_mut(ep as usize).filter(|f| f.kind == Kind::Epoll) else { + return errno::fail(errno::EBADF); + }; + list.watch.retain(|(f, _, _)| *f != fd); + if let Some((events, data)) = entry { + list.watch.push((fd, events, data)); + } + errno::ok(0) +} + +fn read_event(guest: &Guest, at: u64) -> Option<(u32, u64)> { + let raw = guest.read(at, EVENT_LEN)?; + let events = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); + let mut data = [0u8; 8]; + data.copy_from_slice(&raw[4..12]); + Some((events, u64::from_le_bytes(data))) +} diff --git a/userland/capsule_linux/src/linux/file/epoll_wait.rs b/userland/capsule_linux/src/linux/file/epoll_wait.rs new file mode 100644 index 000000000..0b30b275d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/epoll_wait.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 . + +//! `epoll_wait`: which of the watched descriptors are ready now. + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; +use crate::linux::net::ready; + +use super::epoll::EVENT_LEN; + +pub fn epoll_wait(guest: &mut Guest, ep: u64, out: u64, max: u64) -> u64 { + let Some(list) = guest.fds.get(ep as usize).filter(|f| f.kind == Kind::Epoll) else { + return errno::fail(errno::EBADF); + }; + let watch = list.watch.clone(); + let mut blob: Vec = Vec::new(); + let mut hits = 0u64; + for (fd, wanted, data) in watch { + if hits >= max { + break; + } + let live = u32::from(ready(guest, fd)) & wanted; + if live == 0 { + continue; + } + blob.extend_from_slice(&live.to_le_bytes()); + blob.extend_from_slice(&data.to_le_bytes()); + hits += 1; + } + if blob.is_empty() { + return errno::ok(0); + } + if guest.write(out, &blob) < blob.len() as i64 { + return errno::fail(errno::EFAULT); + } + let _ = EVENT_LEN; + errno::ok(hits) +} diff --git a/userland/capsule_linux/src/linux/file/flags.rs b/userland/capsule_linux/src/linux/file/flags.rs index 1ee5b7e7d..33326c570 100644 --- a/userland/capsule_linux/src/linux/file/flags.rs +++ b/userland/capsule_linux/src/linux/file/flags.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 open flags and the special directory descriptor, as Linux defines //! them on x86_64. Transcribed, never chosen. @@ -24,14 +23,12 @@ pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; 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. pub const AT_FDCWD: u64 = (-100i64) as u64; -/// Set by `newfstatat` when the caller means the link and not its target. -pub const AT_EMPTY_PATH: u64 = 0x1000; - /// 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 diff --git a/userland/capsule_linux/src/linux/file/fsync.rs b/userland/capsule_linux/src/linux/file/fsync.rs new file mode 100644 index 000000000..f5c66c497 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/fsync.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 . +//! Getting a descriptor's buffered bytes onto the store. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +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), + } +} diff --git a/userland/capsule_linux/src/linux/file/memfd.rs b/userland/capsule_linux/src/linux/file/memfd.rs new file mode 100644 index 000000000..36bc7afa0 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/memfd.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 . + +//! `memfd_create` and `ftruncate`. + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::slot::install; + +pub fn is_memfd(guest: &Guest, fd: u64) -> bool { + matches!(guest.fds.get(fd as usize), Some(e) if e.kind == Kind::Memfd) +} + +pub fn memfd_create(guest: &mut Guest) -> u64 { + match install(guest, Fd::memfd()) { + Some(n) => errno::ok(n), + 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/memfd_map.rs b/userland/capsule_linux/src/linux/file/memfd_map.rs new file mode 100644 index 000000000..82e9c64bb --- /dev/null +++ b/userland/capsule_linux/src/linux/file/memfd_map.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 . + +//! Where a memfd landed, and what was staged on it. + +use alloc::vec::Vec; + +use crate::linux::guest::{Guest, Kind}; + +/// Content this capsule put on a descriptor before the guest mapped it. +pub fn staged(guest: &Guest, fd: u64) -> Option> { + match guest.fds.get(fd as usize) { + Some(e) if e.kind == Kind::Memfd && !e.pending.is_empty() => Some(e.pending.clone()), + _ => None, + } +} + +/// Where a mapped memfd lives in the guest, which is what a shm pool +/// needs to find its pixels. +pub fn mapped_at(guest: &Guest, fd: u64) -> Option<(u64, u64)> { + match guest.fds.get(fd as usize) { + Some(e) if e.kind == Kind::Memfd && e.offset != 0 => Some((e.offset, e.size)), + _ => None, + } +} + +/// Record the address a mapping landed on. The offset field carries it, +/// because a memfd has no read position for it to mean anything else. +pub fn set_mapped(guest: &mut Guest, fd: u64, at: u64) { + if let Some(entry) = guest.fds.get_mut(fd as usize) { + if entry.kind == Kind::Memfd { + entry.offset = at; + } + } +} diff --git a/userland/capsule_linux/src/linux/file/meta/mod.rs b/userland/capsule_linux/src/linux/file/meta/mod.rs new file mode 100644 index 000000000..84fa77894 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/mod.rs @@ -0,0 +1,30 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! What the store knows about a name, and what a program may do with it. + +mod perms; +mod query; +pub(super) mod stat; +mod statbuf; +mod statfs; +mod statx; + +pub use perms::{chmod, faccessat, fchmod, fchmodat}; +pub use query::{access, readlink}; +pub use stat::{fstat, look, newfstatat}; +pub use statfs::statfs; +pub use statx::statx; diff --git a/userland/capsule_linux/src/linux/file/meta/perms.rs b/userland/capsule_linux/src/linux/file/meta/perms.rs new file mode 100644 index 000000000..affea08c3 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/perms.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 . + +//! 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}; + +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), + } +} + +/// `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); + }; + 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), + } +} + +/// `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), + } +} + +pub fn chmod(guest: &Guest, path: u64, mode: u64) -> u64 { + fchmodat(guest, AT_FDCWD, path, mode) +} diff --git a/userland/capsule_linux/src/linux/file/query.rs b/userland/capsule_linux/src/linux/file/meta/query.rs similarity index 64% rename from userland/capsule_linux/src/linux/file/query.rs rename to userland/capsule_linux/src/linux/file/meta/query.rs index f74eb2b7e..a88df7275 100644 --- a/userland/capsule_linux/src/linux/file/query.rs +++ b/userland/capsule_linux/src/linux/file/meta/query.rs @@ -14,51 +14,34 @@ // 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 nonos_libc::mk_getpid; - use crate::linux::abi::errno; use crate::linux::guest::Guest; -use super::{path, resolve, stat}; - -pub fn getcwd(guest: &Guest, out: u64, size: u64) -> u64 { - let need = guest.cwd.len() + 1; - if size < need as u64 { - return errno::fail(errno::ERANGE); - } - let mut buf = guest.cwd.clone(); - buf.push(0); - if guest.write(out, &buf) < need as i64 { - return errno::fail(errno::EFAULT); - } - // Linux returns the length including the terminator, not a pointer. - errno::ok(need as u64) -} +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 = resolve::absolute(&guest.cwd, &name); - match stat::look(mk_getpid() as u32, &full) { + let full = resolve::visible(&guest.cwd, &name); + match stat::look(&full) { Some(_) => errno::ok(0), None => errno::fail(errno::ENOENT), } } -/// The store holds no symbolic links, so a path that exists is not one -/// and a path that does not exist is absent. Both are real answers, and -/// neither is the invented target a caller would act on. +/// The store holds no symbolic links, so a path that exists is not one and a +/// path that does not exist is absent. pub fn readlink(guest: &Guest, path_ptr: u64) -> u64 { let Some(name) = path::read_path(guest, path_ptr) else { return errno::fail(errno::EFAULT); }; - let full = resolve::absolute(&guest.cwd, &name); - match stat::look(mk_getpid() as u32, &full) { + let full = resolve::visible(&guest.cwd, &name); + match stat::look(&full) { Some(_) => errno::fail(errno::EINVAL), None => errno::fail(errno::ENOENT), } diff --git a/userland/capsule_linux/src/linux/file/stat.rs b/userland/capsule_linux/src/linux/file/meta/stat.rs similarity index 86% rename from userland/capsule_linux/src/linux/file/stat.rs rename to userland/capsule_linux/src/linux/file/meta/stat.rs index 0ee7e87c4..bc902a52d 100644 --- a/userland/capsule_linux/src/linux/file/stat.rs +++ b/userland/capsule_linux/src/linux/file/meta/stat.rs @@ -14,22 +14,19 @@ // 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 nonos_app_skeleton::clients::vfs::stat_full; -use nonos_libc::mk_getpid; - use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; -use super::flags::AT_FDCWD; +use super::super::flags::AT_FDCWD; +use super::super::{path, resolve, store}; use super::statbuf::{build, STAT_LEN}; -use super::{path, resolve}; -/// Size and whether it is a directory, or nothing when the path is absent. -pub fn look(owner: u32, full: &[u8]) -> Option<(u64, bool)> { - match stat_full(owner, full) { +/// 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)) { Ok((size, is_dir, _, _)) => Some((size, is_dir)), Err(_) => None, } @@ -55,8 +52,8 @@ pub fn newfstatat(guest: &mut Guest, dirfd: u64, path_ptr: u64, out: u64) -> u64 if dirfd != AT_FDCWD { return errno::fail(errno::ENOSYS); } - let full = resolve::absolute(&guest.cwd, &name); - match look(mk_getpid() as u32, &full) { + let full = resolve::visible(&guest.cwd, &name); + match look(&full) { Some((size, is_dir)) => write_out(guest, out, size, is_dir), None => errno::fail(errno::ENOENT), } diff --git a/userland/capsule_linux/src/linux/file/statbuf.rs b/userland/capsule_linux/src/linux/file/meta/statbuf.rs similarity index 83% rename from userland/capsule_linux/src/linux/file/statbuf.rs rename to userland/capsule_linux/src/linux/file/meta/statbuf.rs index b74f843fc..844180c60 100644 --- a/userland/capsule_linux/src/linux/file/statbuf.rs +++ b/userland/capsule_linux/src/linux/file/meta/statbuf.rs @@ -14,14 +14,7 @@ // 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. -//! -//! The layout is Linux's and the field offsets are load-bearing: a libc -//! reads st_mode and st_size straight out of these bytes. Everything the -//! store does not know is left at zero rather than invented, except the -//! link count and the block size, where zero would be read as a broken -//! file rather than as an unknown one. /// Bytes of a `struct stat` on this architecture. pub const STAT_LEN: usize = 144; diff --git a/userland/capsule_linux/src/linux/file/meta/statfs.rs b/userland/capsule_linux/src/linux/file/meta/statfs.rs new file mode 100644 index 000000000..e638ee21a --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statfs.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 . + +//! How much room the store has, in the shape `statfs` expects. + +use nonos_app_skeleton::clients::vfs; +use nonos_libc::mk_getpid; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +/// `struct statfs` on x86_64 is 120 bytes. +const STATFS: usize = 120; + +/// The store addresses bytes, not blocks, so a block size is a fiction either +/// way. +const BSIZE: u64 = 1024; + +pub fn statfs(guest: &Guest, out: u64) -> u64 { + let Ok((_, bytes, max)) = vfs::usage(mk_getpid()) else { + return errno::fail(errno::EIO); + }; + // The vfs reports its ceiling as 32 bits and its usage as 64. + let (used, max) = (bytes, u64::from(max)); + let total = max / BSIZE; + let free = max.saturating_sub(used) / BSIZE; + + let mut buf = [0u8; STATFS]; + put(&mut buf, 0, 0x6E6F6E6F); // f_type, "nono" + put(&mut buf, 8, BSIZE); // f_bsize + put(&mut buf, 16, total); // f_blocks + put(&mut buf, 24, free); // f_bfree + put(&mut buf, 32, free); // f_bavail + put(&mut buf, 56, 255); // f_namelen, the vfs path limit + put(&mut buf, 64, BSIZE); // f_frsize + match guest.write(out, &buf) { + n if n < 0 => errno::fail(errno::EFAULT), + _ => errno::ok(0), + } +} + +fn put(buf: &mut [u8; STATFS], at: usize, v: u64) { + buf[at..at + 8].copy_from_slice(&v.to_le_bytes()); +} diff --git a/userland/capsule_linux/src/linux/file/meta/statx.rs b/userland/capsule_linux/src/linux/file/meta/statx.rs new file mode 100644 index 000000000..e20db264e --- /dev/null +++ b/userland/capsule_linux/src/linux/file/meta/statx.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! `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; + +/// `struct statx` is 256 bytes. +const STATX: usize = 256; + +/// The bits for the fields the store can answer: type, mode, size and +/// mtime. Nothing else is claimed. +const STATX_TYPE: u32 = 0x0001; +const STATX_MODE: u32 = 0x0002; +const STATX_SIZE: u32 = 0x0200; +const STATX_MTIME: u32 = 0x0020; + +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 Ok((size, is_dir, mtime, readonly)) = store::stat_full(&key(&at)) else { + return errno::fail(errno::ENOENT); + }; + 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_SIZE | STATX_MTIME).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[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 + buf[96..104].copy_from_slice(&(mtime / 1000).to_le_bytes()); // stx_mtime.sec + 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/mod.rs b/userland/capsule_linux/src/linux/file/mod.rs index 1e1d33c95..33c21506e 100644 --- a/userland/capsule_linux/src/linux/file/mod.rs +++ b/userland/capsule_linux/src/linux/file/mod.rs @@ -14,37 +14,59 @@ // 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. -//! -//! Every path a guest names is resolved here and reached through the store -//! under this capsule's own identity. The kernel is not involved and holds -//! no filesystem view for a hosted process to inherit. -mod close; +mod at; +pub(super) mod close; +mod cstr; mod dir; mod dirent; mod dirents; -mod file; +mod dirops; +mod epoll; +mod epoll_wait; pub mod flags; +mod fsync; +mod memfd; +mod memfd_map; +mod meta; mod open; mod path; mod pread; -mod query; mod read; +mod regular; +mod rename; mod resolve; +mod root; mod seek; mod slot; -mod stat; -mod statbuf; +mod store; +mod store_name; +mod timerfd; +mod timerfd_read; mod write; pub use close::close; +pub use cstr::read_cstr; pub use dirents::getdents64; +pub use dirops::{mkdirat, rmdir, unlinkat}; +pub use epoll::{epoll_create, epoll_ctl}; +pub use epoll_wait::epoll_wait; +pub use fsync::fsync; +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, readlink, statfs, statx, +}; pub use open::openat; +pub use path::read_path; pub use pread::pread64; -pub use query::{access, getcwd, readlink}; pub use read::read; +pub use rename::rename; +pub use resolve::{key, visible}; pub use seek::lseek; -pub use stat::{fstat, newfstatat}; +pub use slot::{install, MAX_FDS}; +pub use store::{read as store_read, write as store_write}; +pub use timerfd::{timerfd_create, timerfd_settime}; +pub use timerfd_read::read as timerfd_read; pub use write::write; diff --git a/userland/capsule_linux/src/linux/file/open.rs b/userland/capsule_linux/src/linux/file/open.rs index e6c3ded4b..20026d90a 100644 --- a/userland/capsule_linux/src/linux/file/open.rs +++ b/userland/capsule_linux/src/linux/file/open.rs @@ -14,19 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! `openat`. Opens run under this capsule's pid, not the guest's: a guest -//! holds no capabilities and the store would refuse it. +//! `openat`. use alloc::vec::Vec; -use nonos_libc::mk_getpid; - use crate::linux::abi::errno; use crate::linux::guest::{Guest, Kind}; -use super::flags::{wants_write, AT_FDCWD, O_CREAT, O_DIRECTORY}; -use super::{dir, file, path, resolve, stat}; +use super::flags::{wants_write, AT_FDCWD, O_CLOEXEC, O_CREAT, O_DIRECTORY}; +use super::{dir, path, regular, resolve, store}; pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64) -> u64 { let Some(name) = path::read_path(guest, path_ptr) else { @@ -36,14 +32,26 @@ pub fn openat(guest: &mut Guest, dirfd: u64, path_ptr: u64, flags: u64) -> u64 { Ok(base) => base, Err(e) => return e, }; - let full = resolve::absolute(&base, &name); - let owner = mk_getpid() as u32; - match stat::look(owner, &full) { - Some((_, true)) => dir::open(guest, owner, full), + let full = resolve::visible(&base, &name); + let got = match store::stat(&resolve::key(&full)).ok() { + Some((_, true)) => dir::open(guest, full), Some((_, false)) if flags & O_DIRECTORY != 0 => errno::fail(errno::ENOTDIR), - Some((size, false)) => file::open(guest, owner, full, size, flags), - None if flags & O_CREAT != 0 && wants_write(flags) => file::create(guest, full), + Some((size, false)) => regular::open(guest, full, size, flags), + None if flags & O_CREAT != 0 && wants_write(flags) => regular::create(guest, full), None => errno::fail(errno::ENOENT), + }; + mark(guest, got, flags & O_CLOEXEC != 0); + got +} + +/// O_CLOEXEC is a property of the descriptor, not of the open, so it is set +/// once the number is known rather than threaded through every one of the +/// paths above. +fn mark(guest: &mut Guest, got: u64, on: bool) { + if let Some(slot) = errno::slot(got).filter(|_| on) { + if let Some(fd) = guest.fds.get_mut(slot) { + fd.cloexec = true; + } } } diff --git a/userland/capsule_linux/src/linux/file/path.rs b/userland/capsule_linux/src/linux/file/path.rs index 716501fea..4fec7a718 100644 --- a/userland/capsule_linux/src/linux/file/path.rs +++ b/userland/capsule_linux/src/linux/file/path.rs @@ -14,34 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! NUL-terminated path out of a guest, a page at a time: a peer copy that -//! crosses into an unmapped page fails whole and loses the mapped part. +//! A path out of a guest, which is a string under the store's ceiling. use alloc::vec::Vec; -use crate::linux::guest::{page_down, Guest, PAGE}; +use crate::linux::guest::Guest; + +use super::cstr::read_cstr; /// The vfs length prefix is one byte. pub const MAX_PATH: usize = 255; pub fn read_path(guest: &Guest, addr: u64) -> Option> { - if addr == 0 { - return None; - } - let mut out: Vec = Vec::new(); - let mut at = addr; - while out.len() <= MAX_PATH { - let page_end = page_down(at) + PAGE; - let room = (MAX_PATH + 1 - out.len()) as u64; - let take = (page_end - at).min(room); - let chunk = guest.read(at, take as usize)?; - if let Some(i) = chunk.iter().position(|b| *b == 0) { - out.extend_from_slice(&chunk[..i]); - return Some(out); - } - out.extend_from_slice(&chunk); - at = page_end; - } - None + read_cstr(guest, addr, MAX_PATH) } diff --git a/userland/capsule_linux/src/linux/file/regular.rs b/userland/capsule_linux/src/linux/file/regular.rs new file mode 100644 index 000000000..3e0f091af --- /dev/null +++ b/userland/capsule_linux/src/linux/file/regular.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 . + +//! Opening a regular file, and creating one that is not there yet. + +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}; + +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)) { + Ok(s) => Some(s), + Err(_) => return errno::fail(errno::EACCES), + } + } else { + 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), + } +} + +/// 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); + match slot::install(guest, fd) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} diff --git a/userland/capsule_linux/src/linux/file/rename.rs b/userland/capsule_linux/src/linux/file/rename.rs new file mode 100644 index 000000000..489c04301 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/rename.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 . + +//! Moving a name. + +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 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), + } +} diff --git a/userland/capsule_linux/src/linux/file/resolve.rs b/userland/capsule_linux/src/linux/file/resolve.rs index 1d9cd249d..f8d2a71c5 100644 --- a/userland/capsule_linux/src/linux/file/resolve.rs +++ b/userland/capsule_linux/src/linux/file/resolve.rs @@ -14,14 +14,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! Guest path to store key. The store has no cwd and no dot-dot, so both -//! are resolved here. +//! Guest path to store key. use alloc::vec::Vec; -/// Dot-dot past the root stops at the root, as Linux does. -pub fn absolute(cwd: &[u8], path: &[u8]) -> Vec { +use super::root::Key; + +/// The store key for a path the guest named. +pub fn key(visible: &[u8]) -> Key { + Key::under_root(visible) +} + +/// The absolute path as the guest sees it, which is what `getcwd` reports and +/// what a descriptor remembers. +pub fn visible(cwd: &[u8], path: &[u8]) -> Vec { let mut joined: Vec = Vec::new(); if path.first() != Some(&b'/') { joined.extend_from_slice(cwd); diff --git a/userland/capsule_linux/src/linux/file/root.rs b/userland/capsule_linux/src/linux/file/root.rs new file mode 100644 index 000000000..4155b82e7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/root.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 subtree the Linux world lives in, and the only name for it. + +use alloc::vec::Vec; + +/// Where the Linux world is kept. Every path a guest sees is relative +/// to this, and it never appears in anything handed back to a guest. +pub const ROOT: &[u8] = b"/linux"; + +/// A path in the store, already confined. Built only from a normalised +/// guest-visible path, by `resolve::key`. +pub struct Key(Vec); + +impl Key { + /// `visible` must be absolute and free of `.` and `..`, which is what + /// `resolve::visible` guarantees and the only thing that calls this. + pub(super) fn under_root(visible: &[u8]) -> Key { + let mut out = Vec::with_capacity(ROOT.len() + visible.len()); + out.extend_from_slice(ROOT); + /* + * The guest's root is the store's `/linux`, not `/linux/`: a trailing + * separator makes every listing prefix wrong by one byte and every + * child look like a sibling of itself. + */ + if visible != b"/" { + out.extend_from_slice(visible); + } + Key(out) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} diff --git a/userland/capsule_linux/src/linux/file/store.rs b/userland/capsule_linux/src/linux/file/store.rs new file mode 100644 index 000000000..9b215fc3c --- /dev/null +++ b/userland/capsule_linux/src/linux/file/store.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 . + +//! Every store operation this personality makes for a guest. + +use alloc::string::String; +use alloc::vec::Vec; + +use nonos_app_skeleton::clients::vfs::{self, VfsStream}; +use nonos_libc::mk_getpid; + +use super::root::Key; + +type Fail = &'static str; + +pub fn read(at: &Key, max: u32) -> Result, Fail> { + vfs::read_file(mk_getpid(), at.as_bytes(), max) +} + +pub fn write(at: &Key, data: &[u8]) -> Result<(), Fail> { + vfs::write_file(mk_getpid(), at.as_bytes(), data) +} + +pub fn stat(at: &Key) -> Result<(u64, bool), Fail> { + vfs::stat(mk_getpid(), at.as_bytes()) +} + +pub fn stat_full(at: &Key) -> Result<(u64, bool, u64, bool), Fail> { + vfs::stat_full(mk_getpid(), at.as_bytes()) +} + +pub fn list(at: &Key) -> Result, Fail> { + vfs::list_paths(mk_getpid(), at.as_bytes()) +} + +pub fn open(at: &Key) -> Result { + VfsStream::open(mk_getpid(), at.as_bytes()) +} diff --git a/userland/capsule_linux/src/linux/file/store_name.rs b/userland/capsule_linux/src/linux/file/store_name.rs new file mode 100644 index 000000000..d2dbf874d --- /dev/null +++ b/userland/capsule_linux/src/linux/file/store_name.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 . + +//! Store operations that change the namespace rather than content. + +use nonos_app_skeleton::clients::vfs; +use nonos_libc::mk_getpid; + +use super::root::Key; + +type Fail = &'static str; + +pub fn mkdir(at: &Key) -> Result<(), Fail> { + vfs::mkdir(mk_getpid(), at.as_bytes()) +} + +pub fn rmdir(at: &Key) -> Result<(), Fail> { + vfs::rmdir(mk_getpid(), at.as_bytes(), false) +} + +pub fn unlink(at: &Key) -> Result<(), Fail> { + vfs::unlink(mk_getpid(), at.as_bytes()) +} + +pub fn rename(from: &Key, to: &Key) -> Result<(), Fail> { + vfs::rename(mk_getpid(), from.as_bytes(), to.as_bytes()) +} + +pub fn chmod(at: &Key, mode: u16) -> Result<(), Fail> { + vfs::chmod(mk_getpid(), at.as_bytes(), mode) +} diff --git a/userland/capsule_linux/src/linux/file/timerfd.rs b/userland/capsule_linux/src/linux/file/timerfd.rs new file mode 100644 index 000000000..d123a9dc7 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/timerfd.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 . + +//! `timerfd_create`, `timerfd_settime`, and reading one. + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::slot::install; + +/// `struct itimerspec`: interval seconds and nanoseconds, then the +/// value's seconds and nanoseconds. Four eight byte fields. +const ITIMERSPEC_LEN: usize = 32; + +pub fn timerfd_create(guest: &mut Guest) -> u64 { + match install(guest, Fd::empty(Kind::Timer)) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} + +pub fn timerfd_settime(guest: &mut Guest, fd: u64, spec: u64) -> u64 { + let Some(raw) = guest.read(spec, ITIMERSPEC_LEN) else { + return errno::fail(errno::EFAULT); + }; + let secs = u64::from_le_bytes(raw[16..24].try_into().unwrap_or([0; 8])); + let nanos = u64::from_le_bytes(raw[24..32].try_into().unwrap_or([0; 8])); + let delay = secs * 1000 + nanos / 1_000_000; + let now = nonos_libc::mk_uptime_ms().max(0) as u64; + match guest.fds.get_mut(fd as usize).filter(|f| f.kind == Kind::Timer) { + Some(entry) => { + entry.expiry = if delay == 0 { 0 } else { now + delay }; + errno::ok(0) + } + None => errno::fail(errno::EBADF), + } +} diff --git a/userland/capsule_linux/src/linux/file/timerfd_read.rs b/userland/capsule_linux/src/linux/file/timerfd_read.rs new file mode 100644 index 000000000..e267ec1b6 --- /dev/null +++ b/userland/capsule_linux/src/linux/file/timerfd_read.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 . + +//! Reading a timer, and whether it has fired. + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +/// A read reports how many times it has fired, which is one or none. +pub fn read(guest: &mut Guest, fd: u64, buf: u64) -> u64 { + let now = nonos_libc::mk_uptime_ms().max(0) as u64; + let fired = match guest.fds.get(fd as usize) { + Some(e) if e.kind == Kind::Timer => e.expiry != 0 && now >= e.expiry, + _ => return errno::fail(errno::EBADF), + }; + if !fired { + return errno::fail(errno::EAGAIN); + } + if let Some(entry) = guest.fds.get_mut(fd as usize) { + entry.expiry = 0; + } + if guest.write(buf, &1u64.to_le_bytes()) < 8 { + return errno::fail(errno::EFAULT); + } + errno::ok(8) +} diff --git a/userland/capsule_linux/src/linux/guest/fd.rs b/userland/capsule_linux/src/linux/guest/fd.rs index 7fb371037..913bd2f71 100644 --- a/userland/capsule_linux/src/linux/guest/fd.rs +++ b/userland/capsule_linux/src/linux/guest/fd.rs @@ -14,10 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! A guest's file descriptors. A descriptor is a number the guest chose to -//! believe in; what it points at is this personality's business, and is -//! never a NONOS handle the guest could name on its own. +//! A guest's file descriptors. use alloc::string::String; use alloc::vec; @@ -25,19 +22,7 @@ use alloc::vec::Vec; use nonos_app_skeleton::clients::vfs::VfsStream; -#[derive(PartialEq, Eq)] -pub enum Kind { - /// Closed, and reusable. - Free, - /// The guest's own console, carried to the host's output. - Stdin, - Stdout, - Stderr, - /// A file in the store, held open on the server. - File, - /// A directory, listed once when it was opened. - Dir, -} +pub use super::fd_kind::Kind; pub struct Fd { pub kind: Kind, @@ -55,6 +40,19 @@ pub struct Fd { pub names: Vec, /// Set when the guest asked to write, so close knows to flush. pub writable: bool, + /// The net.sockets handle behind a socket descriptor. + pub handle: u32, + /// An epoll interest list: descriptor, events, and the token the + /// program gets back, which is its own and never interpreted. + pub watch: Vec<(u64, u32, u64)>, + /// When a timer next fires, in milliseconds of uptime. + pub expiry: u64, + /// Datagrams waiting to be read, oldest first, each with the address it + /// should appear to come from. + pub replies: Vec<(Vec, [u8; 6])>, + /// Closed by exec rather than carried into the new program. A shell + /// leaves its own descriptors set this way before it runs a command. + pub cloexec: bool, } impl Fd { diff --git a/userland/capsule_linux/src/linux/guest/fd_dup.rs b/userland/capsule_linux/src/linux/guest/fd_dup.rs new file mode 100644 index 000000000..c55a42d80 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/fd_dup.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 pipe end, and a second descriptor onto the same thing. + +use super::fd::Fd; +use super::fd_kind::Kind; + +impl Fd { + pub fn pipe(buffer: u32, writable: bool) -> Fd { + let mut fd = Fd::empty(Kind::Pipe); + fd.handle = buffer; + fd.writable = writable; + fd + } + + /// A second descriptor onto the same thing, which is what dup is. + pub fn clone_of(from: &Fd) -> Fd { + let mut fd = Fd::empty(from.kind); + fd.handle = from.handle; + fd.writable = from.writable; + fd.size = from.size; + fd.offset = from.offset; + fd.path = from.path.clone(); + fd + } +} diff --git a/userland/capsule_linux/src/linux/guest/fd_empty.rs b/userland/capsule_linux/src/linux/guest/fd_empty.rs new file mode 100644 index 000000000..4ecd26927 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/fd_empty.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 descriptor with every field off, which every maker starts from. + +use alloc::vec::Vec; + +use super::fd::Fd; +use super::fd_kind::Kind; + +impl Fd { + /// Everything off. A descriptor always leaves here before a maker + /// sets the fields its kind actually uses. + pub fn empty(kind: Kind) -> Fd { + Fd { + kind, + offset: 0, + size: 0, + path: Vec::new(), + stream: None, + pending: Vec::new(), + names: Vec::new(), + writable: false, + handle: 0, + watch: Vec::new(), + expiry: 0, + replies: Vec::new(), + cloexec: false, + } + } +} diff --git a/userland/capsule_linux/src/linux/guest/fd_kind.rs b/userland/capsule_linux/src/linux/guest/fd_kind.rs new file mode 100644 index 000000000..3ca6048af --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/fd_kind.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 . + +//! What a descriptor points at. + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// Closed, and reusable. + Free, + /// The guest's own console, carried to the host's output. + Stdin, + Stdout, + Stderr, + /// A file in the store, held open on the server. + File, + /// A directory, listed once when it was opened. + Dir, + /// A socket net.sockets issued to this capsule. + Socket, + /// A Unix socket, both ends inside this capsule. + Unix, + /// Anonymous memory with a descriptor, for passing over a socket. + Memfd, + /// An interest list a program waits on. + Epoll, + /// A timer a program reads or waits on. + Timer, + /// One end of a pipe this capsule holds. + Pipe, + /// A datagram socket a program opened to talk to a nameserver. + Resolver, +} diff --git a/userland/capsule_linux/src/linux/guest/fd_make.rs b/userland/capsule_linux/src/linux/guest/fd_make.rs index 8d8c6835d..db6fd6c16 100644 --- a/userland/capsule_linux/src/linux/guest/fd_make.rs +++ b/userland/capsule_linux/src/linux/guest/fd_make.rs @@ -14,10 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - -//! The three ways a descriptor comes into being. Kept apart from the type -//! so the fields a caller must fill are a short list in one place, and a -//! field added later cannot be forgotten at one of the call sites. +//! The three ways a descriptor comes into being. use alloc::string::String; use alloc::vec::Vec; @@ -40,25 +37,28 @@ impl Fd { fd } + pub fn memfd() -> Fd { + Fd::empty(Kind::Memfd) + } + + pub fn unix() -> Fd { + Fd::empty(Kind::Unix) + } + + pub fn resolver() -> Fd { + Fd::empty(Kind::Resolver) + } + + pub fn socket(handle: u32) -> Fd { + let mut fd = Fd::empty(Kind::Socket); + fd.handle = handle; + fd + } + pub fn dir(path: Vec, names: Vec) -> Fd { let mut fd = Fd::empty(Kind::Dir); fd.path = path; fd.names = names; fd } - - /// Everything off. A descriptor always leaves here before a maker - /// sets the fields its kind actually uses. - pub fn empty(kind: Kind) -> Fd { - Fd { - kind, - offset: 0, - size: 0, - path: Vec::new(), - stream: None, - pending: Vec::new(), - names: Vec::new(), - writable: false, - } - } } diff --git a/userland/capsule_linux/src/linux/guest/handle.rs b/userland/capsule_linux/src/linux/guest/handle.rs index 41264da24..62bc60b46 100644 --- a/userland/capsule_linux/src/linux/guest/handle.rs +++ b/userland/capsule_linux/src/linux/guest/handle.rs @@ -20,12 +20,6 @@ use alloc::vec::Vec; use super::fd::Fd; -/// Where a guest's heap and its anonymous mappings start. Both are chosen -/// here rather than by the kernel, because a Linux program expects a Linux -/// address space and this is the only place that knows what that means. -pub const BRK_BASE: u64 = 0x0000_1000_0000; -pub const MMAP_BASE: u64 = 0x0000_2000_0000; - pub struct Guest { pub pid: u32, /// The program break, as `brk` moves it. @@ -33,30 +27,40 @@ pub struct Guest { /// The next address an anonymous mapping gets, growing upward. pub mmap_next: u64, pub fds: Vec, + /// Every span this capsule has backed for the guest, in the order + /// it did so. Fork copies exactly this list. + pub regions: Vec, + /// Pipe buffers, named by index from the descriptors at each end. + pub pipes: Vec>, + /// Children this guest has forked, for wait to report on. + pub children: Vec, /// Tids of this guest's threads, not counting itself. pub threads: Vec, /// Threads parked in a futex wait, with the word they wait on. pub waits: Vec<(u32, u64)>, + /// The display connection, when the guest has opened one. + pub display: crate::linux::unix::Conn, + /// The Wayland objects that connection has created. + pub objects: crate::linux::wayland::Objects, + /// What those objects describe, and the surface it reaches. + pub scene: crate::linux::wayland::Scene, + /// Which signals the guest installed a handler for. Nothing is ever + /// raised against them; see `call::signal`. + pub handlers: [bool; 64], /// What a relative path is relative to. pub cwd: Vec, + /// Names this guest has resolved, each with the address it was given. + pub automap: Vec<(Vec, [u8; 4])>, /// Where the guest last asked its thread pointer to be set. pub fs_base: u64, /// Set once the guest asks to end, so the loop can drop it. pub exited: Option, -} - -impl Guest { - pub fn new(pid: u32) -> Self { - Guest { - pid, - brk: BRK_BASE, - mmap_next: MMAP_BASE, - fds: Fd::standard(), - threads: Vec::new(), - waits: Vec::new(), - cwd: alloc::vec![b'/'], - fs_base: 0, - exited: None, - } - } + /// The personality, which hosts every guest it spawns. + pub parent: u32, + /// Process group and session. + pub pgid: u32, + pub sid: u32, + /// Remembered, not enforced: the store does not apply it when it creates a + /// file. + pub umask: u16, } diff --git a/userland/capsule_linux/src/linux/guest/handle_new.rs b/userland/capsule_linux/src/linux/guest/handle_new.rs new file mode 100644 index 000000000..91602fc32 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/handle_new.rs @@ -0,0 +1,55 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! A fresh guest, before anything has been put in it. + +use alloc::vec::Vec; + +use super::fd::Fd; +use super::handle::Guest; +use super::layout::{BRK_BASE, MMAP_BASE}; + +impl Guest { + pub fn new(pid: u32) -> Self { + Guest { + pid, + brk: BRK_BASE, + mmap_next: MMAP_BASE, + fds: Fd::standard(), + regions: Vec::new(), + pipes: Vec::new(), + children: Vec::new(), + threads: Vec::new(), + waits: Vec::new(), + handlers: [false; 64], + display: Default::default(), + objects: Default::default(), + scene: Default::default(), + cwd: alloc::vec![b'/'], + automap: Vec::new(), + fs_base: 0, + exited: None, + /* + * The personality hosts it, and a fresh guest leads its own group + * and session until something says otherwise. + */ + parent: nonos_libc::mk_getpid(), + pgid: pid, + sid: pid, + umask: crate::linux::call::DEFAULT_UMASK, + } + } +} diff --git a/userland/capsule_linux/src/linux/guest/layout.rs b/userland/capsule_linux/src/linux/guest/layout.rs new file mode 100644 index 000000000..20a536f5e --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/layout.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 . + +//! Where things sit in a guest's address space. + +/// The heap, growing up from here as `brk` moves. +pub const BRK_BASE: u64 = 0x0000_1000_0000; + +/// Anonymous and file mappings, growing up from here. +pub const MMAP_BASE: u64 = 0x0000_2000_0000; + +/// Where the loader biases a position-independent executable. +pub const EXEC_BASE: u64 = 0x0000_4000_0000; + +/// Where its interpreter goes, far enough above the executable that +/// neither can grow into the other. +pub const INTERP_BASE: u64 = 0x0000_5000_0000; + +/// The stack top a guest wakes on, above everything it maps for itself. +pub const STACK_TOP: u64 = 0x0000_7FFF_F000; + +/// The stack a guest gets. +pub const STACK_SIZE: u64 = 1 << 20; + +/// The break may not reach the mapping area. +pub const BRK_LIMIT: u64 = MMAP_BASE; + +/// A mapping may not reach the images above it. +pub const MMAP_LIMIT: u64 = EXEC_BASE; diff --git a/userland/capsule_linux/src/linux/guest/mem.rs b/userland/capsule_linux/src/linux/guest/mem.rs index 9b9d48ee8..d7cf6a66c 100644 --- a/userland/capsule_linux/src/linux/guest/mem.rs +++ b/userland/capsule_linux/src/linux/guest/mem.rs @@ -14,59 +14,30 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Reading and writing a guest's memory, and giving it more. -//! -//! Every one of these is a peer call the kernel refuses unless this -//! process created the guest, so a personality bug cannot reach a process -//! it does not own. - -use alloc::vec; -use alloc::vec::Vec; - -use nonos_libc::peer::{mk_peer_map, mk_peer_read, mk_peer_write, PEER_PROT_EXEC, PEER_PROT_WRITE}; - -use super::handle::Guest; +//! A guest's pages: giving it more, and reaching into the ones it has. pub const PAGE: u64 = 4096; +/// The kernel's ceiling on one peer call. Keep in sync with MAX_SPAN in +/// src/process/foreign/peer_guard.rs. +pub const MAX_SPAN: u64 = 1 << 20; + pub fn page_down(addr: u64) -> u64 { addr & !(PAGE - 1) } +/// Rounded up, saturating. pub fn page_up(addr: u64) -> u64 { - (addr + PAGE - 1) & !(PAGE - 1) + addr.saturating_add(PAGE - 1) & !(PAGE - 1) } -impl Guest { - /* - * Pages covering `[addr, addr + len)`. Write and execute are separate - * because the kernel refuses a page that is both, and a loader that - * asked for both would be refused on the first code segment. Bytes - * still reach a read-only page: a peer copy goes through the guest's - * frames and not through its mapping. - */ - pub fn map(&self, addr: u64, len: u64, write: bool, exec: bool) -> i64 { - let start = page_down(addr); - let span = page_up(addr + len) - start; - let mut prot = 0; - if write { - prot |= PEER_PROT_WRITE; - } - if exec { - prot |= PEER_PROT_EXEC; - } - mk_peer_map(self.pid, start, span, prot) - } - - pub fn write(&self, addr: u64, bytes: &[u8]) -> i64 { - mk_peer_write(self.pid, addr, bytes) - } - - pub fn read(&self, addr: u64, len: usize) -> Option> { - let mut out = vec![0u8; len]; - if mk_peer_read(self.pid, addr, &mut out) < 0 { - return None; - } - Some(out) +/// The page-aligned span covering `[addr, addr + len)`, or `None` when that +/// runs past `limit`. +pub fn span_within(addr: u64, len: u64, limit: u64) -> Option<(u64, u64)> { + let start = page_down(addr); + let end = page_up(addr.checked_add(len)?); + if end <= start || end > limit { + return None; } + Some((start, end - start)) } diff --git a/userland/capsule_linux/src/linux/guest/mem_copy.rs b/userland/capsule_linux/src/linux/guest/mem_copy.rs new file mode 100644 index 000000000..8ebe35ebb --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/mem_copy.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 . + + +//! Bytes in and out of a guest, a megabyte at a time. + +use alloc::vec; +use alloc::vec::Vec; + +use nonos_libc::peer::{mk_peer_read, mk_peer_write}; + +use super::handle::Guest; +use super::mem::MAX_SPAN; + +impl Guest { + /// Bytes written, or the first failure. + pub fn write(&self, addr: u64, bytes: &[u8]) -> i64 { + let mut done = 0usize; + while done < bytes.len() { + let take = (bytes.len() - done).min(MAX_SPAN as usize); + let rc = mk_peer_write(self.pid, addr + done as u64, &bytes[done..done + take]); + if rc < 0 { + return rc; + } + done += take; + } + bytes.len() as i64 + } + + pub fn read(&self, addr: u64, len: usize) -> Option> { + let mut out = vec![0u8; len]; + let mut done = 0usize; + while done < len { + let take = (len - done).min(MAX_SPAN as usize); + if mk_peer_read(self.pid, addr + done as u64, &mut out[done..done + take]) < 0 { + return None; + } + done += take; + } + Some(out) + } +} diff --git a/userland/capsule_linux/src/linux/guest/mem_map.rs b/userland/capsule_linux/src/linux/guest/mem_map.rs new file mode 100644 index 000000000..a615617e0 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/mem_map.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 . + +//! Backing a span of a guest with pages. + +use nonos_libc::peer::{mk_peer_map, PEER_PROT_EXEC, PEER_PROT_WRITE}; + +use super::handle::Guest; +use super::layout::STACK_TOP; +use super::mem::{span_within, MAX_SPAN}; +use super::region::Region; + +impl Guest { + /// Pages covering `[addr, addr + len)`. + pub fn map(&mut self, addr: u64, len: u64, write: bool, exec: bool) -> i64 { + // Bounded by the top of the guest's area, which is the stack. + let Some((start, span)) = span_within(addr, len, STACK_TOP) else { + return -1; + }; + let mut prot = 0; + if write { + prot |= PEER_PROT_WRITE; + } + if exec { + prot |= PEER_PROT_EXEC; + } + let mut done = 0; + while done < span { + let take = (span - done).min(MAX_SPAN); + let rc = mk_peer_map(self.pid, start + done, take, prot); + if rc < 0 { + return rc; + } + done += take; + } + /* + * Remembered because fork copies a guest by walking what its + * supervisor gave it. + */ + self.regions.push(Region { at: start, len: span, write, exec }); + 0 + } +} diff --git a/userland/capsule_linux/src/linux/guest/mem_unmap.rs b/userland/capsule_linux/src/linux/guest/mem_unmap.rs new file mode 100644 index 000000000..320220351 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/mem_unmap.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 . + +//! Giving a guest's pages back. + +use nonos_libc::peer::mk_peer_unmap; + +use super::handle::Guest; +use super::layout::STACK_TOP; +use super::mem::{span_within, MAX_SPAN}; +use super::region_cut::cut; + +impl Guest { + /// Return `[addr, addr + len)` to the kernel. + pub fn unmap(&mut self, addr: u64, len: u64) -> i64 { + let Some((start, span)) = span_within(addr, len, STACK_TOP) else { + return -1; + }; + let mut done = 0; + while done < span { + let take = (span - done).min(MAX_SPAN); + let rc = mk_peer_unmap(self.pid, start + done, take); + if rc < 0 { + return rc; + } + done += take; + } + self.regions = cut(&self.regions, start, span); + 0 + } +} diff --git a/userland/capsule_linux/src/linux/guest/mod.rs b/userland/capsule_linux/src/linux/guest/mod.rs index 70985fbb5..19f20d91c 100644 --- a/userland/capsule_linux/src/linux/guest/mod.rs +++ b/userland/capsule_linux/src/linux/guest/mod.rs @@ -18,11 +18,28 @@ //! reaches into it. mod fd; +mod fd_dup; +mod fd_empty; +mod fd_kind; mod fd_make; mod handle; +mod handle_new; +mod layout; mod mem; +mod mem_copy; +mod mem_map; +mod mem_unmap; +mod region; +mod region_cut; +mod region_find; mod threads; -pub use fd::{Fd, Kind}; -pub use handle::{Guest, BRK_BASE}; -pub use mem::{page_down, page_up, PAGE}; +pub use fd::Fd; +pub use fd_kind::Kind; +pub use handle::Guest; +pub use layout::{ + BRK_BASE, BRK_LIMIT, EXEC_BASE, INTERP_BASE, MMAP_BASE, MMAP_LIMIT, STACK_SIZE, + STACK_TOP, +}; +pub use mem::{page_down, page_up, span_within, MAX_SPAN, PAGE}; +pub use region::Region; diff --git a/userland/capsule_linux/src/linux/guest/region.rs b/userland/capsule_linux/src/linux/guest/region.rs new file mode 100644 index 000000000..67ff889c3 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/region.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 . + +//! One span of a guest's address space, as this capsule laid it down. + +#[derive(Clone, Copy)] +pub struct Region { + pub at: u64, + pub len: u64, + pub write: bool, + pub exec: bool, +} diff --git a/userland/capsule_linux/src/linux/guest/region_cut.rs b/userland/capsule_linux/src/linux/guest/region_cut.rs new file mode 100644 index 000000000..81e0a37b6 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/region_cut.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 . + +//! Taking a span out of the list of what a guest holds. + +use alloc::vec::Vec; + +use super::region::Region; + +/// `regions` with `[at, at + len)` removed from every region it meets. +pub fn cut(regions: &[Region], at: u64, len: u64) -> Vec { + let end = at.saturating_add(len); + let mut out: Vec = Vec::with_capacity(regions.len()); + for r in regions { + let r_end = r.at.saturating_add(r.len); + if end <= r.at || at >= r_end { + out.push(*r); + continue; + } + if at > r.at { + out.push(Region { at: r.at, len: at - r.at, ..*r }); + } + if end < r_end { + out.push(Region { at: end, len: r_end - end, ..*r }); + } + } + out +} diff --git a/userland/capsule_linux/src/linux/guest/region_find.rs b/userland/capsule_linux/src/linux/guest/region_find.rs new file mode 100644 index 000000000..aabfe4cd0 --- /dev/null +++ b/userland/capsule_linux/src/linux/guest/region_find.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 . + +//! How much a guest actually holds at an address. + +use super::handle::Guest; + +impl Guest { + /// Bytes mapped contiguously from `addr`, zero when nothing is. + pub fn mapped_from(&self, addr: u64) -> u64 { + let mut reach = addr; + loop { + let Some(r) = self.regions.iter().find(|r| r.at <= reach && reach < r.at + r.len) + else { + break; + }; + /* + * Walked rather than answered from the one region, because a guest + * that maps twice at adjoining addresses holds one run of pages + * and the list remembers two. + */ + reach = r.at + r.len; + } + reach - addr + } +} diff --git a/userland/capsule_linux/src/linux/guest/threads.rs b/userland/capsule_linux/src/linux/guest/threads.rs index 6109f8bb4..4bef8fd9b 100644 --- a/userland/capsule_linux/src/linux/guest/threads.rs +++ b/userland/capsule_linux/src/linux/guest/threads.rs @@ -19,9 +19,18 @@ use nonos_libc::mk_foreign_reply; +use super::fd::Kind; use super::handle::Guest; impl Guest { + /// The net.sockets handle behind `fd`, if it is a socket. + pub fn socket_handle(&self, fd: u64) -> Option { + match self.fds.get(fd as usize) { + Some(f) if f.kind == Kind::Socket => Some(f.handle), + _ => None, + } + } + /// True for the guest and for every thread of it, which is what the /// serve loop needs: a trap arrives under the thread's own tid. pub fn owns(&self, pid: u32) -> bool { diff --git a/userland/capsule_linux/src/linux/image/elf_phdr.rs b/userland/capsule_linux/src/linux/image/elf_phdr.rs index 2118b542c..9a84263e6 100644 --- a/userland/capsule_linux/src/linux/image/elf_phdr.rs +++ b/userland/capsule_linux/src/linux/image/elf_phdr.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 . - //! Walking the program header table of a parsed image. use super::elf::Elf; @@ -22,8 +21,11 @@ use super::phdr::Phdr; use super::read::{u32v, u64v}; impl Elf<'_> { + /// The header at `index`, or nothing when the table it is in does not fit + /// an address. pub fn phdr(&self, index: u16) -> Option { - let at = (self.phoff + index as u64 * self.phentsize as u64) as usize; + let step = (index as u64).checked_mul(self.phentsize as u64)?; + let at = usize::try_from(self.phoff.checked_add(step)?).ok()?; Some(Phdr { kind: u32v(self.bytes, at)?, flags: u32v(self.bytes, at + 4)?, @@ -38,20 +40,19 @@ impl Elf<'_> { self.phnum } - /// The address the program headers land on once the image is in - /// place, which a dynamic linker needs and finds nowhere else. It is - /// the load segment that happens to contain them; an image whose - /// headers are in no segment reports nothing rather than an address - /// the guest cannot read. + /// The address the program headers land on once the image is in place, + /// which a dynamic linker needs and finds nowhere else. pub fn phdr_addr(&self, bias: u64) -> Option { for i in 0..self.phnum { let ph = self.phdr(i)?; if ph.kind != super::elf::PT_LOAD { continue; } - if self.phoff >= ph.offset && self.phoff < ph.offset + ph.filesz { - return Some(bias + ph.vaddr + (self.phoff - ph.offset)); + let end = ph.offset.checked_add(ph.filesz)?; + if self.phoff < ph.offset || self.phoff >= end { + continue; } + return ph.at(bias)?.checked_add(self.phoff - ph.offset); } None } diff --git a/userland/capsule_linux/src/linux/image/interp.rs b/userland/capsule_linux/src/linux/image/interp.rs index 5b1e8f970..56567ae9b 100644 --- a/userland/capsule_linux/src/linux/image/interp.rs +++ b/userland/capsule_linux/src/linux/image/interp.rs @@ -14,57 +14,30 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! Bringing up a program and, when it asks for one, its interpreter. -//! -//! A dynamically linked executable does not start at its own entry: the -//! kernel of a real Linux system loads the interpreter named in -//! `PT_INTERP` beside it and starts there instead, and the interpreter -//! then maps the libraries and jumps to the program. This does the same, -//! with the store standing in for the filesystem. - -use alloc::vec::Vec; - -use nonos_app_skeleton::clients::vfs::read_file; -use nonos_libc::mk_getpid; use super::elf::{Elf, ET_DYN}; use super::load::load_at; use super::loaded::{LoadError, Loaded}; -/// Where a position independent executable goes, and where its -/// interpreter goes. Far apart so neither can grow into the other, and -/// both clear of the heap and the mapping area the guest gets. -const EXEC_BASE: u64 = 0x0000_4000_0000; -const INTERP_BASE: u64 = 0x0000_5000_0000; - -/// The largest image that will be read out of the store. -const MAX_IMAGE: u32 = 64 << 20; +pub use crate::linux::guest::{EXEC_BASE, INTERP_BASE}; /// Load `bytes` as the program, and its interpreter if it names one. -/// Returns the program's own record and the address to start at, which is -/// the interpreter's entry whenever there is an interpreter. pub fn program( - guest: &crate::linux::guest::Guest, + guest: &mut crate::linux::guest::Guest, bytes: &[u8], ) -> Result<(Loaded, u64, u64), LoadError> { /* - * A shared object is position independent and has to be told where - * it went; an executable carries absolute addresses and must not be - * moved. The file says which it is, so nothing upstream has to know - * or can get it wrong. + * A shared object is position independent and has to be told where it + * went; an executable carries absolute addresses and must not be moved. */ let kind = Elf::parse(bytes).ok_or(LoadError::NotElf)?.kind; let bias = if kind == ET_DYN { EXEC_BASE } else { 0 }; let image = load_at(guest, bytes, bias)?; + let entry = image.entry; let Some(path) = image.interp.clone() else { - return Ok((image, image.entry, 0)); + return Ok((image, entry, 0)); }; - let raw = fetch(&path).ok_or(LoadError::Interp)?; - let ld = load_at(guest, &raw, INTERP_BASE).map_err(|_| LoadError::Interp)?; - Ok((image, ld.entry, INTERP_BASE)) -} - -fn fetch(path: &[u8]) -> Option> { - read_file(mk_getpid() as u32, path, MAX_IMAGE).ok() + let entry = super::interp_ld::place(guest, &path)?; + Ok((image, entry, INTERP_BASE)) } diff --git a/userland/capsule_linux/src/linux/image/interp_ld.rs b/userland/capsule_linux/src/linux/image/interp_ld.rs new file mode 100644 index 000000000..2c30e7fc3 --- /dev/null +++ b/userland/capsule_linux/src/linux/image/interp_ld.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 . + +//! Reading, proving and placing a program's interpreter. + +use alloc::vec::Vec; + +use crate::linux::file::{key, store_read, visible}; +use crate::linux::guest::{Guest, INTERP_BASE}; + +use super::load::load_at; +use super::loaded::LoadError; + +/// The largest image that will be read out of the store. +const MAX_IMAGE: u32 = 64 << 20; + +/// Where the interpreter's entry point ended up. +pub(super) fn place(guest: &mut Guest, path: &[u8]) -> Result { + // Already confined: the path came out of the guest's own image. + let at = visible(b"/", path); + let raw: Vec = store_read(&key(&at), MAX_IMAGE).map_err(|_| LoadError::Interp)?; + if crate::linux::attest::verify(&at, &raw).is_err() { + return Err(LoadError::Unproven); + } + let ld = load_at(guest, &raw, INTERP_BASE).map_err(|_| LoadError::Interp)?; + Ok(ld.entry) +} diff --git a/userland/capsule_linux/src/linux/image/load.rs b/userland/capsule_linux/src/linux/image/load.rs index 1b9430bf3..86c8a00e0 100644 --- a/userland/capsule_linux/src/linux/image/load.rs +++ b/userland/capsule_linux/src/linux/image/load.rs @@ -14,23 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! Laying an image's segments into a guest's address space. -//! -//! The kernel maps pages on request and copies bytes on request; which -//! pages and which bytes is the personality's business, because the -//! format is the personality's knowledge. -use super::elf::{Elf, ET_DYN, PF_W, PF_X, PT_INTERP, PT_LOAD}; +use super::elf::{Elf, ET_DYN, PT_INTERP, PT_LOAD}; use super::loaded::{LoadError, Loaded}; use super::segment::{name, segment}; use crate::linux::guest::Guest; -/// Map every `PT_LOAD` at `bias` and report what was learned. A shared -/// object is expected to be biased and an executable is not, so a caller -/// passing a bias for an `ET_EXEC` image is refused rather than moving an -/// image that carries absolute addresses. -pub fn load_at(guest: &Guest, bytes: &[u8], bias: u64) -> Result { +/// Map every `PT_LOAD` at `bias` and report what was learned. +pub fn load_at(guest: &mut Guest, bytes: &[u8], bias: u64) -> Result { let elf = Elf::parse(bytes).ok_or(LoadError::NotElf)?; if bias != 0 && elf.kind != ET_DYN { return Err(LoadError::NotElf); @@ -45,11 +37,14 @@ pub fn load_at(guest: &Guest, bytes: &[u8], bias: u64) -> Result. - //! What the loader learned about an image, which is everything the //! auxiliary vector has to carry and the interpreter has to be told. @@ -30,16 +29,27 @@ pub struct Loaded { pub phnum: u64, /// The interpreter this image asked for, if it asked for one. pub interp: Option>, - /// The bias every address in the image was shifted by. - pub bias: u64, } pub enum LoadError { NotElf, Map, Copy, - /// An interpreter was named and could not be read or parsed. The - /// program cannot start without it, and starting it anyway would - /// fault on the first unresolved call. + /// An interpreter was named and could not be read or parsed. Interp, + /// An interpreter was read and nothing vouches for it. + Unproven, +} + +impl LoadError { + /// What to tell the console. + pub fn why(&self) -> &'static [u8] { + match self { + LoadError::NotElf => b"[LINUX] not an elf\n", + LoadError::Map => b"[LINUX] image would not map\n", + LoadError::Copy => b"[LINUX] image would not copy\n", + LoadError::Interp => b"[LINUX] interpreter missing or broken\n", + LoadError::Unproven => b"[LINUX] refused: nothing vouches for the interpreter\n", + } + } } diff --git a/userland/capsule_linux/src/linux/image/mod.rs b/userland/capsule_linux/src/linux/image/mod.rs index 8359f06a9..8a09fbf87 100644 --- a/userland/capsule_linux/src/linux/image/mod.rs +++ b/userland/capsule_linux/src/linux/image/mod.rs @@ -21,14 +21,16 @@ mod auxv; mod elf; mod elf_phdr; mod interp; +mod interp_ld; mod load; mod loaded; mod phdr; mod read; mod segment; mod stack; +mod stack_guard; +mod stack_strings; +mod stack_words; -pub use interp::program; -pub use load::load_at; -pub use loaded::{LoadError, Loaded}; +pub use interp::{program, EXEC_BASE}; pub use stack::build; diff --git a/userland/capsule_linux/src/linux/image/phdr.rs b/userland/capsule_linux/src/linux/image/phdr.rs index 901153a28..be786881b 100644 --- a/userland/capsule_linux/src/linux/image/phdr.rs +++ b/userland/capsule_linux/src/linux/image/phdr.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 . -//! One program header: what to map, from where, and how much of it is in -//! the file rather than in the zeroes after it. +//! One program header: what to map, from where, and how much of it is in the +//! file rather than in the zeroes after it. + +use core::ops::Range; pub struct Phdr { pub kind: u32, @@ -25,3 +27,17 @@ pub struct Phdr { pub filesz: u64, pub memsz: u64, } + +impl Phdr { + /// The bytes of the image this header names. + pub(super) fn file_range(&self) -> Option> { + let from = usize::try_from(self.offset).ok()?; + let len = usize::try_from(self.filesz).ok()?; + Some(from..from.checked_add(len)?) + } + + /// Where this segment lands once the image is biased. + pub(super) fn at(&self, bias: u64) -> Option { + self.vaddr.checked_add(bias) + } +} diff --git a/userland/capsule_linux/src/linux/image/segment.rs b/userland/capsule_linux/src/linux/image/segment.rs index 78e2cdf67..76825b60f 100644 --- a/userland/capsule_linux/src/linux/image/segment.rs +++ b/userland/capsule_linux/src/linux/image/segment.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 . - //! One load segment into a guest, and the interpreter a header names. use alloc::vec::Vec; @@ -27,27 +26,26 @@ use crate::linux::guest::Guest; /// The interpreter path, without its terminator. A `PT_INTERP` that runs /// off the end of the file names nothing. pub(super) fn name(bytes: &[u8], ph: &Phdr) -> Option> { - let from = ph.offset as usize; - let to = from + ph.filesz as usize; - let raw = bytes.get(from..to)?; + let raw = bytes.get(ph.file_range()?)?; let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len()); Some(raw[..end].to_vec()) } -/// One segment: pages first, then the file bytes into them. The gap -/// between `filesz` and `memsz` is left as the zeroes the fresh frames -/// already hold, which is what a `.bss` is. -pub(super) fn segment(guest: &Guest, bytes: &[u8], ph: &Phdr, bias: u64) -> Result<(), LoadError> { - let at = ph.vaddr + bias; +/// One segment: pages first, then the file bytes into them. +pub(super) fn segment( + guest: &mut Guest, + bytes: &[u8], + ph: &Phdr, + bias: u64, +) -> Result<(), LoadError> { + let at = ph.at(bias).ok_or(LoadError::NotElf)?; if guest.map(at, ph.memsz, ph.flags & PF_W != 0, ph.flags & PF_X != 0) < 0 { return Err(LoadError::Map); } if ph.filesz == 0 { return Ok(()); } - let from = ph.offset as usize; - let to = from + ph.filesz as usize; - let body = bytes.get(from..to).ok_or(LoadError::NotElf)?; + let body = bytes.get(ph.file_range().ok_or(LoadError::NotElf)?).ok_or(LoadError::NotElf)?; if guest.write(at, body) < 0 { return Err(LoadError::Copy); } diff --git a/userland/capsule_linux/src/linux/image/stack.rs b/userland/capsule_linux/src/linux/image/stack.rs index 63631b1b2..ddeac8e20 100644 --- a/userland/capsule_linux/src/linux/image/stack.rs +++ b/userland/capsule_linux/src/linux/image/stack.rs @@ -17,57 +17,46 @@ //! The stack a Linux program wakes up on: argc, then argv, then the //! environment, then the auxiliary vector, each list ended by a null. -//! A C runtime reads all four before `main` and crashes without them. -//! The layout is fixed by the System V supplement, not by us. use alloc::vec::Vec; use super::auxv::pairs; use super::loaded::Loaded; +use super::stack_guard::{random, RANDOM_LEN}; +use super::stack_strings::place; +use super::stack_words::words; use crate::linux::guest::Guest; -/// Sixteen bytes a C runtime turns into its stack guard. -const RANDOM_LEN: u64 = 16; - pub fn build( guest: &Guest, top: u64, image: &Loaded, interp_base: u64, - argv0: &[u8], + argv: &[Vec], + envp: &[Vec], ) -> Option { - let name_at = top - (argv0.len() as u64 + 1); - let random_at = (name_at - RANDOM_LEN) & !0x0F; - let mut words: Vec = alloc::vec![1, name_at, 0, 0]; - words.extend(pairs(image, interp_base, random_at, name_at)); - let bytes = words.len() as u64 * 8; - let rsp = (random_at - bytes) & !0x0F; - - let mut name = argv0.to_vec(); - name.push(0); - if guest.write(name_at, &name) < 0 { - return None; - } + let mut all = argv.to_vec(); + all.extend_from_slice(envp); + let placed = place(guest, top, &all)?; + let random_at = (placed.floor - RANDOM_LEN) & !0x0F; + + let aux = pairs(image, interp_base, random_at, *placed.at.first()?); + let words = words(argv.len(), &placed.at, aux)?; + + /* + * System V wants rsp itself sixteen-byte aligned at the entry point, so + * the block is placed from an aligned base. + */ + let rsp = (random_at - words.len() as u64 * 8) & !0x0F; if guest.write(random_at, &random()?) < 0 { return None; } - let mut blob: Vec = Vec::with_capacity(bytes as usize); + let mut blob: Vec = Vec::with_capacity(words.len() * 8); for word in &words { blob.extend_from_slice(&word.to_le_bytes()); } - if guest.write(rsp, &blob) < 0 { - return None; - } - Some(rsp) -} - -/// Entropy for the guard comes from the system, never from a constant: a -/// fixed value here would make every guest's stack guard the same and -/// the protection worth nothing. -fn random() -> Option<[u8; RANDOM_LEN as usize]> { - let mut out = [0u8; RANDOM_LEN as usize]; - match nonos_libc::crypto_random(out.as_mut_ptr(), out.len()) { + match guest.write(rsp, &blob) { n if n < 0 => None, - _ => Some(out), + _ => Some(rsp), } } diff --git a/userland/capsule_linux/src/linux/image/stack_guard.rs b/userland/capsule_linux/src/linux/image/stack_guard.rs new file mode 100644 index 000000000..595fd1814 --- /dev/null +++ b/userland/capsule_linux/src/linux/image/stack_guard.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 sixteen bytes behind AT_RANDOM. + +/// Sixteen bytes a C runtime turns into its stack guard. +pub const RANDOM_LEN: u64 = 16; + +/// Entropy comes from the system, never from a constant: a fixed value here +/// would make every guest's stack guard the same, and a guard an attacker can +/// predict is not a guard. +pub fn random() -> Option<[u8; RANDOM_LEN as usize]> { + let mut out = [0u8; RANDOM_LEN as usize]; + match nonos_libc::crypto_random(out.as_mut_ptr(), out.len()) { + n if n < 0 => None, + _ => Some(out), + } +} diff --git a/userland/capsule_linux/src/linux/image/stack_strings.rs b/userland/capsule_linux/src/linux/image/stack_strings.rs new file mode 100644 index 000000000..1c44054bc --- /dev/null +++ b/userland/capsule_linux/src/linux/image/stack_strings.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 . + +//! The string block at the very top of a new stack. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +/// Where each string ended up, in the order it was given, and the lowest +/// address the block reached. +pub struct Placed { + pub at: Vec, + pub floor: u64, +} + +pub fn place(guest: &Guest, top: u64, strings: &[Vec]) -> Option { + let mut at = Vec::with_capacity(strings.len()); + let mut cursor = top; + // Downward, so the first string ends up highest. + for s in strings.iter().rev() { + let len = s.len() as u64 + 1; + cursor = cursor.checked_sub(len)?; + let mut bytes = s.clone(); + bytes.push(0); + if guest.write(cursor, &bytes) < 0 { + return None; + } + at.push(cursor); + } + at.reverse(); + Some(Placed { at, floor: cursor }) +} diff --git a/userland/capsule_linux/src/linux/image/stack_words.rs b/userland/capsule_linux/src/linux/image/stack_words.rs new file mode 100644 index 000000000..94d409439 --- /dev/null +++ b/userland/capsule_linux/src/linux/image/stack_words.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 word list a program finds at rsp. + +use alloc::vec::Vec; + +/// `at` is every string address, argv first then envp, and `argc` says where +/// the boundary falls. +pub fn words(argc: usize, at: &[u64], aux: Vec) -> Option> { + let mut out: Vec = alloc::vec![argc as u64]; + out.extend_from_slice(at.get(..argc)?); + out.push(0); + out.extend_from_slice(at.get(argc..)?); + out.push(0); + out.extend(aux); + Some(out) +} diff --git a/userland/capsule_linux/src/linux/install/download.rs b/userland/capsule_linux/src/linux/install/download.rs new file mode 100644 index 000000000..96eaf5654 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/download.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 . + +//! Fetching one package, from whichever branch holds it. + +use alloc::format; +use alloc::vec::Vec; + +use super::http::get; +use super::run::{ARCH, BRANCHES, HOST, PORT, RELEASE}; + +/// Either branch may hold it, and the index does not say which. +pub(super) fn download(name: &str, version: &str) -> Vec { + for branch in BRANCHES { + let path = format!("/alpine/{RELEASE}/{branch}/{ARCH}/{name}-{version}.apk"); + if let Some(bytes) = get(HOST, PORT, &path) { + return bytes; + } + } + Vec::new() +} diff --git a/userland/capsule_linux/src/linux/install/enrol.rs b/userland/capsule_linux/src/linux/install/enrol.rs new file mode 100644 index 000000000..d2dbc46c7 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/enrol.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 . + +//! Vouching for a program the machine just installed. + +use alloc::vec::Vec; + +use nonos_libc::{mk_local_sign, mk_local_sign_len}; + +use crate::linux::file::{key, store_write}; + +use crate::linux::attest_paths::beside; + +/// The capabilities a guest is spawned with, which is none. +const GUEST_CAPS: u64 = 0; + +/// Mint a trailer for `image` and write it beside `path`. +pub fn vouch(path: &[u8], image: &[u8]) -> bool { + let needed = mk_local_sign_len(image, GUEST_CAPS); + let Ok(len) = usize::try_from(needed) else { + // Negative: no enrolled identity, or this capsule may not mint. + return false; + }; + let mut trailer: Vec = alloc::vec![0u8; len]; + let wrote = mk_local_sign(image, GUEST_CAPS, &mut trailer); + let Ok(got) = usize::try_from(wrote) else { + return false; + }; + trailer.truncate(got); + let at = beside(path, b".zk_trailer.bin"); + store_write(&key(&at), &trailer).is_ok() +} diff --git a/userland/capsule_linux/src/linux/install/http.rs b/userland/capsule_linux/src/linux/install/http.rs new file mode 100644 index 000000000..795fccfe5 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/http.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 . + +//! A GET, over the socket service this capsule already uses. + +use alloc::vec::Vec; +use alloc::{format, string::String}; + +use crate::linux::net::raw::{connect_host, open_stream}; +use crate::linux::net::raw_io::{close, recv_all, send_all}; + +/// Enough for the largest package index; a reply beyond it is refused +/// rather than truncated into a half-parsed index. +const MAX_BODY: usize = 64 << 20; + +pub fn get(host: &str, port: u16, path: &str) -> Option> { + let handle = open_stream()?; + if connect_host(handle, host, port).is_none() { + close(handle); + return None; + } + let req = format!( + "GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: nonos\r\nConnection: close\r\n\r\n" + ); + if send_all(handle, req.as_bytes()).is_none() { + close(handle); + return None; + } + let raw = recv_all(handle, MAX_BODY); + close(handle); + body(&raw?) +} + +/// The bytes after the header block. A reply whose status is not 200 is +/// nothing: an error page parsed as a package is the worst outcome here. +fn body(raw: &[u8]) -> Option> { + let head_end = find(raw, b"\r\n\r\n")? + 4; + let head = String::from_utf8_lossy(&raw[..head_end]); + let first = head.lines().next()?; + if !first.contains(" 200 ") { + return None; + } + Some(raw[head_end..].to_vec()) +} + +fn find(hay: &[u8], needle: &[u8]) -> Option { + hay.windows(needle.len()).position(|w| w == needle) +} diff --git a/userland/capsule_linux/src/linux/install/index.rs b/userland/capsule_linux/src/linux/install/index.rs new file mode 100644 index 000000000..37118db32 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/index.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 distribution's package index, as this capsule needs it. + +use alloc::string::String; +use alloc::vec::Vec; + +pub struct Pkg { + pub name: String, + pub version: String, +} + +pub struct Index { + /// soname -> package + pub libs: Vec<(String, Pkg)>, + /// package name -> package + pub names: Vec<(String, Pkg)>, +} + +impl Index { + pub fn parse(raw: &[u8]) -> Index { + let text = String::from_utf8_lossy(raw); + let (mut libs, mut names) = (Vec::new(), Vec::new()); + let (mut name, mut version) = (String::new(), String::new()); + for line in text.lines() { + match line.as_bytes().first() { + Some(b'P') => name = String::from(&line[2..]), + Some(b'V') => { + version = String::from(&line[2..]); + names + .push((name.clone(), Pkg { name: name.clone(), version: version.clone() })); + } + Some(b'p') => { + for token in line[2..].split_whitespace() { + if let Some(so) = token.strip_prefix("so:") { + let so = so.split('=').next().unwrap_or(so); + let pkg = Pkg { name: name.clone(), version: version.clone() }; + libs.push((String::from(so), pkg)); + } + } + } + _ => {} + } + } + Index { libs, names } + } + + pub fn by_lib(&self, soname: &str) -> Option<&Pkg> { + self.libs.iter().find(|(k, _)| k == soname).map(|(_, v)| v) + } + + pub fn by_name(&self, name: &str) -> Option<&Pkg> { + self.names.iter().find(|(k, _)| k == name).map(|(_, v)| v) + } +} diff --git a/userland/capsule_linux/src/linux/install/index_load.rs b/userland/capsule_linux/src/linux/install/index_load.rs new file mode 100644 index 000000000..46bc16dd5 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/index_load.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 . + +//! Fetching and joining the branch indexes. + +use alloc::format; +use alloc::vec::Vec; + +use super::http::get; +use super::index::Index; +use super::run::{ARCH, BRANCHES, HOST, PORT, RELEASE}; +use super::tar::entries; + +pub(super) fn load_index() -> Option { + let mut all: Vec = Vec::new(); + for branch in BRANCHES { + let path = format!("/alpine/{RELEASE}/{branch}/{ARCH}/APKINDEX.tar.gz"); + let raw = get(HOST, PORT, &path)?; + let plain = nonos_inflate::gunzip(&raw)?; + for entry in entries(&plain) { + if entry.name.ends_with(b"APKINDEX") { + all.extend_from_slice(&entry.body); + } + } + } + Some(Index::parse(&all)) +} diff --git a/userland/capsule_linux/src/linux/install/mod.rs b/userland/capsule_linux/src/linux/install/mod.rs new file mode 100644 index 000000000..c92a6de47 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/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 . + +//! Installing a Linux program from within the system. + +mod download; +mod enrol; +mod http; +mod index; +mod index_load; +mod place; +mod place_entry; +mod provenance; +mod run; +mod tar; +mod tar_field; + +pub use run::install; diff --git a/userland/capsule_linux/src/linux/install/place.rs b/userland/capsule_linux/src/linux/install/place.rs new file mode 100644 index 000000000..28012a229 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/place.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 . +//! Putting a package's files into the store, under the Linux root. + +use nonos_inflate::gunzip; +use nonos_libc::mk_debug; + +use super::place_entry::one; +use super::provenance::Provenance; +use super::tar::entries; + +/// Unpack `apk` into the store and report how many files landed. +pub fn unpack(apk: &[u8], from: Provenance) -> usize { + let Some(raw) = gunzip(apk) else { + say(b"[LINUX] package is not readable\n"); + return 0; + }; + entries(&raw).iter().filter(|entry| one(entry, from)).count() +} + +fn say(line: &[u8]) { + let _ = mk_debug(line.as_ptr(), line.len()); +} diff --git a/userland/capsule_linux/src/linux/install/place_entry.rs b/userland/capsule_linux/src/linux/install/place_entry.rs new file mode 100644 index 000000000..75aa0e08e --- /dev/null +++ b/userland/capsule_linux/src/linux/install/place_entry.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 . + +//! One file out of a package: where it lands, and whether the machine +//! says anything about it. + +use nonos_libc::mk_debug; + +use crate::linux::file::{key, store_write, visible}; + +use super::enrol::vouch; +use super::provenance::Provenance; +use super::tar::Entry; + +/// Paths a package may not write: a package dropping one of these +/// would be installing its own proof. +const REFUSED: &[&[u8]] = &[b".nonos_id_cert.bin", b".manifest.bin", b".zk_trailer.bin"]; + +/// True when the file landed in the store. +pub(super) fn one(entry: &Entry, from: Provenance) -> bool { + if entry.name.starts_with(b".") { + return false; + } + if REFUSED.iter().any(|s| entry.name.ends_with(s)) { + say(b"[LINUX] refused a package writing its own proof\n"); + return false; + } + let at = visible(b"/", &entry.name); + if store_write(&key(&at), &entry.body).is_err() { + return false; + } + if is_elf(&entry.body) { + vouch_for(&at, &entry.body, from); + } + true +} + +/// Minting says this machine agreed to run these bytes, so it is only said +/// about bytes something authenticated. +fn vouch_for(at: &[u8], body: &[u8], from: Provenance) { + if from == Provenance::Unauthenticated { + say(b"[LINUX] installed unvouched: package bytes are not authenticated\n"); + return; + } + if !vouch(at, body) { + say(b"[LINUX] installed but unvouched: no enrolled root, or may not mint\n"); + } +} + +/// A shared object counts: an interpreter loads it, and the gate +/// measures whatever it loads. +fn is_elf(body: &[u8]) -> bool { + body.starts_with(b"\x7fELF") +} + +fn say(line: &[u8]) { + let _ = mk_debug(line.as_ptr(), line.len()); +} diff --git a/userland/capsule_linux/src/linux/install/provenance.rs b/userland/capsule_linux/src/linux/install/provenance.rs new file mode 100644 index 000000000..3750b5ccd --- /dev/null +++ b/userland/capsule_linux/src/linux/install/provenance.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 . + +//! Where a package's bytes came from, and whether anything vouches for them +//! arriving intact. + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum Provenance { + /// The bytes match a checksum from a signed index. + Verified, + /// Nothing says these are the bytes the distribution published. + Unauthenticated, +} diff --git a/userland/capsule_linux/src/linux/install/run.rs b/userland/capsule_linux/src/linux/install/run.rs new file mode 100644 index 000000000..05b733a57 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/run.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 . + +//! `install `: fetch a package and everything under it. + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use nonos_libc::mk_debug; + +use super::download::download; +use super::index_load::load_index; +use super::place::unpack; +use super::provenance::Provenance; + +pub(super) const HOST: &str = "dl-cdn.alpinelinux.org"; +pub(super) const PORT: u16 = 80; +pub(super) const RELEASE: &str = "v3.20"; +pub(super) const ARCH: &str = "x86_64"; +pub(super) const BRANCHES: [&str; 2] = ["main", "community"]; + +/// Rounds of resolution. A closure that has not settled by now is a +/// dependency cycle the index cannot satisfy, and looping would hide it. +const ROUNDS: usize = 12; + +pub fn install(name: &str) -> bool { + let Some(index) = load_index() else { + say(b"[LINUX] no package index\n"); + return false; + }; + let mut wanted: Vec = vec![String::from(name)]; + let mut done: Vec = Vec::new(); + for _ in 0..ROUNDS { + let Some(next) = wanted.pop() else { + return true; + }; + if done.contains(&next) { + continue; + } + let Some(pkg) = index.by_name(&next).or_else(|| index.by_lib(&next)) else { + say(b"[LINUX] nothing provides it\n"); + return false; + }; + let apk = download(&pkg.name, &pkg.version); + if apk.is_empty() { + say(b"[LINUX] package would not download\n"); + return false; + } + /* + * Nothing says these are the bytes the distribution + * published: see `provenance`. + */ + unpack(&apk, Provenance::Unauthenticated); + done.push(next); + } + true +} + +fn say(line: &[u8]) { + let _ = mk_debug(line.as_ptr(), line.len()); +} diff --git a/userland/capsule_linux/src/linux/install/tar.rs b/userland/capsule_linux/src/linux/install/tar.rs new file mode 100644 index 000000000..2cecb045f --- /dev/null +++ b/userland/capsule_linux/src/linux/install/tar.rs @@ -0,0 +1,63 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Walking a tar, which is what a package is once it is decompressed. + +use alloc::vec::Vec; + +use super::tar_field::{name_of, octal}; + +const BLOCK: usize = 512; +const SIZE_AT: usize = 124; +const SIZE_LEN: usize = 12; +const TYPE_AT: usize = 156; + +pub struct Entry { + pub name: Vec, + pub body: Vec, +} + +/// Regular files only. A package's directories are implied by its paths +/// and its links are followed at install time, not recreated. +pub fn entries(data: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut at = 0usize; + while at + BLOCK <= data.len() { + let head = &data[at..at + BLOCK]; + /* + * A zero block ends an archive, and an apk is several archives end to + * end: a signature, a control stream, then the data. + */ + if head.iter().all(|b| *b == 0) { + at += BLOCK; + continue; + } + let Some(size) = octal(&head[SIZE_AT..SIZE_AT + SIZE_LEN]) else { + break; + }; + let body_at = at + BLOCK; + let end = body_at + size; + if end > data.len() { + break; + } + if head[TYPE_AT] == b'0' || head[TYPE_AT] == 0 { + let name = name_of(head); + out.push(Entry { name, body: data[body_at..end].to_vec() }); + } + at = body_at + size.div_ceil(BLOCK) * BLOCK; + } + out +} diff --git a/userland/capsule_linux/src/linux/install/tar_field.rs b/userland/capsule_linux/src/linux/install/tar_field.rs new file mode 100644 index 000000000..588d901f2 --- /dev/null +++ b/userland/capsule_linux/src/linux/install/tar_field.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 . + +//! The two header fields this reader needs. + +use alloc::vec::Vec; + +const NAME: usize = 100; + +/// The size field is octal text, space or NUL padded. +pub(super) fn octal(field: &[u8]) -> Option { + let mut value = 0usize; + for b in field { + match b { + b'0'..=b'7' => value = value.checked_mul(8)?.checked_add((b - b'0') as usize)?, + b' ' | 0 => break, + _ => return None, + } + } + Some(value) +} + +pub(super) fn name_of(head: &[u8]) -> Vec { + let raw = &head[..NAME]; + let end = raw.iter().position(|b| *b == 0).unwrap_or(NAME); + raw[..end].to_vec() +} diff --git a/userland/capsule_linux/src/linux/mod.rs b/userland/capsule_linux/src/linux/mod.rs index a86816ef4..cde2e2126 100644 --- a/userland/capsule_linux/src/linux/mod.rs +++ b/userland/capsule_linux/src/linux/mod.rs @@ -18,12 +18,24 @@ //! one capsule that holds the capabilities its guests do not. mod abi; +mod attest; +mod attest_local; +mod attest_paths; +mod attest_publisher; mod call; +mod env; mod file; mod guest; mod image; +mod install; +mod net; +mod origin; +mod request; pub mod serve; mod source; mod start; +mod start_guest; +mod unix; +mod wayland; pub use start::run; diff --git a/userland/capsule_linux/src/linux/net/addr.rs b/userland/capsule_linux/src/linux/net/addr.rs new file mode 100644 index 000000000..330c461cf --- /dev/null +++ b/userland/capsule_linux/src/linux/net/addr.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 . + + +//! `struct sockaddr_in` out of a guest. + +use crate::linux::guest::Guest; + +/// family(2) port(2) addr(4), and the rest of the sixteen bytes unused. +const SOCKADDR_IN: usize = 16; +const AF_INET: u16 = 2; + +/// Port in network order and address in network order, which is the +/// order net.sockets wants as well, so neither is byte swapped here. +pub fn inet(guest: &Guest, at: u64, len: u64) -> Option<(u16, [u8; 4])> { + if len < SOCKADDR_IN as u64 { + return None; + } + let raw = guest.read(at, SOCKADDR_IN)?; + if u16::from_le_bytes([raw[0], raw[1]]) != AF_INET { + return None; + } + let port = u16::from_be_bytes([raw[2], raw[3]]); + Some((port, [raw[4], raw[5], raw[6], raw[7]])) +} diff --git a/userland/capsule_linux/src/linux/net/call.rs b/userland/capsule_linux/src/linux/net/call.rs new file mode 100644 index 000000000..2383aeae1 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/call.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 . + + +//! One round trip to net.sockets. +//! +//! Sockets there are keyed by the caller's pid, so every handle belongs +//! to this capsule and a guest can only reach one through a descriptor +//! this capsule gave it. + +use alloc::{vec, vec::Vec}; + +use nonos_app_skeleton::discover::lookup_service; +use nonos_app_skeleton::wire::{build_request, HDR_LEN}; +use nonos_libc::mk_ipc_call; + +/// Keep in sync with CAPSULE_SERVICE_ENDPOINT in +/// userland/capsule_net_sockets/Capsule.mk. +const FIXED_PORT: u32 = 4460; +const NAME: &[u8] = b"net.sockets"; +const MAGIC: u32 = 0x4E53_4B54; + +/// Status is a u16 at offset 8 of the reply header, payload at 20. +const STATUS_AT: usize = 8; + +pub fn call(op: u16, body: &[u8], want: usize) -> Option<(u16, Vec)> { + let port = match lookup_service(NAME) { + Some(peer) if peer.port != 0 => peer.port, + _ => FIXED_PORT, + }; + let tx = build_request(MAGIC, op, 1, body); + let mut rx = vec![0u8; HDR_LEN + want]; + let n = mk_ipc_call(port as u64, tx.as_ptr(), tx.len(), rx.as_mut_ptr(), rx.len()); + if n < HDR_LEN as i64 { + return None; + } + let status = u16::from_le_bytes([rx[STATUS_AT], rx[STATUS_AT + 1]]); + Some((status, rx[HDR_LEN..n as usize].to_vec())) +} diff --git a/userland/capsule_linux/src/linux/net/connect.rs b/userland/capsule_linux/src/linux/net/connect.rs new file mode 100644 index 000000000..ce5deac6d --- /dev/null +++ b/userland/capsule_linux/src/linux/net/connect.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 . + +//! `connect`, for a socket the guest opened here. + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::addr::inet; +use super::call::call; +use super::dns::host_for; +use super::ops::{OP_CONNECT, OP_CONNECT_HOST}; + +pub fn connect(guest: &mut Guest, fd: u64, at: u64, len: u64) -> u64 { + /* + * A program may connect its nameserver socket before writing to + * it. There is nothing to reach: this capsule is the nameserver. + */ + if guest.fds.get(fd as usize).is_some_and(|f| f.kind == Kind::Resolver) { + return errno::ok(0); + } + let Some(handle) = guest.socket_handle(fd) else { + return errno::fail(errno::ENOTSOCK); + }; + let Some((port, ip)) = inet(guest, at, len) else { + return errno::fail(errno::EAFNOSUPPORT); + }; + // An address this capsule invented for a name goes back to being the name. + if let Some(host) = host_for(guest, ip) { + return by_host(handle, &host, port); + } + let mut body = Vec::with_capacity(10); + body.extend_from_slice(&handle.to_le_bytes()); + body.extend_from_slice(&ip); + body.extend_from_slice(&port.to_le_bytes()); + match call(OP_CONNECT, &body, 0) { + Some((0, _)) => errno::ok(0), + Some(_) => errno::fail(errno::ECONNREFUSED), + None => errno::fail(errno::EIO), + } +} + +fn by_host(handle: u32, host: &[u8], port: u16) -> u64 { + if host.len() > u8::MAX as usize { + return errno::fail(errno::EINVAL); + } + let mut body = Vec::with_capacity(7 + host.len()); + body.extend_from_slice(&handle.to_le_bytes()); + body.extend_from_slice(&port.to_le_bytes()); + body.push(host.len() as u8); + body.extend_from_slice(host); + match call(OP_CONNECT_HOST, &body, 0) { + Some((0, _)) => errno::ok(0), + Some(_) => errno::fail(errno::ECONNREFUSED), + None => errno::fail(errno::EIO), + } +} diff --git a/userland/capsule_linux/src/linux/net/dgram.rs b/userland/capsule_linux/src/linux/net/dgram.rs new file mode 100644 index 000000000..dd3b31bd5 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dgram.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 . + +//! `sendto` and `recvfrom`, which differ from write and read only in carrying +//! an address. + +use crate::linux::call; +use crate::linux::guest::{Guest, Kind}; + +use super::addr::inet; +use super::dgram_addr::{encode, fill}; +use super::dns; + +pub fn sendto(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64, alen: u64) -> u64 { + if !is_resolver(guest, fd) { + return call::write(guest, fd, buf, len); + } + /* + * A program with no `resolv.conf` asks the loopback address, and one with + * a configured nameserver asks that. + */ + let peer = inet(guest, at, alen).unwrap_or((53, [127, 0, 0, 1])); + dns::query(guest, fd, buf, len, encode(peer)) +} + +pub fn recvfrom(guest: &mut Guest, fd: u64, buf: u64, len: u64, at: u64, alen: u64) -> u64 { + if !is_resolver(guest, fd) { + return call::read(guest, fd, buf, len); + } + let (got, from) = dns::answer_out(guest, fd, buf, len); + match from { + Some(peer) if at != 0 => fill(guest, at, alen, peer, got), + _ => got, + } +} + +fn is_resolver(guest: &Guest, fd: u64) -> bool { + guest.fds.get(fd as usize).is_some_and(|f| f.kind == Kind::Resolver) +} diff --git a/userland/capsule_linux/src/linux/net/dgram_addr.rs b/userland/capsule_linux/src/linux/net/dgram_addr.rs new file mode 100644 index 000000000..95b00b07e --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dgram_addr.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 . + +//! The address halves of `sendto` and `recvfrom`. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +const AF_INET: u16 = 2; +const SOCKADDR_IN: usize = 16; + +/// port then address, both already in network order. +pub(super) fn encode((port, ip): (u16, [u8; 4])) -> [u8; 6] { + let p = port.to_be_bytes(); + [p[0], p[1], ip[0], ip[1], ip[2], ip[3]] +} + +/// Write the source back as a `sockaddr_in` and say how long it is. +pub(super) fn fill(guest: &mut Guest, at: u64, alen: u64, peer: [u8; 6], got: u64) -> u64 { + let mut sa = [0u8; SOCKADDR_IN]; + sa[0..2].copy_from_slice(&AF_INET.to_le_bytes()); + sa[2..8].copy_from_slice(&peer); + if guest.write(at, &sa) < sa.len() as i64 { + return errno::fail(errno::EFAULT); + } + if alen != 0 && guest.write(alen, &(sa.len() as u32).to_le_bytes()) < 4 { + return errno::fail(errno::EFAULT); + } + got +} diff --git a/userland/capsule_linux/src/linux/net/dns/automap.rs b/userland/capsule_linux/src/linux/net/dns/automap.rs new file mode 100644 index 000000000..543721e4f --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/automap.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 . + +//! Names the guest asked for, and the addresses it was told. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +/// RFC 6598 shared address space: routable nowhere, and reserved against +/// exactly this kind of use. +const BASE: [u8; 2] = [100, 64]; + +/// A ceiling, because the table is memory a guest grows by asking. +const MAX_NAMES: usize = 4096; + +/// The address for `host`, the same one every time it is asked for. +pub fn address_for(guest: &mut Guest, host: &[u8]) -> Option<[u8; 4]> { + if let Some((_, addr)) = guest.automap.iter().find(|(h, _)| h == host) { + return Some(*addr); + } + if guest.automap.len() >= MAX_NAMES { + return None; + } + let n = guest.automap.len() as u32 + 1; + let addr = [BASE[0], BASE[1] + (n >> 16) as u8, (n >> 8) as u8, n as u8]; + guest.automap.push((host.to_vec(), addr)); + Some(addr) +} + +/// The name an address stands for, when it is one of ours. +pub fn host_for(guest: &Guest, addr: [u8; 4]) -> Option> { + if addr[0] != BASE[0] || addr[1] & 0xC0 != BASE[1] { + return None; + } + guest.automap.iter().find(|(_, a)| *a == addr).map(|(h, _)| h.clone()) +} diff --git a/userland/capsule_linux/src/linux/net/dns/decide.rs b/userland/capsule_linux/src/linux/net/dns/decide.rs new file mode 100644 index 000000000..d2cec491d --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/decide.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 to answer a query with. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::{automap, name, reply}; + +/// A record type this capsule can answer with an address. +const TYPE_A: u16 = 1; + +pub(super) fn answer(guest: &mut Guest, msg: &[u8]) -> Option> { + let (host, end) = name::read(msg)?; + let qtype = u16::from_be_bytes([*msg.get(end)?, *msg.get(end + 1)?]); + let question = msg.get(..end + 4)?; + if qtype != TYPE_A { + /* + * Every other type, AAAA above all, is answered empty rather than + * ignored. + */ + return Some(reply::build(question, None)); + } + Some(reply::build(question, automap::address_for(guest, &host))) +} diff --git a/userland/capsule_linux/src/linux/net/dns/mod.rs b/userland/capsule_linux/src/linux/net/dns/mod.rs new file mode 100644 index 000000000..23b9adad3 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/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 . + +//! Answering a guest's name lookups without asking anyone. + +mod automap; +mod decide; +mod name; +mod open; +mod reply; +mod serve; + +pub use automap::host_for; +pub use open::open; +pub use serve::{answer_out, query}; diff --git a/userland/capsule_linux/src/linux/net/dns/name.rs b/userland/capsule_linux/src/linux/net/dns/name.rs new file mode 100644 index 000000000..4da75209c --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/name.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 . + +//! The name out of a DNS question. + +use alloc::vec::Vec; + +/// Where the question starts: past the twelve byte header. +pub const QUESTION: usize = 12; + +/// The longest name that can be asked for, as the format allows. +const MAX_NAME: usize = 255; + +/// The name, and the offset of the byte after it. +pub fn read(msg: &[u8]) -> Option<(Vec, usize)> { + let mut out: Vec = Vec::new(); + let mut at = QUESTION; + loop { + let len = *msg.get(at)? as usize; + if len == 0 { + return Some((out, at + 1)); + } + if len > 63 || out.len() + len + 1 > MAX_NAME { + return None; + } + let from = at + 1; + let label = msg.get(from..from + len)?; + if !out.is_empty() { + out.push(b'.'); + } + out.extend_from_slice(label); + at = from + len; + } +} diff --git a/userland/capsule_linux/src/linux/net/dns/open.rs b/userland/capsule_linux/src/linux/net/dns/open.rs new file mode 100644 index 000000000..fb3bd77be --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/open.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 . + +//! The descriptor a program opens to reach its nameserver. + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +/// It looks like a datagram socket and holds no handle: there is +/// nothing on the other side of it, which is the point. +pub fn open(guest: &mut Guest) -> u64 { + match crate::linux::file::install(guest, Fd::resolver()) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} diff --git a/userland/capsule_linux/src/linux/net/dns/reply.rs b/userland/capsule_linux/src/linux/net/dns/reply.rs new file mode 100644 index 000000000..389a61be7 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/reply.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 answer this capsule writes back. + +use alloc::vec::Vec; + +/// Response, recursion desired carried back, recursion available. +const FLAGS: u16 = 0x8180; + +/// A short life. +const TTL: u32 = 60; + +const TYPE_A: u16 = 1; +const CLASS_IN: u16 = 1; + +/// `question` is the query's bytes from the header through the qclass. +pub fn build(question: &[u8], addr: Option<[u8; 4]>) -> Vec { + let mut out: Vec = Vec::with_capacity(question.len() + 16); + out.extend_from_slice(&question[..2]); + out.extend_from_slice(&FLAGS.to_be_bytes()); + out.extend_from_slice(&1u16.to_be_bytes()); + out.extend_from_slice(&u16::from(addr.is_some()).to_be_bytes()); + out.extend_from_slice(&[0, 0, 0, 0]); + out.extend_from_slice(&question[12..]); + let Some(addr) = addr else { + return out; + }; + // The name is not written again. + out.extend_from_slice(&0xC00Cu16.to_be_bytes()); + out.extend_from_slice(&TYPE_A.to_be_bytes()); + out.extend_from_slice(&CLASS_IN.to_be_bytes()); + out.extend_from_slice(&TTL.to_be_bytes()); + out.extend_from_slice(&4u16.to_be_bytes()); + out.extend_from_slice(&addr); + out +} diff --git a/userland/capsule_linux/src/linux/net/dns/serve.rs b/userland/capsule_linux/src/linux/net/dns/serve.rs new file mode 100644 index 000000000..8fb35cdf4 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/dns/serve.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 . + +//! A query in, an answer out, and nothing on the wire. + +use crate::linux::abi::errno; +use crate::linux::guest::{Guest, Kind}; + +use super::decide::answer; + +/// Take a query and queue its answer, to appear to come back from `peer`, +/// which is whatever the program believes its nameserver is. +pub fn query(guest: &mut Guest, fd: u64, buf: u64, len: u64, peer: [u8; 6]) -> u64 { + let Some(msg) = guest.read(buf, len as usize) else { + return errno::fail(errno::EFAULT); + }; + if let Some(answer) = answer(guest, &msg) { + if let Some(entry) = guest.fds.get_mut(fd as usize).filter(|f| f.kind == Kind::Resolver) { + entry.replies.push((answer, peer)); + } + } + errno::ok(len) +} + +/// The oldest answer waiting, with the address it came from, or nothing. +pub fn answer_out(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> (u64, Option<[u8; 6]>) { + let Some(entry) = guest.fds.get_mut(fd as usize).filter(|f| f.kind == Kind::Resolver) else { + return (errno::fail(errno::EBADF), None); + }; + if entry.replies.is_empty() { + return (errno::fail(errno::EAGAIN), None); + } + let (msg, peer) = entry.replies.remove(0); + let take = msg.len().min(len as usize); + match guest.write(buf, &msg[..take]) { + n if n < take as i64 => (errno::fail(errno::EFAULT), None), + _ => (errno::ok(take as u64), Some(peer)), + } +} diff --git a/userland/capsule_linux/src/linux/net/mod.rs b/userland/capsule_linux/src/linux/net/mod.rs new file mode 100644 index 000000000..0e91f4ee8 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/mod.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 . + +//! Sockets, over the net.sockets service. + +mod addr; +mod call; +mod connect; +mod dgram; +mod dgram_addr; +pub mod dns; +mod ops; +mod poll; +mod poll_set; +mod poll_socket; +pub mod raw; +pub mod raw_io; +mod select; +mod socket; +mod stream; + +pub use connect::connect; +pub use dgram::{recvfrom, sendto}; +pub use poll::ready; +pub use poll_set::poll; +pub use select::select; +pub use socket::socket; +pub use stream::{close, recv, send}; diff --git a/userland/capsule_linux/src/linux/net/ops.rs b/userland/capsule_linux/src/linux/net/ops.rs new file mode 100644 index 000000000..b5392771c --- /dev/null +++ b/userland/capsule_linux/src/linux/net/ops.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 net.sockets opcodes this capsule uses. + +pub const OP_SOCKET: u16 = 2; +pub const OP_CONNECT_HOST: u16 = 12; +pub const OP_CONNECT: u16 = 6; +pub const OP_SEND: u16 = 7; +pub const OP_RECV: u16 = 8; +pub const OP_CLOSE: u16 = 9; +pub const OP_POLL: u16 = 13; + +/// The socket kinds the server offers: 1 stream, 2 datagram, 3 mixnet. +pub const KIND_MIXNET: u16 = 3; + +/// The address family the server takes. It is not AF_INET: the number +/// is the server's own and the two only look alike. +pub const DOMAIN: u16 = 4; + +/// What OP_POLL reports: a recv would return data, a send would take it. +pub const POLL_READABLE: u8 = 1; +pub const POLL_WRITABLE: u8 = 2; diff --git a/userland/capsule_linux/src/linux/net/poll.rs b/userland/capsule_linux/src/linux/net/poll.rs new file mode 100644 index 000000000..8d6e1130a --- /dev/null +++ b/userland/capsule_linux/src/linux/net/poll.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 . + +//! `poll` over the guest's descriptors. + +use crate::linux::guest::{Guest, Kind}; + +use super::poll_socket::socket_bits; + +const POLLIN: u16 = 0x001; +const POLLOUT: u16 = 0x004; +const POLLNVAL: u16 = 0x020; + +/// What `fd` can do right now, in poll's bits. +pub fn ready(guest: &Guest, fd: u64) -> u16 { + match guest.fds.get(fd as usize).map(|f| &f.kind) { + Some(Kind::Free) | None => POLLNVAL, + Some(Kind::Socket) => match guest.socket_handle(fd) { + Some(handle) => socket_bits(handle), + None => POLLNVAL, + }, + Some(Kind::Timer) => timer_bits(guest, fd), + Some(Kind::Resolver) => resolver_bits(guest, fd), + Some(_) => POLLIN | POLLOUT, + } +} + +/// A timer is readable once it has fired and never writable. +fn timer_bits(guest: &Guest, fd: u64) -> u16 { + let now = nonos_libc::mk_uptime_ms().max(0) as u64; + match guest.fds.get(fd as usize) { + Some(e) if e.expiry != 0 && now >= e.expiry => POLLIN, + _ => 0, + } +} + +/// Readable once an answer is waiting, and always writable: a query is taken +/// whenever it is offered. +fn resolver_bits(guest: &Guest, fd: u64) -> u16 { + match guest.fds.get(fd as usize) { + Some(e) if !e.replies.is_empty() => POLLIN | POLLOUT, + _ => POLLOUT, + } +} diff --git a/userland/capsule_linux/src/linux/net/poll_set.rs b/userland/capsule_linux/src/linux/net/poll_set.rs new file mode 100644 index 000000000..dc7bc808d --- /dev/null +++ b/userland/capsule_linux/src/linux/net/poll_set.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 . + + +//! Walking a guest's `struct pollfd` array. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::poll::ready; + +/// fd, events, revents. +const POLLFD_LEN: usize = 8; +const POLLNVAL: u16 = 0x020; + +pub fn poll(guest: &mut Guest, at: u64, count: u64) -> u64 { + let mut hits = 0; + for i in 0..count { + let entry = at + i * POLLFD_LEN as u64; + let Some(raw) = guest.read(entry, POLLFD_LEN) else { + return errno::fail(errno::EFAULT); + }; + let fd = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]) as u64; + let events = u16::from_le_bytes([raw[4], raw[5]]); + let revents = ready(guest, fd) & (events | POLLNVAL); + if revents != 0 { + hits += 1; + } + if guest.write(entry + 6, &revents.to_le_bytes()) < 2 { + return errno::fail(errno::EFAULT); + } + } + errno::ok(hits) +} diff --git a/userland/capsule_linux/src/linux/net/poll_socket.rs b/userland/capsule_linux/src/linux/net/poll_socket.rs new file mode 100644 index 000000000..7d5fe6ef8 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/poll_socket.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 . + +//! Asking net.sockets whether one handle is ready. + +use super::call::call; +use super::ops::{OP_POLL, POLL_READABLE, POLL_WRITABLE}; + +const POLLIN: u16 = 0x001; +const POLLOUT: u16 = 0x004; + +pub(super) fn socket_bits(handle: u32) -> u16 { + let Some((0, out)) = call(OP_POLL, &handle.to_le_bytes(), 1) else { + return 0; + }; + let Some(bits) = out.first() else { + return 0; + }; + let mut set = 0; + if bits & POLL_READABLE != 0 { + set |= POLLIN; + } + if bits & POLL_WRITABLE != 0 { + set |= POLLOUT; + } + set +} diff --git a/userland/capsule_linux/src/linux/net/raw.rs b/userland/capsule_linux/src/linux/net/raw.rs new file mode 100644 index 000000000..211bf78ba --- /dev/null +++ b/userland/capsule_linux/src/linux/net/raw.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 . + +//! Sockets for this capsule's own use, rather than a guest's. + +use alloc::vec::Vec; + +use super::call::call; +use super::ops::{DOMAIN, KIND_MIXNET, OP_CONNECT_HOST, OP_SOCKET}; + +/// Over the mixnet, like everything else. +pub fn open_stream() -> Option { + let mut body = Vec::with_capacity(4); + body.extend_from_slice(&DOMAIN.to_le_bytes()); + body.extend_from_slice(&KIND_MIXNET.to_le_bytes()); + match call(OP_SOCKET, &body, 8) { + Some((0, out)) if out.len() >= 4 => { + Some(u32::from_le_bytes([out[0], out[1], out[2], out[3]])) + } + _ => None, + } +} + +pub fn connect_host(handle: u32, host: &str, port: u16) -> Option<()> { + let mut body = Vec::with_capacity(7 + host.len()); + body.extend_from_slice(&handle.to_le_bytes()); + body.extend_from_slice(&port.to_le_bytes()); + body.push(host.len() as u8); + body.extend_from_slice(host.as_bytes()); + match call(OP_CONNECT_HOST, &body, 0) { + Some((0, _)) => Some(()), + _ => None, + } +} diff --git a/userland/capsule_linux/src/linux/net/raw_io.rs b/userland/capsule_linux/src/linux/net/raw_io.rs new file mode 100644 index 000000000..268dc3a1f --- /dev/null +++ b/userland/capsule_linux/src/linux/net/raw_io.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 . + +//! Bytes on a socket this capsule opened for itself. + +use alloc::vec::Vec; + +use super::call::call; +use super::ops::{OP_CLOSE, OP_RECV, OP_SEND}; + +/// One transfer. The service caps a reply, so a body arrives in pieces. +const CHUNK: usize = 32 << 10; + +pub fn send_all(handle: u32, bytes: &[u8]) -> Option<()> { + for part in bytes.chunks(CHUNK) { + let mut body = Vec::with_capacity(4 + part.len()); + body.extend_from_slice(&handle.to_le_bytes()); + body.extend_from_slice(part); + match call(OP_SEND, &body, 0) { + Some((0, _)) => {} + _ => return None, + } + } + Some(()) +} + +/// Read until the peer closes, which is what Connection: close gives. +pub fn recv_all(handle: u32, limit: usize) -> Option> { + let mut out: Vec = Vec::new(); + loop { + match call(OP_RECV, &handle.to_le_bytes(), CHUNK) { + Some((0, part)) if part.is_empty() => return Some(out), + Some((0, part)) => out.extend_from_slice(&part), + _ => return Some(out), + } + if out.len() > limit { + return None; + } + } +} + +pub fn close(handle: u32) { + let _ = call(OP_CLOSE, &handle.to_le_bytes(), 0); +} diff --git a/userland/capsule_linux/src/linux/net/select.rs b/userland/capsule_linux/src/linux/net/select.rs new file mode 100644 index 000000000..41e5139ed --- /dev/null +++ b/userland/capsule_linux/src/linux/net/select.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 . +//! `select`, answered from the same readiness the poll path reports. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::ready; + +const POLLIN: u16 = 0x001; +const POLLOUT: u16 = 0x004; + +/// Linux caps a descriptor set at 1024 bits and so does every libc that +/// builds one, so a larger nfds is a caller error rather than a bigger set. +const FD_SETSIZE: u64 = 1024; +const SET_BYTES: usize = (FD_SETSIZE / 8) as usize; + +pub fn select(guest: &mut Guest, nfds: u64, readfds: u64, writefds: u64) -> u64 { + if nfds > FD_SETSIZE { + return errno::fail(errno::EINVAL); + } + let mut hits = 0u64; + for (at, want) in [(readfds, POLLIN), (writefds, POLLOUT)] { + if at == 0 { + continue; + } + let Some(mut set) = guest.read(at, SET_BYTES) else { + return errno::fail(errno::EFAULT); + }; + hits += narrow(guest, &mut set, nfds, want); + if guest.write(at, &set) < 0 { + return errno::fail(errno::EFAULT); + } + } + errno::ok(hits) +} + +/// Clear every bit whose descriptor is not ready for `want`, and report +/// how many were left set. +fn narrow(guest: &Guest, set: &mut [u8], nfds: u64, want: u16) -> u64 { + let mut kept = 0; + for fd in 0..nfds { + let (byte, bit) = ((fd / 8) as usize, (fd % 8) as u32); + if set[byte] & (1 << bit) == 0 { + continue; + } + if ready(guest, fd) & want != 0 { + kept += 1; + } else { + set[byte] &= !(1 << bit); + } + } + kept +} diff --git a/userland/capsule_linux/src/linux/net/socket.rs b/userland/capsule_linux/src/linux/net/socket.rs new file mode 100644 index 000000000..cdcbd5cbe --- /dev/null +++ b/userland/capsule_linux/src/linux/net/socket.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! `socket` and `connect`, over net.sockets. + +use alloc::vec::Vec; + +use super::ops::{DOMAIN, KIND_MIXNET, OP_SOCKET}; + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest}; + +use super::call::call; + +const AF_INET: u64 = 2; +const SOCK_STREAM: u64 = 1; +const SOCK_DGRAM: u64 = 2; +/// Linux ORs these into the type; neither changes what is opened here. +const TYPE_MASK: u64 = 0xFF; + +pub fn socket(guest: &mut Guest, family: u64, kind: u64) -> u64 { + if family != AF_INET { + return errno::fail(errno::EAFNOSUPPORT); + } + /* + * A guest's stream goes over the mixnet, never the open network, and it + * holds no capability that could name a socket: there is no second route + * to disable and no firewall rule to remove. + */ + let want = match kind & TYPE_MASK { + SOCK_STREAM => KIND_MIXNET, + SOCK_DGRAM => return super::dns::open(guest), + _ => return errno::fail(errno::ENOSYS), + }; + let mut body = Vec::with_capacity(4); + body.extend_from_slice(&DOMAIN.to_le_bytes()); + body.extend_from_slice(&want.to_le_bytes()); + let Some((status, out)) = call(OP_SOCKET, &body, 8) else { + return errno::fail(errno::EIO); + }; + if status != 0 || out.len() < 4 { + return errno::fail(errno::ENOMEM); + } + let handle = u32::from_le_bytes([out[0], out[1], out[2], out[3]]); + match crate::linux::file::install(guest, Fd::socket(handle)) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} diff --git a/userland/capsule_linux/src/linux/net/stream.rs b/userland/capsule_linux/src/linux/net/stream.rs new file mode 100644 index 000000000..94c6effd5 --- /dev/null +++ b/userland/capsule_linux/src/linux/net/stream.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 . + + +//! Bytes on and off a connected socket. + +use alloc::vec::Vec; + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::call::call; +use super::ops::{OP_CLOSE, OP_RECV, OP_SEND}; + +/// One transfer. The server's reply buffer is the ceiling, not this. +const MAX_IO: u64 = 32 << 10; + +pub fn send(guest: &Guest, handle: u32, buf: u64, len: u64) -> u64 { + let take = len.min(MAX_IO); + let Some(bytes) = guest.read(buf, take as usize) else { + return errno::fail(errno::EFAULT); + }; + let mut body = Vec::with_capacity(4 + bytes.len()); + body.extend_from_slice(&handle.to_le_bytes()); + body.extend_from_slice(&bytes); + match call(OP_SEND, &body, 0) { + Some((0, _)) => errno::ok(take), + Some(_) => errno::fail(errno::EPIPE), + None => errno::fail(errno::EIO), + } +} + +pub fn recv(guest: &Guest, handle: u32, buf: u64, len: u64) -> u64 { + let want = len.min(MAX_IO) as usize; + let Some((status, bytes)) = call(OP_RECV, &handle.to_le_bytes(), want) else { + return errno::fail(errno::EIO); + }; + if status != 0 { + return errno::fail(errno::ECONNRESET); + } + if bytes.is_empty() { + return errno::ok(0); + } + let n = bytes.len().min(want); + if guest.write(buf, &bytes[..n]) < n as i64 { + return errno::fail(errno::EFAULT); + } + errno::ok(n as u64) +} + +pub fn close(handle: u32) { + let _ = call(OP_CLOSE, &handle.to_le_bytes(), 0); +} diff --git a/userland/capsule_linux/src/linux/origin.rs b/userland/capsule_linux/src/linux/origin.rs new file mode 100644 index 000000000..0caa15d13 --- /dev/null +++ b/userland/capsule_linux/src/linux/origin.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 . + + +//! Where a program came from, which decides what has to prove it. + +/// Where a program came from, which decides what has to prove it. +pub enum Origin { + /// Read out of the store, and therefore unproven until it proves + /// itself against the enrolled set. + Store, + /// Part of this capsule's own ELF, and so already measured by the manifest + /// that admitted this capsule. + BuiltIn, +} + diff --git a/userland/capsule_linux/src/linux/request.rs b/userland/capsule_linux/src/linux/request.rs new file mode 100644 index 000000000..afaa2e9e8 --- /dev/null +++ b/userland/capsule_linux/src/linux/request.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 . + + +//! Reading what this capsule was asked to do. + +use alloc::string::String; + +use nonos_libc::mk_args; + +/// Matches the buffer the program path is read into. +const MAX_ARGS: usize = 256; + +/// Arguments `install` then `` ask this capsule to fetch a package +/// rather than run a program. +pub fn install_request() -> Option { + let mut buf = [0u8; MAX_ARGS]; + let n = mk_args(buf.as_mut_ptr(), buf.len()); + if n <= 0 { + return None; + } + let mut parts = buf[..n as usize].split(|b| *b == 0); + if parts.next()? != b"install" { + return None; + } + let name = parts.next().filter(|s| !s.is_empty())?; + Some(String::from(core::str::from_utf8(name).ok()?)) +} diff --git a/userland/capsule_linux/src/linux/serve/dispatch.rs b/userland/capsule_linux/src/linux/serve/dispatch.rs index 8c2098357..67226625e 100644 --- a/userland/capsule_linux/src/linux/serve/dispatch.rs +++ b/userland/capsule_linux/src/linux/serve/dispatch.rs @@ -30,9 +30,12 @@ pub fn answer(guest: &mut Guest, frame: &ForeignFrame) -> Answer { let a = frame.args(); match frame.nr { nr::CLONE => clone(guest, frame), + nr::FORK | nr::VFORK => crate::linux::call::fork(guest), + nr::EXECVE => crate::linux::call::execve(guest, frame.pid, a[0], a[1], a[2]), + nr::WAIT4 => crate::linux::call::wait4(guest, a[0], a[1], a[2]), // A thread exiting is not the process exiting. nr::EXIT if frame.pid != guest.pid => Answer::Reply(exit_thread(guest, frame.pid)), nr::FUTEX => futex(guest, frame.pid, a[0], a[1], a[2]), - other => Answer::Reply(plain(guest, other, a)), + other => Answer::Reply(plain(guest, frame.pid, other, a)), } } diff --git a/userland/capsule_linux/src/linux/serve/mod.rs b/userland/capsule_linux/src/linux/serve/mod.rs index d8de8ecd3..a10fe22da 100644 --- a/userland/capsule_linux/src/linux/serve/mod.rs +++ b/userland/capsule_linux/src/linux/serve/mod.rs @@ -20,6 +20,10 @@ mod answer; mod dispatch; mod loop_impl; mod table; +mod table_file; +mod table_mem; +mod table_net; +mod table_proc; mod unserved; pub use answer::Answer; diff --git a/userland/capsule_linux/src/linux/serve/table.rs b/userland/capsule_linux/src/linux/serve/table.rs index 2c805d109..0f64f8f2c 100644 --- a/userland/capsule_linux/src/linux/serve/table.rs +++ b/userland/capsule_linux/src/linux/serve/table.rs @@ -14,44 +14,46 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +//! Every number a guest can ask for. -//! Every number a guest can ask for, and where it goes. - -use crate::linux::abi::{errno, nr}; +use crate::linux::abi::{errno, nr, nr_path as np}; use crate::linux::call; -use crate::linux::file; -use crate::linux::file::flags; use crate::linux::guest::Guest; -pub fn plain(guest: &mut Guest, nr: u64, a: [u64; 6]) -> u64 { +use super::table_file::file_ops; +use super::table_mem::mem_ops; +use super::table_net::net_ops; +use super::table_proc::proc_ops; + +pub fn plain(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> u64 { + if let Some(v) = file_ops(guest, tid, nr, a) { + return v; + } + if let Some(v) = net_ops(guest, tid, nr, a) { + return v; + } + if let Some(v) = mem_ops(guest, nr, a) { + return v; + } + if let Some(v) = proc_ops(guest, nr, a) { + return v; + } + rest(guest, tid, nr, a) +} + +fn rest(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> u64 { match nr { - nr::WRITE => call::write(guest, a[0], a[1], a[2]), - nr::WRITEV => call::writev(guest, a[0], a[1], a[2]), - nr::READ => call::read(guest, a[0], a[1], a[2]), - nr::CLOSE => call::close(guest, a[0]), - nr::OPENAT => 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::PREAD64 => file::pread64(guest, a[0], a[1], a[2], a[3]), - nr::GETCWD => file::getcwd(guest, a[0], a[1]), - nr::ACCESS => file::access(guest, a[0]), - nr::READLINK => file::readlink(guest, a[0]), nr::IOCTL => call::ioctl(guest, a[0], a[1]), - nr::FCNTL => call::fcntl(guest, a[0], a[1]), + nr::FCNTL => call::fcntl(guest, a[0], a[1], a[2]), nr::UNAME => call::uname(guest, a[0]), - nr::BRK => call::brk(guest, a[0]), - nr::MMAP => call::mmap(guest, call::MapReq::from_args(a)), - nr::MUNMAP => call::munmap(guest, a[0], a[1]), - nr::MPROTECT => call::mprotect(guest, a[0], a[1], a[2]), - nr::MADVISE | nr::RSEQ | nr::SET_ROBUST_LIST => errno::ok(0), - nr::ARCH_PRCTL => call::arch_prctl(guest, a[0], a[1]), - nr::SET_TID_ADDRESS | nr::GETTID | nr::GETPID => errno::ok(guest.pid as u64), - nr::GETUID | nr::GETEUID | nr::GETGID | nr::GETEGID => errno::ok(0), - nr::CLOCK_GETTIME => call::clock_gettime(guest, a[0], a[1]), + np::GETRLIMIT => call::getrlimit(guest, a[0], a[1]), + np::UMASK => call::umask(guest, a[0]), + np::PRLIMIT64 => call::prlimit64(guest, a[1], a[2], a[3]), + nr::RT_SIGACTION => call::rt_sigaction(guest, a[0], a[1], a[2]), + nr::RT_SIGPROCMASK => call::rt_sigprocmask(guest, a[2]), + nr::SIGALTSTACK => call::sigaltstack(guest, a[1]), + nr::RSEQ | nr::SET_ROBUST_LIST => errno::ok(0), + nr::ARCH_PRCTL => call::arch_prctl(guest, tid, a[0], a[1]), nr::GETRANDOM => call::getrandom(guest, a[0], a[1], a[2]), nr::EXIT | nr::EXIT_GROUP => call::exit(guest, a[0]), other => super::unserved::unserved(other), diff --git a/userland/capsule_linux/src/linux/serve/table_file.rs b/userland/capsule_linux/src/linux/serve/table_file.rs new file mode 100644 index 000000000..96fd7f03a --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_file.rs @@ -0,0 +1,74 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! Calls that name a file or a descriptor. + +use crate::linux::abi::{nr, nr_path as np}; +use crate::linux::call; +use crate::linux::file; +use crate::linux::file::flags; +use crate::linux::guest::Guest; + +pub fn file_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option { + let _ = tid; + Some(match nr { + nr::WRITE => call::write(guest, a[0], a[1], a[2]), + nr::WRITEV => call::writev(guest, a[0], a[1], a[2]), + nr::READ => call::read(guest, a[0], a[1], a[2]), + nr::CLOSE => 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::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), + nr::PIPE => call::pipe2(guest, a[0], 0), + nr::PIPE2 => call::pipe2(guest, a[0], a[1]), + nr::DUP => call::dup(guest, a[0]), + nr::DUP2 | nr::DUP3 => call::dup2(guest, a[0], a[1]), + nr::EPOLL_CTL => file::epoll_ctl(guest, a[0], a[1], a[2], a[3]), + nr::EPOLL_PWAIT => file::epoll_wait(guest, a[0], a[1], a[2]), + nr::TIMERFD_CREATE => file::timerfd_create(guest), + nr::TIMERFD_SETTIME => file::timerfd_settime(guest, a[0], a[2]), + nr::PREAD64 => file::pread64(guest, a[0], a[1], a[2], a[3]), + nr::GETCWD => 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::RMDIR => file::rmdir(guest, a[0]), + np::UNLINK => file::unlinkat(guest, flags::AT_FDCWD, a[0], 0), + np::UNLINKAT => file::unlinkat(guest, a[0], a[1], a[2]), + np::RENAME => file::rename(guest, a[0], a[1]), + np::FSYNC => file::fsync(guest, a[0]), + np::READV => call::readv(guest, a[0], a[1], a[2]), + np::CHMOD => file::chmod(guest, a[0], a[1]), + np::FCHMOD => file::fchmod(guest, a[0], a[1]), + np::FCHMODAT => file::fchmodat(guest, a[0], a[1], a[2]), + np::FACCESSAT | np::FACCESSAT2 => file::faccessat(guest, a[0], a[1]), + np::STATFS | np::FSTATFS => file::statfs(guest, a[1]), + np::STATX => file::statx(guest, a[0], a[1], a[4]), + np::EPOLL_WAIT => file::epoll_wait(guest, a[0], a[1], a[2]), + nr::ACCESS => file::access(guest, a[0]), + nr::READLINK => file::readlink(guest, a[0]), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/serve/table_mem.rs b/userland/capsule_linux/src/linux/serve/table_mem.rs new file mode 100644 index 000000000..f2e562f75 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_mem.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 . + +//! Calls that shape the guest's address space. + +use crate::linux::abi::{errno, nr}; +use crate::linux::call; +use crate::linux::guest::Guest; + +pub fn mem_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { + Some(match nr { + nr::BRK => call::brk(guest, a[0]), + nr::MMAP => call::mmap(guest, call::MapReq::from_args(a)), + nr::MUNMAP => call::munmap(guest, a[0], a[1]), + nr::MPROTECT => call::mprotect(guest, a[0], a[1], a[2]), + // Advice, and this capsule takes none of it. + nr::MADVISE => errno::ok(0), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/serve/table_net.rs b/userland/capsule_linux/src/linux/serve/table_net.rs new file mode 100644 index 000000000..1d65692f7 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_net.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 . + +//! Calls that name a socket. + +use crate::linux::abi::{nr, nr_path as np}; +use crate::linux::call; +use crate::linux::guest::Guest; +use crate::linux::net; +use crate::linux::unix::{self, is_unix}; + +pub fn net_ops(guest: &mut Guest, tid: u32, nr: u64, a: [u64; 6]) -> Option { + let _ = tid; + Some(match nr { + nr::SOCKET if a[0] == 1 => unix::socket(guest, a[1]), + nr::SOCKET => net::socket(guest, a[0], a[1]), + nr::CONNECT if is_unix(guest, a[0]) => unix::connect(guest, a[0], a[1], a[2]), + nr::CONNECT => net::connect(guest, a[0], a[1], a[2]), + nr::SENDTO => net::sendto(guest, a[0], a[1], a[2], a[4], a[5]), + nr::RECVFROM => net::recvfrom(guest, a[0], a[1], a[2], a[4], a[5]), + nr::POLL => net::poll(guest, a[0], a[1]), + np::SELECT | np::PSELECT6 => net::select(guest, a[0], a[1], a[2]), + np::PPOLL => net::poll(guest, a[0], a[1]), + nr::SHUTDOWN => call::close(guest, a[0]), + nr::SENDMSG => unix::sendmsg(guest, a[0], a[1]), + nr::RECVMSG => unix::recvmsg(guest, a[0], a[1]), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/serve/table_proc.rs b/userland/capsule_linux/src/linux/serve/table_proc.rs new file mode 100644 index 000000000..2b0adad92 --- /dev/null +++ b/userland/capsule_linux/src/linux/serve/table_proc.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 . + +//! Who the guest is, what group it is in, and what time it thinks it is. + +use crate::linux::abi::{errno, nr, nr_path as np}; +use crate::linux::call; +use crate::linux::guest::Guest; + +pub fn proc_ops(guest: &mut Guest, nr: u64, a: [u64; 6]) -> Option { + Some(match nr { + np::KILL | np::TKILL => call::kill(guest, a[0], a[1]), + np::GETPPID => call::getppid(guest), + np::SETPGID => call::setpgid(guest, a[0], a[1]), + np::GETPGRP | np::GETPGID => call::getpgid(guest), + np::SETSID => call::setsid(guest), + np::GETSID => call::getsid(guest), + np::SETUID | np::SETGID => call::setuid(a[0]), + np::TIME => call::time(guest, a[0]), + np::GETTIMEOFDAY => call::gettimeofday(guest, a[0]), + np::NANOSLEEP | np::CLOCK_NANOSLEEP => call::nanosleep(guest, a[0]), + // A guest yielding is the personality yielding: one slot. + np::SCHED_YIELD => { + nonos_libc::mk_yield(); + errno::ok(0) + } + nr::SET_TID_ADDRESS | nr::GETTID | nr::GETPID => errno::ok(guest.pid as u64), + nr::GETUID | nr::GETEUID | nr::GETGID | nr::GETEGID => errno::ok(0), + nr::CLOCK_GETTIME => call::clock_gettime(guest, a[0], a[1]), + _ => return None, + }) +} diff --git a/userland/capsule_linux/src/linux/source.rs b/userland/capsule_linux/src/linux/source.rs index 735dddfe1..1ee2ffb30 100644 --- a/userland/capsule_linux/src/linux/source.rs +++ b/userland/capsule_linux/src/linux/source.rs @@ -14,32 +14,28 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! Which program to run. -//! -//! The path comes from this capsule's own arguments, so the personality -//! runs whatever it was asked to run and is not a wrapper around one -//! binary. With no argument it falls back to the image built into it, -//! which exists so the mechanism can be proved on a machine with nothing -//! in the store yet. use alloc::vec::Vec; -use nonos_app_skeleton::clients::vfs::read_file; -use nonos_libc::{mk_args, mk_getpid}; +use nonos_libc::mk_args; + +use crate::linux::file::{key, store_read, visible}; -/// The proof image: a static Linux executable, embedded so the first run -/// needs no disk. -static BUILT_IN: &[u8] = include_bytes!("../../guests/hello.elf"); +use super::origin::Origin; + +/// The built-in program: Alpine's static busybox, embedded so a machine with +/// nothing in the store still runs a real Linux binary. +static BUILT_IN: &[u8] = include_bytes!("../../guests/busybox.elf"); const MAX_IMAGE: u32 = 64 << 20; const MAX_ARGS: usize = 256; -/// The program's path and its bytes. -pub fn source() -> (Vec, Vec) { +/// The program's path, its bytes, and where they came from. +pub fn source() -> (Vec, Vec, Origin) { match named() { - Some(pair) => pair, - None => (b"/guest/hello".to_vec(), BUILT_IN.to_vec()), + Some((path, bytes)) => (path, bytes, Origin::Store), + None => (b"/bin/busybox".to_vec(), BUILT_IN.to_vec(), Origin::BuiltIn), } } @@ -49,18 +45,18 @@ fn named() -> Option<(Vec, Vec)> { if n <= 0 { return None; } - /* - * The first argument is the path. Anything after it is the guest's - * own business and is not passed on yet: a program's argument vector - * has to be built into its stack before it starts, and only the path - * is needed to get it there. - */ + // The first argument is the path. let args = &buf[..n as usize]; let end = args.iter().position(|b| *b == 0 || *b == b' ').unwrap_or(args.len()); let path = args.get(..end)?; if path.is_empty() { return None; } - let bytes = read_file(mk_getpid() as u32, path, MAX_IMAGE).ok()?; - Some((path.to_vec(), bytes)) + /* + * The argument is not a guest's, but the program it names is a + * Linux one and lives where Linux programs live. + */ + let at = visible(b"/", path); + let bytes = store_read(&key(&at), MAX_IMAGE).ok()?; + Some((at, bytes)) } diff --git a/userland/capsule_linux/src/linux/start.rs b/userland/capsule_linux/src/linux/start.rs index 8b50a88e5..971da847d 100644 --- a/userland/capsule_linux/src/linux/start.rs +++ b/userland/capsule_linux/src/linux/start.rs @@ -14,37 +14,35 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - //! Bring one Linux program up and stay with it until it ends. -use nonos_libc::{heap_init, mk_debug, mk_exit, mk_foreign_spawn, mk_foreign_start}; +use nonos_libc::{heap_init, mk_debug, mk_exit, mk_foreign_spawn}; use super::guest::Guest; -use super::image; use super::serve::serve; use super::source::source; - -/// The stack top a guest wakes on, below the personality's own mappings -/// and above everything it maps for itself. -const STACK_TOP: u64 = 0x0000_7FFF_F000; - -/// The stack a guest gets, which is also the largest span one peer call -/// will map. Sixty-four kilobytes served a static binary and would have -/// overflowed under an interpreter recursing through a dependency graph, -/// as a fault inside the linker with nothing to read. -const STACK_SIZE: u64 = 1 << 20; +use super::start_guest::start; pub fn run() -> ! { let _ = heap_init(); say(b"[LINUX] personality up\n"); - let (path, bytes) = source(); + if let Some(name) = super::request::install_request() { + say(b"[LINUX] installing\n"); + let ok = super::install::install(&name); + say(if ok { b"[LINUX] installed\n" } else { b"[LINUX] install failed\n" }); + mk_exit(if ok { 0 } else { 1 }) + } + let (path, bytes, origin) = source(); let pid = mk_foreign_spawn(b"linux"); if pid < 0 { - say(b"[LINUX] no guest: refused\n"); + say(b"[LINUX] no guest, errno "); + let e = (-pid) as u32; + let digits = [b'0' + (e / 10 % 10) as u8, b'0' + (e % 10) as u8, b'\n']; + say(&digits); mk_exit(1) } let mut guest = Guest::new(pid as u32); - let code = match start(&mut guest, &path, &bytes) { + let code = match start(&mut guest, &path, &bytes, origin) { Ok(()) => { say(b"[LINUX] guest running\n"); serve(&mut guest) @@ -58,18 +56,6 @@ pub fn run() -> ! { mk_exit(code) } -fn start(guest: &mut Guest, path: &[u8], bytes: &[u8]) -> Result<(), &'static [u8]> { - let (image, entry, interp_base) = - image::program(guest, bytes).map_err(|_| &b"[LINUX] image refused\n"[..])?; - guest.map(STACK_TOP - STACK_SIZE, STACK_SIZE, true, false); - let rsp = image::build(guest, STACK_TOP, &image, interp_base, path) - .ok_or(&b"[LINUX] stack refused\n"[..])?; - match mk_foreign_start(guest.pid, entry, rsp) { - n if n < 0 => Err(&b"[LINUX] start refused\n"[..]), - _ => Ok(()), - } -} - -fn say(line: &[u8]) { +pub(super) fn say(line: &[u8]) { let _ = mk_debug(line.as_ptr(), line.len()); } diff --git a/userland/capsule_linux/src/linux/start_guest.rs b/userland/capsule_linux/src/linux/start_guest.rs new file mode 100644 index 000000000..fcf34697d --- /dev/null +++ b/userland/capsule_linux/src/linux/start_guest.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 . + +//! Proving a program, laying it out, and making it runnable. + +use nonos_libc::mk_foreign_start; + +use super::guest::Guest; +use super::guest::{STACK_SIZE, STACK_TOP}; +use super::image; +use super::origin::Origin; +use super::start::say; + +pub(super) fn start( + guest: &mut Guest, + path: &[u8], + bytes: &[u8], + origin: Origin, +) -> Result<(), &'static [u8]> { + if let Err(why) = prove(path, bytes, origin) { + say(b"[LINUX] refused: "); + say(why.as_bytes()); + say(b"\n"); + return Err(&b"[LINUX] unproven program\n"[..]); + } + let (image, entry, interp_base) = image::program(guest, bytes).map_err(|e| e.why())?; + guest.map(STACK_TOP - STACK_SIZE, STACK_SIZE, true, false); + let argv = alloc::vec![path.to_vec()]; + let rsp = image::build(guest, STACK_TOP, &image, interp_base, &argv, &super::env::default()) + .ok_or(&b"[LINUX] stack refused\n"[..])?; + match mk_foreign_start(guest.pid, entry, rsp) { + n if n < 0 => Err(&b"[LINUX] start refused\n"[..]), + _ => Ok(()), + } +} + +/// A program out of the store proves itself against the enrolled set. +fn prove(path: &[u8], bytes: &[u8], origin: Origin) -> Result<(), &'static str> { + match origin { + Origin::BuiltIn => Ok(()), + Origin::Store => super::attest::verify(path, bytes).map(|_| ()), + } +} diff --git a/userland/capsule_linux/src/linux/unix/conn.rs b/userland/capsule_linux/src/linux/unix/conn.rs new file mode 100644 index 000000000..3c5da9370 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/conn.rs @@ -0,0 +1,49 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! One connection: what the client has said, and what it has not read. + +use alloc::vec::Vec; + +pub struct Conn { + /// Bytes the guest wrote and the server has not consumed. + pub to_server: Vec, + /// Bytes the server produced and the guest has not read. + pub to_client: Vec, + /// Descriptors the client passed in control data, in order. + pub fds: Vec, + /// Descriptors owed to the client, for the next control block. + pub give: Vec, +} + +impl Conn { + pub fn new() -> Conn { + Conn { to_server: Vec::new(), to_client: Vec::new(), fds: Vec::new(), give: Vec::new() } + } + + /// Take everything the server has produced, up to `want`. + pub fn drain(&mut self, want: usize) -> Vec { + let n = want.min(self.to_client.len()); + self.to_client.drain(..n).collect() + } +} + +impl Default for Conn { + fn default() -> Conn { + Conn::new() + } +} diff --git a/userland/capsule_linux/src/linux/unix/give.rs b/userland/capsule_linux/src/linux/unix/give.rs new file mode 100644 index 000000000..4b6b0577d --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/give.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 . + + +//! Handing a descriptor back to the client in control data. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +/// struct cmsghdr: len, level, type, then the descriptors. +const CMSG_DATA: usize = 16; +const SOL_SOCKET: u32 = 1; +const SCM_RIGHTS: u32 = 1; + +/// The control block for whatever is owed, or nothing when nothing is. +pub fn control(guest: &mut Guest) -> Option> { + if guest.display.give.is_empty() { + return None; + } + let fds = core::mem::take(&mut guest.display.give); + let len = CMSG_DATA + fds.len() * 4; + let mut out = Vec::with_capacity((len + 7) & !7); + out.extend_from_slice(&(len as u64).to_le_bytes()); + out.extend_from_slice(&SOL_SOCKET.to_le_bytes()); + out.extend_from_slice(&SCM_RIGHTS.to_le_bytes()); + for fd in fds { + out.extend_from_slice(&fd.to_le_bytes()); + } + while out.len() % 8 != 0 { + out.push(0); + } + Some(out) +} + +/// Write it into the guest's msghdr and say how long it was, since a +/// client reads msg_controllen and not the block's own length. +pub fn place(guest: &Guest, hdr: u64, block: &[u8]) -> bool { + let Some(raw) = guest.read(hdr, 56) else { + return false; + }; + let at = u64::from_le_bytes([ + raw[32], raw[33], raw[34], raw[35], raw[36], raw[37], raw[38], raw[39], + ]); + let room = u64::from_le_bytes([ + raw[40], raw[41], raw[42], raw[43], raw[44], raw[45], raw[46], raw[47], + ]); + if at == 0 || room < block.len() as u64 { + return false; + } + if guest.write(at, block) < block.len() as i64 { + return false; + } + guest.write(hdr + 40, &(block.len() as u64).to_le_bytes()) >= 8 +} diff --git a/userland/capsule_linux/src/linux/unix/mod.rs b/userland/capsule_linux/src/linux/unix/mod.rs new file mode 100644 index 000000000..b03ee7949 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/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 . + + +//! Unix domain sockets, both ends inside this capsule. + +mod conn; +mod give; +mod msg; +mod msg_parts; +mod msg_rights; +mod path; +mod recvmsg; +mod sendmsg; +mod sock; +mod sock_io; + +pub use conn::Conn; +pub use sock::is_unix; +pub use recvmsg::recvmsg; +pub use sendmsg::sendmsg; +pub use sock::{connect, socket}; +pub use sock_io::{recv, send}; diff --git a/userland/capsule_linux/src/linux/unix/msg.rs b/userland/capsule_linux/src/linux/unix/msg.rs new file mode 100644 index 000000000..7d6bcbe73 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/msg.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 . + + +//! `sendmsg` and `recvmsg` on the display socket. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +/// struct msghdr on x86_64. +const IOV_AT: usize = 16; +const IOVLEN_AT: usize = 24; +const CONTROL_AT: usize = 32; +const CONTROLLEN_AT: usize = 40; +pub const MSGHDR_LEN: usize = 56; + +pub struct Msg { + pub bytes: Vec, + pub fds: Vec, +} + +pub fn read_msghdr(guest: &Guest, at: u64) -> Option { + let raw = guest.read(at, MSGHDR_LEN)?; + let iov = u64le(&raw, IOV_AT); + let iovlen = u64le(&raw, IOVLEN_AT); + let control = u64le(&raw, CONTROL_AT); + let controllen = u64le(&raw, CONTROLLEN_AT); + let bytes = super::msg_parts::gather(guest, iov, iovlen)?; + let fds = super::msg_rights::rights(guest, control, controllen); + Some(Msg { bytes, fds }) +} + +pub(super) fn u64le(b: &[u8], at: usize) -> u64 { + let mut w = [0u8; 8]; + w.copy_from_slice(&b[at..at + 8]); + u64::from_le_bytes(w) +} diff --git a/userland/capsule_linux/src/linux/unix/msg_parts.rs b/userland/capsule_linux/src/linux/unix/msg_parts.rs new file mode 100644 index 000000000..aa8bf62d6 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/msg_parts.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 data half of a msghdr: the iovecs, joined. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::msg::u64le; + +/// The iovecs, joined. Each is a pointer and a length, sixteen bytes. +pub(super) fn gather(guest: &Guest, iov: u64, count: u64) -> Option> { + let mut out = Vec::new(); + for i in 0..count.min(64) { + let entry = guest.read(iov + i * 16, 16)?; + let base = u64le(&entry, 0); + let len = u64le(&entry, 8); + if len == 0 { + continue; + } + out.extend_from_slice(&guest.read(base, len.min(1 << 20) as usize)?); + } + Some(out) +} diff --git a/userland/capsule_linux/src/linux/unix/msg_rights.rs b/userland/capsule_linux/src/linux/unix/msg_rights.rs new file mode 100644 index 000000000..d8c280114 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/msg_rights.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! The descriptors in a control block. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::msg::u64le; + +/// struct cmsghdr: len, level, type, then the data. +const CMSG_DATA: usize = 16; +const SOL_SOCKET: u32 = 1; +const SCM_RIGHTS: u32 = 1; + +/// Every descriptor in an SCM_RIGHTS block. Anything else in the control +/// data is skipped rather than guessed at. +pub(super) fn rights(guest: &Guest, at: u64, len: u64) -> Vec { + let mut out = Vec::new(); + let Some(raw) = guest.read(at, len.min(1024) as usize) else { + return out; + }; + let mut off = 0usize; + while off + CMSG_DATA <= raw.len() { + let size = u64le(&raw, off) as usize; + let level = u32le(&raw, off + 8); + let kind = u32le(&raw, off + 12); + if size < CMSG_DATA || off + size > raw.len() { + break; + } + if level == SOL_SOCKET && kind == SCM_RIGHTS { + let mut at = off + CMSG_DATA; + while at + 4 <= off + size { + out.push(u32le(&raw, at)); + at += 4; + } + } + off += (size + 7) & !7; + } + out +} + +fn u32le(b: &[u8], at: usize) -> u32 { + let mut w = [0u8; 4]; + w.copy_from_slice(&b[at..at + 4]); + u32::from_le_bytes(w) +} diff --git a/userland/capsule_linux/src/linux/unix/path.rs b/userland/capsule_linux/src/linux/unix/path.rs new file mode 100644 index 000000000..834a5d096 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/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 . + + +//! `struct sockaddr_un` out of a guest, and which server it names. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +const AF_UNIX: u16 = 1; +/// family(2) then a 108 byte path. +const SUN_LEN: usize = 110; + +/// The display socket a Wayland client looks for. +const DISPLAY: &[u8] = b"wayland-0"; + +pub fn sun_path(guest: &Guest, at: u64, len: u64) -> Option> { + let take = (len as usize).min(SUN_LEN); + if take < 3 { + return None; + } + let raw = guest.read(at, take)?; + if u16::from_le_bytes([raw[0], raw[1]]) != AF_UNIX { + return None; + } + let body = &raw[2..]; + let end = body.iter().position(|b| *b == 0).unwrap_or(body.len()); + Some(body[..end].to_vec()) +} + +/// True when the path ends in the display socket's name, whatever +/// directory the client was told to look in. +pub fn is_display(path: &[u8]) -> bool { + path.ends_with(DISPLAY) +} diff --git a/userland/capsule_linux/src/linux/unix/recvmsg.rs b/userland/capsule_linux/src/linux/unix/recvmsg.rs new file mode 100644 index 000000000..a4448704b --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/recvmsg.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! `recvmsg` on the display socket. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; + +use super::msg::{u64le, MSGHDR_LEN}; +use super::sock::connected; + +/// The reply goes into the first iovec only. +pub fn recvmsg(guest: &mut Guest, fd: u64, at: u64) -> u64 { + if !connected(guest, fd) { + return errno::fail(errno::ENOTCONN); + } + let Some(raw) = guest.read(at, MSGHDR_LEN) else { + return errno::fail(errno::EFAULT); + }; + let Some((base, len)) = first_iov(guest, &raw) else { + return errno::fail(errno::EFAULT); + }; + crate::linux::wayland::pump(guest); + if let Some(block) = super::give::control(guest) { + super::give::place(guest, at, &block); + } + let bytes = guest.display.drain(len as usize); + if bytes.is_empty() { + return errno::fail(errno::EAGAIN); + } + if guest.write(base, &bytes) < bytes.len() as i64 { + return errno::fail(errno::EFAULT); + } + errno::ok(bytes.len() as u64) +} + +fn first_iov(guest: &Guest, hdr: &[u8]) -> Option<(u64, u64)> { + let iov = u64le(hdr, 16); + let count = u64le(hdr, 24); + if count == 0 { + return None; + } + let entry = guest.read(iov, 16)?; + Some((u64le(&entry, 0), u64le(&entry, 8))) +} diff --git a/userland/capsule_linux/src/linux/unix/sendmsg.rs b/userland/capsule_linux/src/linux/unix/sendmsg.rs new file mode 100644 index 000000000..a1f9ed4f2 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/sendmsg.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 . + + +//! `sendmsg` and `recvmsg`, which is how libwayland actually talks. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::wayland; + +use super::msg::read_msghdr; +use super::sock::connected; + +pub fn sendmsg(guest: &mut Guest, fd: u64, at: u64) -> u64 { + if !connected(guest, fd) { + return errno::fail(errno::ENOTCONN); + } + let Some(msg) = read_msghdr(guest, at) else { + return errno::fail(errno::EFAULT); + }; + let sent = msg.bytes.len() as u64; + guest.display.fds.extend_from_slice(&msg.fds); + guest.display.to_server.extend_from_slice(&msg.bytes); + wayland::serve(guest); + errno::ok(sent) +} diff --git a/userland/capsule_linux/src/linux/unix/sock.rs b/userland/capsule_linux/src/linux/unix/sock.rs new file mode 100644 index 000000000..2e290cd5e --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/sock.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 four calls a client makes on a display socket. + +use crate::linux::abi::errno; +use crate::linux::guest::{Fd, Guest, Kind}; + +use super::path::{is_display, sun_path}; + +const SOCK_STREAM: u64 = 1; +const TYPE_MASK: u64 = 0xFF; + +pub fn socket(guest: &mut Guest, kind: u64) -> u64 { + if kind & TYPE_MASK != SOCK_STREAM { + return errno::fail(errno::ENOSYS); + } + match crate::linux::file::install(guest, Fd::unix()) { + Some(n) => errno::ok(n), + None => errno::fail(errno::EMFILE), + } +} + +pub fn connect(guest: &mut Guest, fd: u64, at: u64, len: u64) -> u64 { + let Some(path) = sun_path(guest, at, len) else { + return errno::fail(errno::EINVAL); + }; + if !is_display(&path) { + /* + * Nothing else listens in here, and a client that reaches a socket + * which silently accepts would block forever on a reply. + */ + return errno::fail(errno::ECONNREFUSED); + } + match guest.fds.get_mut(fd as usize) { + Some(entry) if entry.kind == Kind::Unix => { + entry.writable = true; + errno::ok(0) + } + _ => errno::fail(errno::ENOTSOCK), + } +} + +/// True when this descriptor is one of ours rather than a network one. +pub fn is_unix(guest: &Guest, fd: u64) -> bool { + matches!(guest.fds.get(fd as usize), Some(f) if f.kind == Kind::Unix) +} + +pub(super) fn connected(guest: &Guest, fd: u64) -> bool { + matches!(guest.fds.get(fd as usize), Some(f) if f.kind == Kind::Unix && f.writable) +} diff --git a/userland/capsule_linux/src/linux/unix/sock_io.rs b/userland/capsule_linux/src/linux/unix/sock_io.rs new file mode 100644 index 000000000..a4ec74032 --- /dev/null +++ b/userland/capsule_linux/src/linux/unix/sock_io.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 . + + +//! Bytes to and from the display socket. + +use crate::linux::abi::errno; +use crate::linux::guest::Guest; +use crate::linux::wayland; + +use super::sock::connected; + +/// A write is a batch of requests. +pub fn send(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { + let take = len.min(1 << 20); + let Some(bytes) = guest.read(buf, take as usize) else { + return errno::fail(errno::EFAULT); + }; + if !connected(guest, fd) { + return errno::fail(errno::ENOTCONN); + } + guest.display.to_server.extend_from_slice(&bytes); + wayland::serve(guest); + errno::ok(take) +} + +pub fn recv(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 { + if !connected(guest, fd) { + return errno::fail(errno::ENOTCONN); + } + wayland::pump(guest); + let bytes = guest.display.drain(len as usize); + if bytes.is_empty() { + // Non-blocking is what a toolkit asks for; it polls. + return errno::fail(errno::EAGAIN); + } + if guest.write(buf, &bytes) < bytes.len() as i64 { + return errno::fail(errno::EFAULT); + } + errno::ok(bytes.len() as u64) +} diff --git a/userland/capsule_linux/src/linux/wayland/args.rs b/userland/capsule_linux/src/linux/wayland/args.rs new file mode 100644 index 000000000..14b8453fb --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/args.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 . + + +//! The argument cursor. + +pub struct Args<'a> { + at: usize, + body: &'a [u8], +} + +impl<'a> Args<'a> { + pub fn new(body: &'a [u8]) -> Args<'a> { + Args { at: 0, body } + } + + pub fn u32(&mut self) -> Option { + let w = self.body.get(self.at..self.at + 4)?; + self.at += 4; + Some(u32::from_le_bytes([w[0], w[1], w[2], w[3]])) + } + + /// The bytes without the terminator. The field on the wire is padded + /// to a word and the cursor skips the padding, never the caller. + pub fn string(&mut self) -> Option<&'a [u8]> { + let len = self.u32()? as usize; + if len == 0 { + return Some(&[]); + } + let text = self.body.get(self.at..self.at + len - 1)?; + self.at += (len + 3) & !3; + Some(text) + } +} diff --git a/userland/capsule_linux/src/linux/wayland/buffer.rs b/userland/capsule_linux/src/linux/wayland/buffer.rs new file mode 100644 index 000000000..fd3caff95 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/buffer.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! `wl_shm_pool.create_buffer`: a rectangle inside a pool. + +use crate::linux::guest::Guest; + +use super::args::Args; +use super::object::Object; +use super::state::Buffer; + +/// Bytes per pixel in both formats this capsule advertises. +const BPP: u64 = 4; + +pub fn create_buffer(guest: &mut Guest, pool: u32, args: &mut Args<'_>) { + let (Some(id), Some(offset), Some(width), Some(height), Some(stride), Some(_fmt)) = + (args.u32(), args.u32(), args.u32(), args.u32(), args.u32(), args.u32()) + else { + return; + }; + let Some(size) = guest.scene.pools.iter().find(|p| p.id == pool).map(|p| p.size) else { + return; + }; + if !fits(offset as u64, width as u64, height as u64, stride as u64, size) { + return; + } + guest.objects.put(id, Object::Buffer); + guest.scene.buffers.push(Buffer { id, pool, offset: offset as u64, width, height, stride }); +} + +/// Whether the rectangle lies wholly inside a pool of `size` bytes. +fn fits(offset: u64, width: u64, height: u64, stride: u64, size: u64) -> bool { + if width == 0 || height == 0 { + return false; + } + let Some(row) = width.checked_mul(BPP) else { + return false; + }; + if stride < row { + return false; + } + let Some(span) = stride.checked_mul(height) else { + return false; + }; + matches!(offset.checked_add(span), Some(end) if end <= size) +} diff --git a/userland/capsule_linux/src/linux/wayland/commit.rs b/userland/capsule_linux/src/linux/wayland/commit.rs new file mode 100644 index 000000000..9a715d24b --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/commit.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 . + + +//! `wl_surface.commit`: the only request that reaches the screen. + +use alloc::vec::Vec; + +use crate::linux::guest::Guest; + +use super::ops::ev; +use super::out::Event; +use super::present::present; + +/// A commit with a buffer is the only thing that reaches the screen. +pub fn commit(guest: &mut Guest, id: u32) { + let Some(buffer) = surface_buffer(guest, id) else { + return; + }; + present(guest, buffer); + Event::new(buffer, ev::BUFFER_RELEASE).send(&mut guest.display.to_client); + let owed = take_frames(guest, id); + for callback in owed { + Event::new(callback, ev::CALLBACK_DONE).u32(0).send(&mut guest.display.to_client); + } +} + +fn surface_buffer(guest: &Guest, id: u32) -> Option { + guest.scene.surfaces.iter().find(|s| s.id == id)?.pending +} + +fn take_frames(guest: &mut Guest, id: u32) -> Vec { + match guest.scene.surfaces.iter_mut().find(|s| s.id == id) { + Some(s) => core::mem::take(&mut s.frames), + None => Vec::new(), + } +} diff --git a/userland/capsule_linux/src/linux/wayland/handlers.rs b/userland/capsule_linux/src/linux/wayland/handlers.rs new file mode 100644 index 000000000..f3bc0a341 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/handlers.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 . + + +//! The three requests a client makes before it asks for anything real. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::out::Event; +use super::registry::{by_name, GLOBALS}; +use super::args::Args; + +const CALLBACK_DONE: u16 = 0; +const REGISTRY_GLOBAL: u16 = 0; +const SEAT_CAPABILITIES: u16 = 0; +/// A pointer and a keyboard, which is what the input events carry. +const SEAT_POINTER_AND_KEYBOARD: u32 = 3; + +/// Nothing here is asynchronous, so a sync is done the moment it is +/// asked for and the callback fires in the same batch. +pub fn sync(guest: &mut Guest, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + Event::new(id, CALLBACK_DONE).u32(0).send(&mut guest.display.to_client); +} + +pub fn registry(guest: &mut Guest, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + guest.objects.put(id, Object::Registry); + for global in GLOBALS { + Event::new(id, REGISTRY_GLOBAL) + .u32(global.name) + .string(global.interface) + .u32(global.version) + .send(&mut guest.display.to_client); + } +} + +fn seat_caps(guest: &mut Guest, id: u32) { + Event::new(id, SEAT_CAPABILITIES) + .u32(SEAT_POINTER_AND_KEYBOARD) + .send(&mut guest.display.to_client); +} + +pub fn bind(guest: &mut Guest, args: &mut Args<'_>) { + let (Some(name), Some(_iface), Some(_ver), Some(id)) = + (args.u32(), args.string(), args.u32(), args.u32()) + else { + return; + }; + let Some(global) = by_name(name) else { return }; + guest.objects.put(id, global.object); + match global.object { + /* + * A client reads the format list before it asks for a pool, and + * a seat's capabilities before it asks for a keyboard. Both are + */ + Object::Shm => crate::linux::wayland::shm::formats(guest, id), + Object::Seat => seat_caps(guest, id), + _ => {} + } +} diff --git a/userland/capsule_linux/src/linux/wayland/input.rs b/userland/capsule_linux/src/linux/wayland/input.rs new file mode 100644 index 000000000..83ae31ab0 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/input.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! NONOS input events, as Wayland sees them. + +use nonos_libc::{mk_input_event_drain, InputEvent}; + +use crate::linux::guest::Guest; + +use super::input_key::key; +use super::input_send::{button, motion}; + +/// Events taken in one pass. A deeper backlog is drained on the next. +const BATCH: usize = 32; + +const KEY_DOWN: u16 = 0; +const KEY_UP: u16 = 1; +const POINTER_ABS: u16 = 3; +const BUTTON_DOWN: u16 = 5; +const BUTTON_UP: u16 = 6; + +pub fn pump(guest: &mut Guest) { + if guest.scene.pointer.is_none() && guest.scene.keyboard.is_none() { + return; + } + let mut events = [InputEvent::default(); BATCH]; + let n = mk_input_event_drain(events.as_mut_ptr(), BATCH as u64); + if n <= 0 { + return; + } + for event in events.iter().take(n as usize) { + deliver(guest, event); + } +} + +fn deliver(guest: &mut Guest, event: &InputEvent) { + match event.kind { + KEY_DOWN => key(guest, event.code, 1), + KEY_UP => key(guest, event.code, 0), + POINTER_ABS => motion(guest, event.x, event.y), + BUTTON_DOWN => button(guest, event.code, 1), + BUTTON_UP => button(guest, event.code, 0), + _ => {} + } +} diff --git a/userland/capsule_linux/src/linux/wayland/input_enter.rs b/userland/capsule_linux/src/linux/wayland/input_enter.rs new file mode 100644 index 000000000..5fcbd1047 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/input_enter.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 enter events a client needs before it will act on input. + +use crate::linux::guest::Guest; + +use super::out::Event; + +const POINTER_ENTER: u16 = 0; +const KEYBOARD_ENTER: u16 = 1; + +fn fixed(value: i32) -> u32 { + (value << 8) as u32 +} + +pub fn enter_once(guest: &mut Guest, pointer: u32, x: i32, y: i32) { + if guest.scene.pointer_entered { + return; + } + let Some(surface) = guest.scene.surfaces.first().map(|s| s.id) else { + return; + }; + let serial = guest.scene.next_serial(); + Event::new(pointer, POINTER_ENTER) + .u32(serial) + .u32(surface) + .u32(fixed(x)) + .u32(fixed(y)) + .send(&mut guest.display.to_client); + guest.scene.pointer_entered = true; +} + +/// The keys array is empty: nothing is held down at the moment a client is +/// told it has focus, and claiming otherwise would leave it with a key stuck +/// until the matching release it never saw. +pub fn keyboard_enter_once(guest: &mut Guest, keyboard: u32) { + if guest.scene.keyboard_entered { + return; + } + let Some(surface) = guest.scene.surfaces.first().map(|s| s.id) else { + return; + }; + let serial = guest.scene.next_serial(); + Event::new(keyboard, KEYBOARD_ENTER) + .u32(serial) + .u32(surface) + .u32(0) + .send(&mut guest.display.to_client); + guest.scene.keyboard_entered = true; +} diff --git a/userland/capsule_linux/src/linux/wayland/input_key.rs b/userland/capsule_linux/src/linux/wayland/input_key.rs new file mode 100644 index 000000000..8b86347d6 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/input_key.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 . + + +//! A key event, written to the client. + +use crate::linux::guest::Guest; + +use super::input_enter::keyboard_enter_once; +use super::out::Event; + +/// wl_keyboard: key is 3. +const KEYBOARD_KEY: u16 = 3; + +pub fn key(guest: &mut Guest, code: u32, state: u32) { + let Some(id) = guest.scene.keyboard else { return }; + keyboard_enter_once(guest, id); + let serial = guest.scene.next_serial(); + let time = time_ms(); + Event::new(id, KEYBOARD_KEY) + .u32(serial) + .u32(time) + .u32(code) + .u32(state) + .send(&mut guest.display.to_client); +} + +fn time_ms() -> u32 { + nonos_libc::mk_uptime_ms().max(0) as u32 +} diff --git a/userland/capsule_linux/src/linux/wayland/input_send.rs b/userland/capsule_linux/src/linux/wayland/input_send.rs new file mode 100644 index 000000000..fddb4ef8d --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/input_send.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! One input event, written to the client. + +use crate::linux::guest::Guest; + +use super::input_enter::enter_once; +use super::out::Event; + +/// wl_pointer: motion 2, button 3, frame 5. +const POINTER_MOTION: u16 = 2; +const POINTER_BUTTON: u16 = 3; +const POINTER_FRAME: u16 = 5; + +/// Wayland carries a surface coordinate as 24.8 fixed point. +fn fixed(value: i32) -> u32 { + (value << 8) as u32 +} + +pub fn motion(guest: &mut Guest, x: i32, y: i32) { + let Some(id) = guest.scene.pointer else { return }; + enter_once(guest, id, x, y); + let time = time_ms(); + Event::new(id, POINTER_MOTION) + .u32(time) + .u32(fixed(x)) + .u32(fixed(y)) + .send(&mut guest.display.to_client); + Event::new(id, POINTER_FRAME).send(&mut guest.display.to_client); +} + +pub fn button(guest: &mut Guest, code: u32, state: u32) { + let Some(id) = guest.scene.pointer else { return }; + let serial = guest.scene.next_serial(); + let time = time_ms(); + Event::new(id, POINTER_BUTTON) + .u32(serial) + .u32(time) + .u32(code) + .u32(state) + .send(&mut guest.display.to_client); + Event::new(id, POINTER_FRAME).send(&mut guest.display.to_client); +} + +fn time_ms() -> u32 { + nonos_libc::mk_uptime_ms().max(0) as u32 +} diff --git a/userland/capsule_linux/src/linux/wayland/keymap.rs b/userland/capsule_linux/src/linux/wayland/keymap.rs new file mode 100644 index 000000000..f9560fd6f --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/keymap.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 . + + +//! `wl_keyboard.keymap`. + +use crate::linux::file::install; +use crate::linux::guest::{Fd, Guest}; + +use super::keymap_file::load; +use super::out::Event; + +const KEYBOARD_KEYMAP: u16 = 0; +const FORMAT_NO_KEYMAP: u32 = 0; +const FORMAT_XKB_V1: u32 = 1; + +pub fn send(guest: &mut Guest, keyboard: u32) { + let text = load(); + let (format, size) = match &text { + Some(bytes) => (FORMAT_XKB_V1, bytes.len() as u32 + 1), + None => (FORMAT_NO_KEYMAP, 1), + }; + let Some(fd) = install(guest, Fd::memfd()) else { + return; + }; + if let Some(entry) = guest.fds.get_mut(fd as usize) { + entry.size = size as u64; + if let Some(bytes) = text { + entry.pending = bytes; + entry.pending.push(0); + } + } + guest.display.give.push(fd as u32); + Event::new(keyboard, KEYBOARD_KEYMAP) + .u32(format) + .u32(size) + .send(&mut guest.display.to_client); +} diff --git a/userland/capsule_linux/src/linux/wayland/keymap_file.rs b/userland/capsule_linux/src/linux/wayland/keymap_file.rs new file mode 100644 index 000000000..80ec04e05 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/keymap_file.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 . + + +//! Reading the keymap out of the store, once. + +use alloc::vec::Vec; + +use nonos_app_skeleton::clients::vfs::read_file; +use nonos_libc::{mk_debug, mk_getpid}; + +/// Where a keymap is installed. Produced by xkbcli compile-keymap and +/// packed into the store like any other data file. +const PATH: &[u8] = b"/usr/share/nonos/keymap.xkb"; + +/// An xkb keymap is tens of kilobytes; anything far larger is not one. +const MAX: u32 = 1 << 20; + +pub fn load() -> Option> { + match read_file(mk_getpid(), PATH, MAX) { + Ok(bytes) if !bytes.is_empty() => Some(bytes), + _ => { + let line = b"[WAYLAND] no keymap in the store, keys will carry no symbols\n"; + let _ = mk_debug(line.as_ptr(), line.len()); + None + } + } +} diff --git a/userland/capsule_linux/src/linux/wayland/mod.rs b/userland/capsule_linux/src/linux/wayland/mod.rs new file mode 100644 index 000000000..874b1d63f --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/mod.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 display server a Wayland client finds when it connects. + +mod args; +mod buffer; +mod commit; +mod handlers; +mod input; +mod input_enter; +mod input_key; +mod input_send; +mod object; +mod ops; +mod keymap; +mod keymap_file; +mod scene; +mod seat; +mod present; +mod present_surface; +pub mod shm; +mod surface; +mod xdg; +mod state; +mod out; +mod registry; +mod route; +mod serve; +mod unserved; +mod wire; + +pub use object::Objects; +pub use scene::Scene; +pub use input::pump; +pub use serve::serve; diff --git a/userland/capsule_linux/src/linux/wayland/object.rs b/userland/capsule_linux/src/linux/wayland/object.rs new file mode 100644 index 000000000..59bbec153 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/object.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 client's object table. + +use alloc::vec::Vec; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Object { + Display, + Registry, + Callback, + Compositor, + Shm, + ShmPool, + Buffer, + Surface, + XdgBase, + XdgSurface, + XdgToplevel, + Seat, + Pointer, + Keyboard, +} + +pub struct Objects { + slots: Vec<(u32, Object)>, +} + +impl Objects { + /// Object 1 is the display, always, before a client says anything. + pub fn new() -> Objects { + Objects { slots: alloc::vec![(1, Object::Display)] } + } + + pub fn get(&self, id: u32) -> Option { + self.slots.iter().find(|(k, _)| *k == id).map(|(_, v)| *v) + } + + pub fn put(&mut self, id: u32, what: Object) { + match self.slots.iter_mut().find(|(k, _)| *k == id) { + Some(slot) => slot.1 = what, + None => self.slots.push((id, what)), + } + } + + pub fn drop_id(&mut self, id: u32) { + self.slots.retain(|(k, _)| *k != id); + } +} + +impl Default for Objects { + fn default() -> Objects { + Objects::new() + } +} diff --git a/userland/capsule_linux/src/linux/wayland/ops.rs b/userland/capsule_linux/src/linux/wayland/ops.rs new file mode 100644 index 000000000..5a5a69f62 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/ops.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 . + + +//! Request and event opcodes, transcribed from the protocol definitions. + +pub mod req { + pub const DISPLAY_SYNC: u16 = 0; + pub const DISPLAY_GET_REGISTRY: u16 = 1; + pub const REGISTRY_BIND: u16 = 0; + + pub const COMPOSITOR_CREATE_SURFACE: u16 = 0; + + pub const SHM_CREATE_POOL: u16 = 0; + pub const POOL_CREATE_BUFFER: u16 = 0; + pub const POOL_DESTROY: u16 = 1; + pub const BUFFER_DESTROY: u16 = 0; + + pub const SURFACE_DESTROY: u16 = 0; + pub const SURFACE_ATTACH: u16 = 1; + pub const SURFACE_DAMAGE: u16 = 2; + pub const SURFACE_FRAME: u16 = 3; + pub const SURFACE_COMMIT: u16 = 6; + pub const SURFACE_DAMAGE_BUFFER: u16 = 9; + + pub const XDG_BASE_GET_SURFACE: u16 = 2; + pub const XDG_BASE_PONG: u16 = 3; + pub const XDG_SURFACE_GET_TOPLEVEL: u16 = 1; + pub const XDG_SURFACE_SET_GEOMETRY: u16 = 3; + pub const XDG_SURFACE_ACK_CONFIGURE: u16 = 4; + pub const TOPLEVEL_SET_TITLE: u16 = 2; + pub const TOPLEVEL_SET_APP_ID: u16 = 3; + + pub const SEAT_GET_POINTER: u16 = 0; + pub const SEAT_GET_KEYBOARD: u16 = 1; +} + +pub mod ev { + pub const CALLBACK_DONE: u16 = 0; + pub const SHM_FORMAT: u16 = 0; + pub const BUFFER_RELEASE: u16 = 0; + pub const XDG_SURFACE_CONFIGURE: u16 = 0; + pub const TOPLEVEL_CONFIGURE: u16 = 0; +} + +/// wl_shm formats. Zero and one are the two every client understands. +pub const FORMAT_ARGB8888: u32 = 0; +pub const FORMAT_XRGB8888: u32 = 1; diff --git a/userland/capsule_linux/src/linux/wayland/out.rs b/userland/capsule_linux/src/linux/wayland/out.rs new file mode 100644 index 000000000..e54a7cd91 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/out.rs @@ -0,0 +1,59 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! Building an event for the client. + +use alloc::vec::Vec; + +use super::wire::HEADER; + +pub struct Event { + body: Vec, +} + +impl Event { + pub fn new(object: u32, opcode: u16) -> Event { + let mut body = Vec::with_capacity(HEADER + 16); + body.extend_from_slice(&object.to_le_bytes()); + body.extend_from_slice(&opcode.to_le_bytes()); + body.extend_from_slice(&0u16.to_le_bytes()); + Event { body } + } + + pub fn u32(mut self, value: u32) -> Event { + self.body.extend_from_slice(&value.to_le_bytes()); + self + } + + /// A string is its length including the terminator, the bytes, the + /// terminator, then zeroes up to the next word. + pub fn string(mut self, text: &[u8]) -> Event { + self.body.extend_from_slice(&(text.len() as u32 + 1).to_le_bytes()); + self.body.extend_from_slice(text); + self.body.push(0); + while !self.body.len().is_multiple_of(4) { + self.body.push(0); + } + self + } + + pub fn send(mut self, to_client: &mut Vec) { + let size = self.body.len() as u16; + self.body[6..8].copy_from_slice(&size.to_le_bytes()); + to_client.extend_from_slice(&self.body); + } +} diff --git a/userland/capsule_linux/src/linux/wayland/present.rs b/userland/capsule_linux/src/linux/wayland/present.rs new file mode 100644 index 000000000..3e574c469 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/present.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 . + +//! Getting the client's pixels onto a NONOS surface. + +use crate::linux::guest::Guest; + +use super::present_surface::surface; +use super::scene::Scene; + +pub fn present(guest: &mut Guest, buffer: u32) { + let Some((at, width, height, stride, bytes)) = source(&guest.scene, buffer) else { + return; + }; + /* + * The descriptor handed to the kernel holds this buffer's address, so it + * is allocated once and written in place afterwards. + */ + if guest.scene.pixels.len() < bytes { + if guest.scene.out.is_some() { + return; + } + guest.scene.pixels.resize(bytes, 0); + } + let Some(src) = guest.read(at, bytes) else { + return; + }; + guest.scene.pixels[..bytes].copy_from_slice(&src); + if let Some(handle) = surface(&mut guest.scene, width, height, stride) { + let _ = nonos_libc::mk_surface_present_rect(handle, 0, 0, width, height); + } +} + +/// Where the pixels are, how they are shaped, and how many bytes that is. +fn source(scene: &Scene, buffer: u32) -> Option<(u64, u32, u32, u32, usize)> { + let b = scene.buffers.iter().find(|b| b.id == buffer)?; + let pool = scene.pools.iter().find(|p| p.id == b.pool)?; + let at = pool.at.checked_add(b.offset)?; + let bytes = (b.stride as u64).checked_mul(b.height as u64)?; + if bytes == 0 || b.offset.checked_add(bytes)? > pool.size { + return None; + } + Some((at, b.width, b.height, b.stride, bytes as usize)) +} diff --git a/userland/capsule_linux/src/linux/wayland/present_surface.rs b/userland/capsule_linux/src/linux/wayland/present_surface.rs new file mode 100644 index 000000000..31fe9f6c8 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/present_surface.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 . + + +//! Registering the NONOS surface the pixels land on. + +use nonos_app_skeleton::clients::compositor::scene_submit; +use nonos_app_skeleton::discover::lookup_port; +use nonos_libc::{mk_surface_register, SurfaceDescriptor}; + +use super::scene::Scene; + +/// SURFACE_FORMAT_ARGB8888 in the surface registry, which is one there +/// and zero in wl_shm. The two numbers are unrelated and both are right. +const FORMAT_ARGB8888: u32 = 1; + +/// Registered once, on the first commit: its size is the first buffer's, +/// and a client does not say before then. +pub fn surface(scene: &mut Scene, width: u32, height: u32, stride: u32) -> Option { + if let Some(handle) = scene.out { + return Some(handle); + } + let desc = SurfaceDescriptor { + width, + height, + stride, + format: FORMAT_ARGB8888, + byte_len: scene.pixels.len() as u64, + base_va: scene.pixels.as_ptr() as u64, + flags: 0, + }; + let handle = mk_surface_register(&desc); + if handle < 0 { + return None; + } + scene.out = Some(handle as u64); + place(handle as u64, width, height); + Some(handle as u64) +} + +/// Registering a surface makes it exist; the compositor still has to be +/// told where it goes, or it is never drawn. +fn place(handle: u64, width: u32, height: u32) { + let Some(port) = lookup_port(b"compositor") else { + return; + }; + let _ = scene_submit(port, 1, handle, 0, 0, width, height, 0); +} diff --git a/userland/capsule_linux/src/linux/wayland/registry.rs b/userland/capsule_linux/src/linux/wayland/registry.rs new file mode 100644 index 000000000..efb660c64 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/registry.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 . + + +//! The globals a client is told about, and what binding one means. + +use super::object::Object; + +pub struct Global { + pub name: u32, + pub interface: &'static [u8], + pub version: u32, + pub object: Object, +} + +pub const GLOBALS: &[Global] = &[ + Global { name: 1, interface: b"wl_compositor", version: 4, object: Object::Compositor }, + Global { name: 2, interface: b"wl_shm", version: 1, object: Object::Shm }, + Global { name: 3, interface: b"xdg_wm_base", version: 2, object: Object::XdgBase }, + Global { name: 4, interface: b"wl_seat", version: 5, object: Object::Seat }, +]; + +pub fn by_name(name: u32) -> Option<&'static Global> { + GLOBALS.iter().find(|g| g.name == name) +} diff --git a/userland/capsule_linux/src/linux/wayland/route.rs b/userland/capsule_linux/src/linux/wayland/route.rs new file mode 100644 index 000000000..72099ebf8 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/route.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! Which handler a request belongs to. False means nothing served it. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::ops::req; +use super::serve::is_destructor; +use super::args::Args; +use super::{buffer, commit, handlers, seat, shm, surface, xdg}; + +pub fn route( + guest: &mut Guest, + object: Option, + id: u32, + opcode: u16, + args: &mut Args<'_>, +) -> bool { + match (object, opcode) { + (Some(Object::Display), req::DISPLAY_SYNC) => handlers::sync(guest, args), + (Some(Object::Display), req::DISPLAY_GET_REGISTRY) => handlers::registry(guest, args), + (Some(Object::Registry), req::REGISTRY_BIND) => handlers::bind(guest, args), + (Some(Object::Compositor), req::COMPOSITOR_CREATE_SURFACE) => surface::create(guest, args), + (Some(Object::Shm), req::SHM_CREATE_POOL) => shm::create_pool(guest, args), + (Some(Object::ShmPool), req::POOL_CREATE_BUFFER) => buffer::create_buffer(guest, id, args), + (Some(Object::Surface), req::SURFACE_ATTACH) => surface::attach(guest, id, args), + (Some(Object::Surface), req::SURFACE_FRAME) => surface::frame(guest, id, args), + (Some(Object::Surface), req::SURFACE_COMMIT) => commit::commit(guest, id), + (Some(Object::Surface), req::SURFACE_DAMAGE | req::SURFACE_DAMAGE_BUFFER) => {} + (Some(Object::XdgBase), req::XDG_BASE_GET_SURFACE) => xdg::get_xdg_surface(guest, args), + (Some(Object::XdgBase), req::XDG_BASE_PONG) => {} + (Some(Object::XdgSurface), req::XDG_SURFACE_GET_TOPLEVEL) => { + xdg::get_toplevel(guest, id, args) + } + (Some(Object::XdgSurface), req::XDG_SURFACE_ACK_CONFIGURE) => { + xdg::ack_configure(guest, id, args) + } + (Some(Object::XdgSurface), req::XDG_SURFACE_SET_GEOMETRY) => {} + (Some(Object::XdgToplevel), req::TOPLEVEL_SET_TITLE | req::TOPLEVEL_SET_APP_ID) => {} + (Some(Object::Seat), req::SEAT_GET_POINTER) => seat::get_pointer(guest, args), + (Some(Object::Seat), req::SEAT_GET_KEYBOARD) => seat::get_keyboard(guest, args), + (o, c) if is_destructor(o, c) => guest.objects.drop_id(id), + _ => return false, + } + true +} diff --git a/userland/capsule_linux/src/linux/wayland/scene.rs b/userland/capsule_linux/src/linux/wayland/scene.rs new file mode 100644 index 000000000..8331c7c17 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/scene.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 client's scene, and the NONOS surface it ends up on. + +use alloc::vec::Vec; + +use super::state::{Buffer, Pool, Surface}; + +pub struct Scene { + pub pools: Vec, + pub buffers: Vec, + pub surfaces: Vec, + /// Serial for configure events, which a client echoes back. + pub serial: u32, + /// The NONOS surface the client's pixels end up on. + pub out: Option, + /// This capsule's own copy of those pixels, which the surface + /// descriptor points at. + pub pixels: Vec, + pub pointer: Option, + pub keyboard: Option, + pub pointer_entered: bool, + pub keyboard_entered: bool, +} + +impl Scene { + pub fn new() -> Scene { + Scene { + pools: Vec::new(), + buffers: Vec::new(), + surfaces: Vec::new(), + serial: 1, + out: None, + pixels: Vec::new(), + pointer: None, + keyboard: None, + pointer_entered: false, + keyboard_entered: false, + } + } + + pub fn next_serial(&mut self) -> u32 { + self.serial += 1; + self.serial + } +} + +impl Default for Scene { + fn default() -> Scene { + Scene::new() + } +} diff --git a/userland/capsule_linux/src/linux/wayland/seat.rs b/userland/capsule_linux/src/linux/wayland/seat.rs new file mode 100644 index 000000000..332d26059 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/seat.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 . + + +//! `wl_seat`: the pointer and keyboard objects a client asks for. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::args::Args; + +pub fn get_pointer(guest: &mut Guest, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + guest.objects.put(id, Object::Pointer); + guest.scene.pointer = Some(id); +} + +pub fn get_keyboard(guest: &mut Guest, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + guest.objects.put(id, Object::Keyboard); + guest.scene.keyboard = Some(id); + super::keymap::send(guest, id); +} diff --git a/userland/capsule_linux/src/linux/wayland/serve.rs b/userland/capsule_linux/src/linux/wayland/serve.rs new file mode 100644 index 000000000..6b480e7b2 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/serve.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 . + + +//! Draining the client's requests and answering what is understood. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::ops::req; +use super::route::route; +use super::unserved::unserved; +use super::args::Args; +use super::wire::next; + +pub fn serve(guest: &mut Guest) { + while one(guest).is_some() {} +} + +/// One message. It leaves the queue before it is served, so a handler +/// that refuses cannot leave it there to be served again forever. +fn one(guest: &mut Guest) -> Option<()> { + let (object, opcode, id, body, size) = { + let (msg, size) = next(&guest.display.to_server)?; + (guest.objects.get(msg.object), msg.opcode, msg.object, msg.args.to_vec(), size) + }; + guest.display.to_server.drain(..size); + let mut args = Args::new(&body); + if !route(guest, object, id, opcode, &mut args) { + unserved(object, opcode); + } + Some(()) +} + +/// Requests that only remove something. +pub fn is_destructor(object: Option, opcode: u16) -> bool { + matches!( + (object, opcode), + (Some(Object::Buffer), req::BUFFER_DESTROY) + | (Some(Object::ShmPool), req::POOL_DESTROY) + | (Some(Object::Surface), req::SURFACE_DESTROY) + ) +} diff --git a/userland/capsule_linux/src/linux/wayland/shm.rs b/userland/capsule_linux/src/linux/wayland/shm.rs new file mode 100644 index 000000000..7f1645812 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/shm.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 . + +//! `wl_shm`: the pool a client draws into, and the buffers inside it. + +use crate::linux::file::mapped_at; +use crate::linux::guest::Guest; + +use super::args::Args; +use super::object::Object; +use super::ops::{ev, FORMAT_ARGB8888, FORMAT_XRGB8888}; +use super::out::Event; +use super::state::Pool; + +/// A client reads the format list before it asks for anything, and one it was +/// not offered is a protocol error on its side, so only the two that are +/// actually blitted are advertised. +pub fn formats(guest: &mut Guest, id: u32) { + for format in [FORMAT_ARGB8888, FORMAT_XRGB8888] { + Event::new(id, ev::SHM_FORMAT).u32(format).send(&mut guest.display.to_client); + } +} + +/// The descriptor was passed in the control data of the sendmsg that +/// carried this request, so it is taken from the queue in order. +pub fn create_pool(guest: &mut Guest, args: &mut Args<'_>) { + let (Some(id), Some(_fd_slot), Some(size)) = (args.u32(), args.u32(), args.u32()) else { + return; + }; + let Some(fd) = take_fd(guest) else { + return; + }; + let Some((at, _)) = mapped_at(guest, fd as u64) else { + /* + * A pool over a descriptor the client never mapped has no pixels to + * read, and reading zero would show a black window rather than say + * why. + */ + return; + }; + /* + * The declared size, bounded by what the client actually has at that + * address. + */ + let size = (size as u64).min(guest.mapped_from(at)); + if size == 0 { + return; + } + guest.objects.put(id, Object::ShmPool); + guest.scene.pools.push(Pool { id, at, size }); +} + +fn take_fd(guest: &mut Guest) -> Option { + match guest.display.fds.is_empty() { + true => None, + false => Some(guest.display.fds.remove(0)), + } +} diff --git a/userland/capsule_linux/src/linux/wayland/state.rs b/userland/capsule_linux/src/linux/wayland/state.rs new file mode 100644 index 000000000..057669d07 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/state.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 . + +//! What the client has built: its pools, its buffers, its surfaces. + +use alloc::vec::Vec; + +pub struct Pool { + pub id: u32, + /// Where the client's pixels live in its own address space. + pub at: u64, + /// How many bytes are really there. + pub size: u64, +} + +pub struct Buffer { + pub id: u32, + pub pool: u32, + pub offset: u64, + pub width: u32, + pub height: u32, + pub stride: u32, +} + +pub struct Surface { + pub id: u32, + /// The buffer attached but not yet committed. + pub pending: Option, + /// The xdg_surface wrapping it, once the client asks for one. + pub xdg: Option, + /// Frame callbacks owed, answered after the next present. + pub frames: Vec, + /// Set once a configure has been sent and acked. + pub configured: bool, +} diff --git a/userland/capsule_linux/src/linux/wayland/surface.rs b/userland/capsule_linux/src/linux/wayland/surface.rs new file mode 100644 index 000000000..461a81029 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/surface.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 . + + +//! `wl_surface`: what the client is drawing, and when it says so. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::state::Surface; +use super::args::Args; + +pub fn create(guest: &mut Guest, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + guest.objects.put(id, Object::Surface); + guest.scene.surfaces.push(Surface { + id, + pending: None, + xdg: None, + frames: alloc::vec::Vec::new(), + configured: false, + }); +} + +pub fn attach(guest: &mut Guest, id: u32, args: &mut Args<'_>) { + let Some(buffer) = args.u32() else { return }; + if let Some(s) = guest.scene.surfaces.iter_mut().find(|s| s.id == id) { + s.pending = match buffer { + 0 => None, + b => Some(b), + }; + } +} + +pub fn frame(guest: &mut Guest, id: u32, args: &mut Args<'_>) { + let Some(callback) = args.u32() else { return }; + guest.objects.put(callback, Object::Callback); + if let Some(s) = guest.scene.surfaces.iter_mut().find(|s| s.id == id) { + s.frames.push(callback); + } +} diff --git a/userland/capsule_linux/src/linux/wayland/unserved.rs b/userland/capsule_linux/src/linux/wayland/unserved.rs new file mode 100644 index 000000000..782bd1cc7 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/unserved.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 . + + +//! Naming a request this shim does not serve yet. + +use super::object::Object; + +pub fn name(what: Object) -> &'static [u8] { + match what { + Object::Display => b"wl_display", + Object::Registry => b"wl_registry", + Object::Callback => b"wl_callback", + Object::Compositor => b"wl_compositor", + Object::Shm => b"wl_shm", + Object::ShmPool => b"wl_shm_pool", + Object::Buffer => b"wl_buffer", + Object::Surface => b"wl_surface", + Object::XdgBase => b"xdg_wm_base", + Object::XdgSurface => b"xdg_surface", + Object::XdgToplevel => b"xdg_toplevel", + Object::Seat => b"wl_seat", + Object::Pointer => b"wl_pointer", + Object::Keyboard => b"wl_keyboard", + } +} + +/// A client that stalls should leave behind the interface and opcode it +/// was waiting on, because a protocol stall has no other symptom. +pub fn unserved(what: Option, opcode: u16) { + let mut line = [0u8; 64]; + let head = b"[WAYLAND] unserved "; + let tag = what.map(name).unwrap_or(b"unknown-object"); + let n = head.len(); + line[..n].copy_from_slice(head); + let m = (n + tag.len()).min(line.len() - 4); + line[n..m].copy_from_slice(&tag[..m - n]); + line[m] = b'.'; + line[m + 1] = b'0' + (opcode / 10) as u8 % 10; + line[m + 2] = b'0' + (opcode % 10) as u8; + line[m + 3] = b'\n'; + let _ = nonos_libc::mk_debug(line.as_ptr(), m + 4); +} diff --git a/userland/capsule_linux/src/linux/wayland/wire.rs b/userland/capsule_linux/src/linux/wayland/wire.rs new file mode 100644 index 000000000..7f53b3bf8 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/wire.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 . + + +//! The Wayland message header and the argument cursor. + +pub const HEADER: usize = 8; + +pub struct Msg<'a> { + pub object: u32, + pub opcode: u16, + pub args: &'a [u8], +} + +/// The next message in `buf`, or nothing if it has not all arrived. +pub fn next(buf: &[u8]) -> Option<(Msg<'_>, usize)> { + if buf.len() < HEADER { + return None; + } + let object = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + let opcode = u16::from_le_bytes([buf[4], buf[5]]); + let size = u16::from_le_bytes([buf[6], buf[7]]) as usize; + if size < HEADER || size > buf.len() { + return None; + } + Some((Msg { object, opcode, args: &buf[HEADER..size] }, size)) +} diff --git a/userland/capsule_linux/src/linux/wayland/xdg.rs b/userland/capsule_linux/src/linux/wayland/xdg.rs new file mode 100644 index 000000000..59143c179 --- /dev/null +++ b/userland/capsule_linux/src/linux/wayland/xdg.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 . + + +//! `xdg_wm_base` and the two objects a toplevel window is made of. + +use crate::linux::guest::Guest; + +use super::object::Object; +use super::ops::ev; +use super::out::Event; +use super::args::Args; + +/// The size a toplevel is told to be when nothing has asked otherwise. +const WIDTH: u32 = 800; +const HEIGHT: u32 = 600; + +pub fn get_xdg_surface(guest: &mut Guest, args: &mut Args<'_>) { + let (Some(id), Some(surface)) = (args.u32(), args.u32()) else { + return; + }; + guest.objects.put(id, Object::XdgSurface); + if let Some(s) = guest.scene.surfaces.iter_mut().find(|s| s.id == surface) { + s.xdg = Some(id); + } +} + +pub fn get_toplevel(guest: &mut Guest, xdg: u32, args: &mut Args<'_>) { + let Some(id) = args.u32() else { return }; + guest.objects.put(id, Object::XdgToplevel); + let serial = guest.scene.next_serial(); + // An empty states array, which is a length of zero. + Event::new(id, ev::TOPLEVEL_CONFIGURE) + .u32(WIDTH) + .u32(HEIGHT) + .u32(0) + .send(&mut guest.display.to_client); + Event::new(xdg, ev::XDG_SURFACE_CONFIGURE).u32(serial).send(&mut guest.display.to_client); +} + +pub fn ack_configure(guest: &mut Guest, xdg: u32, args: &mut Args<'_>) { + let Some(_serial) = args.u32() else { return }; + let owner = guest.scene.surfaces.iter_mut().find(|s| s.xdg == Some(xdg)); + if let Some(s) = owner { + s.configured = true; + } +} diff --git a/userland/capsule_linux_proofs/Cargo.lock b/userland/capsule_linux_proofs/Cargo.lock new file mode 100644 index 000000000..b5322f056 --- /dev/null +++ b/userland/capsule_linux_proofs/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "nonos_capsule_linux_proofs" +version = "0.1.0" diff --git a/userland/capsule_linux_proofs/Cargo.toml b/userland/capsule_linux_proofs/Cargo.toml new file mode 100644 index 000000000..e1cfcbc01 --- /dev/null +++ b/userland/capsule_linux_proofs/Cargo.toml @@ -0,0 +1,19 @@ +# NĂ˜NOS userland: capsule_linux_proofs +# eK@nonos.systems +# +# Host proofs for the parts of the Linux personality that are pure: the +# Wayland wire codec, path resolution, directory entries, the stat +# buffer, and the ELF reader. Each includes the shipped source through +# #[path], so a proof here constrains the code that runs and not a copy +# of it. + +[package] +name = "nonos_capsule_linux_proofs" +version = "0.1.0" +edition = "2021" +publish = false +license = "AGPL-3.0" +authors = ["eK@nonos.systems"] + +[lib] +path = "src/lib.rs" diff --git a/userland/capsule_linux_proofs/src/image/mod.rs b/userland/capsule_linux_proofs/src/image/mod.rs new file mode 100644 index 000000000..f9f645513 --- /dev/null +++ b/userland/capsule_linux_proofs/src/image/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 loader's module shape, so the `super::` paths inside these files +//! resolve exactly as they do in the capsule. + +#[path = "../../../capsule_linux/src/linux/image/read.rs"] +pub mod read; + +#[path = "../../../capsule_linux/src/linux/image/phdr.rs"] +pub mod phdr; + +#[path = "../../../capsule_linux/src/linux/image/stack_words.rs"] +pub mod stack_words; + +#[path = "../../../capsule_linux/src/linux/image/elf.rs"] +pub mod elf; + +#[path = "../../../capsule_linux/src/linux/image/elf_phdr.rs"] +pub mod elf_phdr; diff --git a/userland/capsule_linux_proofs/src/install/mod.rs b/userland/capsule_linux_proofs/src/install/mod.rs new file mode 100644 index 000000000..7985afda7 --- /dev/null +++ b/userland/capsule_linux_proofs/src/install/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 . + + +//! The installer's pure parsers, included from the capsule. + +#[path = "../../../capsule_linux/src/linux/install/tar_field.rs"] +pub mod tar_field; + +#[path = "../../../capsule_linux/src/linux/install/tar.rs"] +pub mod tar; + +#[path = "../../../capsule_linux/src/linux/install/index.rs"] +pub mod index; diff --git a/userland/capsule_linux_proofs/src/lib.rs b/userland/capsule_linux_proofs/src/lib.rs new file mode 100644 index 000000000..ab9fae3e5 --- /dev/null +++ b/userland/capsule_linux_proofs/src/lib.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 . + + +//! The shipped source, included and exercised. + +extern crate alloc; + +#[path = "../../capsule_linux/src/linux/wayland/wire.rs"] +pub mod wire; + +#[path = "../../capsule_linux/src/linux/wayland/args.rs"] +pub mod args; + +#[path = "../../capsule_linux/src/linux/file/resolve.rs"] +pub mod resolve; + +#[path = "../../capsule_linux/src/linux/file/dirent.rs"] +pub mod dirent; + +#[path = "../../capsule_linux/src/linux/file/meta/statbuf.rs"] +pub mod statbuf; + +#[path = "../../capsule_linux/src/linux/call/spawn/exec_shebang.rs"] +pub mod exec_shebang; + +pub mod image; + +/// The installer's parsers, which read bytes fetched off a network. +pub mod install; + +#[cfg(test)] +mod tests; diff --git a/userland/capsule_linux_proofs/src/tests.rs b/userland/capsule_linux_proofs/src/tests.rs new file mode 100644 index 000000000..ae67ca5b1 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests.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 . + + +//! Every proof, by the thing it constrains. + +mod dirent_tests; +mod elf_tests; +mod exec_shebang_tests; +mod resolve_tests; +mod stack_words_tests; +mod stat_tests; +mod tar_tests; +mod wire_tests; diff --git a/userland/capsule_linux_proofs/src/tests/dirent_tests.rs b/userland/capsule_linux_proofs/src/tests/dirent_tests.rs new file mode 100644 index 000000000..dcd2372ac --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/dirent_tests.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 . + + +//! getdents64 records, which a libc walks by adding d_reclen. + +use alloc::vec::Vec; + +use crate::dirent::{encode, record_len, DT_UNKNOWN, HEADER}; + +#[test] +fn a_record_is_eight_byte_aligned_and_holds_the_terminator() { + for name in ["a", "ab", "abc", "abcd", "abcde"] { + let want = HEADER + name.len() + 1; + let got = record_len(name.as_bytes()); + assert!(got >= want, "{name}: {got} must hold {want}"); + assert_eq!(got % 8, 0, "{name}: {got} is not aligned"); + assert!(got - want < 8, "{name}: {got} pads more than a word"); + } +} + +#[test] +fn walking_by_reclen_reaches_every_name() { + let names = ["bin", "lib", "usr", "a-much-longer-name"]; + let mut out: Vec = Vec::new(); + for (i, name) in names.iter().enumerate() { + encode(&mut out, name.as_bytes(), i as u64, DT_UNKNOWN); + } + let mut at = 0usize; + let mut seen = Vec::new(); + while at < out.len() { + let reclen = u16::from_le_bytes([out[at + 16], out[at + 17]]) as usize; + assert!(reclen >= HEADER + 1 && at + reclen <= out.len()); + let body = &out[at + 19..at + reclen]; + let end = body.iter().position(|b| *b == 0).unwrap(); + seen.push(String::from_utf8(body[..end].to_vec()).unwrap()); + at += reclen; + } + assert_eq!(seen, names); +} + +#[test] +fn the_offset_field_advances_with_the_index() { + let mut out: Vec = Vec::new(); + encode(&mut out, b"x", 7, DT_UNKNOWN); + assert_eq!(u64::from_le_bytes(out[0..8].try_into().unwrap()), 8); + assert_eq!(i64::from_le_bytes(out[8..16].try_into().unwrap()), 8); +} + +#[test] +fn the_type_is_unknown_and_not_a_guess() { + let mut out: Vec = Vec::new(); + encode(&mut out, b"x", 0, DT_UNKNOWN); + assert_eq!(out[18], 0); +} diff --git a/userland/capsule_linux_proofs/src/tests/elf_tests.rs b/userland/capsule_linux_proofs/src/tests/elf_tests.rs new file mode 100644 index 000000000..3f4e84909 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/elf_tests.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 ELF reader, against a real dynamic image. + +use crate::image::elf::{Elf, ET_DYN, PT_INTERP, PT_LOAD}; + +/// The loader never reads the dynamic section, so it names no constant +/// for it. The interpreter is what parses that, which is the point. +const PT_DYNAMIC: u32 = 2; + +const IMAGE: &[u8] = include_bytes!("../../vectors/dynamic.elf"); + +#[test] +fn the_header_reads_as_a_shared_object_for_x86_64() { + let elf = Elf::parse(IMAGE).expect("a 64-bit little-endian x86_64 image"); + assert_eq!(elf.kind, ET_DYN); + assert_eq!(elf.entry, 0x1000); + assert_eq!(elf.phnum(), 3); + assert_eq!(elf.phentsize, 56); +} + +#[test] +fn the_program_headers_are_where_the_header_says() { + let elf = Elf::parse(IMAGE).unwrap(); + let kinds: Vec = (0..elf.phnum()).map(|i| elf.phdr(i).unwrap().kind).collect(); + assert_eq!(kinds, vec![PT_LOAD, PT_INTERP, PT_DYNAMIC]); +} + +#[test] +fn the_interpreter_path_is_the_one_in_the_file() { + let elf = Elf::parse(IMAGE).unwrap(); + let ph = (0..elf.phnum()) + .map(|i| elf.phdr(i).unwrap()) + .find(|p| p.kind == PT_INTERP) + .expect("PT_INTERP"); + let raw = &IMAGE[ph.offset as usize..(ph.offset + ph.filesz) as usize]; + let end = raw.iter().position(|b| *b == 0).unwrap(); + assert_eq!(&raw[..end], b"/lib/ld-musl-x86_64.so.1"); +} + +#[test] +fn the_header_address_is_inside_a_load_segment() { + let elf = Elf::parse(IMAGE).unwrap(); + let at = elf.phdr_addr(0).expect("headers are in a load segment"); + assert_eq!(at, 0x400000 + elf.phoff); + assert_eq!(elf.phdr_addr(0x1000_0000), Some(0x1000_0000 + 0x400000 + elf.phoff)); +} + +#[test] +fn anything_that_is_not_an_image_is_refused() { + assert!(Elf::parse(b"").is_none()); + assert!(Elf::parse(&[0u8; 64]).is_none()); + let mut wrong = IMAGE.to_vec(); + wrong[4] = 1; + assert!(Elf::parse(&wrong).is_none(), "a 32-bit class must be refused"); +} diff --git a/userland/capsule_linux_proofs/src/tests/exec_shebang_tests.rs b/userland/capsule_linux_proofs/src/tests/exec_shebang_tests.rs new file mode 100644 index 000000000..462be2364 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/exec_shebang_tests.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 . + +//! `#!` as Linux reads it, which is not as a shell would. + +use crate::exec_shebang::parse; + +#[test] +fn a_bare_interpreter_has_no_argument() { + let got = parse(b"#!/bin/sh\necho hi\n").expect("a plain shebang"); + assert_eq!(got.path, b"/bin/sh"); + assert!(got.arg.is_none()); +} + +#[test] +fn everything_after_the_interpreter_is_one_argument() { + // Linux splits once. + let got = parse(b"#!/bin/sh -e -u\n").expect("an argument follows"); + assert_eq!(got.path, b"/bin/sh"); + assert_eq!(got.arg.as_deref(), Some(&b"-e -u"[..])); +} + +#[test] +fn leading_space_after_the_bang_is_skipped() { + let got = parse(b"#! /usr/bin/env python3\n").expect("spaces before the path"); + assert_eq!(got.path, b"/usr/bin/env"); + assert_eq!(got.arg.as_deref(), Some(&b"python3"[..])); +} + +#[test] +fn a_carriage_return_does_not_become_part_of_the_path() { + /* + * A script edited on Windows would otherwise ask for "/bin/sh\r", which + * resolves to nothing and reports a missing interpreter. + */ + let got = parse(b"#!/bin/sh\r\n").expect("a CRLF script"); + assert_eq!(got.path, b"/bin/sh"); +} + +#[test] +fn an_elf_is_not_a_script() { + assert!(parse(b"\x7fELF\x02\x01\x01").is_none()); +} + +#[test] +fn a_bang_with_nothing_after_it_is_not_an_interpreter() { + assert!(parse(b"#!\n").is_none()); + assert!(parse(b"#! \n").is_none()); +} + +#[test] +fn a_line_without_a_newline_still_parses() { + let got = parse(b"#!/bin/sh").expect("a file that is only its shebang"); + assert_eq!(got.path, b"/bin/sh"); +} diff --git a/userland/capsule_linux_proofs/src/tests/resolve_tests.rs b/userland/capsule_linux_proofs/src/tests/resolve_tests.rs new file mode 100644 index 000000000..ea6096bd8 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/resolve_tests.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 . + + +//! Turning a guest's path into a store key. + +use crate::resolve::absolute; + +fn at(cwd: &str, path: &str) -> String { + String::from_utf8(absolute(cwd.as_bytes(), path.as_bytes())).unwrap() +} + +#[test] +fn an_absolute_path_ignores_the_working_directory() { + assert_eq!(at("/home", "/etc/passwd"), "/etc/passwd"); +} + +#[test] +fn a_relative_path_hangs_off_the_working_directory() { + assert_eq!(at("/home", "lib/libc.so"), "/home/lib/libc.so"); +} + +#[test] +fn dot_components_disappear() { + assert_eq!(at("/", "./a/./b"), "/a/b"); + assert_eq!(at("/a", "b/../c"), "/a/c"); +} + +#[test] +fn dot_dot_past_the_root_stops_at_the_root() { + assert_eq!(at("/", "../../../etc"), "/etc"); + assert_eq!(at("/a", "../../.."), "/"); +} + +#[test] +fn repeated_separators_collapse() { + assert_eq!(at("/", "//lib///libc.so"), "/lib/libc.so"); +} + +#[test] +fn the_root_itself_survives() { + assert_eq!(at("/", "/"), "/"); + assert_eq!(at("/", ""), "/"); +} diff --git a/userland/capsule_linux_proofs/src/tests/stack_words_tests.rs b/userland/capsule_linux_proofs/src/tests/stack_words_tests.rs new file mode 100644 index 000000000..fa825239f --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/stack_words_tests.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 shape of the block at rsp. + +use alloc::vec::Vec; + +use crate::image::stack_words::words; + +#[test] +fn argc_leads_and_both_lists_are_null_terminated() { + let at = [0x1000, 0x1010, 0x2000]; + let got = words(2, &at, alloc_aux()).expect("two of three is a valid split"); + assert_eq!(got[0], 2); + assert_eq!(&got[1..3], &[0x1000, 0x1010]); + assert_eq!(got[3], 0, "argv is terminated before envp starts"); + assert_eq!(got[4], 0x2000); + assert_eq!(got[5], 0, "envp is terminated before the auxv"); + assert_eq!(&got[6..], &[7, 7]); +} + +#[test] +fn no_arguments_still_produces_both_terminators() { + let got = words(0, &[], Vec::new()).expect("an empty vector is a valid one"); + assert_eq!(got, alloc::vec![0, 0, 0]); +} + +#[test] +fn an_environment_with_no_arguments_keeps_its_place() { + let got = words(0, &[0x3000], Vec::new()).expect("zero argv, one envp"); + // argc, the empty argv's null, the one envp entry, envp's null. + assert_eq!(got, alloc::vec![0, 0, 0x3000, 0]); +} + +#[test] +fn a_count_past_the_end_is_refused_rather_than_truncated() { + assert!(words(3, &[0x1000], Vec::new()).is_none()); +} + +fn alloc_aux() -> Vec { + alloc::vec![7, 7] +} diff --git a/userland/capsule_linux_proofs/src/tests/stat_tests.rs b/userland/capsule_linux_proofs/src/tests/stat_tests.rs new file mode 100644 index 000000000..80055e630 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/stat_tests.rs @@ -0,0 +1,63 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +//! The struct a libc reads out of fstat. + +use crate::statbuf::{build, S_IFDIR, S_IFREG, STAT_LEN}; + +fn u32_at(b: &[u8], at: usize) -> u32 { + u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) +} + +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); + 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); +} + +#[test] +fn a_directory_says_so_in_the_mode() { + let s = build(0, true); + assert_eq!(u32_at(&s, 24), S_IFDIR | 0o755); + assert_eq!(u64_at(&s, 48), 0); +} + +#[test] +fn block_count_rounds_up_to_the_next_five_hundred_and_twelve() { + assert_eq!(u64_at(&build(1, false), 64), 1); + assert_eq!(u64_at(&build(512, false), 64), 1); + assert_eq!(u64_at(&build(513, false), 64), 2); +} + +#[test] +fn everything_unknown_is_left_at_zero() { + let s = build(10, false); + for at in [0, 8, 40, 72, 88, 104] { + assert_eq!(u64_at(&s, at), 0, "offset {at} should be untouched"); + } +} diff --git a/userland/capsule_linux_proofs/src/tests/tar_tests.rs b/userland/capsule_linux_proofs/src/tests/tar_tests.rs new file mode 100644 index 000000000..e2f7b3d0b --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/tar_tests.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 . + + +//! The tar reader, against a real Alpine package. + +use crate::install::tar::entries; + +/// libffi-3.4.6-r0.apk, decompressed. An independent reader sees one +/// regular file in it: usr/lib/libffi.so.8.1.4 at 38960 bytes. +const PKG: &[u8] = include_bytes!("../../vectors/libffi.tar"); + +#[test] +fn it_finds_the_package_payload_and_nothing_else() { + let found = entries(PKG); + let named: Vec<&[u8]> = found.iter().map(|e| e.name.as_slice()).collect(); + assert!( + named.contains(&b"usr/lib/libffi.so.8.1.4".as_slice()), + "the shared library must be there, saw {named:?}" + ); +} + +#[test] +fn the_payload_is_the_right_length_and_is_an_elf() { + let found = entries(PKG); + let lib = found + .iter() + .find(|e| e.name == b"usr/lib/libffi.so.8.1.4") + .expect("the library"); + assert_eq!(lib.body.len(), 38960); + assert_eq!(&lib.body[..4], b"\x7fELF"); +} + +#[test] +fn a_truncated_archive_stops_rather_than_inventing_entries() { + for cut in [0usize, 100, 512, 1024, PKG.len() / 2] { + let found = entries(&PKG[..cut]); + for e in &found { + assert!(e.body.len() <= cut, "an entry longer than the input"); + } + } +} + +#[test] +fn rubbish_is_not_an_archive() { + assert!(entries(&[0u8; 512]).is_empty()); + assert!(entries(b"not a tar at all").is_empty()); +} diff --git a/userland/capsule_linux_proofs/src/tests/wire_tests.rs b/userland/capsule_linux_proofs/src/tests/wire_tests.rs new file mode 100644 index 000000000..c60628748 --- /dev/null +++ b/userland/capsule_linux_proofs/src/tests/wire_tests.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 Wayland wire format, in both directions. + +use crate::args::Args; +use crate::wire::{next, HEADER}; + +/// A real wl_display.get_registry: object 1, opcode 1, one new_id of 2. +fn get_registry() -> [u8; 12] { + let mut m = [0u8; 12]; + m[0..4].copy_from_slice(&1u32.to_le_bytes()); + m[4..6].copy_from_slice(&1u16.to_le_bytes()); + m[6..8].copy_from_slice(&12u16.to_le_bytes()); + m[8..12].copy_from_slice(&2u32.to_le_bytes()); + m +} + +#[test] +fn a_message_reports_its_object_opcode_and_length() { + let buf = get_registry(); + let (msg, size) = next(&buf).expect("a whole message parses"); + assert_eq!((msg.object, msg.opcode, size), (1, 1, 12)); + assert_eq!(Args::new(msg.args).u32(), Some(2)); +} + +#[test] +fn a_message_that_has_not_all_arrived_is_not_parsed() { + let buf = get_registry(); + for cut in 0..buf.len() { + assert!(next(&buf[..cut]).is_none(), "{cut} bytes must not parse"); + } +} + +#[test] +fn a_size_smaller_than_the_header_is_refused() { + let mut buf = get_registry(); + buf[6..8].copy_from_slice(&4u16.to_le_bytes()); + assert!(next(&buf).is_none()); +} + +#[test] +fn two_messages_back_to_back_are_taken_in_order() { + let mut buf = Vec::new(); + buf.extend_from_slice(&get_registry()); + buf.extend_from_slice(&get_registry()); + let (_, first) = next(&buf).expect("first"); + let (second, _) = next(&buf[first..]).expect("second"); + assert_eq!(second.object, 1); + assert_eq!(first, HEADER + 4); +} diff --git a/userland/capsule_linux_proofs/vectors/apkindex.tar b/userland/capsule_linux_proofs/vectors/apkindex.tar new file mode 100644 index 000000000..74295174d Binary files /dev/null and b/userland/capsule_linux_proofs/vectors/apkindex.tar differ diff --git a/userland/capsule_linux_proofs/vectors/dynamic.elf b/userland/capsule_linux_proofs/vectors/dynamic.elf new file mode 100644 index 000000000..5dbba42bf Binary files /dev/null and b/userland/capsule_linux_proofs/vectors/dynamic.elf differ diff --git a/userland/capsule_linux_proofs/vectors/libffi.tar b/userland/capsule_linux_proofs/vectors/libffi.tar new file mode 100644 index 000000000..dca5c8372 Binary files /dev/null and b/userland/capsule_linux_proofs/vectors/libffi.tar differ diff --git a/userland/libc/src/consent.rs b/userland/libc/src/consent.rs new file mode 100644 index 000000000..49801df34 --- /dev/null +++ b/userland/libc/src/consent.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 . + +//! Consent to run what this machine builds and fetches. + +use crate::syscall::{call_raw, N_MK_DEV_ROOT_CONFIRM, N_MK_DEV_ROOT_LOCAL}; + +/// Ask to enrol this machine's own build root, so what it installs can be +/// proved. +pub fn mk_dev_root_local() -> i64 { + call_raw(N_MK_DEV_ROOT_LOCAL, [0, 0, 0, 0, 0, 0]) +} + +/// Complete the pending enrolment with the code the user read and typed. +pub fn mk_dev_root_confirm(code: u32) -> i64 { + call_raw(N_MK_DEV_ROOT_CONFIRM, [code as u64, 0, 0, 0, 0, 0]) +} diff --git a/userland/libc/src/foreign.rs b/userland/libc/src/foreign.rs index f4e65e36d..f3217b39e 100644 --- a/userland/libc/src/foreign.rs +++ b/userland/libc/src/foreign.rs @@ -14,13 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Hosting a guest: create it, build its address space, answer the calls -//! the kernel refuses on its behalf. The guest holds no capabilities, so -//! everything it can do passes through the supervisor that made it. +//! Hosting a guest: create it, build its address space, answer the calls the +//! kernel refuses on its behalf. use crate::syscall::{ - call_raw, N_MK_FOREIGN_REPLY, N_MK_FOREIGN_SPAWN, N_MK_FOREIGN_START, N_MK_FOREIGN_THREAD, - N_MK_FOREIGN_WAIT, + call_raw, N_MK_FOREIGN_EXEC, N_MK_FOREIGN_FORK, N_MK_FOREIGN_REPLY, N_MK_FOREIGN_SPAWN, + N_MK_FOREIGN_START, + N_MK_FOREIGN_THREAD, N_MK_FOREIGN_WAIT, }; pub use crate::foreign_frame::ForeignFrame; @@ -32,13 +32,27 @@ pub fn mk_foreign_spawn(name: &[u8]) -> i64 { } /// Give a built guest its entry point and make it runnable. -/// `rsp` of zero lets the kernel allocate an ordinary user stack; any -/// other value is the stack the supervisor built, which is what a program -/// expecting argv and an auxiliary vector needs. pub fn mk_foreign_start(pid: u32, entry: u64, rsp: u64) -> i64 { call_raw(N_MK_FOREIGN_START, [pid as u64, entry, rsp, 0, 0, 0]) } +/// Make a guest runnable on the state it already carries, which is +/// what a forked child was born holding. +pub fn mk_foreign_resume(pid: u32) -> i64 { + call_raw(N_MK_FOREIGN_START, [pid as u64, 0, 0, 0, 0, 0]) +} + +/// A second process holding a guest's register state, with zero in its return +/// register. +pub fn mk_foreign_fork(pid: u32) -> i64 { + call_raw(N_MK_FOREIGN_FORK, [pid as u64, 0, 0, 0, 0, 0]) +} + +/// Replace the program a parked guest is running. +pub fn mk_foreign_exec(pid: u32, entry: u64, rsp: u64) -> i64 { + call_raw(N_MK_FOREIGN_EXEC, [pid as u64, entry, rsp, 0, 0, 0]) +} + /// A thread in a guest, sharing its address space. `tls` is the FS base /// it wakes with, which a C runtime reads before anything else. pub fn mk_foreign_thread(pid: u32, entry: u64, rsp: u64, tls: u64) -> i64 { diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 2fe9aa1c0..227bb6cb8 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -66,19 +66,26 @@ pub use caps::{mk_cap_check, mk_cap_grant, mk_cap_revoke}; pub use capsule_load::{mk_capsule_load, CapsuleLoadRequest}; pub use capsule_verify::{mk_capsule_verify, CapsuleVerifyRequest, CapsuleVerifySummary}; pub use crypto::{ - crypto_decrypt, crypto_decrypt_aad, crypto_encrypt, crypto_encrypt_aad, crypto_hash, - crypto_hkdf_sha256, crypto_hmac_sha256, crypto_keccak256, crypto_machine_key, crypto_random, - crypto_x25519_public, crypto_x25519_shared, machine_key, MACHINE_KEY_LABEL_MAX, - MACHINE_KEY_NO_TPM, MACHINE_KEY_WRONG_STATE, + crypto_decrypt, crypto_decrypt_aad, crypto_ed25519_pubkey, crypto_ed25519_sign, + crypto_ed25519_verify, crypto_encrypt, crypto_encrypt_aad, crypto_hash, crypto_hkdf_sha256, + crypto_hmac_sha256, crypto_keccak256, crypto_random, crypto_secp256k1_pubkey, + crypto_secp256k1_sign, crypto_x25519_public, crypto_x25519_shared, }; +mod consent; +mod local_sign; + pub use debug::mk_debug; pub use foreign::{ - mk_foreign_reply, mk_foreign_spawn, mk_foreign_start, mk_foreign_thread, mk_foreign_wait, + mk_foreign_exec, mk_foreign_fork, mk_foreign_reply, mk_foreign_resume, mk_foreign_spawn, + mk_foreign_start, + mk_foreign_thread, mk_foreign_wait, }; pub use foreign_frame::ForeignFrame; pub use graphics::nonos_display_dimensions; #[cfg(feature = "heap")] pub use heap::{init as heap_init, init_sized as heap_init_sized, HeapError}; +pub use consent::{mk_dev_root_confirm, mk_dev_root_local}; +pub use local_sign::{mk_app_install, mk_local_sign, mk_local_sign_len, mk_local_verify}; pub use ipc::{ mk_ipc_call, mk_ipc_call_timeout, mk_ipc_recv, mk_ipc_recv_from, mk_ipc_reply, mk_ipc_send, mk_ipc_send_to_pid, mk_service_lookup, mk_service_register, diff --git a/userland/libc/src/local_sign.rs b/userland/libc/src/local_sign.rs new file mode 100644 index 000000000..ad420f119 --- /dev/null +++ b/userland/libc/src/local_sign.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 . + +//! A trailer for something this machine is installing. + +use crate::syscall::{call_raw, N_MK_APP_INSTALL, N_MK_LOCAL_SIGN, N_MK_LOCAL_VERIFY}; + +/// How many bytes a trailer for `elf` takes, or a negative errno. +pub fn mk_local_sign_len(elf: &[u8], caps: u64) -> i64 { + call_raw(N_MK_LOCAL_SIGN, [elf.as_ptr() as u64, elf.len() as u64, caps, 0, 0, 0]) +} + +/// Mint a trailer proving this machine may run `elf` holding `caps`, into +/// `out`. +pub fn mk_local_sign(elf: &[u8], caps: u64, out: &mut [u8]) -> i64 { + let args = + [elf.as_ptr() as u64, elf.len() as u64, caps, out.as_mut_ptr() as u64, out.len() as u64, 0]; + call_raw(N_MK_LOCAL_SIGN, args) +} + +/// True when `elf` is proved under a root this machine trusts for exactly +/// `caps`. +pub fn mk_local_verify(elf: &[u8], caps: u64, trailer: &[u8]) -> bool { + let args = [ + elf.as_ptr() as u64, + elf.len() as u64, + caps, + trailer.as_ptr() as u64, + trailer.len() as u64, + 0, + ]; + call_raw(N_MK_LOCAL_VERIFY, args) == 0 +} + +/// Ask for a distribution package to be installed. +pub fn mk_app_install(package: &[u8]) -> i64 { + call_raw(N_MK_APP_INSTALL, [package.as_ptr() as u64, package.len() as u64, 0, 0, 0, 0]) +} + diff --git a/userland/libc/src/peer.rs b/userland/libc/src/peer.rs index a99d9b728..988f4cff9 100644 --- a/userland/libc/src/peer.rs +++ b/userland/libc/src/peer.rs @@ -15,11 +15,12 @@ // along with this program. If not, see . -//! Reaching into a guest this process supervises: its pages, their -//! protection, and the bytes in them. Every one of these is refused by -//! the kernel unless the caller created the process it names. +//! Reaching into a guest this process supervises: its pages, their protection, +//! and the bytes in them. -use crate::syscall::{call_raw, N_MK_PEER_COPY, N_MK_PEER_MAP, N_MK_PEER_PROTECT}; +use crate::syscall::{ + call_raw, N_MK_PEER_COPY, N_MK_PEER_MAP, N_MK_PEER_PROTECT, N_MK_PEER_TLS, N_MK_PEER_UNMAP, +}; /// Pages of a guest may be written, and may be executed. pub const PEER_PROT_WRITE: u64 = 1 << 0; @@ -30,14 +31,23 @@ pub fn mk_peer_map(pid: u32, addr: u64, len: u64, prot: u64) -> i64 { call_raw(N_MK_PEER_MAP, [pid as u64, addr, len, prot, 0, 0]) } -/// Set the protection of pages a guest already has. Every page in the -/// span must be mapped: the kernel refuses a hole rather than filling it, -/// because a caller asking for execute on a range it has not written is -/// not asking for what it thinks. +/// Set the protection of pages a guest already has. pub fn mk_peer_protect(pid: u32, addr: u64, len: u64, prot: u64) -> i64 { call_raw(N_MK_PEER_PROTECT, [pid as u64, addr, len, prot, 0, 0]) } +/// Take a span back from a guest. What exec needs, so a replacing +/// image cannot see the pages of the one it replaced. +pub fn mk_peer_unmap(pid: u32, addr: u64, len: u64) -> i64 { + call_raw(N_MK_PEER_UNMAP, [pid as u64, addr, len, 0, 0, 0]) +} + +/// The thread pointer a guest thread wakes with. A C runtime sets this +/// during startup and dereferences it immediately afterwards. +pub fn mk_peer_tls(pid: u32, base: u64) -> i64 { + call_raw(N_MK_PEER_TLS, [pid as u64, base, 0, 0, 0, 0]) +} + /// Copy into a guest this process supervises. pub fn mk_peer_write(pid: u32, guest_addr: u64, src: &[u8]) -> i64 { let args = [pid as u64, guest_addr, src.as_ptr() as u64, src.len() as u64, 1, 0]; diff --git a/userland/libc/src/syscall/mod.rs b/userland/libc/src/syscall/mod.rs index a9308b755..1567851b0 100644 --- a/userland/libc/src/syscall/mod.rs +++ b/userland/libc/src/syscall/mod.rs @@ -22,22 +22,25 @@ pub(crate) use bridge::call_diverging; pub use bridge::call_raw; pub(crate) use numbers::{ N_ADMIN_POLICY_PUSH, N_ADMIN_REBOOT, N_ADMIN_SHUTDOWN, N_CRYPTO_DECRYPT, N_CRYPTO_DECRYPT_AAD, - N_CRYPTO_ENCRYPT, N_CRYPTO_ENCRYPT_AAD, N_CRYPTO_HASH, N_CRYPTO_HKDF_SHA256, - N_CRYPTO_HMAC_SHA256, N_CRYPTO_KECCAK256, N_CRYPTO_MACHINE_KEY, N_CRYPTO_RANDOM, + N_CRYPTO_ED25519_PUBKEY, N_CRYPTO_ED25519_SIGN, N_CRYPTO_ED25519_VERIFY, N_CRYPTO_ENCRYPT, + N_CRYPTO_ENCRYPT_AAD, N_CRYPTO_HASH, N_CRYPTO_HKDF_SHA256, N_CRYPTO_HMAC_SHA256, + N_CRYPTO_KECCAK256, N_CRYPTO_RANDOM, N_CRYPTO_SECP256K1_PUBKEY, N_CRYPTO_SECP256K1_SIGN, N_CRYPTO_X25519_PUBLIC, N_CRYPTO_X25519_SHARED, N_GFX_DISPLAY_DIMENSIONS, N_MK_ARGS, N_MK_ATTEST_DOC, N_MK_ATTEST_ENTRIES, N_MK_ATTEST_STATUS, N_MK_BATTERY_STATUS, N_MK_CAPSULE_LOAD, N_MK_CAPSULE_VERIFY, N_MK_CAP_CHECK, N_MK_CAP_GRANT, N_MK_CAP_REVOKE, N_MK_DEBUG, N_MK_DEVICE_CLAIM, N_MK_DEVICE_LIST, N_MK_DEVICE_RELEASE, N_MK_DISPLAY_VSYNC_WAIT, N_MK_DMA_MAP, N_MK_DMA_UNMAP, N_MK_EXIT, N_MK_FOREIGN_REPLY, N_MK_FOREIGN_SPAWN, - N_MK_FOREIGN_START, N_MK_FOREIGN_THREAD, N_MK_FOREIGN_WAIT, N_MK_FUTEX_WAIT, N_MK_GETPID, - N_MK_INPUT_EVENT_DRAIN, N_MK_INPUT_EVENT_POST, N_MK_INPUT_EVENT_WAIT, N_MK_IPC_CALL, + N_MK_APP_INSTALL, N_MK_DEV_ROOT_CONFIRM, N_MK_DEV_ROOT_LOCAL, N_MK_FOREIGN_EXEC, N_MK_FOREIGN_FORK, N_MK_LOCAL_SIGN, N_MK_LOCAL_VERIFY, N_MK_FOREIGN_START, N_MK_FOREIGN_THREAD, + N_MK_FOREIGN_WAIT, N_MK_FUTEX_WAIT, N_MK_GETPID, N_MK_INPUT_EVENT_DRAIN, + N_MK_INPUT_EVENT_POST, N_MK_INPUT_EVENT_WAIT, N_MK_IPC_CALL, N_MK_IPC_RECV, N_MK_IPC_RECV_FROM, N_MK_IPC_REPLY, N_MK_IPC_SEND, N_MK_IPC_SEND_TO_PID, N_MK_IRQ_ACK, N_MK_IRQ_BIND, N_MK_IRQ_POLL, N_MK_IRQ_UNBIND, N_MK_IRQ_WAIT, N_MK_KILL, N_MK_MMAP, N_MK_MMIO_MAP, N_MK_MMIO_UNMAP, N_MK_PCI_CONFIG_READ, N_MK_PCI_CONFIG_WRITE, - N_MK_PEER_COPY, N_MK_PEER_MAP, N_MK_PEER_PROTECT, N_MK_PID_ALIVE, N_MK_PIO_GRANT, - N_MK_PIO_READ, N_MK_PIO_RELEASE, N_MK_PIO_WRITE, N_MK_PROC_INPUT, N_MK_PROC_OUTPUT, - N_MK_PROC_STAT, N_MK_SERVICE_LOOKUP, N_MK_SERVICE_REGISTER, N_MK_SPAWN_INSTANCE, - N_MK_STDIN_READ, N_MK_STORE_WRITE, N_MK_SURFACE_ATTACH, N_MK_SURFACE_PRESENT, - N_MK_SURFACE_REGISTER, N_MK_SURFACE_RELEASE, N_MK_SURFACE_SHARE, N_MK_TIME_ADJUST, - N_MK_TIME_MILLIS, N_MK_TIME_MONOTONIC, N_MK_TIME_RTC, N_MK_TOOL_RUN, N_MK_WAIT, N_MK_YIELD, + N_MK_PEER_COPY, N_MK_PEER_MAP, N_MK_PEER_PROTECT, N_MK_PEER_TLS, N_MK_PEER_UNMAP, N_MK_PID_ALIVE, N_MK_PIO_GRANT, N_MK_PIO_READ, + N_MK_PIO_RELEASE, N_MK_PIO_WRITE, + N_MK_PROC_INPUT, N_MK_PROC_OUTPUT, N_MK_PROC_STAT, N_MK_SERVICE_LOOKUP, N_MK_SERVICE_REGISTER, + N_MK_SPAWN_INSTANCE, N_MK_STDIN_READ, N_MK_STORE_WRITE, N_MK_SURFACE_ATTACH, + N_MK_SURFACE_PRESENT, N_MK_SURFACE_REGISTER, N_MK_SURFACE_RELEASE, N_MK_SURFACE_SHARE, + N_MK_TIME_ADJUST, N_MK_TIME_MILLIS, N_MK_TIME_MONOTONIC, N_MK_TIME_RTC, N_MK_TOOL_RUN, + N_MK_WAIT, N_MK_YIELD, }; diff --git a/userland/libc/src/syscall/numbers/foreign.rs b/userland/libc/src/syscall/numbers/foreign.rs index fadd48edb..530eaf2f6 100644 --- a/userland/libc/src/syscall/numbers/foreign.rs +++ b/userland/libc/src/syscall/numbers/foreign.rs @@ -27,3 +27,12 @@ pub(crate) const N_MK_PEER_MAP: i64 = tag4(b"MPMP"); pub(crate) const N_MK_PEER_COPY: i64 = tag4(b"MPCP"); pub(crate) const N_MK_PEER_PROTECT: i64 = tag4(b"MPPT"); pub(crate) const N_MK_FOREIGN_THREAD: i64 = tag4(b"MFTH"); +pub(crate) const N_MK_PEER_TLS: i64 = tag4(b"MPTL"); +pub(crate) const N_MK_FOREIGN_FORK: i64 = tag4(b"MFFK"); +pub(crate) const N_MK_PEER_UNMAP: i64 = tag4(b"MPUN"); +pub(crate) const N_MK_FOREIGN_EXEC: i64 = tag4(b"MFEX"); +pub(crate) const N_MK_LOCAL_SIGN: i64 = tag4(b"MLSG"); +pub(crate) const N_MK_LOCAL_VERIFY: i64 = tag4(b"MLVF"); +pub(crate) const N_MK_APP_INSTALL: i64 = tag4(b"MAIN"); +pub(crate) const N_MK_DEV_ROOT_LOCAL: i64 = tag4(b"MDRO"); +pub(crate) const N_MK_DEV_ROOT_CONFIRM: i64 = tag4(b"MDRC");