From 086e67449baa761f39563d62efa951f50b847868 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 21:25:05 +0600 Subject: [PATCH 01/72] feat(toolchain): add clang x86_64-unknown-nonos config and cc-nonos wrapper --- toolchain/nonos-c/cc-nonos | 2 ++ toolchain/nonos-c/x86_64-unknown-nonos.cfg | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100755 toolchain/nonos-c/cc-nonos create mode 100644 toolchain/nonos-c/x86_64-unknown-nonos.cfg diff --git a/toolchain/nonos-c/cc-nonos b/toolchain/nonos-c/cc-nonos new file mode 100755 index 0000000000..05be42ad82 --- /dev/null +++ b/toolchain/nonos-c/cc-nonos @@ -0,0 +1,2 @@ +#!/bin/sh +exec clang --config "$(dirname "$0")/x86_64-unknown-nonos.cfg" "$@" diff --git a/toolchain/nonos-c/x86_64-unknown-nonos.cfg b/toolchain/nonos-c/x86_64-unknown-nonos.cfg new file mode 100644 index 0000000000..55c5a6ce9f --- /dev/null +++ b/toolchain/nonos-c/x86_64-unknown-nonos.cfg @@ -0,0 +1,8 @@ +--target=x86_64-unknown-none-elf +-D__nonos__ +-ffreestanding +-fno-stack-protector +-fno-PIC +-mno-red-zone +-static +-nostdlib From 4115224f23e7955a83b4da61b5247a7b3f6a694e Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 21:37:50 +0600 Subject: [PATCH 02/72] feat(toolchain): add x86_64-unknown-nonos rustc target spec relibc selects its platform backend with cfg(target_os="nonos") and auto-detects TARGET from the spec's llvm-target. Mirror the userland x86_64 spec but set os=nonos so the relibc nonos arm is selected when building C userland against this target. --- toolchain/nonos-c/x86_64-unknown-nonos.json | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 toolchain/nonos-c/x86_64-unknown-nonos.json diff --git a/toolchain/nonos-c/x86_64-unknown-nonos.json b/toolchain/nonos-c/x86_64-unknown-nonos.json new file mode 100644 index 0000000000..388933d9f4 --- /dev/null +++ b/toolchain/nonos-c/x86_64-unknown-nonos.json @@ -0,0 +1,40 @@ +{ + "llvm-target": "x86_64-unknown-nonos", + "arch": "x86_64", + "vendor": "unknown", + "os": "nonos", + "env": "", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + + "target-endian": "little", + "target-pointer-width": 64, + "target-c-int-width": 32, + "max-atomic-width": 64, + + "executables": true, + "panic-strategy": "abort", + "disable-redzone": true, + + "frame-pointer": "always", + + "cpu": "x86-64", + "features": "+sse,+sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,-mmx", + + "linker-flavor": "ld.lld", + "linker": "rust-lld", + + "pre-link-args": { + "ld.lld": ["-nostdlib", "-pie", "--gc-sections", "-z", "max-page-size=0x1000"] + }, + + "relocation-model": "pic", + "code-model": "small", + "position-independent-executables": true, + "static-position-independent-executables": true, + + "no-default-libraries": true, + "has-rpath": false, + + "stack-probes": { "kind": "inline" }, + "eh-frame-header": false +} From 90152b769d4510d3c54cbeef159d80910e4071c9 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 21:37:50 +0600 Subject: [PATCH 03/72] feat(relibc): add nonos raw-syscall layer The NONOS syscall ABI passes a 4-byte ASCII tag op in rax, args in rdi,rsi,rdx,r10,r8,r9, returns a signed rax (negative = errno) and clobbers rcx,r11. Provide tag4 + the MK_* op tags and syscall0..6 inline-asm wrappers that the Pal slice (Task 0.3) builds on. --- .../nonos-relibc/platform/nonos/lowlevel.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 toolchain/nonos-relibc/platform/nonos/lowlevel.rs diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs new file mode 100644 index 0000000000..0f7433d9a6 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -0,0 +1,70 @@ +use core::arch::asm; + +pub const fn tag4(s: &[u8; 4]) -> u64 { + (s[0] as u64) | (s[1] as u64) << 8 | (s[2] as u64) << 16 | (s[3] as u64) << 24 +} + +pub const MK_DEBUG: u64 = tag4(b"MDBG"); +pub const MK_EXIT: u64 = tag4(b"MEXT"); +pub const MK_GETPID: u64 = tag4(b"MGPD"); +pub const MK_MMAP: u64 = tag4(b"MMAP"); +pub const MK_MUNMAP: u64 = tag4(b"MUMP"); + +#[inline] +pub unsafe fn syscall0(n: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall1(n: u64, a0: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall2(n: u64, a0: u64, a1: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall3(n: u64, a0: u64, a1: u64, a2: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall4(n: u64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall5(n: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, in("r8") a4, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} + +#[inline] +pub unsafe fn syscall6(n: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 { + let r: i64; + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, in("r8") a4, in("r9") a5, + lateout("rcx") _, lateout("r11") _, options(nostack)); + r +} From 9fd11a5a5c1d252f172fc2a4eaeec0875d361432 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 21:37:50 +0600 Subject: [PATCH 04/72] feat(relibc): add nonos platform backend stub + apply.sh graft third_party/redox is gitignored; author the backend as tracked source and graft it in. apply.sh copies platform/nonos/* into the vendored relibc and idempotently patches the platform selector (cfg arm) and config.mk (toolchain TARGET block), mirroring nonos-std/apply.sh, so make TARGET=x86_64-unknown-nonos headers builds the nonos arm. --- toolchain/nonos-relibc/apply.sh | 51 ++++++++++++++++++++ toolchain/nonos-relibc/platform/nonos/mod.rs | 7 +++ 2 files changed, 58 insertions(+) create mode 100755 toolchain/nonos-relibc/apply.sh create mode 100644 toolchain/nonos-relibc/platform/nonos/mod.rs diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh new file mode 100755 index 0000000000..ff5f06306f --- /dev/null +++ b/toolchain/nonos-relibc/apply.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Graft the NONOS relibc platform backend into the gitignored vendored relibc +# (third_party/redox/src-repos/relibc). The backend is authored as tracked +# source here; this copies it in and idempotently patches the vendored platform +# selector + config.mk so `make TARGET=x86_64-unknown-nonos headers` builds the +# nonos arm. Re-run after a relibc re-import or `git clean`. Idempotent. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +RELIBC="$REPO/third_party/redox/src-repos/relibc" +CC_NONOS="$REPO/toolchain/nonos-c/cc-nonos" + +# 1. copy the platform backend +mkdir -p "$RELIBC/src/platform/nonos" +cp "$HERE/platform/nonos/mod.rs" "$RELIBC/src/platform/nonos/mod.rs" +cp "$HERE/platform/nonos/lowlevel.rs" "$RELIBC/src/platform/nonos/lowlevel.rs" + +# 2. patch the platform selector + config.mk (idempotent) +python3 - "$RELIBC" "$CC_NONOS" <<'PY' +import sys +relibc, cc = sys.argv[1], sys.argv[2] + +modrs = f"{relibc}/src/platform/mod.rs" +with open(modrs) as f: + s = f.read() +if 'target_os = "nonos"' not in s: + anchor = '\npub use self::rlb::{Line, RawLineBuffer};' + arm = ('\n#[cfg(target_os = "nonos")]\n#[path = "nonos/mod.rs"]\n' + 'pub(crate) mod sys;\n') + i = s.index(anchor) + with open(modrs, "w") as f: + f.write(s[:i] + arm + s[i:]) + +cfgmk = f"{relibc}/config.mk" +with open(cfgmk) as f: + s = f.read() +if 'x86_64-unknown-nonos' not in s: + block = ("\nifeq ($(TARGET),x86_64-unknown-nonos)\n" + f"\texport CC={cc}\n" + "\texport LD=ld.lld\n" + "\texport AR=llvm-ar\n" + "\texport NM=llvm-nm\n" + "\texport CPPFLAGS=\n" + "endif\n") + with open(cfgmk, "w") as f: + f.write(s.rstrip("\n") + "\n" + block) +PY + +echo "NONOS relibc backend grafted into $RELIBC" +echo " src/platform/nonos/{mod.rs,lowlevel.rs}; cfg arm + config.mk TARGET block" diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs new file mode 100644 index 0000000000..84b45fd3d4 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -0,0 +1,7 @@ +//! NONOS platform backend for relibc. Task 0.2 ships the compiling skeleton: +//! the raw-syscall layer plus the `Sys` carrier. The `Pal` method slice lands +//! in Task 0.3 and the crt0 in Task 0.4. + +pub mod lowlevel; + +pub struct Sys; From 8c72f1f4791b838675fd8d65ef46179ce2bcfde7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 21:45:01 +0600 Subject: [PATCH 05/72] fix(relibc): include CARGO_TEST= in nonos config.mk block Without this, relibc's `make libs` attempts to compile and cross-execute nonos test binaries on the host and fails. Also adds a comment above the vendored-relibc-version-specific mod.rs anchor to flag it for re-check on a relibc re-import. --- toolchain/nonos-relibc/apply.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index ff5f06306f..ce8691ac1f 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -25,6 +25,7 @@ modrs = f"{relibc}/src/platform/mod.rs" with open(modrs) as f: s = f.read() if 'target_os = "nonos"' not in s: + # vendored-relibc-version-specific anchor — re-check on a relibc re-import anchor = '\npub use self::rlb::{Line, RawLineBuffer};' arm = ('\n#[cfg(target_os = "nonos")]\n#[path = "nonos/mod.rs"]\n' 'pub(crate) mod sys;\n') @@ -42,6 +43,7 @@ if 'x86_64-unknown-nonos' not in s: "\texport AR=llvm-ar\n" "\texport NM=llvm-nm\n" "\texport CPPFLAGS=\n" + "\texport CARGO_TEST=\n" "endif\n") with open(cfgmk, "w") as f: f.write(s.rstrip("\n") + "\n" + block) From 5cc1d4e86e495be8926e0a36e187d348967085e5 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:27:21 +0600 Subject: [PATCH 06/72] =?UTF-8?q?feat(relibc):=20nonos=20Pal=20backend=20?= =?UTF-8?q?=E2=80=94=20scaffold=20+=205=20real=20MK=5F*=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit write→MkDebug, exit→MkExit, getpid→MkGetPid, mmap→MkMmap, munmap→MkMunmap, brk→ENOSYS (dlmalloc uses mmap, never brk). Remaining required methods land as ENOSYS stubs in following commits. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 75 +++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 84b45fd3d4..e7f9ef0a80 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -1,7 +1,76 @@ -//! NONOS platform backend for relibc. Task 0.2 ships the compiling skeleton: -//! the raw-syscall layer plus the `Sys` carrier. The `Pal` method slice lands -//! in Task 0.3 and the crt0 in Task 0.4. +//! NONOS platform backend for relibc. Task 0.3 ships the full `Pal` surface: +//! five real methods backed by `MK_*` syscalls (write, exit, getpid, mmap, +//! munmap) and the remaining required methods stubbed to `ENOSYS` (or the +//! obvious constant). Phase 1 de-stubs this backend method by method. + +use core::num::NonZeroU64; + +use super::{Pal, types::*}; +use crate::{ + c_str::CStr, + error::{Errno, Result}, + header::{ + errno::{EBADF, ENOMEM, ENOSYS}, + signal::sigevent, + sys_resource::{rlimit, rusage}, + sys_select::timeval, + sys_stat::stat, + sys_statvfs::statvfs, + sys_time::timezone, + sys_utsname::utsname, + time::{itimerspec, timespec}, + }, + iter::NulTerminated, + ld_so::tcb::OsSpecific, + out::Out, + pthread, +}; pub mod lowlevel; +mod epoll; +mod ptrace; +mod signal; +mod socket; + pub struct Sys; + +impl Pal for Sys { + fn write(fildes: c_int, buf: &[u8]) -> Result { + if fildes == 1 || fildes == 2 { + unsafe { lowlevel::syscall2(lowlevel::MK_DEBUG, buf.as_ptr() as u64, buf.len() as u64); } + return Ok(buf.len()); + } + Err(Errno(EBADF)) + } + + fn exit(status: c_int) -> ! { + unsafe { lowlevel::syscall1(lowlevel::MK_EXIT, status as u64); } + loop {} + } + + fn getpid() -> pid_t { + unsafe { lowlevel::syscall0(lowlevel::MK_GETPID) as pid_t } + } + + unsafe fn mmap(_addr: *mut c_void, len: usize, _prot: c_int, _flags: c_int, _fildes: c_int, _off: off_t) -> Result<*mut c_void> { + let r = unsafe { lowlevel::syscall4(lowlevel::MK_MMAP, 0, len as u64, 3, 0) }; + if r <= 0 { + return Err(Errno(ENOMEM)); + } + Ok(r as *mut c_void) + } + + unsafe fn munmap(addr: *mut c_void, len: usize) -> Result<()> { + unsafe { lowlevel::syscall2(lowlevel::MK_MUNMAP, addr as u64, len as u64); } + Ok(()) + } + + unsafe fn brk(_addr: *mut c_void) -> Result<*mut c_void> { + Err(Errno(ENOSYS)) + } + + fn verify() -> bool { + true + } +} From bef2f7d4dca2de4227b7a8de4c7fc0f81639a974 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:28:01 +0600 Subject: [PATCH 07/72] feat(relibc): nonos Pal fs + mem ENOSYS stubs faccessat/openat/read/write-family/dirents/links/stat + mmap-family (mlock/mprotect/mremap/msync/madvise/nanosleep) + getpagesize=4096. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index e7f9ef0a80..5499c45ddb 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -70,6 +70,54 @@ impl Pal for Sys { Err(Errno(ENOSYS)) } + fn faccessat(_fd: c_int, _path: CStr, _amode: c_int, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn chdir(_path: CStr) -> Result<()> { Err(Errno(ENOSYS)) } + fn close(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn dup2(_fildes: c_int, _fildes2: c_int) -> Result { Err(Errno(ENOSYS)) } + fn fchdir(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fchmodat(_dirfd: c_int, _path: Option, _mode: mode_t, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fchownat(_fildes: c_int, _path: CStr, _owner: uid_t, _group: gid_t, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fdatasync(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn flock(_fd: c_int, _operation: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fstatat(_fildes: c_int, _path: Option, _buf: Out, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fstatvfs(_fildes: c_int, _buf: Out) -> Result<()> { Err(Errno(ENOSYS)) } + fn fcntl(_fildes: c_int, _cmd: c_int, _arg: c_ulonglong) -> Result { Err(Errno(ENOSYS)) } + fn fpath(_fildes: c_int, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } + fn fsync(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn ftruncate(_fildes: c_int, _length: off_t) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn utimensat(_dirfd: c_int, _path: CStr, _times: *const timespec, _flag: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn getcwd(_buf: Out<[u8]>) -> Result<()> { Err(Errno(ENOSYS)) } + fn getdents(_fd: c_int, _buf: &mut [u8], _opaque_offset: u64) -> Result { Err(Errno(ENOSYS)) } + fn dir_seek(_fd: c_int, _opaque_offset: u64) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn dent_reclen_offset(_this_dent: &[u8], _offset: usize) -> Option<(u16, u64)> { None } + fn linkat(_fd1: c_int, _oldpath: CStr, _fd2: c_int, _newpath: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn lseek(_fildes: c_int, _offset: off_t, _whence: c_int) -> Result { Err(Errno(ENOSYS)) } + fn mkdirat(_fildes: c_int, _path: CStr, _mode: mode_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn mkfifoat(_dir_fd: c_int, _path: CStr, _mode: mode_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn mknodat(_fildes: c_int, _path: CStr, _mode: mode_t, _dev: dev_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn openat(_dirfd: c_int, _path: CStr, _oflag: c_int, _mode: mode_t) -> Result { Err(Errno(ENOSYS)) } + fn pipe2(_fildes: Out<[c_int; 2]>, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn posix_fallocate(_fd: c_int, _offset: u64, _length: NonZeroU64) -> Result<()> { Err(Errno(ENOSYS)) } + fn posix_getdents(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } + fn read(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } + fn pread(_fildes: c_int, _buf: &mut [u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } + fn pwrite(_fildes: c_int, _buf: &[u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } + fn readlinkat(_dirfd: c_int, _pathname: CStr, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } + fn renameat2(_old_dir: c_int, _old_path: CStr, _new_dir: c_int, _new_path: CStr, _flags: c_uint) -> Result<()> { Err(Errno(ENOSYS)) } + fn symlinkat(_path1: CStr, _fd: c_int, _path2: CStr) -> Result<()> { Err(Errno(ENOSYS)) } + fn sync() -> Result<()> { Err(Errno(ENOSYS)) } + fn unlinkat(_fd: c_int, _path: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn mlock(_addr: *const c_void, _len: usize) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn mlockall(_flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn mremap(_addr: *mut c_void, _len: usize, _new_len: usize, _flags: c_int, _args: *mut c_void) -> Result<*mut c_void> { Err(Errno(ENOSYS)) } + unsafe fn mprotect(_addr: *mut c_void, _len: usize, _prot: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn msync(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn munlock(_addr: *const c_void, _len: usize) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn madvise(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn munlockall() -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn nanosleep(_rqtp: *const timespec, _rmtp: *mut timespec) -> Result<()> { Err(Errno(ENOSYS)) } + fn getpagesize() -> usize { 4096 } + fn verify() -> bool { true } From 20d38bfcec54e05368e7a12e5ee3792434b4d53c Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:28:42 +0600 Subject: [PATCH 08/72] feat(relibc): nonos Pal time + ids + proc ENOSYS stubs clock/timer + id constants (getuid/getgid/getpid-family=0, umask noop) + process ops (fork/execve/spawn/waitpid/rlct_*/futex). Completes the required Pal surface (0 E0046); Phase 1 de-stubs from here. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 5499c45ddb..538cb49b87 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -117,6 +117,50 @@ impl Pal for Sys { unsafe fn munlockall() -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn nanosleep(_rqtp: *const timespec, _rmtp: *mut timespec) -> Result<()> { Err(Errno(ENOSYS)) } fn getpagesize() -> usize { 4096 } + fn clock_getres(_clk_id: clockid_t, _tp: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn clock_gettime(_clk_id: clockid_t, _tp: Out) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn clock_settime(_clk_id: clockid_t, _tp: *const timespec) -> Result<()> { Err(Errno(ENOSYS)) } + fn gettimeofday(_tp: Out, _tzp: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn timer_create(_clock_id: clockid_t, _evp: &sigevent, _timerid: Out) -> Result<()> { Err(Errno(ENOSYS)) } + fn timer_delete(_timerid: timer_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn timer_gettime(_timerid: timer_t, _value: Out) -> Result<()> { Err(Errno(ENOSYS)) } + fn timer_settime(_timerid: timer_t, _flags: c_int, _value: &itimerspec, _ovalue: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn getegid() -> gid_t { 0 } + fn geteuid() -> uid_t { 0 } + fn getgid() -> gid_t { 0 } + fn getgroups(_list: Out<[gid_t]>) -> Result { Err(Errno(ENOSYS)) } + fn getpgid(_pid: pid_t) -> Result { Err(Errno(ENOSYS)) } + fn getppid() -> pid_t { 0 } + fn getpriority(_which: c_int, _who: id_t) -> Result { Err(Errno(ENOSYS)) } + fn getrandom(_buf: &mut [u8], _flags: c_uint) -> Result { Err(Errno(ENOSYS)) } + fn getresgid(_rgid: Option>, _egid: Option>, _sgid: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn getresuid(_ruid: Option>, _euid: Option>, _suid: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn getrlimit(_resource: c_int, _rlim: Out) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn setrlimit(_resource: c_int, _rlim: *const rlimit) -> Result<()> { Err(Errno(ENOSYS)) } + fn getrusage(_who: c_int, _r_usage: Out) -> Result<()> { Err(Errno(ENOSYS)) } + fn getsid(_pid: pid_t) -> Result { Err(Errno(ENOSYS)) } + fn gettid() -> pid_t { 0 } + fn getuid() -> uid_t { 0 } + unsafe fn setgroups(_size: size_t, _list: *const gid_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn setpgid(_pid: pid_t, _pgid: pid_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn setpriority(_which: c_int, _who: id_t, _prio: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn setresgid(_rgid: gid_t, _egid: gid_t, _sgid: gid_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn setresuid(_ruid: uid_t, _euid: uid_t, _suid: uid_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn setsid() -> Result { Err(Errno(ENOSYS)) } + fn umask(_mask: mode_t) -> mode_t { 0 } + unsafe fn execve(_path: CStr, _argv: *const *mut c_char, _envp: *const *mut c_char) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn fexecve(_fildes: c_int, _argv: *const *mut c_char, _envp: *const *mut c_char) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn exit_thread(_stack_base: *mut (), _stack_size: usize) -> ! { Self::exit(0) } + unsafe fn fork() -> Result { Err(Errno(ENOSYS)) } + unsafe fn futex_wait(_addr: *mut u32, _val: u32, _deadline: Option<×pec>) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn futex_wake(_addr: *mut u32, _num: u32) -> Result { Err(Errno(ENOSYS)) } + unsafe fn rlct_clone(_stack: *mut usize, _os_specific: &mut OsSpecific) -> Result { Err(Errno(ENOSYS)) } + unsafe fn rlct_kill(_os_tid: pthread::OsTid, _signal: usize) -> Result<()> { Err(Errno(ENOSYS)) } + fn current_os_tid() -> pthread::OsTid { pthread::OsTid::default() } + unsafe fn spawn(_program: CStr, _fac: Option<&crate::header::spawn::posix_spawn_file_actions_t>, _fat: Option<&crate::header::spawn::posix_spawnattr_t>, _argv: NulTerminated<*mut c_char>, _envp: Option>) -> Result { Err(Errno(ENOSYS)) } + fn waitpid(_pid: pid_t, _stat_loc: Option>, _options: c_int) -> Result { Err(Errno(ENOSYS)) } + fn sched_yield() -> Result<()> { Err(Errno(ENOSYS)) } + fn uname(_utsname: Out) -> Result<()> { Err(Errno(ENOSYS)) } fn verify() -> bool { true From 65566a9ce5ae2ee6188c4946172657f8c29b1d1a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:28:55 +0600 Subject: [PATCH 09/72] feat(relibc): nonos Pal sub-trait stubs (socket/signal/epoll/ptrace) All of PalSocket(15)/PalSignal(12)/PalEpoll(3)/PalPtrace(1) stubbed to ENOSYS (sigsuspend returns Errno directly). Completes the sub-trait surface. --- .../nonos-relibc/platform/nonos/epoll.rs | 14 ++++++++ .../nonos-relibc/platform/nonos/ptrace.rs | 12 +++++++ .../nonos-relibc/platform/nonos/signal.rs | 32 +++++++++++++++++++ .../nonos-relibc/platform/nonos/socket.rs | 29 +++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 toolchain/nonos-relibc/platform/nonos/epoll.rs create mode 100644 toolchain/nonos-relibc/platform/nonos/ptrace.rs create mode 100644 toolchain/nonos-relibc/platform/nonos/signal.rs create mode 100644 toolchain/nonos-relibc/platform/nonos/socket.rs diff --git a/toolchain/nonos-relibc/platform/nonos/epoll.rs b/toolchain/nonos-relibc/platform/nonos/epoll.rs new file mode 100644 index 0000000000..3bf6315258 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/epoll.rs @@ -0,0 +1,14 @@ +use super::{ + super::{PalEpoll, types::*}, + Sys, +}; +use crate::{ + error::{Errno, Result}, + header::{bits_sigset_t::sigset_t, errno::ENOSYS, sys_epoll::epoll_event}, +}; + +impl PalEpoll for Sys { + fn epoll_create1(_flags: c_int) -> Result { Err(Errno(ENOSYS)) } + unsafe fn epoll_ctl(_epfd: c_int, _op: c_int, _fd: c_int, _event: *mut epoll_event) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn epoll_pwait(_epfd: c_int, _events: *mut epoll_event, _maxevents: c_int, _timeout: c_int, _sigmask: *const sigset_t) -> Result { Err(Errno(ENOSYS)) } +} diff --git a/toolchain/nonos-relibc/platform/nonos/ptrace.rs b/toolchain/nonos-relibc/platform/nonos/ptrace.rs new file mode 100644 index 0000000000..0d7b682dca --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/ptrace.rs @@ -0,0 +1,12 @@ +use super::{ + super::{PalPtrace, types::*}, + Sys, +}; +use crate::{ + error::{Errno, Result}, + header::errno::ENOSYS, +}; + +impl PalPtrace for Sys { + unsafe fn ptrace(_request: c_int, _pid: pid_t, _addr: *mut c_void, _data: *mut c_void) -> Result { Err(Errno(ENOSYS)) } +} diff --git a/toolchain/nonos-relibc/platform/nonos/signal.rs b/toolchain/nonos-relibc/platform/nonos/signal.rs new file mode 100644 index 0000000000..637af9ec78 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/signal.rs @@ -0,0 +1,32 @@ +use super::{ + super::{PalSignal, types::*}, + Sys, +}; +#[allow(deprecated)] +use crate::header::sys_time::itimerval; +use crate::{ + error::{Errno, Result}, + header::{ + bits_sigset_t::sigset_t, + errno::ENOSYS, + signal::{sigaction, siginfo_t, sigval, stack_t}, + time::timespec, + }, +}; + +impl PalSignal for Sys { + #[allow(deprecated)] + fn getitimer(_which: c_int, _out: &mut itimerval) -> Result<()> { Err(Errno(ENOSYS)) } + fn kill(_pid: pid_t, _sig: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn sigqueue(_pid: pid_t, _sig: c_int, _val: sigval) -> Result<()> { Err(Errno(ENOSYS)) } + fn killpg(_pgrp: pid_t, _sig: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn raise(_sig: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + #[allow(deprecated)] + fn setitimer(_which: c_int, _new: &itimerval, _old: Option<&mut itimerval>) -> Result<()> { Err(Errno(ENOSYS)) } + fn sigaction(_sig: c_int, _act: Option<&sigaction>, _oact: Option<&mut sigaction>) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn sigaltstack(_ss: Option<&stack_t>, _old_ss: Option<&mut stack_t>) -> Result<()> { Err(Errno(ENOSYS)) } + fn sigpending(_set: &mut sigset_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn sigprocmask(_how: c_int, _set: Option<&sigset_t>, _oset: Option<&mut sigset_t>) -> Result<()> { Err(Errno(ENOSYS)) } + fn sigsuspend(_mask: &sigset_t) -> Errno { Errno(ENOSYS) } + fn sigtimedwait(_set: &sigset_t, _sig: Option<&mut siginfo_t>, _tp: Option<×pec>) -> Result { Err(Errno(ENOSYS)) } +} diff --git a/toolchain/nonos-relibc/platform/nonos/socket.rs b/toolchain/nonos-relibc/platform/nonos/socket.rs new file mode 100644 index 0000000000..9532a61ccb --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/socket.rs @@ -0,0 +1,29 @@ +use super::{ + super::{PalSocket, types::*}, + Sys, +}; +use crate::{ + error::{Errno, Result}, + header::{ + errno::ENOSYS, + sys_socket::{msghdr, sockaddr, socklen_t}, + }, +}; + +impl PalSocket for Sys { + unsafe fn accept(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn bind(_socket: c_int, _address: *const sockaddr, _len: socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn connect(_socket: c_int, _address: *const sockaddr, _len: socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn getpeername(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn getsockname(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn getsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *mut c_void, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn listen(_socket: c_int, _backlog: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn recvfrom(_socket: c_int, _buf: *mut c_void, _len: size_t, _flags: c_int, _addr: *mut sockaddr, _alen: *mut socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn recvmsg(_socket: c_int, _msg: *mut msghdr, _flags: c_int) -> Result { Err(Errno(ENOSYS)) } + unsafe fn sendmsg(_socket: c_int, _msg: *const msghdr, _flags: c_int) -> Result { Err(Errno(ENOSYS)) } + unsafe fn sendto(_socket: c_int, _buf: *const c_void, _len: size_t, _flags: c_int, _addr: *const sockaddr, _alen: socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn setsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *const c_void, _len: socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn shutdown(_socket: c_int, _how: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn socket(_domain: c_int, _kind: c_int, _protocol: c_int) -> Result { Err(Errno(ENOSYS)) } + fn socketpair(_domain: c_int, _kind: c_int, _protocol: c_int, _sv: &mut [c_int; 2]) -> Result<()> { Err(Errno(ENOSYS)) } +} From c99297a0d9fe6f70c12f432897e1d72664d15798 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:28:55 +0600 Subject: [PATCH 10/72] fix(relibc): wrap nonos syscall asm in unsafe blocks relibc builds with -D unsafe-op-in-unsafe-fn (edition 2024); bare asm! in an unsafe fn is E0133. Wrap each syscallN asm! in an explicit unsafe block. --- .../nonos-relibc/platform/nonos/lowlevel.rs | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs index 0f7433d9a6..d90674c19a 100644 --- a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -13,58 +13,72 @@ pub const MK_MUNMAP: u64 = tag4(b"MUMP"); #[inline] pub unsafe fn syscall0(n: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall1(n: u64, a0: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall2(n: u64, a0: u64, a1: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall3(n: u64, a0: u64, a1: u64, a2: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, - in("rdx") a2, lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall4(n: u64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, - in("rdx") a2, in("r10") a3, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall5(n: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, - in("rdx") a2, in("r10") a3, in("r8") a4, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, in("r8") a4, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } #[inline] pub unsafe fn syscall6(n: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 { let r: i64; - asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, - in("rdx") a2, in("r10") a3, in("r8") a4, in("r9") a5, - lateout("rcx") _, lateout("r11") _, options(nostack)); + unsafe { + asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + in("rdx") a2, in("r10") a3, in("r8") a4, in("r9") a5, + lateout("rcx") _, lateout("r11") _, options(nostack)); + } r } From 199805fcf1f4edbf15739e5b16b48afc096c7eb7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 22:29:37 +0600 Subject: [PATCH 11/72] feat(relibc): graft nonos sub-traits, Sync errno, json-target CARGOFLAGS apply.sh now copies socket/signal/epoll/ptrace.rs, replaces relibc's thread_local ERRNO with a Sync errno static for nonos (no per-thread TLS; plain static Cell isn't Sync), and writes an idempotent config.mk `override CARGOFLAGS` that passes the json target spec + -Z json-target-spec (cargo build-std ignores RUST_TARGET_PATH for custom targets). --- toolchain/nonos-relibc/apply.sh | 69 +++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index ce8691ac1f..825586185b 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -2,8 +2,10 @@ # Graft the NONOS relibc platform backend into the gitignored vendored relibc # (third_party/redox/src-repos/relibc). The backend is authored as tracked # source here; this copies it in and idempotently patches the vendored platform -# selector + config.mk so `make TARGET=x86_64-unknown-nonos headers` builds the -# nonos arm. Re-run after a relibc re-import or `git clean`. Idempotent. +# selector, a Sync errno static (kernel has no per-thread TLS), and config.mk +# (cc/ld block + an `override CARGOFLAGS` carrying the json target spec) so +# `make TARGET=x86_64-unknown-nonos libs` builds the nonos arm. Re-run after a +# relibc re-import or `git clean`. Idempotent. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" @@ -15,39 +17,72 @@ CC_NONOS="$REPO/toolchain/nonos-c/cc-nonos" mkdir -p "$RELIBC/src/platform/nonos" cp "$HERE/platform/nonos/mod.rs" "$RELIBC/src/platform/nonos/mod.rs" cp "$HERE/platform/nonos/lowlevel.rs" "$RELIBC/src/platform/nonos/lowlevel.rs" +cp "$HERE/platform/nonos/socket.rs" "$RELIBC/src/platform/nonos/socket.rs" +cp "$HERE/platform/nonos/signal.rs" "$RELIBC/src/platform/nonos/signal.rs" +cp "$HERE/platform/nonos/epoll.rs" "$RELIBC/src/platform/nonos/epoll.rs" +cp "$HERE/platform/nonos/ptrace.rs" "$RELIBC/src/platform/nonos/ptrace.rs" # 2. patch the platform selector + config.mk (idempotent) python3 - "$RELIBC" "$CC_NONOS" <<'PY' -import sys +import re, sys relibc, cc = sys.argv[1], sys.argv[2] +tgtdir = cc.rsplit("/", 1)[0] modrs = f"{relibc}/src/platform/mod.rs" with open(modrs) as f: s = f.read() -if 'target_os = "nonos"' not in s: +orig = s +if '#[path = "nonos/mod.rs"]' not in s: # vendored-relibc-version-specific anchor — re-check on a relibc re-import anchor = '\npub use self::rlb::{Line, RawLineBuffer};' arm = ('\n#[cfg(target_os = "nonos")]\n#[path = "nonos/mod.rs"]\n' 'pub(crate) mod sys;\n') i = s.index(anchor) + s = s[:i] + arm + s[i:] +errno_block = ( + '#[cfg(not(target_os = "nonos"))]\n' + '#[thread_local]\n' + 'pub static ERRNO: Cell = Cell::new(0);\n' + '#[cfg(target_os = "nonos")]\n' + 'pub struct SyncErrno(Cell);\n' + '#[cfg(target_os = "nonos")]\n' + 'unsafe impl Sync for SyncErrno {}\n' + '#[cfg(target_os = "nonos")]\n' + 'impl SyncErrno {\n' + ' pub fn get(&self) -> c_int { self.0.get() }\n' + ' pub fn set(&self, v: c_int) { self.0.set(v) }\n' + ' pub fn as_ptr(&self) -> *mut c_int { self.0.as_ptr() }\n' + '}\n' + '#[cfg(target_os = "nonos")]\n' + 'pub static ERRNO: SyncErrno = SyncErrno(Cell::new(0));' +) +if 'struct SyncErrno' not in s: + s = re.sub( + r'(#\[cfg_attr\(not\(target_os = "nonos"\), thread_local\)\]|#\[thread_local\])' + r'\npub static ERRNO: Cell = Cell::new\(0\);', + lambda _m: errno_block, s, count=1) +if s != orig: with open(modrs, "w") as f: - f.write(s[:i] + arm + s[i:]) + f.write(s) cfgmk = f"{relibc}/config.mk" with open(cfgmk) as f: s = f.read() -if 'x86_64-unknown-nonos' not in s: - block = ("\nifeq ($(TARGET),x86_64-unknown-nonos)\n" - f"\texport CC={cc}\n" - "\texport LD=ld.lld\n" - "\texport AR=llvm-ar\n" - "\texport NM=llvm-nm\n" - "\texport CPPFLAGS=\n" - "\texport CARGO_TEST=\n" - "endif\n") - with open(cfgmk, "w") as f: - f.write(s.rstrip("\n") + "\n" + block) +block = ("\nifeq ($(TARGET),x86_64-unknown-nonos)\n" + f"\texport CC={cc}\n" + "\texport LD=ld.lld\n" + "\texport AR=llvm-ar\n" + "\texport NM=llvm-nm\n" + "\texport CPPFLAGS=\n" + "\texport CARGO_TEST=\n" + "\toverride CARGOFLAGS := -Z build-std=core,alloc,compiler_builtins " + f"-Z json-target-spec --target={tgtdir}/x86_64-unknown-nonos.json\n" + "endif\n") +s = re.sub(r"\nifeq \(\$\(TARGET\),x86_64-unknown-nonos\).*?endif\n", "", s, flags=re.S) +with open(cfgmk, "w") as f: + f.write(s.rstrip("\n") + "\n" + block) PY echo "NONOS relibc backend grafted into $RELIBC" -echo " src/platform/nonos/{mod.rs,lowlevel.rs}; cfg arm + config.mk TARGET block" +echo " src/platform/nonos/{mod,lowlevel,socket,signal,epoll,ptrace}.rs" +echo " cfg arm + Sync-errno patch + config.mk TARGET block (override CARGOFLAGS)" From 8ec655dcb14efb81ff470ab4cf8b3af01bfaf2b7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sun, 28 Jun 2026 23:30:38 +0600 Subject: [PATCH 12/72] feat(relibc): disable libc layout-check + wire objcopy for nonos The default `check_against_libc_crate` feature pulls in the `libc` crate (aliased `__libc_only_for_layout_checks`) to size-assert every struct, but `libc` has no `x86_64-unknown-nonos` arm so the layout types are missing. Drop default features (keep `no_trace`) for the nonos build. Also export `OBJCOPY=llvm-objcopy` so relibc's `renamesyms.sh` post-step resolves on a host with only LLVM binutils. --- toolchain/nonos-relibc/apply.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 825586185b..33aeb8ab67 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -73,10 +73,12 @@ block = ("\nifeq ($(TARGET),x86_64-unknown-nonos)\n" "\texport LD=ld.lld\n" "\texport AR=llvm-ar\n" "\texport NM=llvm-nm\n" + "\texport OBJCOPY=llvm-objcopy\n" "\texport CPPFLAGS=\n" "\texport CARGO_TEST=\n" "\toverride CARGOFLAGS := -Z build-std=core,alloc,compiler_builtins " - f"-Z json-target-spec --target={tgtdir}/x86_64-unknown-nonos.json\n" + f"-Z json-target-spec --target={tgtdir}/x86_64-unknown-nonos.json " + "--no-default-features --features no_trace\n" "endif\n") s = re.sub(r"\nifeq \(\$\(TARGET\),x86_64-unknown-nonos\).*?endif\n", "", s, flags=re.S) with open(cfgmk, "w") as f: From 9453ada7b0cd58a55abe23e9c0a382f64089d2ac Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 00:47:40 +0600 Subject: [PATCH 13/72] feat(relibc): reuse linux ABI defs for nonos headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relibc gates per-target_os: header `sys` submodules (where O_*/CLOCK_*/AT_*/ EPOLL_* live), `auxv_defs`, the `termios` struct + cfspeed accessors, the passwd/group/shadow field SEPARATOR, PAGE_SIZE, SHM_PATH, the pty path selection, and the sigevent struct — all linux/redox only. nonos targets generic x86_64 SysV, so point these at the existing linux definitions via a `#[cfg(any(target_os = "linux", target_os = "nonos"))]` broaden. Idempotent graft patches; the vendored relibc stays gitignored. --- toolchain/nonos-relibc/apply.sh | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 33aeb8ab67..d9f99cae59 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -85,6 +85,66 @@ with open(cfgmk, "w") as f: f.write(s.rstrip("\n") + "\n" + block) PY +# 3. reuse linux generic-x86_64 ABI definitions for nonos (idempotent) +python3 - "$RELIBC" <<'PY' +import sys +relibc = sys.argv[1] + +def patch(rel, old, new): + p = f"{relibc}/{rel}" + with open(p) as f: + s = f.read() + if new in s: + return + if old not in s: + raise SystemExit(f"anchor not found in {rel}") + with open(p, "w") as f: + f.write(s.replace(old, new, 1)) + +LX = '#[cfg(target_os = "linux")]' +NX = '#[cfg(any(target_os = "linux", target_os = "nonos"))]' + +def broaden(rel, marker): + patch(rel, f'{LX}\n{marker}', f'{NX}\n{marker}') + +for m in ("_paths", "bits_open-flags", "fcntl", "netdb", "signal", + "sys_mman", "sys_syslog", "termios"): + patch(f"src/header/{m}/mod.rs", + f'{LX}\n#[path = "linux.rs"]', f'{NX}\n#[path = "linux.rs"]') + +patch("src/header/time/constants.rs", + f'{LX}\n#[path = "linux.rs"]', f'{NX}\n#[path = "linux.rs"]') +patch("src/header/unistd/sysconf.rs", + f'{LX}\n#[path = "sysconf/linux.rs"]', f'{NX}\n#[path = "sysconf/linux.rs"]') +patch("src/platform/mod.rs", f'{LX}\npub mod auxv_defs;', f'{NX}\npub mod auxv_defs;') +patch("src/header/sys_epoll/mod.rs", + f'{LX}\npub const EPOLL_CLOEXEC: c_int = 0x8_0000;', + f'{NX}\npub const EPOLL_CLOEXEC: c_int = 0x8_0000;') + +broaden("src/header/termios/mod.rs", + '#[repr(C)]\n#[derive(Default, Clone)]\npub struct termios {') +for fn in ("cfgetispeed", "cfgetospeed", "cfsetispeed", "cfsetospeed"): + broaden("src/header/termios/mod.rs", + f'#[unsafe(no_mangle)]\npub unsafe extern "C" fn {fn}') + +broaden("src/header/grp/mod.rs", "const SEPARATOR: char = ':';") +broaden("src/header/pwd/mod.rs", "const SEPARATOR: u8 = b':';") +broaden("src/header/shadow/mod.rs", "const SEPARATOR: char = ':';") +broaden("src/header/pwd/mod.rs", "mod linux;") +broaden("src/header/pwd/mod.rs", "use self::linux as sys;") +broaden("src/header/limits/mod.rs", "pub const PAGE_SIZE: usize = 4096;") +broaden("src/header/sys_mman/mod.rs", 'static SHM_PATH: &[u8] = b"/dev/shm/";') +broaden("src/header/stdlib/mod.rs", + ' let r = unsafe { open(c"/dev/ptmx".as_ptr(), flags) };') +broaden("src/header/stdlib/mod.rs", + ' let name = format!("/dev/pts/{}", pty);') + +patch("src/header/signal/mod.rs", + '#[cfg(not(target_os = "linux"))]\npub struct sigevent {', + '#[cfg(not(any(target_os = "linux", target_os = "nonos")))]\npub struct sigevent {') +broaden("src/header/signal/mod.rs", "pub struct sigevent {") +PY + echo "NONOS relibc backend grafted into $RELIBC" echo " src/platform/nonos/{mod,lowlevel,socket,signal,epoll,ptrace}.rs" echo " cfg arm + Sync-errno patch + config.mk TARGET block (override CARGOFLAGS)" From 211886e48d9a344b87fb1a9e705e631f12a525c8 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 00:48:26 +0600 Subject: [PATCH 14/72] feat(relibc): nonos arms for ioctl, TCB, and ld_so init These need nonos-specific behavior, not a linux reuse: `ioctl` returns -1 (no tty ioctls) and gates the now-unused ResultExt import off; the TCB gets `OsSpecific = ()`, the shared `os_new`, and a no-op `os_arch_activate` (the kernel sets no per-thread FS base, and errno is a Sync static, so TLS activation is a no-op); `ld_so::init` is enabled for nonos with `tp = 0` instead of the linux ARCH_GET_FS arch_prctl (no `syscall!` macro on nonos). With this, librelibc.a compiles and post-processes clean for nonos. --- toolchain/nonos-relibc/apply.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index d9f99cae59..668fabc780 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -143,6 +143,38 @@ patch("src/header/signal/mod.rs", '#[cfg(not(target_os = "linux"))]\npub struct sigevent {', '#[cfg(not(any(target_os = "linux", target_os = "nonos")))]\npub struct sigevent {') broaden("src/header/signal/mod.rs", "pub struct sigevent {") + +patch("src/header/sys_ioctl/mod.rs", + 'use crate::{\n error::ResultExt,\n' + ' platform::types::{c_char, c_int, c_ulong, c_ushort, c_void},\n};', + '#[cfg(not(target_os = "nonos"))]\nuse crate::error::ResultExt;\n' + 'use crate::platform::types::{c_char, c_int, c_ulong, c_ushort, c_void};') +patch("src/header/sys_ioctl/mod.rs", + ' #[cfg(target_os = "redox")]\n' + ' unsafe { self::redox::ioctl_inner(fd, request, out) }.or_minus_one_errno()\n}', + ' #[cfg(target_os = "nonos")]\n { let _ = (fd, request, out); -1 }\n' + ' #[cfg(target_os = "redox")]\n' + ' unsafe { self::redox::ioctl_inner(fd, request, out) }.or_minus_one_errno()\n}') + +patch("src/ld_so/tcb.rs", + '#[cfg(target_os = "linux")]\npub type OsSpecific = ();', + '#[cfg(any(target_os = "linux", target_os = "nonos"))]\npub type OsSpecific = ();') +patch("src/ld_so/tcb.rs", + '#[cfg(any(target_os = "linux", target_os = "redox"))]\n unsafe fn os_new(', + '#[cfg(any(target_os = "linux", target_os = "redox", target_os = "nonos"))]\n unsafe fn os_new(') +patch("src/ld_so/tcb.rs", + '#[cfg(all(target_os = "linux", target_arch = "x86_64"))]', + '#[cfg(all(target_os = "nonos", target_arch = "x86_64"))]\n' + ' unsafe fn os_arch_activate(_os: &(), _tls_end: usize, _tls_len: usize) {}\n\n' + ' #[cfg(all(target_os = "linux", target_arch = "x86_64"))]') + +patch("src/ld_so/mod.rs", + '#[cfg(any(target_os = "linux", target_os = "redox"))]\npub unsafe fn init(', + '#[cfg(any(target_os = "linux", target_os = "redox", target_os = "nonos"))]\npub unsafe fn init(') +patch("src/ld_so/mod.rs", + '#[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS', + '#[cfg(all(target_os = "nonos", target_arch = "x86_64"))]\n { tp = 0; }\n' + ' #[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS') PY echo "NONOS relibc backend grafted into $RELIBC" From 8b9c614b565624d726179e6ad2a010c8866fbea0 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 00:52:45 +0600 Subject: [PATCH 15/72] fix(relibc): drop no_trace from nonos CARGOFLAGS CARGOFLAGS applies to every relibc sub-crate build, but crt0/crti/crtn/ld_so have no `no_trace` feature, so `--features no_trace` aborts their object builds. `--no-default-features` alone already excludes check_against_libc_crate (the real fix); trace logging is a runtime no-op without a registered logger. With this, libc.a + crt0.o/crti.o/crtn.o all build for nonos. --- toolchain/nonos-relibc/apply.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 668fabc780..7685b412e4 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -78,7 +78,7 @@ block = ("\nifeq ($(TARGET),x86_64-unknown-nonos)\n" "\texport CARGO_TEST=\n" "\toverride CARGOFLAGS := -Z build-std=core,alloc,compiler_builtins " f"-Z json-target-spec --target={tgtdir}/x86_64-unknown-nonos.json " - "--no-default-features --features no_trace\n" + "--no-default-features\n" "endif\n") s = re.sub(r"\nifeq \(\$\(TARGET\),x86_64-unknown-nonos\).*?endif\n", "", s, flags=re.S) with open(cfgmk, "w") as f: From 1ac2497de9dd7caa7ccf4a4624c44df1fca696e2 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 00:55:26 +0600 Subject: [PATCH 16/72] feat(relibc): nonos crt0 synthesizes SysV initial stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel enters a capsule at the ELF entry with all GPRs zeroed and an empty stack — no argc/argv/envp/auxv, unlike a SysV process start. relibc's generic x86_64 _start does `mov rdi, rsp` and hands that straight to relibc_start_v1, which would read garbage argc off the empty stack. Add a nonos _start that synthesizes argc=0, an empty argv/envp, and an AT_NULL auxv (16-byte aligned: entry rsp ≡ 8 mod 16) before calling relibc_crt0, and exclude nonos from the generic arm. crt0.o builds; _start disassembles to the synthesized frame. --- toolchain/nonos-relibc/apply.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 7685b412e4..0596750b77 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -175,6 +175,23 @@ patch("src/ld_so/mod.rs", '#[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS', '#[cfg(all(target_os = "nonos", target_arch = "x86_64"))]\n { tp = 0; }\n' ' #[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS') + +patch("src/crt0/src/lib.rs", + '#[cfg(target_arch = "x86_64")]\nglobal_asm!(', + '#[cfg(target_os = "nonos")]\nglobal_asm!(\n' + ' "\n' + ' .globl _start\n' + ' .type _start, @function\n' + '_start:\n' + ' xor rbp, rbp\n' + ' and rsp, -16\n' + ' sub rsp, 8\n' + ' push 0\n push 0\n push 0\n push 0\n push 0\n' + ' mov rdi, rsp\n' + ' call relibc_crt0\n' + ' .size _start, . - _start\n' + '"\n);\n\n' + '#[cfg(all(target_arch = "x86_64", not(target_os = "nonos")))]\nglobal_asm!(') PY echo "NONOS relibc backend grafted into $RELIBC" From e3814888381b7962819908e1503792ecf43a87a3 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 00:58:23 +0600 Subject: [PATCH 17/72] feat(toolchain): assemble nonos C sysroot from relibc output build-sysroot.sh grafts the relibc nonos backend, builds the static artifacts (libc.a + crt0/crti/crtn objects) and cbindgen headers for x86_64-unknown-nonos, and stages them into toolchain/nonos-c/sysroot/ {lib,include}. Static-link only: libc.so/ld.so are not built (the capsule link is fully static), so it builds the specific targets, not `all`. The sysroot itself is a build artifact and gitignored. --- .gitignore | 3 +++ toolchain/nonos-c/build-sysroot.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100755 toolchain/nonos-c/build-sysroot.sh diff --git a/.gitignore b/.gitignore index 4c5d9d8061..84a9961e05 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ CONTEXT.md .continue/ .superpowers/ + +# nonos C sysroot (build artifact of toolchain/nonos-c/build-sysroot.sh) +toolchain/nonos-c/sysroot/ diff --git a/toolchain/nonos-c/build-sysroot.sh b/toolchain/nonos-c/build-sysroot.sh new file mode 100755 index 0000000000..c5b8175fba --- /dev/null +++ b/toolchain/nonos-c/build-sysroot.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Assemble the NONOS C sysroot from the vendored relibc build output: +# sysroot/lib/{libc.a,crt0.o,crti.o,crtn.o} + sysroot/include (cbindgen + +# hand-written headers). Static-link only — libc.so/ld.so are deliberately +# not built (the nonos capsule link is fully static), so this builds the +# specific targets rather than `all`. Re-run after a relibc change. Idempotent. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +RELIBC="$REPO/third_party/redox/src-repos/relibc" +OUT="$HERE/sysroot" +B="$RELIBC/target/x86_64-unknown-nonos/release" + +export PATH="/usr/local/opt/llvm/bin:/usr/local/opt/lld/bin:/usr/local/opt/findutils/libexec/gnubin:$PATH" + +"$REPO/toolchain/nonos-relibc/apply.sh" + +make -C "$RELIBC" TARGET=x86_64-unknown-nonos NONOS_C_DIR="$HERE" USE_RUST_LIBM=1 \ + headers "$B/libc.a" "$B/crt0.o" "$B/crti.o" "$B/crtn.o" + +rm -rf "$OUT" +mkdir -p "$OUT/lib" "$OUT/include" +cp "$B/libc.a" "$B/crt0.o" "$B/crti.o" "$B/crtn.o" "$OUT/lib/" +cp -r "$RELIBC/target/x86_64-unknown-nonos/include/." "$OUT/include/" +echo "[sysroot] assembled at $OUT" From 84b379cce0d3e5097bc49af6b8105b7ec80f4149 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 01:18:51 +0600 Subject: [PATCH 18/72] fix(toolchain): fix cc-nonos cfg for static-PIE ELF capsule build Replace -fno-PIC with -fPIE so compiled objects carry PIC relocs compatible with -pie linking. Remove -static, which conflicts with -static-pie passed at link time. Add -fno-builtin to prevent clang from redirecting write() to _write() (a reserved name libc.a does not export). Add -fuse-ld=lld so clang finds lld when used as a combined compile+link driver in future contexts. --- toolchain/nonos-c/x86_64-unknown-nonos.cfg | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-c/x86_64-unknown-nonos.cfg b/toolchain/nonos-c/x86_64-unknown-nonos.cfg index 55c5a6ce9f..e0754f23a5 100644 --- a/toolchain/nonos-c/x86_64-unknown-nonos.cfg +++ b/toolchain/nonos-c/x86_64-unknown-nonos.cfg @@ -2,7 +2,8 @@ -D__nonos__ -ffreestanding -fno-stack-protector --fno-PIC +-fPIE -mno-red-zone --static -nostdlib +-fno-builtin +-fuse-ld=lld From b5af81f600dbbf65925ac94773e73cc1fc2f77fa Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 01:19:54 +0600 Subject: [PATCH 19/72] feat(capsule): add capsule_c_proof C capsule source and build wiring Adds the first clang-built C capsule to NONOS: capsule_c_proof prints [C-PROOF] PASS via write(1,...) using relibc from the nonos-c sysroot. Files added: - userland/capsule_c_proof/src/main.c: minimal C entry point - userland/capsule_c_proof/Capsule.mk: slug c-proof, ports 4504/4505, caps 0x19; uses CAPSULE_PREBUILT_BIN so capsule.mk stages and signs the clang ELF rather than invoking cargo - Makefile: include line next to capsule_std_proof The nonos-mk submodule is bumped to pick up capsule-c.mk (new build rule). Build artifacts are gitignored via .gitignore. --- Makefile | 1 + nonos-mk | 2 +- userland/capsule_c_proof/.gitignore | 1 + userland/capsule_c_proof/Capsule.mk | 16 ++++++++++++++++ userland/capsule_c_proof/src/main.c | 6 ++++++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 userland/capsule_c_proof/.gitignore create mode 100644 userland/capsule_c_proof/Capsule.mk create mode 100644 userland/capsule_c_proof/src/main.c diff --git a/Makefile b/Makefile index 4f0b5e74f8..96ce9ade5c 100644 --- a/Makefile +++ b/Makefile @@ -534,6 +534,7 @@ nonos-mk-trust-policy: $(NONOS_TRUST_ANCHOR_POLICY_BIN) # nonos-mk-check--keys assert publisher seeds + pubs exist include userland/capsule_proof_io/Capsule.mk include userland/capsule_std_proof/Capsule.mk +include userland/capsule_c_proof/Capsule.mk include userland/capsule_ripgrep/Capsule.mk include userland/capsule_ramfs/Capsule.mk include userland/capsule_keyring/Capsule.mk diff --git a/nonos-mk b/nonos-mk index 164ce49161..15205476cc 160000 --- a/nonos-mk +++ b/nonos-mk @@ -1 +1 @@ -Subproject commit 164ce491614688bbdaf436acdc15fd4ddf51cdc9 +Subproject commit 15205476cc34a5ec0f893b51219c27be0c497377 diff --git a/userland/capsule_c_proof/.gitignore b/userland/capsule_c_proof/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/userland/capsule_c_proof/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/userland/capsule_c_proof/Capsule.mk b/userland/capsule_c_proof/Capsule.mk new file mode 100644 index 0000000000..64603dda06 --- /dev/null +++ b/userland/capsule_c_proof/Capsule.mk @@ -0,0 +1,16 @@ +CAPSULE_SLUG := c-proof +CAPSULE_HANDLE := c_proof +CAPSULE_DOMAIN := systems.nonos +CAPSULE_DIR := userland/capsule_c_proof +CAPSULE_BIN_NAME := c_proof +CAPSULE_FEATURE := nonos-capsule-c-proof +CAPSULE_NAMESPACE := systems.nonos.c_proof +CAPSULE_SERVICE_ENDPOINT := service:4504:c_proof +CAPSULE_REPLY_ENDPOINT := reply:4505:endpoint.c_proof.reply +# CoreExec | IPC | Memory = 0x01 | 0x08 | 0x10 = 0x19 +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_KERNEL_MIRROR := src/userspace/capsule_c_proof +CAPSULE_PREBUILT_BIN := userland/capsule_c_proof/build/c_proof + +include nonos-mk/capsule-c.mk +include nonos-mk/capsule.mk diff --git a/userland/capsule_c_proof/src/main.c b/userland/capsule_c_proof/src/main.c new file mode 100644 index 0000000000..ebca3afd44 --- /dev/null +++ b/userland/capsule_c_proof/src/main.c @@ -0,0 +1,6 @@ +#include +static const char PASS[] = "[C-PROOF] PASS\n"; +int main(void) { + write(1, PASS, sizeof(PASS) - 1); + return 0; +} From b295116e4566ba1171b9c3132950af55ea2829cc Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 09:53:39 +0600 Subject: [PATCH 20/72] feat(capsule_c_proof): add kernel-side mirror, Cargo feature+profile, init wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror capsule_std_proof exactly for the new c_proof capsule: - src/userspace/capsule_c_proof/{mod,embed,spawn}.rs — embed/spawn mirror - src/userspace/mod.rs — capsule_c_proof module declaration - Cargo.toml — nonos-capsule-c-proof feature + microkernel-c-proof profile - src/userspace/init/entry.rs — feature-gated run_c_proof() + call in run_init() Service port 4504/4505. Empty-slice fallbacks keep nonos-mk-check clean with the feature off; signed trust artifacts are out of scope (controller handles). --- Cargo.toml | 8 ++++ src/userspace/capsule_c_proof/embed.rs | 44 +++++++++++++++++++++ src/userspace/capsule_c_proof/mod.rs | 20 ++++++++++ src/userspace/capsule_c_proof/spawn.rs | 55 ++++++++++++++++++++++++++ src/userspace/init/entry.rs | 12 ++++++ src/userspace/mod.rs | 1 + 6 files changed, 140 insertions(+) create mode 100644 src/userspace/capsule_c_proof/embed.rs create mode 100644 src/userspace/capsule_c_proof/mod.rs create mode 100644 src/userspace/capsule_c_proof/spawn.rs diff --git a/Cargo.toml b/Cargo.toml index 4701999832..e59224caa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ sched = [] # below maps 1:1 to a capsule directory under `userland/`. nonos-capsule-proof-io = [] nonos-capsule-std-proof = [] +nonos-capsule-c-proof = [] nonos-capsule-ripgrep = [] nonos-capsule-ramfs = [] nonos-capsule-wallpaper = [] @@ -212,6 +213,13 @@ microkernel-ramfs = [ "nonos-capsule-ramfs", ] +microkernel-c-proof = [ + "microkernel-core", + "nonos-production", + "nonos-capsule-proof-io", + "nonos-capsule-c-proof", +] + microkernel-keyring = [ "microkernel-core", "nonos-production", diff --git a/src/userspace/capsule_c_proof/embed.rs b/src/userspace/capsule_c_proof/embed.rs new file mode 100644 index 0000000000..edc4f58189 --- /dev/null +++ b/src/userspace/capsule_c_proof/embed.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 . + +#[cfg(feature = "nonos-capsule-c-proof")] +pub(crate) const C_PROOF_ELF: &[u8] = include_bytes!( + "../../../userland/capsule_c_proof/target/x86_64-nonos-user/release/c_proof" +); + +#[cfg(feature = "nonos-capsule-c-proof")] +pub(crate) const C_PROOF_NONOS_ID_CERT_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_proof.nonos_id_cert.bin"); + +#[cfg(feature = "nonos-capsule-c-proof")] +pub(crate) const C_PROOF_MANIFEST_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_proof.manifest.bin"); + +#[cfg(feature = "nonos-capsule-c-proof")] +pub(crate) const C_PROOF_ATTESTATION_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_proof.zk_trailer.bin"); + +#[cfg(not(feature = "nonos-capsule-c-proof"))] +pub(crate) const C_PROOF_ELF: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-proof"))] +pub(crate) const C_PROOF_NONOS_ID_CERT_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-proof"))] +pub(crate) const C_PROOF_MANIFEST_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-proof"))] +pub(crate) const C_PROOF_ATTESTATION_BYTES: &[u8] = &[]; diff --git a/src/userspace/capsule_c_proof/mod.rs b/src/userspace/capsule_c_proof/mod.rs new file mode 100644 index 0000000000..9f79b072b1 --- /dev/null +++ b/src/userspace/capsule_c_proof/mod.rs @@ -0,0 +1,20 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod embed; +mod spawn; + +pub use spawn::spawn_c_proof_capsule; diff --git a/src/userspace/capsule_c_proof/spawn.rs b/src/userspace/capsule_c_proof/spawn.rs new file mode 100644 index 0000000000..a60c8b8f12 --- /dev/null +++ b/src/userspace/capsule_c_proof/spawn.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 . + +use super::embed::{ + C_PROOF_ATTESTATION_BYTES, C_PROOF_ELF, C_PROOF_MANIFEST_BYTES, + C_PROOF_NONOS_ID_CERT_BYTES, +}; +use crate::capabilities::Capability; +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, +}; + +const SERVICE_NAME: &str = "c_proof"; +const SERVICE_PORT: u32 = 4504; +const REPLY_INBOX: &str = "endpoint.c_proof.reply"; +const REPLY_PORT: u32 = 4505; +const TARGET_TRIPLE: &str = "x86_64-nonos-user"; + +pub fn spawn_c_proof_capsule() -> Result<(), SpawnError> { + 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: C_PROOF_ELF, + nonos_id_cert_bytes: C_PROOF_NONOS_ID_CERT_BYTES, + manifest_bytes: C_PROOF_MANIFEST_BYTES, + attestation_trailer: C_PROOF_ATTESTATION_BYTES, + target_triple: TARGET_TRIPLE, + requested_caps: Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit(), + debug_tag: b"", + }; + capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + Ok(()) +} diff --git a/src/userspace/init/entry.rs b/src/userspace/init/entry.rs index 150debe0ab..2b31c03b32 100644 --- a/src/userspace/init/entry.rs +++ b/src/userspace/init/entry.rs @@ -21,6 +21,7 @@ pub fn run_init() -> ! { boot_log::ok("INIT", "Starting"); run_user_entry_proof(); run_std_proof(); + run_c_proof(); run_ripgrep(); spawn_plan::spawn_ramfs(); spawn_plan::spawn_core_after_ramfs(); @@ -57,6 +58,17 @@ fn run_std_proof() { #[cfg(not(feature = "nonos-capsule-std-proof"))] fn run_std_proof() {} +#[cfg(feature = "nonos-capsule-c-proof")] +fn run_c_proof() { + match crate::userspace::capsule_c_proof::spawn_c_proof_capsule() { + Ok(()) => boot_log::ok("C-PROOF", "capsule spawned"), + Err(_) => boot_log::error("C-PROOF capsule spawn failed"), + } +} + +#[cfg(not(feature = "nonos-capsule-c-proof"))] +fn run_c_proof() {} + #[cfg(feature = "nonos-capsule-ripgrep")] fn run_ripgrep() { match crate::userspace::capsule_ripgrep::spawn_ripgrep_capsule() { diff --git a/src/userspace/mod.rs b/src/userspace/mod.rs index 5b9262c021..da168343de 100644 --- a/src/userspace/mod.rs +++ b/src/userspace/mod.rs @@ -61,6 +61,7 @@ pub mod capsule_settings; #[cfg(feature = "nonos-capsule-setup-wizard")] pub mod capsule_setup_wizard; pub mod capsule_snake; +pub mod capsule_c_proof; pub mod capsule_std_proof; pub mod capsule_terminal; pub mod capsule_text_editor; From 28d2ee4e508c552b775d3d3cdc0326750fb6c3e5 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 10:14:18 +0600 Subject: [PATCH 21/72] feat(test): add nonos-mk-cproof prod/boot/test targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 boot-gate infrastructure for the clang/relibc C capsule: nonos-mk-cproof-prod builds the microkernel-c-proof kernel (verified-spawn, proof-io + c_proof embedded); nonos-mk-boot-cproof / nonos-mk-cproof-test run tests/boot/cproof.sh, which boots under QEMU/hvf and greps [C-PROOF] PASS. Kernel builds clean under nonos-production with c_proof embedded; the boot gate itself is pending a valid per-capsule ZK attestation trailer (see CONTEXT.md — capsule attestation is mid NZKCAPS1->NZKCAPS2 migration). --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index 96ce9ade5c..a4dbe4b2af 100644 --- a/Makefile +++ b/Makefile @@ -810,6 +810,20 @@ nonos-mk-std-proof-prod: $(proof-io_ARTIFACTS) $(std-proof_ARTIFACTS) nonos-mk-c $(CARGO) build $(KERNEL_BUILD_FLAGS) \ --no-default-features --features microkernel-proof-io,nonos-capsule-std-proof +.PHONY: nonos-mk-cproof-prod nonos-mk-boot-cproof nonos-mk-cproof-test +nonos-mk-cproof-prod: $(proof-io_ARTIFACTS) $(c-proof_ARTIFACTS) \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + @echo "Building kernel (microkernel-c-proof)..." + @$(SDK_FLAGS) NONOS_SIGNING_KEY=$(KERNEL_SIGNING_KEY) \ + RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + $(CARGO) build $(KERNEL_BUILD_FLAGS) \ + --no-default-features --features microkernel-c-proof + +nonos-mk-boot-cproof: + @./tests/boot/cproof.sh + +nonos-mk-cproof-test: nonos-mk-boot-cproof + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." From aaae189ab6e5b917a4ab6fd959de3c559014f44a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 12:53:09 +0600 Subject: [PATCH 22/72] feat(relibc): TLS-less nonos startup + minimal crt0 for the C capsule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relibc's full startup is TLS/pthread-coupled and faults on nonos (no FS base): ld_so::static_init panics on the empty auxv, and init_array's pthread::init needs a TCB. The plan defers TLS to Phase 3, so for the Phase-0 C-capsule proof: ld_so::init is a no-op for nonos (tp=1 routes to the "TCB already present" branch), Tcb::current() returns None (no FS-base read to fault on), and the nonos crt0 _start calls main directly then MkExit — bypassing relibc_start_v1's TLS/stdio/pthread init. relibc's libc functions (write -> MkDebug) still work; full relibc startup lands when TLS is brought up. --- toolchain/nonos-relibc/apply.sh | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 0596750b77..062502540e 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -173,9 +173,18 @@ patch("src/ld_so/mod.rs", '#[cfg(any(target_os = "linux", target_os = "redox", target_os = "nonos"))]\npub unsafe fn init(') patch("src/ld_so/mod.rs", '#[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS', - '#[cfg(all(target_os = "nonos", target_arch = "x86_64"))]\n { tp = 0; }\n' + '#[cfg(all(target_os = "nonos", target_arch = "x86_64"))]\n { tp = 1; }\n' ' #[cfg(all(target_os = "linux", target_arch = "x86_64"))]\n {\n const ARCH_GET_FS') +patch("src/ld_so/tcb.rs", + " pub unsafe fn current() -> Option<&'static mut Self> {\n" + " unsafe { Some(&mut *GenericTcb::::current_ptr()?.cast()) }\n }", + " #[cfg(target_os = \"nonos\")]\n" + " pub unsafe fn current() -> Option<&'static mut Self> { None }\n" + " #[cfg(not(target_os = \"nonos\"))]\n" + " pub unsafe fn current() -> Option<&'static mut Self> {\n" + " unsafe { Some(&mut *GenericTcb::::current_ptr()?.cast()) }\n }") + patch("src/crt0/src/lib.rs", '#[cfg(target_arch = "x86_64")]\nglobal_asm!(', '#[cfg(target_os = "nonos")]\nglobal_asm!(\n' @@ -185,10 +194,14 @@ patch("src/crt0/src/lib.rs", '_start:\n' ' xor rbp, rbp\n' ' and rsp, -16\n' - ' sub rsp, 8\n' - ' push 0\n push 0\n push 0\n push 0\n push 0\n' - ' mov rdi, rsp\n' - ' call relibc_crt0\n' + ' xor edi, edi\n' + ' xor esi, esi\n' + ' xor edx, edx\n' + ' call main\n' + ' mov edi, eax\n' + ' mov rax, 0x5458454d\n' + ' syscall\n' + ' ud2\n' ' .size _start, . - _start\n' '"\n);\n\n' '#[cfg(all(target_arch = "x86_64", not(target_os = "nonos")))]\nglobal_asm!(') From 1d78e5e5a92f4ff0de86ba61b8ab6a1ed8250ec7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 12:53:34 +0600 Subject: [PATCH 23/72] feat(capsule): c_proof Debug cap + unverified smoke spawn path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MkDebug requires Capability::Debug (cap_table/mk.rs), so the write(1,...) serial marker needs it — add Debug to the hand-synced cap lists (spawn.rs requested_caps + Capsule.mk 0x19 -> 0x119). Add a cfg(nonos-dev-unverified- capsules) spawn variant using CapsuleSpec/capsule_spawn::spawn so the smoketest can boot c_proof without the (mid-migration) ZK attestation; the verified spawn_verified path is retained for nonos-production. --- src/userspace/capsule_c_proof/spawn.rs | 41 ++++++++++++++++++++++---- userland/capsule_c_proof/Capsule.mk | 5 ++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/userspace/capsule_c_proof/spawn.rs b/src/userspace/capsule_c_proof/spawn.rs index a60c8b8f12..9ce9c38db7 100644 --- a/src/userspace/capsule_c_proof/spawn.rs +++ b/src/userspace/capsule_c_proof/spawn.rs @@ -14,13 +14,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use super::embed::C_PROOF_ELF; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] use super::embed::{ - C_PROOF_ATTESTATION_BYTES, C_PROOF_ELF, C_PROOF_MANIFEST_BYTES, - C_PROOF_NONOS_ID_CERT_BYTES, + C_PROOF_ATTESTATION_BYTES, C_PROOF_MANIFEST_BYTES, C_PROOF_NONOS_ID_CERT_BYTES, }; use crate::capabilities::Capability; -use crate::kernel_core::process_spawn::capsule_spawn::{self, CapsuleSpecVerified, SpawnError}; +use crate::kernel_core::process_spawn::capsule_spawn::{self, SpawnError}; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpecVerified; +#[cfg(feature = "nonos-dev-unverified-capsules")] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpec; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] use crate::security::nonos_id_cert::IdCertVerifyError; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] use crate::security::nonos_trust_anchor::{ decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY, }; @@ -29,8 +36,17 @@ const SERVICE_NAME: &str = "c_proof"; const SERVICE_PORT: u32 = 4504; const REPLY_INBOX: &str = "endpoint.c_proof.reply"; const REPLY_PORT: u32 = 4505; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] const TARGET_TRIPLE: &str = "x86_64-nonos-user"; +fn requested_caps() -> u64 { + Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit() + | Capability::Debug.bit() +} + +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] pub fn spawn_c_proof_capsule() -> Result<(), SpawnError> { let trust_anchor = decode_trust_anchor(BAKED_TRUST_ANCHOR_POLICY) .map_err(|_| SpawnError::NonosIdCertRejected(IdCertVerifyError::TrustAnchorPolicy))?; @@ -45,11 +61,24 @@ pub fn spawn_c_proof_capsule() -> Result<(), SpawnError> { manifest_bytes: C_PROOF_MANIFEST_BYTES, attestation_trailer: C_PROOF_ATTESTATION_BYTES, target_triple: TARGET_TRIPLE, - requested_caps: Capability::CoreExec.bit() - | Capability::IPC.bit() - | Capability::Memory.bit(), + requested_caps: requested_caps(), debug_tag: b"", }; capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; Ok(()) } + +#[cfg(feature = "nonos-dev-unverified-capsules")] +pub fn spawn_c_proof_capsule() -> Result<(), SpawnError> { + let spec = CapsuleSpec { + name: SERVICE_NAME, + service_port: SERVICE_PORT, + reply_inbox: REPLY_INBOX, + reply_port: REPLY_PORT, + elf: C_PROOF_ELF, + caps_bits: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn(&spec)?; + Ok(()) +} diff --git a/userland/capsule_c_proof/Capsule.mk b/userland/capsule_c_proof/Capsule.mk index 64603dda06..e65514c0cf 100644 --- a/userland/capsule_c_proof/Capsule.mk +++ b/userland/capsule_c_proof/Capsule.mk @@ -7,8 +7,9 @@ CAPSULE_FEATURE := nonos-capsule-c-proof CAPSULE_NAMESPACE := systems.nonos.c_proof CAPSULE_SERVICE_ENDPOINT := service:4504:c_proof CAPSULE_REPLY_ENDPOINT := reply:4505:endpoint.c_proof.reply -# CoreExec | IPC | Memory = 0x01 | 0x08 | 0x10 = 0x19 -CAPSULE_REQUIRED_CAPS := 0x19 +# CoreExec | IPC | Memory | Debug = 0x01 | 0x08 | 0x10 | 0x100 = 0x119 +# Debug is required for the write(1,...) -> MkDebug serial marker. +CAPSULE_REQUIRED_CAPS := 0x119 CAPSULE_KERNEL_MIRROR := src/userspace/capsule_c_proof CAPSULE_PREBUILT_BIN := userland/capsule_c_proof/build/c_proof From 6d9a6e1e75eb8ea9b9eb2acaf2a49e30af8c56fe Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 12:53:34 +0600 Subject: [PATCH 24/72] feat(test): c_proof unverified smoketest profile + gate microkernel-c-proof-smoketest (core + nonos-dev-unverified-capsules + c-proof) and nonos-mk-cproof-smoke-{prod,test} prove the clang/relibc C capsule boots and runs without ZK attestation, which is mid NZKCAPS1->2 migration. The harness builds via CPROOF_PROD_TARGET; run with NONOS_DEV=1. --- Cargo.toml | 10 ++++++++++ Makefile | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e59224caa7..7e2513dd3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -220,6 +220,16 @@ microkernel-c-proof = [ "nonos-capsule-c-proof", ] +# Temporary unverified boot proof of the clang/relibc C capsule: spawns +# c_proof via the legacy unverified path (no ZK attestation) while the +# capsule-attestation NZKCAPS2 migration is owned elsewhere. Superseded by +# microkernel-c-proof once a valid c_proof trailer is available. +microkernel-c-proof-smoketest = [ + "microkernel-core", + "nonos-dev-unverified-capsules", + "nonos-capsule-c-proof", +] + microkernel-keyring = [ "microkernel-core", "nonos-production", diff --git a/Makefile b/Makefile index a4dbe4b2af..815443d257 100644 --- a/Makefile +++ b/Makefile @@ -824,6 +824,18 @@ nonos-mk-boot-cproof: nonos-mk-cproof-test: nonos-mk-boot-cproof +.PHONY: nonos-mk-cproof-smoke-prod nonos-mk-cproof-smoke-test +nonos-mk-cproof-smoke-prod: $(c-proof_ARTIFACTS) \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + @echo "Building kernel (microkernel-c-proof-smoketest, unverified)..." + @$(SDK_FLAGS) NONOS_SIGNING_KEY=$(KERNEL_SIGNING_KEY) \ + RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + $(CARGO) build $(KERNEL_BUILD_FLAGS) \ + --no-default-features --features microkernel-c-proof-smoketest + +nonos-mk-cproof-smoke-test: + @CPROOF_PROD_TARGET=nonos-mk-cproof-smoke-prod ./tests/boot/cproof.sh + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." From b412d81f925646cb77bbe7da79278f590107adf2 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:04:36 +0600 Subject: [PATCH 25/72] =?UTF-8?q?feat(relibc):=20full=20nonos=20startup=20?= =?UTF-8?q?=E2=80=94=20no-op=20pthread::init,=20restore=20relibc=5Fcrt0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-0 used a minimal crt0 that bypassed relibc_start_v1 (no allocator), which is insufficient for Phase 1 (malloc/stdio). Restore the crt0 -> relibc_crt0 -> relibc_start_v1 path and no-op pthread::init for nonos (it ended in Tcb::current().expect_notls(), which panics with no TCB). Now the full startup runs on nonos — alloc_init (heap), io_init (stdio), environ — with TLS/pthread skipped (deferred to Phase 3). c_proof re-proven through full startup: [C-PROOF] PASS. --- toolchain/nonos-relibc/apply.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 062502540e..128d04e0a2 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -185,6 +185,12 @@ patch("src/ld_so/tcb.rs", " pub unsafe fn current() -> Option<&'static mut Self> {\n" " unsafe { Some(&mut *GenericTcb::::current_ptr()?.cast()) }\n }") +patch("src/pthread/mod.rs", + 'pub unsafe fn init() {\n let mut thread = Pthread {', + '#[cfg(target_os = "nonos")]\npub unsafe fn init() {}\n' + '#[cfg(not(target_os = "nonos"))]\n' + 'pub unsafe fn init() {\n let mut thread = Pthread {') + patch("src/crt0/src/lib.rs", '#[cfg(target_arch = "x86_64")]\nglobal_asm!(', '#[cfg(target_os = "nonos")]\nglobal_asm!(\n' @@ -194,14 +200,10 @@ patch("src/crt0/src/lib.rs", '_start:\n' ' xor rbp, rbp\n' ' and rsp, -16\n' - ' xor edi, edi\n' - ' xor esi, esi\n' - ' xor edx, edx\n' - ' call main\n' - ' mov edi, eax\n' - ' mov rax, 0x5458454d\n' - ' syscall\n' - ' ud2\n' + ' sub rsp, 8\n' + ' push 0\n push 0\n push 0\n push 0\n push 0\n' + ' mov rdi, rsp\n' + ' call relibc_crt0\n' ' .size _start, . - _start\n' '"\n);\n\n' '#[cfg(all(target_arch = "x86_64", not(target_os = "nonos")))]\nglobal_asm!(') From 07545ee39e4fdcbbad1fd42ca1adccc042ff1912 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:27:52 +0600 Subject: [PATCH 26/72] feat(capsule): add capsule_relibc_test userland source and Capsule.mk Exerciser capsule that tests malloc (1MB + 4KB) and clock_gettime on the relibc runtime. Emits [RELIBC-TEST] PASS on success. Ports 4506/4507, caps 0x179 (CoreExec|IPC|Memory|Crypto|FileSystem|Debug). --- .../nonos-relibc/platform/nonos/lowlevel.rs | 3 +++ userland/capsule_relibc_test/Capsule.mk | 17 +++++++++++++++++ userland/capsule_relibc_test/src/main.c | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 userland/capsule_relibc_test/Capsule.mk create mode 100644 userland/capsule_relibc_test/src/main.c diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs index d90674c19a..864bdb2bc9 100644 --- a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -9,6 +9,9 @@ pub const MK_EXIT: u64 = tag4(b"MEXT"); pub const MK_GETPID: u64 = tag4(b"MGPD"); pub const MK_MMAP: u64 = tag4(b"MMAP"); pub const MK_MUNMAP: u64 = tag4(b"MUMP"); +pub const MK_TIME_MILLIS: u64 = tag4(b"MTMS"); +pub const MK_YIELD: u64 = tag4(b"MYLD"); +pub const MK_CRYPTO_RANDOM: u64 = tag4(b"CRND"); #[inline] pub unsafe fn syscall0(n: u64) -> i64 { diff --git a/userland/capsule_relibc_test/Capsule.mk b/userland/capsule_relibc_test/Capsule.mk new file mode 100644 index 0000000000..fafa099403 --- /dev/null +++ b/userland/capsule_relibc_test/Capsule.mk @@ -0,0 +1,17 @@ +CAPSULE_SLUG := relibc-test +CAPSULE_HANDLE := relibc_test +CAPSULE_DOMAIN := systems.nonos +CAPSULE_DIR := userland/capsule_relibc_test +CAPSULE_BIN_NAME := relibc_test +CAPSULE_FEATURE := nonos-capsule-relibc-test +CAPSULE_NAMESPACE := systems.nonos.relibc_test +CAPSULE_SERVICE_ENDPOINT := service:4506:relibc_test +CAPSULE_REPLY_ENDPOINT := reply:4507:endpoint.relibc_test.reply +# CoreExec | IPC | Memory | Crypto | FileSystem | Debug +# = 0x01 | 0x08 | 0x10 | 0x20 | 0x40 | 0x100 = 0x179 +CAPSULE_REQUIRED_CAPS := 0x179 +CAPSULE_KERNEL_MIRROR := src/userspace/capsule_relibc_test +CAPSULE_PREBUILT_BIN := userland/capsule_relibc_test/build/relibc_test + +include nonos-mk/capsule-c.mk +include nonos-mk/capsule.mk diff --git a/userland/capsule_relibc_test/src/main.c b/userland/capsule_relibc_test/src/main.c new file mode 100644 index 0000000000..132d979693 --- /dev/null +++ b/userland/capsule_relibc_test/src/main.c @@ -0,0 +1,18 @@ +#include +#include +#include +static void emit(const char *s, unsigned n) { write(1, s, n); } +int main(void) { + char *p = (char *)malloc(1u << 20); + if (!p) { emit("[RELIBC-TEST] FAIL malloc\n", 26); return 1; } + for (unsigned i = 0; i < (1u << 20); i += 4096) p[i] = (char)i; + char *q = (char *)malloc(4096); + if (!q) { emit("[RELIBC-TEST] FAIL malloc2\n", 27); return 1; } + free(q); free(p); + struct timespec a, b; + clock_gettime(CLOCK_MONOTONIC, &a); + clock_gettime(CLOCK_MONOTONIC, &b); + if (b.tv_sec < a.tv_sec) { emit("[RELIBC-TEST] FAIL clock\n", 25); return 1; } + emit("[RELIBC-TEST] PASS\n", 19); + return 0; +} From 38ebecded738922203accace608838212d061ac0 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:28:47 +0600 Subject: [PATCH 27/72] feat(kernel): add capsule_relibc_test kernel mirror and init wiring Mirrors capsule_c_proof pattern: embed.rs with empty-slice fallbacks when feature is off, spawn.rs with both verified and unverified-capsules variants, requested_caps() = CoreExec|IPC|Memory|Crypto|FileSystem|Debug (0x179). Ports 4506/4507. run_relibc_test() added to init/entry.rs after run_c_proof(); no-ops when feature is absent. --- src/userspace/capsule_relibc_test/embed.rs | 44 +++++++++++ src/userspace/capsule_relibc_test/mod.rs | 20 +++++ src/userspace/capsule_relibc_test/spawn.rs | 86 ++++++++++++++++++++++ src/userspace/init/entry.rs | 12 +++ src/userspace/mod.rs | 1 + 5 files changed, 163 insertions(+) create mode 100644 src/userspace/capsule_relibc_test/embed.rs create mode 100644 src/userspace/capsule_relibc_test/mod.rs create mode 100644 src/userspace/capsule_relibc_test/spawn.rs diff --git a/src/userspace/capsule_relibc_test/embed.rs b/src/userspace/capsule_relibc_test/embed.rs new file mode 100644 index 0000000000..31f0f444e7 --- /dev/null +++ b/src/userspace/capsule_relibc_test/embed.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 . + +#[cfg(feature = "nonos-capsule-relibc-test")] +pub(crate) const RELIBC_TEST_ELF: &[u8] = include_bytes!( + "../../../userland/capsule_relibc_test/target/x86_64-nonos-user/release/relibc_test" +); + +#[cfg(feature = "nonos-capsule-relibc-test")] +pub(crate) const RELIBC_TEST_NONOS_ID_CERT_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/relibc_test.nonos_id_cert.bin"); + +#[cfg(feature = "nonos-capsule-relibc-test")] +pub(crate) const RELIBC_TEST_MANIFEST_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/relibc_test.manifest.bin"); + +#[cfg(feature = "nonos-capsule-relibc-test")] +pub(crate) const RELIBC_TEST_ATTESTATION_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/relibc_test.zk_trailer.bin"); + +#[cfg(not(feature = "nonos-capsule-relibc-test"))] +pub(crate) const RELIBC_TEST_ELF: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-relibc-test"))] +pub(crate) const RELIBC_TEST_NONOS_ID_CERT_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-relibc-test"))] +pub(crate) const RELIBC_TEST_MANIFEST_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-relibc-test"))] +pub(crate) const RELIBC_TEST_ATTESTATION_BYTES: &[u8] = &[]; diff --git a/src/userspace/capsule_relibc_test/mod.rs b/src/userspace/capsule_relibc_test/mod.rs new file mode 100644 index 0000000000..2be0eb79f1 --- /dev/null +++ b/src/userspace/capsule_relibc_test/mod.rs @@ -0,0 +1,20 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod embed; +mod spawn; + +pub use spawn::spawn_relibc_test_capsule; diff --git a/src/userspace/capsule_relibc_test/spawn.rs b/src/userspace/capsule_relibc_test/spawn.rs new file mode 100644 index 0000000000..4c7d322b7a --- /dev/null +++ b/src/userspace/capsule_relibc_test/spawn.rs @@ -0,0 +1,86 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use super::embed::RELIBC_TEST_ELF; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use super::embed::{ + RELIBC_TEST_ATTESTATION_BYTES, RELIBC_TEST_MANIFEST_BYTES, RELIBC_TEST_NONOS_ID_CERT_BYTES, +}; +use crate::capabilities::Capability; +use crate::kernel_core::process_spawn::capsule_spawn::{self, SpawnError}; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpecVerified; +#[cfg(feature = "nonos-dev-unverified-capsules")] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpec; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_id_cert::IdCertVerifyError; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_trust_anchor::{ + decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY, +}; + +const SERVICE_NAME: &str = "relibc_test"; +const SERVICE_PORT: u32 = 4506; +const REPLY_INBOX: &str = "endpoint.relibc_test.reply"; +const REPLY_PORT: u32 = 4507; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +const TARGET_TRIPLE: &str = "x86_64-nonos-user"; + +fn requested_caps() -> u64 { + Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit() + | Capability::Crypto.bit() + | Capability::FileSystem.bit() + | Capability::Debug.bit() +} + +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +pub fn spawn_relibc_test_capsule() -> Result<(), SpawnError> { + 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: RELIBC_TEST_ELF, + nonos_id_cert_bytes: RELIBC_TEST_NONOS_ID_CERT_BYTES, + manifest_bytes: RELIBC_TEST_MANIFEST_BYTES, + attestation_trailer: RELIBC_TEST_ATTESTATION_BYTES, + target_triple: TARGET_TRIPLE, + requested_caps: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + Ok(()) +} + +#[cfg(feature = "nonos-dev-unverified-capsules")] +pub fn spawn_relibc_test_capsule() -> Result<(), SpawnError> { + let spec = CapsuleSpec { + name: SERVICE_NAME, + service_port: SERVICE_PORT, + reply_inbox: REPLY_INBOX, + reply_port: REPLY_PORT, + elf: RELIBC_TEST_ELF, + caps_bits: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn(&spec)?; + Ok(()) +} diff --git a/src/userspace/init/entry.rs b/src/userspace/init/entry.rs index 2b31c03b32..1091a9ead2 100644 --- a/src/userspace/init/entry.rs +++ b/src/userspace/init/entry.rs @@ -22,6 +22,7 @@ pub fn run_init() -> ! { run_user_entry_proof(); run_std_proof(); run_c_proof(); + run_relibc_test(); run_ripgrep(); spawn_plan::spawn_ramfs(); spawn_plan::spawn_core_after_ramfs(); @@ -69,6 +70,17 @@ fn run_c_proof() { #[cfg(not(feature = "nonos-capsule-c-proof"))] fn run_c_proof() {} +#[cfg(feature = "nonos-capsule-relibc-test")] +fn run_relibc_test() { + match crate::userspace::capsule_relibc_test::spawn_relibc_test_capsule() { + Ok(()) => boot_log::ok("RELIBC-TEST", "capsule spawned"), + Err(_) => boot_log::error("RELIBC-TEST capsule spawn failed"), + } +} + +#[cfg(not(feature = "nonos-capsule-relibc-test"))] +fn run_relibc_test() {} + #[cfg(feature = "nonos-capsule-ripgrep")] fn run_ripgrep() { match crate::userspace::capsule_ripgrep::spawn_ripgrep_capsule() { diff --git a/src/userspace/mod.rs b/src/userspace/mod.rs index da168343de..d521639877 100644 --- a/src/userspace/mod.rs +++ b/src/userspace/mod.rs @@ -62,6 +62,7 @@ pub mod capsule_settings; pub mod capsule_setup_wizard; pub mod capsule_snake; pub mod capsule_c_proof; +pub mod capsule_relibc_test; pub mod capsule_std_proof; pub mod capsule_terminal; pub mod capsule_text_editor; From b3ed63ebd72f7514d6983e1f5de09a2e6433fe4c Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:28:55 +0600 Subject: [PATCH 28/72] feat(build): add nonos-capsule-relibc-test feature and smoke profile nonos-capsule-relibc-test = [] capsule embed feature. Profile microkernel-relibc-test-smoketest = microkernel-core + nonos-dev-unverified-capsules + nonos-capsule-relibc-test for the unverified smoke gate. Makefile: include capsule_relibc_test/Capsule.mk, nonos-mk-relibc-smoke-prod, nonos-mk-relibc-smoke-test targets. --- Cargo.toml | 7 +++++++ Makefile | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7e2513dd3e..39c6a9f3bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ sched = [] nonos-capsule-proof-io = [] nonos-capsule-std-proof = [] nonos-capsule-c-proof = [] +nonos-capsule-relibc-test = [] nonos-capsule-ripgrep = [] nonos-capsule-ramfs = [] nonos-capsule-wallpaper = [] @@ -230,6 +231,12 @@ microkernel-c-proof-smoketest = [ "nonos-capsule-c-proof", ] +microkernel-relibc-test-smoketest = [ + "microkernel-core", + "nonos-dev-unverified-capsules", + "nonos-capsule-relibc-test", +] + microkernel-keyring = [ "microkernel-core", "nonos-production", diff --git a/Makefile b/Makefile index 815443d257..67360e1589 100644 --- a/Makefile +++ b/Makefile @@ -535,6 +535,7 @@ nonos-mk-trust-policy: $(NONOS_TRUST_ANCHOR_POLICY_BIN) include userland/capsule_proof_io/Capsule.mk include userland/capsule_std_proof/Capsule.mk include userland/capsule_c_proof/Capsule.mk +include userland/capsule_relibc_test/Capsule.mk include userland/capsule_ripgrep/Capsule.mk include userland/capsule_ramfs/Capsule.mk include userland/capsule_keyring/Capsule.mk @@ -836,6 +837,18 @@ nonos-mk-cproof-smoke-prod: $(c-proof_ARTIFACTS) \ nonos-mk-cproof-smoke-test: @CPROOF_PROD_TARGET=nonos-mk-cproof-smoke-prod ./tests/boot/cproof.sh +.PHONY: nonos-mk-relibc-smoke-prod nonos-mk-relibc-smoke-test +nonos-mk-relibc-smoke-prod: $(relibc-test_ARTIFACTS) \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + @echo "Building kernel (microkernel-relibc-test-smoketest, unverified)..." + @$(SDK_FLAGS) NONOS_SIGNING_KEY=$(KERNEL_SIGNING_KEY) \ + RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + $(CARGO) build $(KERNEL_BUILD_FLAGS) \ + --no-default-features --features microkernel-relibc-test-smoketest + +nonos-mk-relibc-smoke-test: + @CPROOF_PROD_TARGET=nonos-mk-relibc-smoke-prod ./tests/boot/relibc.sh + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." From 78f754ed0b5e60bcbb63a343aee4f4be8b4ce9f4 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:30:20 +0600 Subject: [PATCH 29/72] feat(relibc): nonos time via MkTimeMillis Wire clock_gettime, clock_getres, gettimeofday, and nanosleep to the MTMS/MYLD syscalls. now_ms() converts the ms-since-epoch i64 return into sec/nsec/usec fields; nanosleep busy-yields via MK_YIELD until the deadline. suseconds_t cast applied: nonos target resolves it as c_int (not c_long) per the #[cfg(not(target_os="linux"))] branch. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 33 +++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 538cb49b87..721da9e7c0 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -33,6 +33,11 @@ mod ptrace; mod signal; mod socket; +fn now_ms() -> i64 { + let r = unsafe { lowlevel::syscall0(lowlevel::MK_TIME_MILLIS) }; + if r < 0 { 0 } else { r } +} + pub struct Sys; impl Pal for Sys { @@ -115,12 +120,32 @@ impl Pal for Sys { unsafe fn munlock(_addr: *const c_void, _len: usize) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn madvise(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn munlockall() -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn nanosleep(_rqtp: *const timespec, _rmtp: *mut timespec) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn nanosleep(rqtp: *const timespec, _rmtp: *mut timespec) -> Result<()> { + let rq = unsafe { &*rqtp }; + let deadline = now_ms() + rq.tv_sec * 1000 + rq.tv_nsec / 1_000_000; + while now_ms() < deadline { + unsafe { lowlevel::syscall0(lowlevel::MK_YIELD); } + } + Ok(()) + } fn getpagesize() -> usize { 4096 } - fn clock_getres(_clk_id: clockid_t, _tp: Option>) -> Result<()> { Err(Errno(ENOSYS)) } - fn clock_gettime(_clk_id: clockid_t, _tp: Out) -> Result<()> { Err(Errno(ENOSYS)) } + fn clock_getres(_clk_id: clockid_t, tp: Option>) -> Result<()> { + if let Some(mut tp) = tp { + tp.write(timespec { tv_sec: 0, tv_nsec: 1_000_000 }); + } + Ok(()) + } + fn clock_gettime(_clk_id: clockid_t, mut tp: Out) -> Result<()> { + let ms = now_ms(); + tp.write(timespec { tv_sec: ms / 1000, tv_nsec: (ms % 1000) * 1_000_000 }); + Ok(()) + } unsafe fn clock_settime(_clk_id: clockid_t, _tp: *const timespec) -> Result<()> { Err(Errno(ENOSYS)) } - fn gettimeofday(_tp: Out, _tzp: Option>) -> Result<()> { Err(Errno(ENOSYS)) } + fn gettimeofday(mut tp: Out, _tzp: Option>) -> Result<()> { + let ms = now_ms(); + tp.write(timeval { tv_sec: ms / 1000, tv_usec: ((ms % 1000) * 1000) as suseconds_t }); + Ok(()) + } fn timer_create(_clock_id: clockid_t, _evp: &sigevent, _timerid: Out) -> Result<()> { Err(Errno(ENOSYS)) } fn timer_delete(_timerid: timer_t) -> Result<()> { Err(Errno(ENOSYS)) } fn timer_gettime(_timerid: timer_t, _value: Out) -> Result<()> { Err(Errno(ENOSYS)) } From d2a9701e008e7b48d53805efcc2018660bfb930e Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:31:31 +0600 Subject: [PATCH 30/72] feat(relibc): nonos getrandom + mem advisory no-ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement getrandom via MK_CRYPTO_RANDOM (CRND syscall): chunks the buffer in ≤4096-byte slices (kernel cap), returns Err(EIO) on negative return. Flip mprotect/madvise/msync/mlock/munlock/mlockall/munlockall from ENOSYS to Ok(()) — they are advisory on NONOS; mremap stays ENOSYS. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 721da9e7c0..05005b37ef 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -10,7 +10,7 @@ use crate::{ c_str::CStr, error::{Errno, Result}, header::{ - errno::{EBADF, ENOMEM, ENOSYS}, + errno::{EBADF, EIO, ENOMEM, ENOSYS}, signal::sigevent, sys_resource::{rlimit, rusage}, sys_select::timeval, @@ -112,14 +112,14 @@ impl Pal for Sys { fn symlinkat(_path1: CStr, _fd: c_int, _path2: CStr) -> Result<()> { Err(Errno(ENOSYS)) } fn sync() -> Result<()> { Err(Errno(ENOSYS)) } fn unlinkat(_fd: c_int, _path: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn mlock(_addr: *const c_void, _len: usize) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn mlockall(_flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn mlock(_addr: *const c_void, _len: usize) -> Result<()> { Ok(()) } + unsafe fn mlockall(_flags: c_int) -> Result<()> { Ok(()) } unsafe fn mremap(_addr: *mut c_void, _len: usize, _new_len: usize, _flags: c_int, _args: *mut c_void) -> Result<*mut c_void> { Err(Errno(ENOSYS)) } - unsafe fn mprotect(_addr: *mut c_void, _len: usize, _prot: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn msync(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn munlock(_addr: *const c_void, _len: usize) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn madvise(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn munlockall() -> Result<()> { Err(Errno(ENOSYS)) } + unsafe fn mprotect(_addr: *mut c_void, _len: usize, _prot: c_int) -> Result<()> { Ok(()) } + unsafe fn msync(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Ok(()) } + unsafe fn munlock(_addr: *const c_void, _len: usize) -> Result<()> { Ok(()) } + unsafe fn madvise(_addr: *mut c_void, _len: usize, _flags: c_int) -> Result<()> { Ok(()) } + unsafe fn munlockall() -> Result<()> { Ok(()) } unsafe fn nanosleep(rqtp: *const timespec, _rmtp: *mut timespec) -> Result<()> { let rq = unsafe { &*rqtp }; let deadline = now_ms() + rq.tv_sec * 1000 + rq.tv_nsec / 1_000_000; @@ -157,7 +157,20 @@ impl Pal for Sys { fn getpgid(_pid: pid_t) -> Result { Err(Errno(ENOSYS)) } fn getppid() -> pid_t { 0 } fn getpriority(_which: c_int, _who: id_t) -> Result { Err(Errno(ENOSYS)) } - fn getrandom(_buf: &mut [u8], _flags: c_uint) -> Result { Err(Errno(ENOSYS)) } + fn getrandom(buf: &mut [u8], _flags: c_uint) -> Result { + if buf.is_empty() { + return Ok(0); + } + let mut filled = 0usize; + for chunk in buf.chunks_mut(4096) { + let r = unsafe { lowlevel::syscall2(lowlevel::MK_CRYPTO_RANDOM, chunk.as_mut_ptr() as u64, chunk.len() as u64) }; + if r < 0 { + return Err(Errno(EIO)); + } + filled += r as usize; + } + Ok(filled) + } fn getresgid(_rgid: Option>, _egid: Option>, _sgid: Option>) -> Result<()> { Err(Errno(ENOSYS)) } fn getresuid(_ruid: Option>, _euid: Option>, _suid: Option>) -> Result<()> { Err(Errno(ENOSYS)) } fn getrlimit(_resource: c_int, _rlim: Out) -> Result<()> { Err(Errno(ENOSYS)) } From 39f3aeb3c1bea1860adfa560ac0c8cb74204b2ac Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:53:18 +0600 Subject: [PATCH 31/72] fix(relibc): declare rdi/rsi/rdx clobbered in nonos syscall wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel syscall path (arch/x86_64/asm/syscall.S) saves/restores rbp/r11/rcx/r10/r9/r8/rax but NOT rdi/rsi/rdx — it overwrites them in the arg shuffle and never restores, so a syscall clobbers rdi/rsi/rdx in addition to the hardware rcx/r11. syscall0/1/2 only declared rcx/r11, so the compiler kept caller values (e.g. clock_gettime's `tp` in rsi across the MkTimeMillis call) → wild write to a kernel address (#PF). Declare the clobbers. (write() survived only by not holding a reg across its syscall.) --- toolchain/nonos-relibc/platform/nonos/lowlevel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs index 864bdb2bc9..8b526837fa 100644 --- a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -18,6 +18,7 @@ pub unsafe fn syscall0(n: u64) -> i64 { let r: i64; unsafe { asm!("syscall", inlateout("rax") n => r, + lateout("rdi") _, lateout("rsi") _, lateout("rdx") _, lateout("rcx") _, lateout("r11") _, options(nostack)); } r @@ -28,6 +29,7 @@ pub unsafe fn syscall1(n: u64, a0: u64) -> i64 { let r: i64; unsafe { asm!("syscall", inlateout("rax") n => r, in("rdi") a0, + lateout("rsi") _, lateout("rdx") _, lateout("rcx") _, lateout("r11") _, options(nostack)); } r @@ -38,6 +40,7 @@ pub unsafe fn syscall2(n: u64, a0: u64, a1: u64) -> i64 { let r: i64; unsafe { asm!("syscall", inlateout("rax") n => r, in("rdi") a0, in("rsi") a1, + lateout("rdx") _, lateout("rcx") _, lateout("r11") _, options(nostack)); } r From 6686f2978621dcc9fff16bc3c711e5476893e172 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 13:53:18 +0600 Subject: [PATCH 32/72] feat(relibc): map target_os=nonos to __nonos__ for cbindgen headers cbindgen.globdefs.toml had no nonos mapping, so the reused-linux constants (broadened to any(linux, nonos)) emitted only `#if defined(__linux__)` and were invisible to C compiled with -D__nonos__ (e.g. CLOCK_MONOTONIC). Add the target_os=nonos -> __nonos__ mapping so the headers guard them with `defined(__linux__) || defined(__nonos__)`. --- toolchain/nonos-relibc/apply.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 128d04e0a2..5baffef60b 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -121,6 +121,10 @@ patch("src/header/sys_epoll/mod.rs", f'{LX}\npub const EPOLL_CLOEXEC: c_int = 0x8_0000;', f'{NX}\npub const EPOLL_CLOEXEC: c_int = 0x8_0000;') +patch("cbindgen.globdefs.toml", + '"target_os=linux" = "__linux__"', + '"target_os=linux" = "__linux__"\n"target_os=nonos" = "__nonos__"') + broaden("src/header/termios/mod.rs", '#[repr(C)]\n#[derive(Default, Clone)]\npub struct termios {') for fn in ("cfgetispeed", "cfgetospeed", "cfsetispeed", "cfsetospeed"): From b2dc6b4def4ce8a6f93e9c769e9e4beebff0541b Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 14:46:57 +0600 Subject: [PATCH 33/72] feat(relibc): add MK_IPC_CALL tag to lowlevel.rs Exposes the mk_ipc_call syscall (tag "MICL") so the nonos Pal can reach the vfs capsule over IPC. syscall6 already handles the full 6-arg ABI (endpoint, req_ptr, req_len, resp_ptr, resp_len, timeout_ms). Refs: Phase 1 Task 1.1 --- toolchain/nonos-relibc/platform/nonos/lowlevel.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs index 8b526837fa..e1442c4075 100644 --- a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -12,6 +12,7 @@ pub const MK_MUNMAP: u64 = tag4(b"MUMP"); pub const MK_TIME_MILLIS: u64 = tag4(b"MTMS"); pub const MK_YIELD: u64 = tag4(b"MYLD"); pub const MK_CRYPTO_RANDOM: u64 = tag4(b"CRND"); +pub const MK_IPC_CALL: u64 = tag4(b"MICL"); #[inline] pub unsafe fn syscall0(n: u64) -> i64 { From 179d0e9366d427b8763ffd4910356f97b63a3af9 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 14:47:11 +0600 Subject: [PATCH 34/72] feat(relibc): nonos vfs ipc_call helper + fd table fs.rs provides: 20-byte VFS wire-protocol header builder + syscall6 round-trip (vfs_call); a 64-slot static fd table behind SyncUnsafeCell (fd_alloc/fd_vfs/fd_free/fd_dup/fd_set); VFS op and flag constants. apply.sh now copies fs.rs into the vendored relibc tree. Refs: Phase 1 Task 1.1 --- toolchain/nonos-relibc/apply.sh | 1 + toolchain/nonos-relibc/platform/nonos/fs.rs | 66 +++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 toolchain/nonos-relibc/platform/nonos/fs.rs diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index 5baffef60b..ade6a46d34 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -21,6 +21,7 @@ cp "$HERE/platform/nonos/socket.rs" "$RELIBC/src/platform/nonos/socket.rs" cp "$HERE/platform/nonos/signal.rs" "$RELIBC/src/platform/nonos/signal.rs" cp "$HERE/platform/nonos/epoll.rs" "$RELIBC/src/platform/nonos/epoll.rs" cp "$HERE/platform/nonos/ptrace.rs" "$RELIBC/src/platform/nonos/ptrace.rs" +cp "$HERE/platform/nonos/fs.rs" "$RELIBC/src/platform/nonos/fs.rs" # 2. patch the platform selector + config.mk (idempotent) python3 - "$RELIBC" "$CC_NONOS" <<'PY' diff --git a/toolchain/nonos-relibc/platform/nonos/fs.rs b/toolchain/nonos-relibc/platform/nonos/fs.rs new file mode 100644 index 0000000000..06d0412542 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/fs.rs @@ -0,0 +1,66 @@ +use alloc::vec::Vec; +use core::cell::SyncUnsafeCell; +use crate::{error::{Errno, Result}, header::errno::EIO}; +use super::{super::types::c_int, lowlevel::{syscall6, MK_IPC_CALL}}; + +const VFS_ENDPOINT: u64 = 4104; +const VFS_MAGIC: u32 = 0x4E4F_5646; +const VFS_VERSION: u16 = 1; +const HDR_LEN: usize = 20; +pub const OP_OPEN: u16 = 1; +pub const OP_CLOSE: u16 = 2; +pub const OP_READ: u16 = 3; +pub const OP_WRITE: u16 = 4; +pub const OP_STAT: u16 = 5; +pub const OP_MKDIR: u16 = 8; +pub const OP_UNLINK: u16 = 9; +pub const OP_RENAME: u16 = 10; +pub const VFS_O_CREATE: u32 = 1 << 0; +pub const VFS_O_TRUNC: u32 = 1 << 1; +pub const VFS_O_APPEND: u32 = 1 << 2; + +const FD_SLOTS: usize = 64; +static FD_TABLE: SyncUnsafeCell<[i32; FD_SLOTS]> = SyncUnsafeCell::new([-1; FD_SLOTS]); +fn table() -> &'static mut [i32; FD_SLOTS] { unsafe { &mut *FD_TABLE.get() } } + +pub fn fd_alloc(vfs_fd: u32) -> Option { + for (i, s) in table().iter_mut().enumerate() { + if *s < 0 { *s = vfs_fd as i32; return Some((i + 3) as c_int); } + } + None +} +pub fn fd_vfs(fd: c_int) -> Option { + let i = fd.checked_sub(3)? as usize; + if i >= FD_SLOTS { return None; } + let v = table()[i]; + if v < 0 { None } else { Some(v as u32) } +} +pub fn fd_free(fd: c_int) { + if let Some(i) = fd.checked_sub(3).map(|v| v as usize) { + if i < FD_SLOTS { table()[i] = -1; } + } +} +pub fn fd_dup(fd: c_int) -> Option { fd_alloc(fd_vfs(fd)?) } +pub fn fd_set(fd: c_int, vfs_fd: u32) -> bool { + match fd.checked_sub(3).map(|v| v as usize) { + Some(i) if i < FD_SLOTS => { table()[i] = vfs_fd as i32; true } + _ => false, + } +} +pub fn vfs_call(op: u16, payload: &[u8], resp: &mut [u8]) -> Result<(i32, usize)> { + let mut req = Vec::with_capacity(HDR_LEN + payload.len()); + req.extend_from_slice(&VFS_MAGIC.to_le_bytes()); + req.extend_from_slice(&VFS_VERSION.to_le_bytes()); + req.extend_from_slice(&op.to_le_bytes()); + req.extend_from_slice(&[0u8; 8]); + req.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + req.extend_from_slice(payload); + let ret = unsafe { + syscall6(MK_IPC_CALL, VFS_ENDPOINT, + req.as_ptr() as u64, req.len() as u64, + resp.as_mut_ptr() as u64, resp.len() as u64, 0) + }; + if ret < 24 { return Err(Errno(EIO)); } + let s = i32::from_le_bytes([resp[20], resp[21], resp[22], resp[23]]); + Ok((s, (ret as usize) - 24)) +} From bcc4bc96bc43ff7b262b5d4529c81b0d8b84710d Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 14:49:20 +0600 Subject: [PATCH 35/72] feat(relibc): nonos write/close/read/dup2 via vfs IPC Wire write(), close(), read(), and dup2() through the new fs module: write routes fd>2 to OP_WRITE; close sends OP_CLOSE and frees the slot; read issues OP_READ capped at 65536 bytes; dup2 reuses the same vfs_fd via fd_set. Imports and `mod fs` declaration added. Refs: Phase 1 Task 1.2 --- toolchain/nonos-relibc/platform/nonos/mod.rs | 52 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 05005b37ef..c1dd64ec7c 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -3,6 +3,7 @@ //! munmap) and the remaining required methods stubbed to `ENOSYS` (or the //! obvious constant). Phase 1 de-stubs this backend method by method. +use alloc::vec::Vec; use core::num::NonZeroU64; use super::{Pal, types::*}; @@ -10,7 +11,7 @@ use crate::{ c_str::CStr, error::{Errno, Result}, header::{ - errno::{EBADF, EIO, ENOMEM, ENOSYS}, + errno::{EBADF, EIO, EINVAL, EMFILE, ENOMEM, ENOSYS}, signal::sigevent, sys_resource::{rlimit, rusage}, sys_select::timeval, @@ -27,6 +28,7 @@ use crate::{ }; pub mod lowlevel; +mod fs; mod epoll; mod ptrace; @@ -46,7 +48,17 @@ impl Pal for Sys { unsafe { lowlevel::syscall2(lowlevel::MK_DEBUG, buf.as_ptr() as u64, buf.len() as u64); } return Ok(buf.len()); } - Err(Errno(EBADF)) + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + if buf.len() > 65536 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = alloc::vec![0u8; 8 + buf.len()]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + payload[8..].copy_from_slice(buf); + let mut resp = [0u8; 28]; + let (status, _) = fs::vfs_call(fs::OP_WRITE, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + Ok(u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]) as usize) } fn exit(status: c_int) -> ! { @@ -77,8 +89,25 @@ impl Pal for Sys { fn faccessat(_fd: c_int, _path: CStr, _amode: c_int, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn chdir(_path: CStr) -> Result<()> { Err(Errno(ENOSYS)) } - fn close(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - fn dup2(_fildes: c_int, _fildes2: c_int) -> Result { Err(Errno(ENOSYS)) } + fn close(fildes: c_int) -> Result<()> { + if fildes <= 2 { return Ok(()); } + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + let pid = Self::getpid() as u32; + let mut payload = [0u8; 8]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + let mut resp = [0u8; 24]; + let (status, _) = fs::vfs_call(fs::OP_CLOSE, &payload, &mut resp)?; + fs::fd_free(fildes); + if status < 0 { Err(Errno(-status)) } else { Ok(()) } + } + fn dup2(fildes: c_int, fildes2: c_int) -> Result { + if fildes == fildes2 { return Ok(fildes2); } + if fildes2 < 3 { return Err(Errno(EBADF)); } + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + if !fs::fd_set(fildes2, vfs_fd) { return Err(Errno(EBADF)); } + Ok(fildes2) + } fn fchdir(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn fchmodat(_dirfd: c_int, _path: Option, _mode: mode_t, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn fchownat(_fildes: c_int, _path: CStr, _owner: uid_t, _group: gid_t, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } @@ -104,7 +133,20 @@ impl Pal for Sys { fn pipe2(_fildes: Out<[c_int; 2]>, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn posix_fallocate(_fd: c_int, _offset: u64, _length: NonZeroU64) -> Result<()> { Err(Errno(ENOSYS)) } fn posix_getdents(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } - fn read(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } + fn read(fildes: c_int, buf: &mut [u8]) -> Result { + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + let count = (buf.len() as u32).min(65536); + let pid = Self::getpid() as u32; + let mut payload = [0u8; 12]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + payload[8..12].copy_from_slice(&count.to_le_bytes()); + let mut resp = alloc::vec![0u8; 24 + count as usize]; + let (status, n) = fs::vfs_call(fs::OP_READ, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + buf[..n].copy_from_slice(&resp[24..24 + n]); + Ok(n) + } fn pread(_fildes: c_int, _buf: &mut [u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } fn pwrite(_fildes: c_int, _buf: &[u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } fn readlinkat(_dirfd: c_int, _pathname: CStr, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } From a4084017f9d2e2e574e5112047ef504668a834e7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 14:50:42 +0600 Subject: [PATCH 36/72] feat(relibc): nonos openat/fstatat/faccessat/mkdirat via vfs IPC openat maps O_CREAT/O_TRUNC/O_APPEND to vfs flags and allocates a relibc fd via fd_alloc. fstatat (path-only; fd-based returns ENOSYS) parses the OP_STAT response for st_size and st_mode. faccessat probes existence via OP_STAT. mkdirat sends OP_MKDIR. Refs: Phase 1 Task 1.2 --- toolchain/nonos-relibc/platform/nonos/mod.rs | 75 ++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index c1dd64ec7c..fb70a3c477 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -12,10 +12,11 @@ use crate::{ error::{Errno, Result}, header::{ errno::{EBADF, EIO, EINVAL, EMFILE, ENOMEM, ENOSYS}, + fcntl::{O_APPEND, O_CREAT, O_TRUNC}, signal::sigevent, sys_resource::{rlimit, rusage}, sys_select::timeval, - sys_stat::stat, + sys_stat::{stat, S_IFDIR, S_IFREG}, sys_statvfs::statvfs, sys_time::timezone, sys_utsname::utsname, @@ -87,7 +88,18 @@ impl Pal for Sys { Err(Errno(ENOSYS)) } - fn faccessat(_fd: c_int, _path: CStr, _amode: c_int, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn faccessat(_fd: c_int, path: CStr, _amode: c_int, _flags: c_int) -> Result<()> { + let pb = path.to_bytes(); + if pb.is_empty() || pb.len() > 255 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = Vec::with_capacity(5 + pb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(pb.len() as u8); + payload.extend_from_slice(pb); + let mut resp = [0u8; 36]; + let (status, _) = fs::vfs_call(fs::OP_STAT, &payload, &mut resp)?; + if status < 0 { Err(Errno(-status)) } else { Ok(()) } + } fn chdir(_path: CStr) -> Result<()> { Err(Errno(ENOSYS)) } fn close(fildes: c_int) -> Result<()> { if fildes <= 2 { return Ok(()); } @@ -113,7 +125,31 @@ impl Pal for Sys { fn fchownat(_fildes: c_int, _path: CStr, _owner: uid_t, _group: gid_t, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn fdatasync(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn flock(_fd: c_int, _operation: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - fn fstatat(_fildes: c_int, _path: Option, _buf: Out, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn fstatat(_fildes: c_int, path: Option, mut buf: Out, _flags: c_int) -> Result<()> { + let p = match path { + Some(p) if !p.to_bytes().is_empty() => p, + _ => return Err(Errno(ENOSYS)), + }; + let pb = p.to_bytes(); + if pb.len() > 255 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = Vec::with_capacity(5 + pb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(pb.len() as u8); + payload.extend_from_slice(pb); + let mut resp = [0u8; 36]; + let (status, _) = fs::vfs_call(fs::OP_STAT, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + let size = u64::from_le_bytes([resp[24], resp[25], resp[26], resp[27], + resp[28], resp[29], resp[30], resp[31]]); + let vfs_flags = u32::from_le_bytes([resp[32], resp[33], resp[34], resp[35]]); + buf.write(stat { + st_size: size as off_t, + st_mode: if vfs_flags & 1 != 0 { S_IFDIR | 0o555 } else { S_IFREG | 0o644 }, + ..Default::default() + }); + Ok(()) + } fn fstatvfs(_fildes: c_int, _buf: Out) -> Result<()> { Err(Errno(ENOSYS)) } fn fcntl(_fildes: c_int, _cmd: c_int, _arg: c_ulonglong) -> Result { Err(Errno(ENOSYS)) } fn fpath(_fildes: c_int, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } @@ -126,10 +162,39 @@ impl Pal for Sys { unsafe fn dent_reclen_offset(_this_dent: &[u8], _offset: usize) -> Option<(u16, u64)> { None } fn linkat(_fd1: c_int, _oldpath: CStr, _fd2: c_int, _newpath: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn lseek(_fildes: c_int, _offset: off_t, _whence: c_int) -> Result { Err(Errno(ENOSYS)) } - fn mkdirat(_fildes: c_int, _path: CStr, _mode: mode_t) -> Result<()> { Err(Errno(ENOSYS)) } + fn mkdirat(_fildes: c_int, path: CStr, _mode: mode_t) -> Result<()> { + let pb = path.to_bytes(); + if pb.is_empty() || pb.len() > 255 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = Vec::with_capacity(5 + pb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(pb.len() as u8); + payload.extend_from_slice(pb); + let mut resp = [0u8; 24]; + let (status, _) = fs::vfs_call(fs::OP_MKDIR, &payload, &mut resp)?; + if status < 0 { Err(Errno(-status)) } else { Ok(()) } + } fn mkfifoat(_dir_fd: c_int, _path: CStr, _mode: mode_t) -> Result<()> { Err(Errno(ENOSYS)) } fn mknodat(_fildes: c_int, _path: CStr, _mode: mode_t, _dev: dev_t) -> Result<()> { Err(Errno(ENOSYS)) } - fn openat(_dirfd: c_int, _path: CStr, _oflag: c_int, _mode: mode_t) -> Result { Err(Errno(ENOSYS)) } + fn openat(_dirfd: c_int, path: CStr, oflag: c_int, _mode: mode_t) -> Result { + let pb = path.to_bytes(); + if pb.is_empty() || pb.len() > 255 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let vfs_flags = + (if oflag & O_CREAT != 0 { fs::VFS_O_CREATE } else { 0 }) | + (if oflag & O_TRUNC != 0 { fs::VFS_O_TRUNC } else { 0 }) | + (if oflag & O_APPEND != 0 { fs::VFS_O_APPEND } else { 0 }); + let mut payload = Vec::with_capacity(9 + pb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(pb.len() as u8); + payload.extend_from_slice(pb); + payload.extend_from_slice(&vfs_flags.to_le_bytes()); + let mut resp = [0u8; 28]; + let (status, _) = fs::vfs_call(fs::OP_OPEN, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + let vfs_fd = u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]); + fs::fd_alloc(vfs_fd).ok_or(Errno(EMFILE)) + } fn pipe2(_fildes: Out<[c_int; 2]>, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn posix_fallocate(_fd: c_int, _offset: u64, _length: NonZeroU64) -> Result<()> { Err(Errno(ENOSYS)) } fn posix_getdents(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } From 4b6cce2c91bca76ede81ca7dea5a65c13786d3e9 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 14:51:34 +0600 Subject: [PATCH 37/72] feat(relibc): nonos unlinkat/renameat2 via vfs IPC unlinkat sends OP_UNLINK with pid+path_len+path. renameat2 sends OP_RENAME with pid+old_len+old+new_len+new; ignores rename flags (no RENAME_EXCHANGE/NOREPLACE at vfs layer yet). Refs: Phase 1 Task 1.2 --- toolchain/nonos-relibc/platform/nonos/mod.rs | 31 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index fb70a3c477..795b860528 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -215,10 +215,37 @@ impl Pal for Sys { fn pread(_fildes: c_int, _buf: &mut [u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } fn pwrite(_fildes: c_int, _buf: &[u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } fn readlinkat(_dirfd: c_int, _pathname: CStr, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } - fn renameat2(_old_dir: c_int, _old_path: CStr, _new_dir: c_int, _new_path: CStr, _flags: c_uint) -> Result<()> { Err(Errno(ENOSYS)) } + fn renameat2(_old_dir: c_int, old_path: CStr, _new_dir: c_int, new_path: CStr, _flags: c_uint) -> Result<()> { + let ob = old_path.to_bytes(); + let nb = new_path.to_bytes(); + if ob.is_empty() || ob.len() > 255 || nb.is_empty() || nb.len() > 255 { + return Err(Errno(EINVAL)); + } + let pid = Self::getpid() as u32; + let mut payload = Vec::with_capacity(4 + 1 + ob.len() + 1 + nb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(ob.len() as u8); + payload.extend_from_slice(ob); + payload.push(nb.len() as u8); + payload.extend_from_slice(nb); + let mut resp = [0u8; 24]; + let (status, _) = fs::vfs_call(fs::OP_RENAME, &payload, &mut resp)?; + if status < 0 { Err(Errno(-status)) } else { Ok(()) } + } fn symlinkat(_path1: CStr, _fd: c_int, _path2: CStr) -> Result<()> { Err(Errno(ENOSYS)) } fn sync() -> Result<()> { Err(Errno(ENOSYS)) } - fn unlinkat(_fd: c_int, _path: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } + fn unlinkat(_fd: c_int, path: CStr, _flags: c_int) -> Result<()> { + let pb = path.to_bytes(); + if pb.is_empty() || pb.len() > 255 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = Vec::with_capacity(5 + pb.len()); + payload.extend_from_slice(&pid.to_le_bytes()); + payload.push(pb.len() as u8); + payload.extend_from_slice(pb); + let mut resp = [0u8; 24]; + let (status, _) = fs::vfs_call(fs::OP_UNLINK, &payload, &mut resp)?; + if status < 0 { Err(Errno(-status)) } else { Ok(()) } + } unsafe fn mlock(_addr: *const c_void, _len: usize) -> Result<()> { Ok(()) } unsafe fn mlockall(_flags: c_int) -> Result<()> { Ok(()) } unsafe fn mremap(_addr: *mut c_void, _len: usize, _new_len: usize, _flags: c_int, _args: *mut c_void) -> Result<*mut c_void> { Err(Errno(ENOSYS)) } From 1ec8d77c89d61a09af95eb7218695f2506981e5f Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 15:07:02 +0600 Subject: [PATCH 38/72] fix(relibc): dup2 closes displaced vfs fd; tighten vfs response guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dup2 previously overwrote the fd-table slot at fildes2 without closing whatever was open there, leaking the displaced vfs fd. POSIX requires closing fildes2 first; the fix checks fd_vfs(fildes2) and issues OP_CLOSE via Self::close() (which already handles the fd-free) before installing the new mapping. The fildes==fildes2 identity guard that returns early was already present. vfs_call only asserts ret>=24 (status present). Callers that consume bytes past the status field could silently get zeroed data on a truncated response. Added per-caller minimum-length guards using the data_len return value: OP_WRITE/OP_OPEN require data_len>=4 (ret>=28), OP_STAT in fstatat requires data_len>=12 (ret>=36). All three return Err(Errno(EIO)) on underrun. OP_READ is unchanged (data_len=0 is a valid EOF). faccessat uses OP_STAT but only reads status so no guard needed there. Build: librelibc.a x86_64-unknown-nonos release — 0 errors, 13.70s. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 795b860528..48067d04a4 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -57,7 +57,8 @@ impl Pal for Sys { payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); payload[8..].copy_from_slice(buf); let mut resp = [0u8; 28]; - let (status, _) = fs::vfs_call(fs::OP_WRITE, &payload, &mut resp)?; + let (status, data_len) = fs::vfs_call(fs::OP_WRITE, &payload, &mut resp)?; + if data_len < 4 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } Ok(u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]) as usize) } @@ -117,6 +118,7 @@ impl Pal for Sys { if fildes == fildes2 { return Ok(fildes2); } if fildes2 < 3 { return Err(Errno(EBADF)); } let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + if fs::fd_vfs(fildes2).is_some() { let _ = Self::close(fildes2); } if !fs::fd_set(fildes2, vfs_fd) { return Err(Errno(EBADF)); } Ok(fildes2) } @@ -138,7 +140,8 @@ impl Pal for Sys { payload.push(pb.len() as u8); payload.extend_from_slice(pb); let mut resp = [0u8; 36]; - let (status, _) = fs::vfs_call(fs::OP_STAT, &payload, &mut resp)?; + let (status, data_len) = fs::vfs_call(fs::OP_STAT, &payload, &mut resp)?; + if data_len < 12 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } let size = u64::from_le_bytes([resp[24], resp[25], resp[26], resp[27], resp[28], resp[29], resp[30], resp[31]]); @@ -190,7 +193,8 @@ impl Pal for Sys { payload.extend_from_slice(pb); payload.extend_from_slice(&vfs_flags.to_le_bytes()); let mut resp = [0u8; 28]; - let (status, _) = fs::vfs_call(fs::OP_OPEN, &payload, &mut resp)?; + let (status, data_len) = fs::vfs_call(fs::OP_OPEN, &payload, &mut resp)?; + if data_len < 4 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } let vfs_fd = u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]); fs::fd_alloc(vfs_fd).ok_or(Errno(EMFILE)) From 0795d8dc23cca2cce383a77a4a7e6460b2085202 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 15:23:40 +0600 Subject: [PATCH 39/72] fix(relibc): futex no-op for single-threaded nonos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relibc's stdio locks the FILE mutex on fwrite/fread; the mutex calls Sys::futex_wake().unwrap() (sync/mod.rs:103). Our futex stubs returned ENOSYS → unwrap panic ("Errno(38)") on the first fwrite. nonos is single-threaded (no real contention, no per-thread TLS), so futex_wait returns Ok (spurious-wakeup; caller re-checks the atomic) and futex_wake returns Ok(0) (no waiters). The plan's "futex→ENOSYS until Phase 3" breaks relibc's locking; a no-op is the correct single-threaded behavior. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 48067d04a4..17067a92c2 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -328,8 +328,8 @@ impl Pal for Sys { unsafe fn fexecve(_fildes: c_int, _argv: *const *mut c_char, _envp: *const *mut c_char) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn exit_thread(_stack_base: *mut (), _stack_size: usize) -> ! { Self::exit(0) } unsafe fn fork() -> Result { Err(Errno(ENOSYS)) } - unsafe fn futex_wait(_addr: *mut u32, _val: u32, _deadline: Option<×pec>) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn futex_wake(_addr: *mut u32, _num: u32) -> Result { Err(Errno(ENOSYS)) } + unsafe fn futex_wait(_addr: *mut u32, _val: u32, _deadline: Option<×pec>) -> Result<()> { Ok(()) } + unsafe fn futex_wake(_addr: *mut u32, _num: u32) -> Result { Ok(0) } unsafe fn rlct_clone(_stack: *mut usize, _os_specific: &mut OsSpecific) -> Result { Err(Errno(ENOSYS)) } unsafe fn rlct_kill(_os_tid: pthread::OsTid, _signal: usize) -> Result<()> { Err(Errno(ENOSYS)) } fn current_os_tid() -> pthread::OsTid { pthread::OsTid::default() } From 3cf3a2856102ec7cefc1e4745ab2018387a9f32a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 15:23:41 +0600 Subject: [PATCH 40/72] test(relibc): exercise fs (fopen/fwrite/fread) in the relibc-test gate Extend the exerciser to create+write+read a vfs file and verify the bytes, and add vfs (+ramfs+proof-io, nonos-zk-rollout to tolerate their stale attestation trailers) to microkernel-relibc-test-smoketest so the vfs service is running. Proven: [RELIBC-TEST] PASS with fs+malloc+clock under NONOS_DEV=1 nonos-mk-relibc-smoke-test. --- Cargo.toml | 4 ++++ userland/capsule_relibc_test/src/main.c | 31 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39c6a9f3bb..cc21ad1fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -234,6 +234,10 @@ microkernel-c-proof-smoketest = [ microkernel-relibc-test-smoketest = [ "microkernel-core", "nonos-dev-unverified-capsules", + "nonos-zk-rollout", + "nonos-capsule-proof-io", + "nonos-capsule-ramfs", + "nonos-capsule-vfs", "nonos-capsule-relibc-test", ] diff --git a/userland/capsule_relibc_test/src/main.c b/userland/capsule_relibc_test/src/main.c index 132d979693..a89fbf0845 100644 --- a/userland/capsule_relibc_test/src/main.c +++ b/userland/capsule_relibc_test/src/main.c @@ -1,18 +1,37 @@ #include #include +#include #include -static void emit(const char *s, unsigned n) { write(1, s, n); } + +#define EMIT(s) write(1, s, sizeof(s) - 1) + int main(void) { char *p = (char *)malloc(1u << 20); - if (!p) { emit("[RELIBC-TEST] FAIL malloc\n", 26); return 1; } + if (!p) { EMIT("[RELIBC-TEST] FAIL malloc\n"); return 1; } for (unsigned i = 0; i < (1u << 20); i += 4096) p[i] = (char)i; char *q = (char *)malloc(4096); - if (!q) { emit("[RELIBC-TEST] FAIL malloc2\n", 27); return 1; } - free(q); free(p); + if (!q) { EMIT("[RELIBC-TEST] FAIL malloc2\n"); return 1; } + free(q); + free(p); + struct timespec a, b; clock_gettime(CLOCK_MONOTONIC, &a); clock_gettime(CLOCK_MONOTONIC, &b); - if (b.tv_sec < a.tv_sec) { emit("[RELIBC-TEST] FAIL clock\n", 25); return 1; } - emit("[RELIBC-TEST] PASS\n", 19); + if (b.tv_sec < a.tv_sec) { EMIT("[RELIBC-TEST] FAIL clock\n"); return 1; } + + FILE *f = fopen("/relibctest.txt", "w"); + if (!f) { EMIT("[RELIBC-TEST] FAIL fopen-w\n"); return 1; } + if (fwrite("hello", 1, 5, f) != 5) { EMIT("[RELIBC-TEST] FAIL fwrite\n"); return 1; } + fclose(f); + + f = fopen("/relibctest.txt", "r"); + if (!f) { EMIT("[RELIBC-TEST] FAIL fopen-r\n"); return 1; } + char buf[8]; + for (int i = 0; i < 8; i++) buf[i] = 0; + size_t n = fread(buf, 1, 5, f); + fclose(f); + if (n != 5 || buf[0] != 'h' || buf[4] != 'o') { EMIT("[RELIBC-TEST] FAIL fread\n"); return 1; } + + EMIT("[RELIBC-TEST] PASS\n"); return 0; } From 9ff5b84e9111ee5c7764b5ae07f7c74d92fcc1e4 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 15:36:42 +0600 Subject: [PATCH 41/72] feat(relibc): nonos thread spawn via MkThreadSpawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MK_THREAD_SPAWN syscall tag and an x86_64 rlct_trampoline in lowlevel.rs. The trampoline pops the seven words relibc's pthread_create arranges on the new thread stack (entry fn + 6 args) and calls the entry exactly as the linux clone inline-asm child path does, then falls through to MK_EXIT. De-stub rlct_clone: call syscall2(MK_THREAD_SPAWN, rlct_trampoline, stack) with the pre-built stack; return OsTid {} on success (nonos OsTid has no fields — thread_id is linux-only). Concerns: pthread_create panics before reaching rlct_clone on nonos because Tcb::current() is patched to return None and pthread::init() is a no-op; per-thread TLS is Phase-3 work. --- .../nonos-relibc/platform/nonos/lowlevel.rs | 28 ++++++++++++++++++- toolchain/nonos-relibc/platform/nonos/mod.rs | 6 +++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs index e1442c4075..a491b6fcc7 100644 --- a/toolchain/nonos-relibc/platform/nonos/lowlevel.rs +++ b/toolchain/nonos-relibc/platform/nonos/lowlevel.rs @@ -1,4 +1,4 @@ -use core::arch::asm; +use core::arch::{asm, global_asm}; pub const fn tag4(s: &[u8; 4]) -> u64 { (s[0] as u64) | (s[1] as u64) << 8 | (s[2] as u64) << 16 | (s[3] as u64) << 24 @@ -13,6 +13,7 @@ pub const MK_TIME_MILLIS: u64 = tag4(b"MTMS"); pub const MK_YIELD: u64 = tag4(b"MYLD"); pub const MK_CRYPTO_RANDOM: u64 = tag4(b"CRND"); pub const MK_IPC_CALL: u64 = tag4(b"MICL"); +pub const MK_THREAD_SPAWN: u64 = tag4(b"MTSP"); #[inline] pub unsafe fn syscall0(n: u64) -> i64 { @@ -89,3 +90,28 @@ pub unsafe fn syscall6(n: u64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: } r } + +#[cfg(target_arch = "x86_64")] +global_asm!( + ".globl rlct_trampoline", + ".type rlct_trampoline, @function", + "rlct_trampoline:", + "pop rax", + "pop rdi", + "pop rsi", + "pop rdx", + "pop rcx", + "pop r8", + "pop r9", + "call rax", + "xor rdi, rdi", + "mov rax, 0x5458454d", + "syscall", + "ud2", + ".size rlct_trampoline, . - rlct_trampoline", +); + +#[cfg(target_arch = "x86_64")] +unsafe extern "C" { + pub fn rlct_trampoline(); +} diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 17067a92c2..83d5911b3b 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -330,7 +330,11 @@ impl Pal for Sys { unsafe fn fork() -> Result { Err(Errno(ENOSYS)) } unsafe fn futex_wait(_addr: *mut u32, _val: u32, _deadline: Option<×pec>) -> Result<()> { Ok(()) } unsafe fn futex_wake(_addr: *mut u32, _num: u32) -> Result { Ok(0) } - unsafe fn rlct_clone(_stack: *mut usize, _os_specific: &mut OsSpecific) -> Result { Err(Errno(ENOSYS)) } + unsafe fn rlct_clone(stack: *mut usize, _os_specific: &mut OsSpecific) -> Result { + let r = unsafe { lowlevel::syscall2(lowlevel::MK_THREAD_SPAWN, lowlevel::rlct_trampoline as *const () as u64, stack as u64) }; + if r < 0 { return Err(Errno(-r as c_int)); } + Ok(pthread::OsTid {}) + } unsafe fn rlct_kill(_os_tid: pthread::OsTid, _signal: usize) -> Result<()> { Err(Errno(ENOSYS)) } fn current_os_tid() -> pthread::OsTid { pthread::OsTid::default() } unsafe fn spawn(_program: CStr, _fac: Option<&crate::header::spawn::posix_spawn_file_actions_t>, _fat: Option<&crate::header::spawn::posix_spawnattr_t>, _argv: NulTerminated<*mut c_char>, _envp: Option>) -> Result { Err(Errno(ENOSYS)) } From 4e3ab3963c6a90601fcd23c213fd9884e8ec45e4 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 15:37:13 +0600 Subject: [PATCH 42/72] feat(relibc): nonos sched_yield/exit_thread/current_os_tid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De-stub three remaining thread helpers: - sched_yield: syscall0(MK_YIELD); Ok(()) - exit_thread: direct syscall1(MK_EXIT, 0) + loop{} (was delegating through Self::exit; now explicit so the call site is unambiguous) - current_os_tid: calls MK_GETPID and returns OsTid {} — all threads report the process pid in Phase 1; per-thread tid requires Phase 3 (kernel must set FS base per thread and expose a gettid syscall). OsTid for target_os="nonos" has no fields, so the pid is intentionally discarded. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 83d5911b3b..0b97cede77 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -326,7 +326,10 @@ impl Pal for Sys { fn umask(_mask: mode_t) -> mode_t { 0 } unsafe fn execve(_path: CStr, _argv: *const *mut c_char, _envp: *const *mut c_char) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn fexecve(_fildes: c_int, _argv: *const *mut c_char, _envp: *const *mut c_char) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn exit_thread(_stack_base: *mut (), _stack_size: usize) -> ! { Self::exit(0) } + unsafe fn exit_thread(_stack_base: *mut (), _stack_size: usize) -> ! { + unsafe { lowlevel::syscall1(lowlevel::MK_EXIT, 0); } + loop {} + } unsafe fn fork() -> Result { Err(Errno(ENOSYS)) } unsafe fn futex_wait(_addr: *mut u32, _val: u32, _deadline: Option<×pec>) -> Result<()> { Ok(()) } unsafe fn futex_wake(_addr: *mut u32, _num: u32) -> Result { Ok(0) } @@ -336,10 +339,16 @@ impl Pal for Sys { Ok(pthread::OsTid {}) } unsafe fn rlct_kill(_os_tid: pthread::OsTid, _signal: usize) -> Result<()> { Err(Errno(ENOSYS)) } - fn current_os_tid() -> pthread::OsTid { pthread::OsTid::default() } + fn current_os_tid() -> pthread::OsTid { + let _pid = unsafe { lowlevel::syscall0(lowlevel::MK_GETPID) }; + pthread::OsTid {} + } unsafe fn spawn(_program: CStr, _fac: Option<&crate::header::spawn::posix_spawn_file_actions_t>, _fat: Option<&crate::header::spawn::posix_spawnattr_t>, _argv: NulTerminated<*mut c_char>, _envp: Option>) -> Result { Err(Errno(ENOSYS)) } fn waitpid(_pid: pid_t, _stat_loc: Option>, _options: c_int) -> Result { Err(Errno(ENOSYS)) } - fn sched_yield() -> Result<()> { Err(Errno(ENOSYS)) } + fn sched_yield() -> Result<()> { + unsafe { lowlevel::syscall0(lowlevel::MK_YIELD); } + Ok(()) + } fn uname(_utsname: Out) -> Result<()> { Err(Errno(ENOSYS)) } fn verify() -> bool { From 610afed8b04b9c0a476df37c36dde808056b485c Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 16:46:20 +0600 Subject: [PATCH 43/72] feat(vfs): OP_SEEK random-access offset over NOVF relibc's lseek backend needs a server-side seek op so the NOVF filesystem capsule can track per-fd positions independently of read/write advancement. Adds OP_SEEK=11 (SEEK_SET/CUR/END, POSIX semantics: past-EOF allowed, negative result is EINVAL) plus a StoreError::Invalid variant, and hardens store.read to clamp pos before slicing so a post-seek pos>len yields a 0-byte EOF read instead of a panic. --- userland/capsule_vfs/src/protocol/mod.rs | 4 +- userland/capsule_vfs/src/protocol/types.rs | 1 + userland/capsule_vfs/src/server/dispatch.rs | 3 +- .../capsule_vfs/src/server/handlers/mod.rs | 2 + .../capsule_vfs/src/server/handlers/seek.rs | 40 +++++++++++++++++++ .../capsule_vfs/src/server/handlers/util.rs | 1 + userland/capsule_vfs/src/store/fdtable.rs | 1 + .../capsule_vfs/src/store/fdtable/read.rs | 4 +- .../capsule_vfs/src/store/fdtable/seek.rs | 40 +++++++++++++++++++ .../capsule_vfs/src/store/fdtable/types.rs | 1 + 10 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 userland/capsule_vfs/src/server/handlers/seek.rs create mode 100644 userland/capsule_vfs/src/store/fdtable/seek.rs diff --git a/userland/capsule_vfs/src/protocol/mod.rs b/userland/capsule_vfs/src/protocol/mod.rs index 185de4e0ef..6b74b3b9ea 100644 --- a/userland/capsule_vfs/src/protocol/mod.rs +++ b/userland/capsule_vfs/src/protocol/mod.rs @@ -26,6 +26,6 @@ pub use errno::{ }; pub use types::{ Request, KERNEL_REPLY_ENDPOINT, MAX_DATA_BYTES, MAX_LIST_BYTES, MAX_PATH_BYTES, OP_CLOSE, - OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, OP_READ, OP_RENAME, OP_STAT, OP_UNLINK, OP_WRITE, - O_APPEND, O_CREATE, O_TRUNC, + OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, OP_READ, OP_RENAME, OP_SEEK, OP_STAT, OP_UNLINK, + OP_WRITE, O_APPEND, O_CREATE, O_TRUNC, }; diff --git a/userland/capsule_vfs/src/protocol/types.rs b/userland/capsule_vfs/src/protocol/types.rs index 31508d74ec..ec800dbfb4 100644 --- a/userland/capsule_vfs/src/protocol/types.rs +++ b/userland/capsule_vfs/src/protocol/types.rs @@ -27,6 +27,7 @@ pub const OP_HEALTHCHECK: u16 = 7; pub const OP_MKDIR: u16 = 8; pub const OP_UNLINK: u16 = 9; pub const OP_RENAME: u16 = 10; +pub const OP_SEEK: u16 = 11; pub const O_CREATE: u32 = 1 << 0; pub const O_TRUNC: u32 = 1 << 1; diff --git a/userland/capsule_vfs/src/server/dispatch.rs b/userland/capsule_vfs/src/server/dispatch.rs index 4723373725..73337f7051 100644 --- a/userland/capsule_vfs/src/server/dispatch.rs +++ b/userland/capsule_vfs/src/server/dispatch.rs @@ -19,7 +19,7 @@ use alloc::vec::Vec; use super::handlers; use crate::protocol::{ encode_response, Request, EINVAL, OP_CLOSE, OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, - OP_READ, OP_RENAME, OP_STAT, OP_UNLINK, OP_WRITE, + OP_READ, OP_RENAME, OP_SEEK, OP_STAT, OP_UNLINK, OP_WRITE, }; use crate::store::Store; @@ -34,6 +34,7 @@ pub fn dispatch(store: &mut Store, req: Request<'_>, sender_pid: u32) -> Vec OP_MKDIR => handlers::mkdir(store, req, sender_pid), OP_UNLINK => handlers::unlink(store, req, sender_pid), OP_RENAME => handlers::rename(store, req, sender_pid), + OP_SEEK => handlers::seek(store, req, sender_pid), OP_HEALTHCHECK => handlers::healthcheck(req), _ => encode_response(req.op, req.flags, req.request_id, EINVAL, &[]), } diff --git a/userland/capsule_vfs/src/server/handlers/mod.rs b/userland/capsule_vfs/src/server/handlers/mod.rs index a9b4810d44..b5c62a6d92 100644 --- a/userland/capsule_vfs/src/server/handlers/mod.rs +++ b/userland/capsule_vfs/src/server/handlers/mod.rs @@ -21,6 +21,7 @@ mod mkdir; mod open; mod read; mod rename; +mod seek; mod stat; mod unlink; mod util; @@ -33,6 +34,7 @@ pub(super) use mkdir::mkdir; pub(super) use open::open; pub(super) use read::read; pub(super) use rename::rename; +pub(super) use seek::seek; pub(super) use stat::stat; pub(super) use unlink::unlink; pub(super) use write::write; diff --git a/userland/capsule_vfs/src/server/handlers/seek.rs b/userland/capsule_vfs/src/server/handlers/seek.rs new file mode 100644 index 0000000000..14a460d1fc --- /dev/null +++ b/userland/capsule_vfs/src/server/handlers/seek.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 . + +use alloc::vec::Vec; + +use super::util::{map_store_err, split_caller}; +use crate::protocol::{encode_response, Request, EINVAL, OP_SEEK}; +use crate::store::Store; + +pub fn seek(store: &mut Store, req: Request<'_>, sender_pid: u32) -> Vec { + let (pid, rest) = match split_caller(req.payload, sender_pid) { + Ok(v) => v, + Err(s) => return encode_response(OP_SEEK, req.flags, req.request_id, s, &[]), + }; + if rest.len() != 16 { + return encode_response(OP_SEEK, req.flags, req.request_id, EINVAL, &[]); + } + let fd = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]); + let whence = u16::from_le_bytes([rest[4], rest[5]]); + let offset = i64::from_le_bytes([ + rest[8], rest[9], rest[10], rest[11], rest[12], rest[13], rest[14], rest[15], + ]); + match store.seek(fd, pid, whence, offset) { + Ok(new_pos) => encode_response(OP_SEEK, req.flags, req.request_id, 0, &new_pos.to_le_bytes()), + Err(e) => encode_response(OP_SEEK, req.flags, req.request_id, map_store_err(e), &[]), + } +} diff --git a/userland/capsule_vfs/src/server/handlers/util.rs b/userland/capsule_vfs/src/server/handlers/util.rs index cc581e3f41..ebc92d1b96 100644 --- a/userland/capsule_vfs/src/server/handlers/util.rs +++ b/userland/capsule_vfs/src/server/handlers/util.rs @@ -28,6 +28,7 @@ pub(super) fn map_store_err(e: StoreError) -> i32 { StoreError::Exists => EEXIST, StoreError::NotEmpty => ENOTEMPTY, StoreError::IsDir => EISDIR, + StoreError::Invalid => EINVAL, } } diff --git a/userland/capsule_vfs/src/store/fdtable.rs b/userland/capsule_vfs/src/store/fdtable.rs index 0091276f81..58341ea807 100644 --- a/userland/capsule_vfs/src/store/fdtable.rs +++ b/userland/capsule_vfs/src/store/fdtable.rs @@ -22,6 +22,7 @@ mod open; mod query; mod read; mod rename; +mod seek; mod seed; mod types; mod unlink; diff --git a/userland/capsule_vfs/src/store/fdtable/read.rs b/userland/capsule_vfs/src/store/fdtable/read.rs index 6c82631ce5..7a18663261 100644 --- a/userland/capsule_vfs/src/store/fdtable/read.rs +++ b/userland/capsule_vfs/src/store/fdtable/read.rs @@ -25,11 +25,13 @@ impl Store { (entry.file_idx, entry.pos) }; let data = &self.files[file_idx].data; + let raw_pos = pos; + let pos = pos.min(data.len()); let avail = data.len().saturating_sub(pos); let n = if max < avail { max } else { avail }; let out = data[pos..pos + n].to_vec(); if let Some(entry) = self.fds[fd as usize].as_mut() { - entry.pos = pos + n; + entry.pos = raw_pos + n; } Ok(out) } diff --git a/userland/capsule_vfs/src/store/fdtable/seek.rs b/userland/capsule_vfs/src/store/fdtable/seek.rs new file mode 100644 index 0000000000..4ef792493a --- /dev/null +++ b/userland/capsule_vfs/src/store/fdtable/seek.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 . + +use super::types::{Store, StoreError, StoreResult}; + +impl Store { + pub fn seek(&mut self, fd: u32, owner_pid: u32, whence: u16, offset: i64) -> StoreResult { + let (file_idx, pos) = { + let e = self.entry(fd, owner_pid)?; + (e.file_idx, e.pos) + }; + let base: i64 = match whence { + 0 => 0, + 1 => pos as i64, + 2 => self.files[file_idx].data.len() as i64, + _ => return Err(StoreError::Invalid), + }; + let new = base.checked_add(offset).ok_or(StoreError::Invalid)?; + if new < 0 { + return Err(StoreError::Invalid); + } + if let Some(e) = self.fds[fd as usize].as_mut() { + e.pos = new as usize; + } + Ok(new as u64) + } +} diff --git a/userland/capsule_vfs/src/store/fdtable/types.rs b/userland/capsule_vfs/src/store/fdtable/types.rs index f6623e173c..266106e952 100644 --- a/userland/capsule_vfs/src/store/fdtable/types.rs +++ b/userland/capsule_vfs/src/store/fdtable/types.rs @@ -30,6 +30,7 @@ pub enum StoreError { Exists, NotEmpty, IsDir, + Invalid, } pub(super) type StoreResult = Result; From 077516587b7cff2810ab5cffba2c35b17c53f249 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Mon, 29 Jun 2026 16:56:13 +0600 Subject: [PATCH 44/72] feat(vfs): pread/pwrite positional IO over NOVF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds OP_PREAD=12 and OP_PWRITE=13 to the capsule_vfs NOVF protocol. Unlike OP_READ/OP_WRITE, these operations take an explicit byte offset and leave the fd's pos field untouched — POSIX pread/pwrite semantics. This is required by the relibc nonos Pal so that pread(2)/pwrite(2) can be forwarded to the VFS capsule without disturbing the sequential file position maintained by the fd, enabling independent concurrent access patterns that relibc expects. --- userland/capsule_vfs/src/protocol/mod.rs | 4 +- userland/capsule_vfs/src/protocol/types.rs | 2 + userland/capsule_vfs/src/server/dispatch.rs | 4 +- .../capsule_vfs/src/server/handlers/mod.rs | 4 ++ .../capsule_vfs/src/server/handlers/pread.rs | 47 +++++++++++++++++++ .../capsule_vfs/src/server/handlers/pwrite.rs | 47 +++++++++++++++++++ userland/capsule_vfs/src/store/fdtable.rs | 2 + .../capsule_vfs/src/store/fdtable/pread.rs | 30 ++++++++++++ .../capsule_vfs/src/store/fdtable/pwrite.rs | 40 ++++++++++++++++ 9 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 userland/capsule_vfs/src/server/handlers/pread.rs create mode 100644 userland/capsule_vfs/src/server/handlers/pwrite.rs create mode 100644 userland/capsule_vfs/src/store/fdtable/pread.rs create mode 100644 userland/capsule_vfs/src/store/fdtable/pwrite.rs diff --git a/userland/capsule_vfs/src/protocol/mod.rs b/userland/capsule_vfs/src/protocol/mod.rs index 6b74b3b9ea..b0c9792295 100644 --- a/userland/capsule_vfs/src/protocol/mod.rs +++ b/userland/capsule_vfs/src/protocol/mod.rs @@ -26,6 +26,6 @@ pub use errno::{ }; pub use types::{ Request, KERNEL_REPLY_ENDPOINT, MAX_DATA_BYTES, MAX_LIST_BYTES, MAX_PATH_BYTES, OP_CLOSE, - OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, OP_READ, OP_RENAME, OP_SEEK, OP_STAT, OP_UNLINK, - OP_WRITE, O_APPEND, O_CREATE, O_TRUNC, + OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, OP_PREAD, OP_PWRITE, OP_READ, OP_RENAME, OP_SEEK, + OP_STAT, OP_UNLINK, OP_WRITE, O_APPEND, O_CREATE, O_TRUNC, }; diff --git a/userland/capsule_vfs/src/protocol/types.rs b/userland/capsule_vfs/src/protocol/types.rs index ec800dbfb4..b6159f94bb 100644 --- a/userland/capsule_vfs/src/protocol/types.rs +++ b/userland/capsule_vfs/src/protocol/types.rs @@ -28,6 +28,8 @@ pub const OP_MKDIR: u16 = 8; pub const OP_UNLINK: u16 = 9; pub const OP_RENAME: u16 = 10; pub const OP_SEEK: u16 = 11; +pub const OP_PREAD: u16 = 12; +pub const OP_PWRITE: u16 = 13; pub const O_CREATE: u32 = 1 << 0; pub const O_TRUNC: u32 = 1 << 1; diff --git a/userland/capsule_vfs/src/server/dispatch.rs b/userland/capsule_vfs/src/server/dispatch.rs index 73337f7051..3b4e2ea8e2 100644 --- a/userland/capsule_vfs/src/server/dispatch.rs +++ b/userland/capsule_vfs/src/server/dispatch.rs @@ -19,7 +19,7 @@ use alloc::vec::Vec; use super::handlers; use crate::protocol::{ encode_response, Request, EINVAL, OP_CLOSE, OP_HEALTHCHECK, OP_LIST, OP_MKDIR, OP_OPEN, - OP_READ, OP_RENAME, OP_SEEK, OP_STAT, OP_UNLINK, OP_WRITE, + OP_PREAD, OP_PWRITE, OP_READ, OP_RENAME, OP_SEEK, OP_STAT, OP_UNLINK, OP_WRITE, }; use crate::store::Store; @@ -35,6 +35,8 @@ pub fn dispatch(store: &mut Store, req: Request<'_>, sender_pid: u32) -> Vec OP_UNLINK => handlers::unlink(store, req, sender_pid), OP_RENAME => handlers::rename(store, req, sender_pid), OP_SEEK => handlers::seek(store, req, sender_pid), + OP_PREAD => handlers::pread(store, req, sender_pid), + OP_PWRITE => handlers::pwrite(store, req, sender_pid), OP_HEALTHCHECK => handlers::healthcheck(req), _ => encode_response(req.op, req.flags, req.request_id, EINVAL, &[]), } diff --git a/userland/capsule_vfs/src/server/handlers/mod.rs b/userland/capsule_vfs/src/server/handlers/mod.rs index b5c62a6d92..10751e1ca7 100644 --- a/userland/capsule_vfs/src/server/handlers/mod.rs +++ b/userland/capsule_vfs/src/server/handlers/mod.rs @@ -19,6 +19,8 @@ mod healthcheck; mod list; mod mkdir; mod open; +mod pread; +mod pwrite; mod read; mod rename; mod seek; @@ -32,6 +34,8 @@ pub(super) use healthcheck::healthcheck; pub(super) use list::list; pub(super) use mkdir::mkdir; pub(super) use open::open; +pub(super) use pread::pread; +pub(super) use pwrite::pwrite; pub(super) use read::read; pub(super) use rename::rename; pub(super) use seek::seek; diff --git a/userland/capsule_vfs/src/server/handlers/pread.rs b/userland/capsule_vfs/src/server/handlers/pread.rs new file mode 100644 index 0000000000..2a6fd43469 --- /dev/null +++ b/userland/capsule_vfs/src/server/handlers/pread.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 . + +use alloc::vec::Vec; + +use super::util::{map_store_err, split_caller}; +use crate::protocol::{encode_response, Request, EINVAL, EMSGSIZE, MAX_DATA_BYTES, OP_PREAD}; +use crate::store::Store; + +pub fn pread(store: &mut Store, req: Request<'_>, sender_pid: u32) -> Vec { + let (pid, rest) = match split_caller(req.payload, sender_pid) { + Ok(v) => v, + Err(s) => return encode_response(OP_PREAD, req.flags, req.request_id, s, &[]), + }; + if rest.len() != 16 { + return encode_response(OP_PREAD, req.flags, req.request_id, EINVAL, &[]); + } + let fd = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]); + let offset = i64::from_le_bytes([ + rest[4], rest[5], rest[6], rest[7], + rest[8], rest[9], rest[10], rest[11], + ]); + let max = u32::from_le_bytes([rest[12], rest[13], rest[14], rest[15]]); + if offset < 0 { + return encode_response(OP_PREAD, req.flags, req.request_id, EINVAL, &[]); + } + if max > MAX_DATA_BYTES { + return encode_response(OP_PREAD, req.flags, req.request_id, EMSGSIZE, &[]); + } + match store.pread(fd, pid, offset as u64, max as usize) { + Ok(data) => encode_response(OP_PREAD, req.flags, req.request_id, 0, &data), + Err(e) => encode_response(OP_PREAD, req.flags, req.request_id, map_store_err(e), &[]), + } +} diff --git a/userland/capsule_vfs/src/server/handlers/pwrite.rs b/userland/capsule_vfs/src/server/handlers/pwrite.rs new file mode 100644 index 0000000000..873357bb3c --- /dev/null +++ b/userland/capsule_vfs/src/server/handlers/pwrite.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 . + +use alloc::vec::Vec; + +use super::util::{map_store_err, split_caller}; +use crate::protocol::{encode_response, Request, EINVAL, EMSGSIZE, MAX_DATA_BYTES, OP_PWRITE}; +use crate::store::Store; + +pub fn pwrite(store: &mut Store, req: Request<'_>, sender_pid: u32) -> Vec { + let (pid, rest) = match split_caller(req.payload, sender_pid) { + Ok(v) => v, + Err(s) => return encode_response(OP_PWRITE, req.flags, req.request_id, s, &[]), + }; + if rest.len() < 12 { + return encode_response(OP_PWRITE, req.flags, req.request_id, EINVAL, &[]); + } + let fd = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]); + let offset = i64::from_le_bytes([ + rest[4], rest[5], rest[6], rest[7], + rest[8], rest[9], rest[10], rest[11], + ]); + let data = &rest[12..]; + if offset < 0 { + return encode_response(OP_PWRITE, req.flags, req.request_id, EINVAL, &[]); + } + if data.len() > MAX_DATA_BYTES as usize { + return encode_response(OP_PWRITE, req.flags, req.request_id, EMSGSIZE, &[]); + } + match store.pwrite(fd, pid, offset as u64, data) { + Ok(n) => encode_response(OP_PWRITE, req.flags, req.request_id, 0, &n.to_le_bytes()), + Err(e) => encode_response(OP_PWRITE, req.flags, req.request_id, map_store_err(e), &[]), + } +} diff --git a/userland/capsule_vfs/src/store/fdtable.rs b/userland/capsule_vfs/src/store/fdtable.rs index 58341ea807..5e7129d9fe 100644 --- a/userland/capsule_vfs/src/store/fdtable.rs +++ b/userland/capsule_vfs/src/store/fdtable.rs @@ -20,6 +20,8 @@ mod mkdir; mod new; mod open; mod query; +mod pread; +mod pwrite; mod read; mod rename; mod seek; diff --git a/userland/capsule_vfs/src/store/fdtable/pread.rs b/userland/capsule_vfs/src/store/fdtable/pread.rs new file mode 100644 index 0000000000..3a5fa070f8 --- /dev/null +++ b/userland/capsule_vfs/src/store/fdtable/pread.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 . + +use alloc::vec::Vec; + +use super::types::{Store, StoreError}; + +impl Store { + pub fn pread(&mut self, fd: u32, owner_pid: u32, offset: u64, max: usize) -> Result, StoreError> { + let file_idx = self.entry(fd, owner_pid)?.file_idx; + let data = &self.files[file_idx].data; + let off = (offset as usize).min(data.len()); + let avail = data.len() - off; + let n = if max < avail { max } else { avail }; + Ok(data[off..off + n].to_vec()) + } +} diff --git a/userland/capsule_vfs/src/store/fdtable/pwrite.rs b/userland/capsule_vfs/src/store/fdtable/pwrite.rs new file mode 100644 index 0000000000..f958697e93 --- /dev/null +++ b/userland/capsule_vfs/src/store/fdtable/pwrite.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 . + +use super::types::{Store, StoreError, MAX_FILE_BYTES}; + +impl Store { + pub fn pwrite(&mut self, fd: u32, owner_pid: u32, offset: u64, bytes: &[u8]) -> Result { + let (file_idx, writable) = { + let e = self.entry(fd, owner_pid)?; + (e.file_idx, e.writable) + }; + if !writable { + return Err(StoreError::AccessDenied); + } + let start = offset as usize; + let end = start.saturating_add(bytes.len()); + if end > MAX_FILE_BYTES { + return Err(StoreError::Full); + } + let data = &mut self.files[file_idx].data; + if end > data.len() { + data.resize(end, 0); + } + data[start..end].copy_from_slice(bytes); + Ok(bytes.len() as u32) + } +} From 5d785bb4b5039c53b24283f70f35d433a59ab9c7 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 00:32:09 +0600 Subject: [PATCH 45/72] feat(relibc): nonos lseek/pread/pwrite over vfs IPC De-stubs the three positional-IO Pal methods in the nonos relibc backend. The capsule_vfs server already landed OP_SEEK=11, OP_PREAD=12, OP_PWRITE=13 over the NOVF protocol; without the matching client side, C code that calls lseek/pread/pwrite would get ENOSYS, breaking fseek and any positional read. Wire format follows the confirmed server layout: - lseek: payload [pid:4][vfs_fd:4][whence:2][reserved:2][offset:8]; response body = u64 new_pos - pread: payload [pid:4][vfs_fd:4][offset:8][max:4]; response body = raw data - pwrite: payload [pid:4][vfs_fd:4][offset:8][data...]; response body = u32 written ftruncate stays ENOSYS (no server op, not needed for the gate). --- toolchain/nonos-relibc/platform/nonos/fs.rs | 3 ++ toolchain/nonos-relibc/platform/nonos/mod.rs | 47 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/fs.rs b/toolchain/nonos-relibc/platform/nonos/fs.rs index 06d0412542..924a300adb 100644 --- a/toolchain/nonos-relibc/platform/nonos/fs.rs +++ b/toolchain/nonos-relibc/platform/nonos/fs.rs @@ -15,6 +15,9 @@ pub const OP_STAT: u16 = 5; pub const OP_MKDIR: u16 = 8; pub const OP_UNLINK: u16 = 9; pub const OP_RENAME: u16 = 10; +pub const OP_SEEK: u16 = 11; +pub const OP_PREAD: u16 = 12; +pub const OP_PWRITE: u16 = 13; pub const VFS_O_CREATE: u32 = 1 << 0; pub const VFS_O_TRUNC: u32 = 1 << 1; pub const VFS_O_APPEND: u32 = 1 << 2; diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 0b97cede77..718b817886 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -164,7 +164,20 @@ impl Pal for Sys { fn dir_seek(_fd: c_int, _opaque_offset: u64) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn dent_reclen_offset(_this_dent: &[u8], _offset: usize) -> Option<(u16, u64)> { None } fn linkat(_fd1: c_int, _oldpath: CStr, _fd2: c_int, _newpath: CStr, _flags: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - fn lseek(_fildes: c_int, _offset: off_t, _whence: c_int) -> Result { Err(Errno(ENOSYS)) } + fn lseek(fildes: c_int, offset: off_t, whence: c_int) -> Result { + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + let pid = Self::getpid() as u32; + let mut payload = [0u8; 20]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + payload[8..10].copy_from_slice(&(whence as u16).to_le_bytes()); + payload[12..20].copy_from_slice(&offset.to_le_bytes()); + let mut resp = [0u8; 32]; + let (status, data_len) = fs::vfs_call(fs::OP_SEEK, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + if data_len < 8 { return Err(Errno(EIO)); } + Ok(i64::from_le_bytes([resp[24], resp[25], resp[26], resp[27], resp[28], resp[29], resp[30], resp[31]]) as off_t) + } fn mkdirat(_fildes: c_int, path: CStr, _mode: mode_t) -> Result<()> { let pb = path.to_bytes(); if pb.is_empty() || pb.len() > 255 { return Err(Errno(EINVAL)); } @@ -216,8 +229,36 @@ impl Pal for Sys { buf[..n].copy_from_slice(&resp[24..24 + n]); Ok(n) } - fn pread(_fildes: c_int, _buf: &mut [u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } - fn pwrite(_fildes: c_int, _buf: &[u8], _offset: off_t) -> Result { Err(Errno(ENOSYS)) } + fn pread(fildes: c_int, buf: &mut [u8], offset: off_t) -> Result { + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + let count = (buf.len() as u32).min(65536); + let pid = Self::getpid() as u32; + let mut payload = [0u8; 20]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + payload[8..16].copy_from_slice(&(offset as i64).to_le_bytes()); + payload[16..20].copy_from_slice(&count.to_le_bytes()); + let mut resp = alloc::vec![0u8; 24 + count as usize]; + let (status, n) = fs::vfs_call(fs::OP_PREAD, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + buf[..n].copy_from_slice(&resp[24..24 + n]); + Ok(n) + } + fn pwrite(fildes: c_int, buf: &[u8], offset: off_t) -> Result { + let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; + if buf.len() > 65536 { return Err(Errno(EINVAL)); } + let pid = Self::getpid() as u32; + let mut payload = alloc::vec![0u8; 16 + buf.len()]; + payload[0..4].copy_from_slice(&pid.to_le_bytes()); + payload[4..8].copy_from_slice(&vfs_fd.to_le_bytes()); + payload[8..16].copy_from_slice(&(offset as i64).to_le_bytes()); + payload[16..].copy_from_slice(buf); + let mut resp = [0u8; 28]; + let (status, data_len) = fs::vfs_call(fs::OP_PWRITE, &payload, &mut resp)?; + if status < 0 { return Err(Errno(-status)); } + if data_len < 4 { return Err(Errno(EIO)); } + Ok(u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]) as usize) + } fn readlinkat(_dirfd: c_int, _pathname: CStr, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } fn renameat2(_old_dir: c_int, old_path: CStr, _new_dir: c_int, new_path: CStr, _flags: c_uint) -> Result<()> { let ob = old_path.to_bytes(); From 9ef4f34e4d26537bfa639773a39ecf0599db5e9d Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 00:55:36 +0600 Subject: [PATCH 46/72] test(relibc): exercise lseek/pread/pwrite in the relibc-test gate Extends the relibc-test boot exerciser with a raw open/lseek/pread/pwrite round-trip over the vfs NOVF service, validating Phase 2 Deliverable A (VFS OP_SEEK/OP_PREAD/OP_PWRITE + the matching relibc clients) end-to-end. Asserts the POSIX positional-IO invariant the new ops exist to provide: lseek SET/CUR/END move the fd offset, while pread/pwrite at an explicit offset leave it untouched (proven by the interleaved read that must still see the byte at the lseek-advanced position). Gate: NONOS_DEV=1 nonos-mk-relibc-smoke-test -> [RELIBC-TEST] PASS. --- userland/capsule_relibc_test/src/main.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/userland/capsule_relibc_test/src/main.c b/userland/capsule_relibc_test/src/main.c index a89fbf0845..a5c7759eaf 100644 --- a/userland/capsule_relibc_test/src/main.c +++ b/userland/capsule_relibc_test/src/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #define EMIT(s) write(1, s, sizeof(s) - 1) @@ -32,6 +33,28 @@ int main(void) { fclose(f); if (n != 5 || buf[0] != 'h' || buf[4] != 'o') { EMIT("[RELIBC-TEST] FAIL fread\n"); return 1; } + int fd = open("/relibctest.txt", O_RDONLY); + if (fd < 0) { EMIT("[RELIBC-TEST] FAIL open\n"); return 1; } + char c; + if (lseek(fd, 1, SEEK_SET) != 1) { EMIT("[RELIBC-TEST] FAIL lseek-set\n"); return 1; } + if (read(fd, &c, 1) != 1 || c != 'e') { EMIT("[RELIBC-TEST] FAIL read-set\n"); return 1; } + if (lseek(fd, 1, SEEK_CUR) != 3) { EMIT("[RELIBC-TEST] FAIL lseek-cur\n"); return 1; } + char pb[5]; + for (int i = 0; i < 5; i++) pb[i] = 0; + if (pread(fd, pb, 5, 0) != 5 || pb[0] != 'h' || pb[4] != 'o') { EMIT("[RELIBC-TEST] FAIL pread\n"); return 1; } + if (read(fd, &c, 1) != 1 || c != 'l') { EMIT("[RELIBC-TEST] FAIL pread-pos\n"); return 1; } + if (lseek(fd, 0, SEEK_END) != 5) { EMIT("[RELIBC-TEST] FAIL lseek-end\n"); return 1; } + close(fd); + + fd = open("/relibctest.txt", O_RDWR); + if (fd < 0) { EMIT("[RELIBC-TEST] FAIL open-rdwr\n"); return 1; } + if (pwrite(fd, "XY", 2, 1) != 2) { EMIT("[RELIBC-TEST] FAIL pwrite\n"); return 1; } + char vb[5]; + for (int i = 0; i < 5; i++) vb[i] = 0; + if (pread(fd, vb, 5, 0) != 5 || vb[1] != 'X' || vb[2] != 'Y') { EMIT("[RELIBC-TEST] FAIL pwrite-verify\n"); return 1; } + if (read(fd, &c, 1) != 1 || c != 'h') { EMIT("[RELIBC-TEST] FAIL pwrite-pos\n"); return 1; } + close(fd); + EMIT("[RELIBC-TEST] PASS\n"); return 0; } From 5bde9bb9c1fbe9db7805ae37c88fd394dfa3a3e0 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 00:58:21 +0600 Subject: [PATCH 47/72] fix(relibc): nonos vfs client surfaces server errno before EIO guard write/openat/fstatat checked the response data_len (returning EIO on a short body) before checking the NOVF status field. On a server error the body is empty, so a real errno (EACCES, ENOSPC, ENOENT, ...) was masked as EIO, breaking errno-based error handling for the C callers. Reorder so the status<0 check runs first, matching read/lseek/pread/pwrite. Error-path only; no happy-path behavior change. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 718b817886..7f4d7f4903 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -58,8 +58,8 @@ impl Pal for Sys { payload[8..].copy_from_slice(buf); let mut resp = [0u8; 28]; let (status, data_len) = fs::vfs_call(fs::OP_WRITE, &payload, &mut resp)?; - if data_len < 4 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } + if data_len < 4 { return Err(Errno(EIO)); } Ok(u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]) as usize) } @@ -141,8 +141,8 @@ impl Pal for Sys { payload.extend_from_slice(pb); let mut resp = [0u8; 36]; let (status, data_len) = fs::vfs_call(fs::OP_STAT, &payload, &mut resp)?; - if data_len < 12 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } + if data_len < 12 { return Err(Errno(EIO)); } let size = u64::from_le_bytes([resp[24], resp[25], resp[26], resp[27], resp[28], resp[29], resp[30], resp[31]]); let vfs_flags = u32::from_le_bytes([resp[32], resp[33], resp[34], resp[35]]); @@ -207,8 +207,8 @@ impl Pal for Sys { payload.extend_from_slice(&vfs_flags.to_le_bytes()); let mut resp = [0u8; 28]; let (status, data_len) = fs::vfs_call(fs::OP_OPEN, &payload, &mut resp)?; - if data_len < 4 { return Err(Errno(EIO)); } if status < 0 { return Err(Errno(-status)); } + if data_len < 4 { return Err(Errno(EIO)); } let vfs_fd = u32::from_le_bytes([resp[24], resp[25], resp[26], resp[27]]); fs::fd_alloc(vfs_fd).ok_or(Errno(EMFILE)) } From 8de07f7426a0f05bb48ed27046c11249a919f80a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 01:24:07 +0600 Subject: [PATCH 48/72] feat(net-sockets): O_NONBLOCK flag ops (SETFLAGS/GETFLAGS) Sockets need a per-handle nonblock bit before connect/recv/send can return EAGAIN instead of blocking. Adds OP_SETFLAGS(13)/OP_GETFLAGS(14) to read/write FLAG_NONBLOCK on each socket; the bit is stored as a bool on Socket (which stays Copy) and initialised to false in Socket::new. OP_12 is intentionally left free for the upcoming OP_POLL handler. --- .../capsule_net_sockets/src/protocol/mod.rs | 4 +- .../capsule_net_sockets/src/protocol/ops.rs | 3 ++ .../src/server/handlers/dispatch.rs | 5 ++- .../src/server/handlers/getflags.rs | 41 ++++++++++++++++++ .../src/server/handlers/mod.rs | 2 + .../src/server/handlers/setflags.rs | 43 +++++++++++++++++++ .../src/sockets/table/open.rs | 1 + .../src/sockets/table/types.rs | 1 + 8 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 userland/capsule_net_sockets/src/server/handlers/getflags.rs create mode 100644 userland/capsule_net_sockets/src/server/handlers/setflags.rs diff --git a/userland/capsule_net_sockets/src/protocol/mod.rs b/userland/capsule_net_sockets/src/protocol/mod.rs index cdf25ff374..a413c321ec 100644 --- a/userland/capsule_net_sockets/src/protocol/mod.rs +++ b/userland/capsule_net_sockets/src/protocol/mod.rs @@ -24,6 +24,6 @@ pub use errno::{ }; pub use header::MAGIC; pub use ops::{ - OP_ACCEPT, OP_BIND, OP_CLOSE, OP_CONNECT, OP_GETSOCKOPT, OP_HEALTHCHECK, OP_LISTEN, OP_RECV, - OP_SEND, OP_SETSOCKOPT, OP_SOCKET, + FLAG_NONBLOCK, OP_ACCEPT, OP_BIND, OP_CLOSE, OP_CONNECT, OP_GETFLAGS, OP_GETSOCKOPT, + OP_HEALTHCHECK, OP_LISTEN, OP_RECV, OP_SEND, OP_SETFLAGS, OP_SETSOCKOPT, OP_SOCKET, }; diff --git a/userland/capsule_net_sockets/src/protocol/ops.rs b/userland/capsule_net_sockets/src/protocol/ops.rs index 1e683c4d4f..21e5b80b97 100644 --- a/userland/capsule_net_sockets/src/protocol/ops.rs +++ b/userland/capsule_net_sockets/src/protocol/ops.rs @@ -25,3 +25,6 @@ pub const OP_RECV: u16 = 8; pub const OP_CLOSE: u16 = 9; pub const OP_GETSOCKOPT: u16 = 10; pub const OP_SETSOCKOPT: u16 = 11; +pub const OP_SETFLAGS: u16 = 13; +pub const OP_GETFLAGS: u16 = 14; +pub const FLAG_NONBLOCK: u32 = 1; diff --git a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs index d4b2e87a91..c7c5428d2f 100644 --- a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs +++ b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs @@ -15,7 +15,8 @@ // along with this program. If not, see . use super::{ - accept, bind, close, connect, getsockopt, health, listen, recv, send, setsockopt, socket, + accept, bind, close, connect, getflags, getsockopt, health, listen, recv, send, setflags, + setsockopt, socket, }; use crate::protocol::*; use crate::server::parse_req::Request; @@ -33,6 +34,8 @@ pub fn dispatch(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) -> bool { OP_CLOSE => close::handle(pid, req, body, tx), OP_GETSOCKOPT => getsockopt::handle(pid, req, body, tx), OP_SETSOCKOPT => setsockopt::handle(pid, req, body, tx), + OP_SETFLAGS => setflags::handle(pid, req, body, tx), + OP_GETFLAGS => getflags::handle(pid, req, body, tx), _ => return false, } true diff --git a/userland/capsule_net_sockets/src/server/handlers/getflags.rs b/userland/capsule_net_sockets/src/server/handlers/getflags.rs new file mode 100644 index 0000000000..6571257ddc --- /dev/null +++ b/userland/capsule_net_sockets/src/server/handlers/getflags.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 . + +use crate::protocol::{E_NO_HANDLE, E_OK, FLAG_NONBLOCK, OP_GETFLAGS}; +use crate::server::handlers::io::u32_at; +use crate::server::parse_req::Request; +use crate::server::respond::respond; +use crate::sockets::{SocketKey, SOCKETS}; + +pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { + let handle = match u32_at(body, 0) { + Ok(h) => h, + Err(e) => { + let _ = respond(pid, OP_GETFLAGS, e, req.request_id, 0, tx); + return; + } + }; + let key = SocketKey { pid, handle }; + match SOCKETS.with(key, |s| if s.nonblock { FLAG_NONBLOCK } else { 0 }) { + Some(flags) => { + tx[20..24].copy_from_slice(&flags.to_le_bytes()); + let _ = respond(pid, OP_GETFLAGS, E_OK, req.request_id, 4, tx); + } + None => { + let _ = respond(pid, OP_GETFLAGS, E_NO_HANDLE, req.request_id, 0, tx); + } + } +} diff --git a/userland/capsule_net_sockets/src/server/handlers/mod.rs b/userland/capsule_net_sockets/src/server/handlers/mod.rs index e8656dd545..a154ae24a9 100644 --- a/userland/capsule_net_sockets/src/server/handlers/mod.rs +++ b/userland/capsule_net_sockets/src/server/handlers/mod.rs @@ -19,12 +19,14 @@ mod bind; mod close; mod connect; mod dispatch; +mod getflags; mod getsockopt; mod health; mod io; mod listen; mod recv; mod send; +mod setflags; mod setsockopt; mod socket; diff --git a/userland/capsule_net_sockets/src/server/handlers/setflags.rs b/userland/capsule_net_sockets/src/server/handlers/setflags.rs new file mode 100644 index 0000000000..915acc48a8 --- /dev/null +++ b/userland/capsule_net_sockets/src/server/handlers/setflags.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 . + +use crate::protocol::{E_NO_HANDLE, E_OK, FLAG_NONBLOCK, OP_SETFLAGS}; +use crate::server::handlers::io::u32_at; +use crate::server::parse_req::Request; +use crate::server::respond::respond; +use crate::sockets::{SocketKey, SOCKETS}; + +pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { + let handle = match u32_at(body, 0) { + Ok(h) => h, + Err(e) => { + let _ = respond(pid, OP_SETFLAGS, e, req.request_id, 0, tx); + return; + } + }; + let flags = match u32_at(body, 4) { + Ok(f) => f, + Err(e) => { + let _ = respond(pid, OP_SETFLAGS, e, req.request_id, 0, tx); + return; + } + }; + let key = SocketKey { pid, handle }; + let errno = SOCKETS + .with(key, |s| s.nonblock = (flags & FLAG_NONBLOCK) != 0) + .map_or(E_NO_HANDLE, |_| E_OK); + let _ = respond(pid, OP_SETFLAGS, errno, req.request_id, 0, tx); +} diff --git a/userland/capsule_net_sockets/src/sockets/table/open.rs b/userland/capsule_net_sockets/src/sockets/table/open.rs index 39f17d95a0..0b843a585d 100644 --- a/userland/capsule_net_sockets/src/sockets/table/open.rs +++ b/userland/capsule_net_sockets/src/sockets/table/open.rs @@ -45,6 +45,7 @@ impl Socket { transport_handle: 0, bound: false, listening: false, + nonblock: false, } } } diff --git a/userland/capsule_net_sockets/src/sockets/table/types.rs b/userland/capsule_net_sockets/src/sockets/table/types.rs index 54edac00f5..37d7c8e65d 100644 --- a/userland/capsule_net_sockets/src/sockets/table/types.rs +++ b/userland/capsule_net_sockets/src/sockets/table/types.rs @@ -30,6 +30,7 @@ pub struct Socket { pub transport_handle: u32, pub bound: bool, pub listening: bool, + pub nonblock: bool, } pub struct Table { From 60bc2931b94d07462857f65caf3c799e7f990874 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 01:32:09 +0600 Subject: [PATCH 49/72] feat(net-sockets): surface rx-empty as E_WOULD_BLOCK for non-blocking recv recv_socket previously discarded the transport errno via map_err(|_| E_NO_TRANSPORT), masking net.tcp E_RX_EMPTY(11) and net.udp E_RX_EMPTY(8) as the generic E_NO_TRANSPORT(6). The relibc PAL (Phase 2B.5) needs a distinct errno to translate to POSIX EAGAIN; this adds E_WOULD_BLOCK=11 (free slot in net.sockets errno space) and routes empty-rx transport errors to it via a per-transport map_recv_err helper. Non-empty transport errors still surface as E_NO_TRANSPORT. The Mixnet arm is unchanged (out of scope). The server returns E_WOULD_BLOCK unconditionally on empty; blocking-vs-retry is the relibc client's responsibility (2B.5). --- userland/capsule_net_sockets/src/clients/tcp.rs | 2 ++ userland/capsule_net_sockets/src/clients/udp.rs | 2 ++ userland/capsule_net_sockets/src/protocol/errno.rs | 1 + userland/capsule_net_sockets/src/protocol/mod.rs | 2 +- .../capsule_net_sockets/src/server/handlers/recv.rs | 12 +++++++++--- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/userland/capsule_net_sockets/src/clients/tcp.rs b/userland/capsule_net_sockets/src/clients/tcp.rs index 3cd7bfc048..83d6fff822 100644 --- a/userland/capsule_net_sockets/src/clients/tcp.rs +++ b/userland/capsule_net_sockets/src/clients/tcp.rs @@ -18,6 +18,8 @@ use alloc::vec; use super::envelope::call; +pub const RX_EMPTY: u16 = 11; + const MAGIC: u32 = 0x4E54_4350; const LISTEN: u16 = 2; const CONNECT: u16 = 3; diff --git a/userland/capsule_net_sockets/src/clients/udp.rs b/userland/capsule_net_sockets/src/clients/udp.rs index f9a72cf239..a9a63d5188 100644 --- a/userland/capsule_net_sockets/src/clients/udp.rs +++ b/userland/capsule_net_sockets/src/clients/udp.rs @@ -18,6 +18,8 @@ use alloc::vec; use super::envelope::call; +pub const RX_EMPTY: u16 = 8; + const MAGIC: u32 = 0x4E55_4450; const BIND: u16 = 2; const UNBIND: u16 = 3; diff --git a/userland/capsule_net_sockets/src/protocol/errno.rs b/userland/capsule_net_sockets/src/protocol/errno.rs index 249aaf907a..359ef70a90 100644 --- a/userland/capsule_net_sockets/src/protocol/errno.rs +++ b/userland/capsule_net_sockets/src/protocol/errno.rs @@ -25,4 +25,5 @@ pub const E_TABLE_FULL: u16 = 7; pub const E_BAD_FAMILY: u16 = 8; pub const E_BAD_KIND: u16 = 9; pub const E_NOT_BOUND: u16 = 10; +pub const E_WOULD_BLOCK: u16 = 11; pub const E_NOT_CONNECTED: u16 = 12; diff --git a/userland/capsule_net_sockets/src/protocol/mod.rs b/userland/capsule_net_sockets/src/protocol/mod.rs index a413c321ec..9fb64db8f8 100644 --- a/userland/capsule_net_sockets/src/protocol/mod.rs +++ b/userland/capsule_net_sockets/src/protocol/mod.rs @@ -20,7 +20,7 @@ mod ops; pub use errno::{ E_BAD_FAMILY, E_BAD_KIND, E_BAD_LEN, E_BAD_MAGIC, E_BAD_OP, E_BAD_VERSION, E_NOT_BOUND, - E_NOT_CONNECTED, E_NO_HANDLE, E_NO_TRANSPORT, E_OK, E_TABLE_FULL, + E_NOT_CONNECTED, E_NO_HANDLE, E_NO_TRANSPORT, E_OK, E_TABLE_FULL, E_WOULD_BLOCK, }; pub use header::MAGIC; pub use ops::{ diff --git a/userland/capsule_net_sockets/src/server/handlers/recv.rs b/userland/capsule_net_sockets/src/server/handlers/recv.rs index 284fdf9aa6..049c644c9b 100644 --- a/userland/capsule_net_sockets/src/server/handlers/recv.rs +++ b/userland/capsule_net_sockets/src/server/handlers/recv.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . use crate::clients::{nym, tcp, udp}; -use crate::protocol::{E_NOT_CONNECTED, E_NO_HANDLE, E_NO_TRANSPORT, E_OK, OP_RECV}; +use crate::protocol::{E_NOT_CONNECTED, E_NO_HANDLE, E_NO_TRANSPORT, E_OK, E_WOULD_BLOCK, OP_RECV}; use crate::server::handlers::io::u32_at; use crate::server::parse_req::Request; use crate::server::respond::respond; @@ -39,14 +39,20 @@ pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { } } +fn map_recv_err(e: u16, empty: u16) -> u16 { + if e == empty { E_WOULD_BLOCK } else { E_NO_TRANSPORT } +} + fn recv_socket(sock: Socket, out: &mut [u8]) -> Result { match sock.kind { Kind::Stream if sock.transport_handle != 0 => { - tcp::recv(state::tcp(), sock.transport_handle, out).map_err(|_| E_NO_TRANSPORT) + tcp::recv(state::tcp(), sock.transport_handle, out) + .map_err(|e| map_recv_err(e, tcp::RX_EMPTY)) } Kind::Datagram => { let Some(local) = sock.local else { return Err(E_NOT_CONNECTED) }; - udp::recv(state::udp(), local.port, out).map_err(|_| E_NO_TRANSPORT) + udp::recv(state::udp(), local.port, out) + .map_err(|e| map_recv_err(e, udp::RX_EMPTY)) } Kind::Mixnet if sock.transport_handle != 0 => { nym::recv(state::nym(), sock.transport_handle, out).map_err(|_| E_NO_TRANSPORT) From 633ab60c1938521f2977a0d95f8390491eecd152 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 01:37:29 +0600 Subject: [PATCH 50/72] feat(net-sockets): OP_SETTIMEOUT + server-side timed recv (SO_RCVTIMEO) Adds a per-socket receive timeout so the server bounds the wait instead of returning E_WOULD_BLOCK immediately on an empty socket. When timeout_ms is set via OP_SETTIMEOUT the recv handler retries up to the deadline using mk_yield before giving up; timeout_ms == 0 preserves the existing single-attempt behaviour. --- .../capsule_net_sockets/src/protocol/mod.rs | 3 +- .../capsule_net_sockets/src/protocol/ops.rs | 1 + .../src/server/handlers/dispatch.rs | 3 +- .../src/server/handlers/mod.rs | 1 + .../src/server/handlers/recv.rs | 17 ++++++-- .../src/server/handlers/settimeout.rs | 42 +++++++++++++++++++ .../src/sockets/table/open.rs | 1 + .../src/sockets/table/types.rs | 1 + 8 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 userland/capsule_net_sockets/src/server/handlers/settimeout.rs diff --git a/userland/capsule_net_sockets/src/protocol/mod.rs b/userland/capsule_net_sockets/src/protocol/mod.rs index 9fb64db8f8..716b5a82e2 100644 --- a/userland/capsule_net_sockets/src/protocol/mod.rs +++ b/userland/capsule_net_sockets/src/protocol/mod.rs @@ -25,5 +25,6 @@ pub use errno::{ pub use header::MAGIC; pub use ops::{ FLAG_NONBLOCK, OP_ACCEPT, OP_BIND, OP_CLOSE, OP_CONNECT, OP_GETFLAGS, OP_GETSOCKOPT, - OP_HEALTHCHECK, OP_LISTEN, OP_RECV, OP_SEND, OP_SETFLAGS, OP_SETSOCKOPT, OP_SOCKET, + OP_HEALTHCHECK, OP_LISTEN, OP_RECV, OP_SEND, OP_SETFLAGS, OP_SETSOCKOPT, OP_SETTIMEOUT, + OP_SOCKET, }; diff --git a/userland/capsule_net_sockets/src/protocol/ops.rs b/userland/capsule_net_sockets/src/protocol/ops.rs index 21e5b80b97..e29581ae5b 100644 --- a/userland/capsule_net_sockets/src/protocol/ops.rs +++ b/userland/capsule_net_sockets/src/protocol/ops.rs @@ -27,4 +27,5 @@ pub const OP_GETSOCKOPT: u16 = 10; pub const OP_SETSOCKOPT: u16 = 11; pub const OP_SETFLAGS: u16 = 13; pub const OP_GETFLAGS: u16 = 14; +pub const OP_SETTIMEOUT: u16 = 15; pub const FLAG_NONBLOCK: u32 = 1; diff --git a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs index c7c5428d2f..63444a45ba 100644 --- a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs +++ b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs @@ -16,7 +16,7 @@ use super::{ accept, bind, close, connect, getflags, getsockopt, health, listen, recv, send, setflags, - setsockopt, socket, + setsockopt, settimeout, socket, }; use crate::protocol::*; use crate::server::parse_req::Request; @@ -36,6 +36,7 @@ pub fn dispatch(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) -> bool { OP_SETSOCKOPT => setsockopt::handle(pid, req, body, tx), OP_SETFLAGS => setflags::handle(pid, req, body, tx), OP_GETFLAGS => getflags::handle(pid, req, body, tx), + OP_SETTIMEOUT => settimeout::handle(pid, req, body, tx), _ => return false, } true diff --git a/userland/capsule_net_sockets/src/server/handlers/mod.rs b/userland/capsule_net_sockets/src/server/handlers/mod.rs index a154ae24a9..946ec43f9c 100644 --- a/userland/capsule_net_sockets/src/server/handlers/mod.rs +++ b/userland/capsule_net_sockets/src/server/handlers/mod.rs @@ -28,6 +28,7 @@ mod recv; mod send; mod setflags; mod setsockopt; +mod settimeout; mod socket; pub use dispatch::dispatch; diff --git a/userland/capsule_net_sockets/src/server/handlers/recv.rs b/userland/capsule_net_sockets/src/server/handlers/recv.rs index 049c644c9b..0389b44983 100644 --- a/userland/capsule_net_sockets/src/server/handlers/recv.rs +++ b/userland/capsule_net_sockets/src/server/handlers/recv.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use nonos_libc::{mk_time_millis, mk_yield}; + use crate::clients::{nym, tcp, udp}; use crate::protocol::{E_NOT_CONNECTED, E_NO_HANDLE, E_NO_TRANSPORT, E_OK, E_WOULD_BLOCK, OP_RECV}; use crate::server::handlers::io::u32_at; @@ -31,11 +33,18 @@ pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { let Some(sock) = SOCKETS.with(key, |s| *s) else { return status(pid, req, E_NO_HANDLE, tx); }; - match recv_socket(sock, &mut tx[20..]) { - Ok(n) => { - respond(pid, OP_RECV, E_OK, req.request_id, n as u32, tx); + let deadline = mk_time_millis().saturating_add(sock.timeout_ms as i64); + loop { + match recv_socket(sock, &mut tx[20..]) { + Ok(n) => { + let _ = respond(pid, OP_RECV, E_OK, req.request_id, n as u32, tx); + return; + } + Err(E_WOULD_BLOCK) if sock.timeout_ms > 0 && mk_time_millis() < deadline => { + mk_yield(); + } + Err(e) => return status(pid, req, e, tx), } - Err(e) => status(pid, req, e, tx), } } diff --git a/userland/capsule_net_sockets/src/server/handlers/settimeout.rs b/userland/capsule_net_sockets/src/server/handlers/settimeout.rs new file mode 100644 index 0000000000..1a0395c9a8 --- /dev/null +++ b/userland/capsule_net_sockets/src/server/handlers/settimeout.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 . + +use crate::protocol::{E_NO_HANDLE, E_OK, OP_SETTIMEOUT}; +use crate::server::handlers::io::u32_at; +use crate::server::parse_req::Request; +use crate::server::respond::respond; +use crate::sockets::{SocketKey, SOCKETS}; + +pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { + let handle = match u32_at(body, 0) { + Ok(h) => h, + Err(e) => { + let _ = respond(pid, OP_SETTIMEOUT, e, req.request_id, 0, tx); + return; + } + }; + let ms = match u32_at(body, 4) { + Ok(v) => v, + Err(e) => { + let _ = respond(pid, OP_SETTIMEOUT, e, req.request_id, 0, tx); + return; + } + }; + let errno = SOCKETS + .with(SocketKey { pid, handle }, |s| s.timeout_ms = ms) + .map_or(E_NO_HANDLE, |_| E_OK); + let _ = respond(pid, OP_SETTIMEOUT, errno, req.request_id, 0, tx); +} diff --git a/userland/capsule_net_sockets/src/sockets/table/open.rs b/userland/capsule_net_sockets/src/sockets/table/open.rs index 0b843a585d..1bbe9169c2 100644 --- a/userland/capsule_net_sockets/src/sockets/table/open.rs +++ b/userland/capsule_net_sockets/src/sockets/table/open.rs @@ -46,6 +46,7 @@ impl Socket { bound: false, listening: false, nonblock: false, + timeout_ms: 0, } } } diff --git a/userland/capsule_net_sockets/src/sockets/table/types.rs b/userland/capsule_net_sockets/src/sockets/table/types.rs index 37d7c8e65d..686c5e9d06 100644 --- a/userland/capsule_net_sockets/src/sockets/table/types.rs +++ b/userland/capsule_net_sockets/src/sockets/table/types.rs @@ -31,6 +31,7 @@ pub struct Socket { pub bound: bool, pub listening: bool, pub nonblock: bool, + pub timeout_ms: u32, } pub struct Table { From 8f08d0bc26c4fc44230d9afe319282d8d38132a4 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 01:41:26 +0600 Subject: [PATCH 51/72] feat(net-sockets): per-handle rx stash drained by recv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OP_POLL (Task 2B.3b) will probe the transport and buffer any bytes it pulls so a later recv can return them without re-entering the deadline loop. This task adds that buffer: a static BTreeMap keyed by (pid,handle) storing a Vec per socket, plus a take() drain that recv calls before the deadline loop. The stash cannot live on Socket because Socket is Copy and Vec is not. No put() is added here — that lands in 2B.3b — so until then take() always returns 0 and the drain is inert-but- wired. take() is called by recv, so no dead_code warning fires. --- .../src/server/handlers/recv.rs | 5 +++ .../capsule_net_sockets/src/sockets/mod.rs | 1 + .../capsule_net_sockets/src/sockets/stash.rs | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 userland/capsule_net_sockets/src/sockets/stash.rs diff --git a/userland/capsule_net_sockets/src/server/handlers/recv.rs b/userland/capsule_net_sockets/src/server/handlers/recv.rs index 0389b44983..9533723bca 100644 --- a/userland/capsule_net_sockets/src/server/handlers/recv.rs +++ b/userland/capsule_net_sockets/src/server/handlers/recv.rs @@ -33,6 +33,11 @@ pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { let Some(sock) = SOCKETS.with(key, |s| *s) else { return status(pid, req, E_NO_HANDLE, tx); }; + let buffered = crate::sockets::stash::take(pid, handle, &mut tx[20..]); + if buffered > 0 { + let _ = respond(pid, OP_RECV, E_OK, req.request_id, buffered as u32, tx); + return; + } let deadline = mk_time_millis().saturating_add(sock.timeout_ms as i64); loop { match recv_socket(sock, &mut tx[20..]) { diff --git a/userland/capsule_net_sockets/src/sockets/mod.rs b/userland/capsule_net_sockets/src/sockets/mod.rs index 96553c288b..0605faeb9b 100644 --- a/userland/capsule_net_sockets/src/sockets/mod.rs +++ b/userland/capsule_net_sockets/src/sockets/mod.rs @@ -16,6 +16,7 @@ mod table; mod types; +pub mod stash; pub use table::{Socket, SOCKETS}; pub use types::{Kind, LocalAddr4, RemoteAddr4, SocketKey}; diff --git a/userland/capsule_net_sockets/src/sockets/stash.rs b/userland/capsule_net_sockets/src/sockets/stash.rs new file mode 100644 index 0000000000..ede3f7d428 --- /dev/null +++ b/userland/capsule_net_sockets/src/sockets/stash.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 . + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use spin::Mutex; + +static STASH: Mutex>> = Mutex::new(BTreeMap::new()); + +pub fn take(pid: u32, handle: u32, out: &mut [u8]) -> usize { + let mut g = STASH.lock(); + let Some(buf) = g.get_mut(&(pid, handle)) else { + return 0; + }; + let n = core::cmp::min(out.len(), buf.len()); + out[..n].copy_from_slice(&buf[..n]); + buf.drain(..n); + if buf.is_empty() { + g.remove(&(pid, handle)); + } + n +} From 0735ca3ca710354aec25dddde5eeb66f659346b9 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 01:48:49 +0600 Subject: [PATCH 52/72] feat(net-sockets): OP_POLL readiness via rx-stash probe + TCP state POLLIN buffers a transport probe into the per-(pid,handle) rx stash so a subsequent OP_RECV returns those bytes without hitting the network again. POLLOUT reads net.tcp OP_STATE==Established(3) for Stream sockets; Datagram is always writable. A bounded wait loop via timeout_ms yields until the deadline; timeout_ms==0 returns after the first poll_once. revents is a u16 LE written at tx[20..22] with payload_len=2. OP_POLL=12 fills the reserved slot between OP_SETSOCKOPT(11) and OP_SETFLAGS(13). --- .../capsule_net_sockets/src/clients/tcp.rs | 11 +++ .../capsule_net_sockets/src/protocol/mod.rs | 4 +- .../capsule_net_sockets/src/protocol/ops.rs | 3 + .../src/server/handlers/dispatch.rs | 3 +- .../src/server/handlers/mod.rs | 1 + .../src/server/handlers/poll.rs | 98 +++++++++++++++++++ .../capsule_net_sockets/src/sockets/stash.rs | 11 +++ 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 userland/capsule_net_sockets/src/server/handlers/poll.rs diff --git a/userland/capsule_net_sockets/src/clients/tcp.rs b/userland/capsule_net_sockets/src/clients/tcp.rs index 83d6fff822..e1c34f3d1d 100644 --- a/userland/capsule_net_sockets/src/clients/tcp.rs +++ b/userland/capsule_net_sockets/src/clients/tcp.rs @@ -27,6 +27,9 @@ const ACCEPT: u16 = 4; const SEND: u16 = 5; const RECV: u16 = 6; const CLOSE: u16 = 7; +const STATE: u16 = 9; + +pub const ESTABLISHED: u8 = 3; pub fn listen(port: u32, local: u16) -> Result { call_handle(port, LISTEN, &local.to_le_bytes()) @@ -58,6 +61,14 @@ pub fn close(port: u32, handle: u32) -> Result<(), u16> { call(port, MAGIC, CLOSE, &handle.to_le_bytes(), &mut []).map(|_| ()) } +pub fn state(port: u32, handle: u32) -> Result { + let mut out = [0u8; 1]; + if call(port, MAGIC, STATE, &handle.to_le_bytes(), &mut out)? != 1 { + return Err(4); + } + Ok(out[0]) +} + fn call_handle(port: u32, op: u16, body: &[u8]) -> Result { let mut out = [0u8; 4]; if call(port, MAGIC, op, body, &mut out)? != 4 { diff --git a/userland/capsule_net_sockets/src/protocol/mod.rs b/userland/capsule_net_sockets/src/protocol/mod.rs index 716b5a82e2..57f6c26ce9 100644 --- a/userland/capsule_net_sockets/src/protocol/mod.rs +++ b/userland/capsule_net_sockets/src/protocol/mod.rs @@ -25,6 +25,6 @@ pub use errno::{ pub use header::MAGIC; pub use ops::{ FLAG_NONBLOCK, OP_ACCEPT, OP_BIND, OP_CLOSE, OP_CONNECT, OP_GETFLAGS, OP_GETSOCKOPT, - OP_HEALTHCHECK, OP_LISTEN, OP_RECV, OP_SEND, OP_SETFLAGS, OP_SETSOCKOPT, OP_SETTIMEOUT, - OP_SOCKET, + OP_HEALTHCHECK, OP_LISTEN, OP_POLL, OP_RECV, OP_SEND, OP_SETFLAGS, OP_SETSOCKOPT, + OP_SETTIMEOUT, OP_SOCKET, POLLIN, POLLOUT, }; diff --git a/userland/capsule_net_sockets/src/protocol/ops.rs b/userland/capsule_net_sockets/src/protocol/ops.rs index e29581ae5b..912e71c853 100644 --- a/userland/capsule_net_sockets/src/protocol/ops.rs +++ b/userland/capsule_net_sockets/src/protocol/ops.rs @@ -25,7 +25,10 @@ pub const OP_RECV: u16 = 8; pub const OP_CLOSE: u16 = 9; pub const OP_GETSOCKOPT: u16 = 10; pub const OP_SETSOCKOPT: u16 = 11; +pub const OP_POLL: u16 = 12; pub const OP_SETFLAGS: u16 = 13; pub const OP_GETFLAGS: u16 = 14; pub const OP_SETTIMEOUT: u16 = 15; pub const FLAG_NONBLOCK: u32 = 1; +pub const POLLIN: u16 = 1; +pub const POLLOUT: u16 = 2; diff --git a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs index 63444a45ba..c9bba75430 100644 --- a/userland/capsule_net_sockets/src/server/handlers/dispatch.rs +++ b/userland/capsule_net_sockets/src/server/handlers/dispatch.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . use super::{ - accept, bind, close, connect, getflags, getsockopt, health, listen, recv, send, setflags, + accept, bind, close, connect, getflags, getsockopt, health, listen, poll, recv, send, setflags, setsockopt, settimeout, socket, }; use crate::protocol::*; @@ -34,6 +34,7 @@ pub fn dispatch(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) -> bool { OP_CLOSE => close::handle(pid, req, body, tx), OP_GETSOCKOPT => getsockopt::handle(pid, req, body, tx), OP_SETSOCKOPT => setsockopt::handle(pid, req, body, tx), + OP_POLL => poll::handle(pid, req, body, tx), OP_SETFLAGS => setflags::handle(pid, req, body, tx), OP_GETFLAGS => getflags::handle(pid, req, body, tx), OP_SETTIMEOUT => settimeout::handle(pid, req, body, tx), diff --git a/userland/capsule_net_sockets/src/server/handlers/mod.rs b/userland/capsule_net_sockets/src/server/handlers/mod.rs index 946ec43f9c..5aa8ab7a29 100644 --- a/userland/capsule_net_sockets/src/server/handlers/mod.rs +++ b/userland/capsule_net_sockets/src/server/handlers/mod.rs @@ -24,6 +24,7 @@ mod getsockopt; mod health; mod io; mod listen; +mod poll; mod recv; mod send; mod setflags; diff --git a/userland/capsule_net_sockets/src/server/handlers/poll.rs b/userland/capsule_net_sockets/src/server/handlers/poll.rs new file mode 100644 index 0000000000..15c1eb3deb --- /dev/null +++ b/userland/capsule_net_sockets/src/server/handlers/poll.rs @@ -0,0 +1,98 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use nonos_libc::{mk_time_millis, mk_yield}; + +use crate::clients::{tcp, udp}; +use crate::protocol::{E_NO_HANDLE, E_OK, OP_POLL, POLLIN, POLLOUT}; +use crate::server::handlers::io::{u16_at, u32_at}; +use crate::server::parse_req::Request; +use crate::server::respond::respond; +use crate::sockets::{stash, Kind, Socket, SocketKey, SOCKETS}; +use crate::state; + +pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { + let (handle, events, timeout_ms) = match parse(body) { + Ok(v) => v, + Err(e) => return reply_err(pid, req, e, tx), + }; + let Some(sock) = SOCKETS.with(SocketKey { pid, handle }, |s| *s) else { + return reply_err(pid, req, E_NO_HANDLE, tx); + }; + let deadline = mk_time_millis().saturating_add(timeout_ms as i64); + loop { + let revents = poll_once(pid, handle, sock, events); + if revents != 0 || !(timeout_ms > 0 && mk_time_millis() < deadline) { + tx[20..22].copy_from_slice(&revents.to_le_bytes()); + let _ = respond(pid, OP_POLL, E_OK, req.request_id, 2, tx); + return; + } + mk_yield(); + } +} + +fn parse(body: &[u8]) -> Result<(u32, u16, u32), u16> { + Ok((u32_at(body, 0)?, u16_at(body, 4)?, u32_at(body, 8)?)) +} + +fn poll_once(pid: u32, handle: u32, sock: Socket, events: u16) -> u16 { + let mut revents = 0; + if events & POLLIN != 0 && pollin_ready(pid, handle, sock) { + revents |= POLLIN; + } + if events & POLLOUT != 0 && pollout_ready(sock) { + revents |= POLLOUT; + } + revents +} + +fn pollin_ready(pid: u32, handle: u32, sock: Socket) -> bool { + if stash::has(pid, handle) { + return true; + } + let mut buf = [0u8; 2048]; + let got = match sock.kind { + Kind::Stream if sock.transport_handle != 0 => { + tcp::recv(state::tcp(), sock.transport_handle, &mut buf) + } + Kind::Datagram => match sock.local { + Some(local) => udp::recv(state::udp(), local.port, &mut buf), + None => return false, + }, + _ => return false, + }; + match got { + Ok(n) if n > 0 => { + stash::put(pid, handle, &buf[..n]); + true + } + _ => false, + } +} + +fn pollout_ready(sock: Socket) -> bool { + match sock.kind { + Kind::Stream if sock.transport_handle != 0 => tcp::state(state::tcp(), sock.transport_handle) + .map(|s| s == tcp::ESTABLISHED) + .unwrap_or(false), + Kind::Datagram => true, + _ => false, + } +} + +fn reply_err(pid: u32, req: &Request, errno: u16, tx: &mut [u8]) { + let _ = respond(pid, OP_POLL, errno, req.request_id, 0, tx); +} diff --git a/userland/capsule_net_sockets/src/sockets/stash.rs b/userland/capsule_net_sockets/src/sockets/stash.rs index ede3f7d428..fdccb27d6f 100644 --- a/userland/capsule_net_sockets/src/sockets/stash.rs +++ b/userland/capsule_net_sockets/src/sockets/stash.rs @@ -34,3 +34,14 @@ pub fn take(pid: u32, handle: u32, out: &mut [u8]) -> usize { } n } + +pub fn put(pid: u32, handle: u32, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + STASH.lock().entry((pid, handle)).or_default().extend_from_slice(bytes); +} + +pub fn has(pid: u32, handle: u32) -> bool { + STASH.lock().get(&(pid, handle)).is_some_and(|b| !b.is_empty()) +} From fa55c8479faa7fe152c3f5fa3c92ac5d0edaf3ea Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 05:27:24 +0600 Subject: [PATCH 53/72] feat(relibc): nonos socket runtime + socket()/close over NSKT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greenfield socket backend for Phase 2B. Lays the shared runtime that every later socket method reuses: a 64-slot fd table at the disjoint high range (base 256) so the Phase-1 vfs fd table (fds 3..66) is untouched; an nskt_call IPC helper speaking the NSKT wire protocol (magic 0x4E534B54, endpoint 4460, errno@8-9 in response, body@20) to net.sockets; and a POSIX errno map for the net.sockets-specific error codes. De-stubs socket() (OP_SOCKET=2, AF_INET(2)→wire family 4) and wires close() dispatch in mod.rs to route socket fds through socket::close_fd (OP_CLOSE=9) before the vfs path. --- toolchain/nonos-relibc/apply.sh | 3 +- toolchain/nonos-relibc/platform/nonos/mod.rs | 4 + .../nonos-relibc/platform/nonos/socket.rs | 32 +++++- .../nonos-relibc/platform/nonos/socket_rt.rs | 97 +++++++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 toolchain/nonos-relibc/platform/nonos/socket_rt.rs diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index ade6a46d34..f65b67cddf 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -17,7 +17,8 @@ CC_NONOS="$REPO/toolchain/nonos-c/cc-nonos" mkdir -p "$RELIBC/src/platform/nonos" cp "$HERE/platform/nonos/mod.rs" "$RELIBC/src/platform/nonos/mod.rs" cp "$HERE/platform/nonos/lowlevel.rs" "$RELIBC/src/platform/nonos/lowlevel.rs" -cp "$HERE/platform/nonos/socket.rs" "$RELIBC/src/platform/nonos/socket.rs" +cp "$HERE/platform/nonos/socket.rs" "$RELIBC/src/platform/nonos/socket.rs" +cp "$HERE/platform/nonos/socket_rt.rs" "$RELIBC/src/platform/nonos/socket_rt.rs" cp "$HERE/platform/nonos/signal.rs" "$RELIBC/src/platform/nonos/signal.rs" cp "$HERE/platform/nonos/epoll.rs" "$RELIBC/src/platform/nonos/epoll.rs" cp "$HERE/platform/nonos/ptrace.rs" "$RELIBC/src/platform/nonos/ptrace.rs" diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 7f4d7f4903..c857d7a0a9 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -35,6 +35,7 @@ mod epoll; mod ptrace; mod signal; mod socket; +mod socket_rt; fn now_ms() -> i64 { let r = unsafe { lowlevel::syscall0(lowlevel::MK_TIME_MILLIS) }; @@ -104,6 +105,9 @@ impl Pal for Sys { fn chdir(_path: CStr) -> Result<()> { Err(Errno(ENOSYS)) } fn close(fildes: c_int) -> Result<()> { if fildes <= 2 { return Ok(()); } + if socket_rt::is_socket_fd(fildes) { + return socket::close_fd(fildes); + } let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; let pid = Self::getpid() as u32; let mut payload = [0u8; 8]; diff --git a/toolchain/nonos-relibc/platform/nonos/socket.rs b/toolchain/nonos-relibc/platform/nonos/socket.rs index 9532a61ccb..cd71313910 100644 --- a/toolchain/nonos-relibc/platform/nonos/socket.rs +++ b/toolchain/nonos-relibc/platform/nonos/socket.rs @@ -5,7 +5,7 @@ use super::{ use crate::{ error::{Errno, Result}, header::{ - errno::ENOSYS, + errno::{EAFNOSUPPORT, EBADF, EINVAL, EMFILE, ENOSYS}, sys_socket::{msghdr, sockaddr, socklen_t}, }, }; @@ -24,6 +24,34 @@ impl PalSocket for Sys { unsafe fn sendto(_socket: c_int, _buf: *const c_void, _len: size_t, _flags: c_int, _addr: *const sockaddr, _alen: socklen_t) -> Result { Err(Errno(ENOSYS)) } unsafe fn setsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *const c_void, _len: socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } fn shutdown(_socket: c_int, _how: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn socket(_domain: c_int, _kind: c_int, _protocol: c_int) -> Result { Err(Errno(ENOSYS)) } + unsafe fn socket(domain: c_int, kind: c_int, _protocol: c_int) -> Result { + use super::socket_rt as rt; + if domain != 2 { return Err(Errno(EAFNOSUPPORT)); } + let wire_kind = match kind { + 1 => rt::KIND_STREAM, + 2 => rt::KIND_DGRAM, + _ => return Err(Errno(EINVAL)), + }; + let mut payload = [0u8; 4]; + payload[0..2].copy_from_slice(&rt::FAMILY_INET.to_le_bytes()); + payload[2..4].copy_from_slice(&wire_kind.to_le_bytes()); + let mut resp = [0u8; 24]; + let (errno, n) = rt::nskt_call(rt::OP_SOCKET, &payload, &mut resp)?; + if errno != 0 || n < 4 { return Err(Errno(rt::map_errno(errno))); } + let handle = u32::from_le_bytes([resp[20], resp[21], resp[22], resp[23]]); + rt::alloc(handle).ok_or(Errno(EMFILE)) + } fn socketpair(_domain: c_int, _kind: c_int, _protocol: c_int, _sv: &mut [c_int; 2]) -> Result<()> { Err(Errno(ENOSYS)) } } + +pub fn close_fd(fd: c_int) -> Result<()> { + use super::socket_rt as rt; + let handle = rt::handle_of(fd).ok_or(Errno(EBADF))?; + let mut payload = [0u8; 4]; + payload[0..4].copy_from_slice(&handle.to_le_bytes()); + let mut resp = [0u8; 24]; + let (errno, _) = rt::nskt_call(rt::OP_CLOSE, &payload, &mut resp)?; + rt::free(fd); + if errno != 0 { return Err(Errno(rt::map_errno(errno))); } + Ok(()) +} diff --git a/toolchain/nonos-relibc/platform/nonos/socket_rt.rs b/toolchain/nonos-relibc/platform/nonos/socket_rt.rs new file mode 100644 index 0000000000..420a2d33c8 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/socket_rt.rs @@ -0,0 +1,97 @@ +use alloc::vec::Vec; +use core::cell::SyncUnsafeCell; + +use crate::error::{Errno, Result}; +use crate::header::errno::{EAFNOSUPPORT, EAGAIN, EBADF, ECONNREFUSED, EINVAL, EIO, EMFILE, ENOTCONN}; +use super::super::types::c_int; +use super::lowlevel::{syscall6, MK_IPC_CALL}; + +pub const NSKT_ENDPOINT: u64 = 4460; +pub const NSKT_MAGIC: u32 = 0x4E53_4B54; +pub const HDR_LEN: usize = 20; + +pub const OP_SOCKET: u16 = 2; +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 = 12; +pub const OP_SETFLAGS: u16 = 13; +pub const OP_SETTIMEOUT: u16 = 15; +pub const E_WOULD_BLOCK: u16 = 11; + +pub const FAMILY_INET: u16 = 4; +pub const KIND_STREAM: u16 = 1; +pub const KIND_DGRAM: u16 = 2; + +const SOCK_FD_BASE: c_int = 256; +const SOCK_SLOTS: usize = 64; + +#[derive(Clone, Copy)] +struct SockEntry { handle: u32, nonblock: bool, used: bool } + +static SOCK_TABLE: SyncUnsafeCell<[SockEntry; SOCK_SLOTS]> = + SyncUnsafeCell::new([SockEntry { handle: 0, nonblock: false, used: false }; SOCK_SLOTS]); + +fn table() -> &'static mut [SockEntry; SOCK_SLOTS] { unsafe { &mut *SOCK_TABLE.get() } } + +pub fn is_socket_fd(fd: c_int) -> bool { + fd >= SOCK_FD_BASE && (fd - SOCK_FD_BASE) < SOCK_SLOTS as c_int +} +fn slot(fd: c_int) -> Option { + if is_socket_fd(fd) { Some((fd - SOCK_FD_BASE) as usize) } else { None } +} +pub fn handle_of(fd: c_int) -> Option { + let e = table()[slot(fd)?]; + if e.used { Some(e.handle) } else { None } +} +pub fn alloc(handle: u32) -> Option { + for (i, e) in table().iter_mut().enumerate() { + if !e.used { + *e = SockEntry { handle, nonblock: false, used: true }; + return Some(SOCK_FD_BASE + i as c_int); + } + } + None +} +pub fn free(fd: c_int) { + if let Some(i) = slot(fd) { table()[i].used = false; } +} +pub fn nonblock_of(fd: c_int) -> bool { + slot(fd).map(|i| table()[i].nonblock).unwrap_or(false) +} +pub fn set_nonblock(fd: c_int, on: bool) { + if let Some(i) = slot(fd) { table()[i].nonblock = on; } +} + +pub fn map_errno(e: u16) -> c_int { + match e { + 5 => EBADF, + 6 => ECONNREFUSED, + 7 => EMFILE, + 8 => EAFNOSUPPORT, + 9 => EINVAL, + 11 => EAGAIN, + 12 => ENOTCONN, + _ => EIO, + } +} + +pub fn nskt_call(op: u16, payload: &[u8], resp: &mut [u8]) -> Result<(u16, usize)> { + let mut req = Vec::with_capacity(HDR_LEN + payload.len()); + req.extend_from_slice(&NSKT_MAGIC.to_le_bytes()); + req.extend_from_slice(&1u16.to_le_bytes()); + req.extend_from_slice(&op.to_le_bytes()); + req.extend_from_slice(&[0u8; 4]); + req.extend_from_slice(&1u32.to_le_bytes()); + req.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + req.extend_from_slice(payload); + let ret = unsafe { + syscall6(MK_IPC_CALL, NSKT_ENDPOINT, req.as_ptr() as u64, req.len() as u64, + resp.as_mut_ptr() as u64, resp.len() as u64, 0) + }; + if ret < HDR_LEN as i64 { return Err(Errno(EIO)); } + let errno = u16::from_le_bytes([resp[8], resp[9]]); + let len = u32::from_le_bytes([resp[16], resp[17], resp[18], resp[19]]) as usize; + Ok((errno, len.min(ret as usize - HDR_LEN))) +} From 6d21a574b342dee507ee5974779c612403e32aee Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 05:34:15 +0600 Subject: [PATCH 54/72] feat(relibc): nonos connect() over NSKT (EINPROGRESS for non-blocking) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse sockaddr_in (family@0 host u16, port@2 big-endian→host, ip@4..7 octets), reject null address or address_len < 16 (EINVAL), non-AF_INET family (EAFNOSUPPORT), missing fd table entry (EBADF). Build OP_CONNECT payload: handle@0 LE u32, ip@4 four octets, port@8 LE u16. On success from net.sockets, a non-blocking socket returns EINPROGRESS per POSIX so the caller can poll(POLLOUT) for ESTABLISHED; blocking returns Ok(0). bind/listen/accept and all other PalSocket methods remain ENOSYS — NetSurf needs the client path only (socket→connect→send→recv). --- .../nonos-relibc/platform/nonos/socket.rs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/socket.rs b/toolchain/nonos-relibc/platform/nonos/socket.rs index cd71313910..fcc47514db 100644 --- a/toolchain/nonos-relibc/platform/nonos/socket.rs +++ b/toolchain/nonos-relibc/platform/nonos/socket.rs @@ -5,7 +5,7 @@ use super::{ use crate::{ error::{Errno, Result}, header::{ - errno::{EAFNOSUPPORT, EBADF, EINVAL, EMFILE, ENOSYS}, + errno::{EAFNOSUPPORT, EBADF, EINPROGRESS, EINVAL, EMFILE, ENOSYS}, sys_socket::{msghdr, sockaddr, socklen_t}, }, }; @@ -13,7 +13,33 @@ use crate::{ impl PalSocket for Sys { unsafe fn accept(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result { Err(Errno(ENOSYS)) } unsafe fn bind(_socket: c_int, _address: *const sockaddr, _len: socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn connect(_socket: c_int, _address: *const sockaddr, _len: socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn connect(socket: c_int, address: *const sockaddr, address_len: socklen_t) -> Result { + use super::socket_rt as rt; + if address.is_null() || (address_len as usize) < 16 { + return Err(Errno(EINVAL)); + } + let sa = address as *const u8; + let family = u16::from_ne_bytes([unsafe { *sa }, unsafe { *sa.add(1) }]); + if family != 2 { + return Err(Errno(EAFNOSUPPORT)); + } + let port = u16::from_be_bytes([unsafe { *sa.add(2) }, unsafe { *sa.add(3) }]); + let ip = [unsafe { *sa.add(4) }, unsafe { *sa.add(5) }, unsafe { *sa.add(6) }, unsafe { *sa.add(7) }]; + let handle = rt::handle_of(socket).ok_or(Errno(EBADF))?; + let mut payload = [0u8; 10]; + payload[0..4].copy_from_slice(&handle.to_le_bytes()); + payload[4..8].copy_from_slice(&ip); + payload[8..10].copy_from_slice(&port.to_le_bytes()); + let mut resp = [0u8; 24]; + let (errno, _) = rt::nskt_call(rt::OP_CONNECT, &payload, &mut resp)?; + if errno != 0 { + return Err(Errno(rt::map_errno(errno))); + } + if rt::nonblock_of(socket) { + return Err(Errno(EINPROGRESS)); + } + Ok(0) + } unsafe fn getpeername(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn getsockname(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn getsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *mut c_void, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } From ed4a8a52303f0f503b480d840185c2276df814a8 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 05:40:39 +0600 Subject: [PATCH 55/72] feat(relibc): nonos socket send/recv + read/write dispatch over NSKT Add sock_send/sock_recv as public helpers in socket.rs. sock_send builds OP_SEND payload with handle@0 LE + data@4, capped at 65536 bytes, and treats E_OK as "all bytes accepted". sock_recv sends an OP_RECV request with handle@0 LE and reads the response body from resp[20..20+n], bounding n by min(n, buf.len()) so no overread is possible; E_WOULD_BLOCK surfaces as EAGAIN automatically via map_errno. sendto/recvfrom in the PalSocket impl are de-stubbed to call these helpers after constructing a safe slice from the raw pointer args (unsafe confined to the from_raw_parts/from_raw_parts_mut calls). write/read in mod.rs now check is_socket_fd first: in write after the fildes==1||2 MK_DEBUG block and before fd_vfs; in read before fd_vfs. This lets C send/recv (which route through sendto/recvfrom with a null addr) flow correctly over the NSKT cap. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 6 ++++ .../nonos-relibc/platform/nonos/socket.rs | 35 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index c857d7a0a9..3d2a9cec3e 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -50,6 +50,9 @@ impl Pal for Sys { unsafe { lowlevel::syscall2(lowlevel::MK_DEBUG, buf.as_ptr() as u64, buf.len() as u64); } return Ok(buf.len()); } + if socket_rt::is_socket_fd(fildes) { + return socket::sock_send(fildes, buf); + } let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; if buf.len() > 65536 { return Err(Errno(EINVAL)); } let pid = Self::getpid() as u32; @@ -220,6 +223,9 @@ impl Pal for Sys { fn posix_fallocate(_fd: c_int, _offset: u64, _length: NonZeroU64) -> Result<()> { Err(Errno(ENOSYS)) } fn posix_getdents(_fildes: c_int, _buf: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } fn read(fildes: c_int, buf: &mut [u8]) -> Result { + if socket_rt::is_socket_fd(fildes) { + return socket::sock_recv(fildes, buf); + } let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; let count = (buf.len() as u32).min(65536); let pid = Self::getpid() as u32; diff --git a/toolchain/nonos-relibc/platform/nonos/socket.rs b/toolchain/nonos-relibc/platform/nonos/socket.rs index fcc47514db..5c233876b9 100644 --- a/toolchain/nonos-relibc/platform/nonos/socket.rs +++ b/toolchain/nonos-relibc/platform/nonos/socket.rs @@ -44,10 +44,16 @@ impl PalSocket for Sys { unsafe fn getsockname(_socket: c_int, _address: *mut sockaddr, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn getsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *mut c_void, _len: *mut socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } fn listen(_socket: c_int, _backlog: c_int) -> Result<()> { Err(Errno(ENOSYS)) } - unsafe fn recvfrom(_socket: c_int, _buf: *mut c_void, _len: size_t, _flags: c_int, _addr: *mut sockaddr, _alen: *mut socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn recvfrom(socket: c_int, buf: *mut c_void, len: size_t, _flags: c_int, _addr: *mut sockaddr, _alen: *mut socklen_t) -> Result { + let slice = unsafe { core::slice::from_raw_parts_mut(buf as *mut u8, len) }; + sock_recv(socket, slice) + } unsafe fn recvmsg(_socket: c_int, _msg: *mut msghdr, _flags: c_int) -> Result { Err(Errno(ENOSYS)) } unsafe fn sendmsg(_socket: c_int, _msg: *const msghdr, _flags: c_int) -> Result { Err(Errno(ENOSYS)) } - unsafe fn sendto(_socket: c_int, _buf: *const c_void, _len: size_t, _flags: c_int, _addr: *const sockaddr, _alen: socklen_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn sendto(socket: c_int, buf: *const c_void, len: size_t, _flags: c_int, _addr: *const sockaddr, _alen: socklen_t) -> Result { + let slice = unsafe { core::slice::from_raw_parts(buf as *const u8, len) }; + sock_send(socket, slice) + } unsafe fn setsockopt(_socket: c_int, _level: c_int, _name: c_int, _val: *const c_void, _len: socklen_t) -> Result<()> { Err(Errno(ENOSYS)) } fn shutdown(_socket: c_int, _how: c_int) -> Result<()> { Err(Errno(ENOSYS)) } unsafe fn socket(domain: c_int, kind: c_int, _protocol: c_int) -> Result { @@ -70,6 +76,31 @@ impl PalSocket for Sys { fn socketpair(_domain: c_int, _kind: c_int, _protocol: c_int, _sv: &mut [c_int; 2]) -> Result<()> { Err(Errno(ENOSYS)) } } +pub fn sock_send(fd: c_int, buf: &[u8]) -> Result { + use super::socket_rt as rt; + let handle = rt::handle_of(fd).ok_or(Errno(EBADF))?; + let n = core::cmp::min(buf.len(), 65536); + let mut payload = alloc::vec![0u8; 4 + n]; + payload[0..4].copy_from_slice(&handle.to_le_bytes()); + payload[4..].copy_from_slice(&buf[..n]); + let mut resp = [0u8; 24]; + let (errno, _) = rt::nskt_call(rt::OP_SEND, &payload, &mut resp)?; + if errno != 0 { return Err(Errno(rt::map_errno(errno))); } + Ok(n) +} + +pub fn sock_recv(fd: c_int, buf: &mut [u8]) -> Result { + use super::socket_rt as rt; + let handle = rt::handle_of(fd).ok_or(Errno(EBADF))?; + let payload = handle.to_le_bytes(); + let mut resp = alloc::vec![0u8; 24 + buf.len()]; + let (errno, n) = rt::nskt_call(rt::OP_RECV, &payload, &mut resp)?; + if errno != 0 { return Err(Errno(rt::map_errno(errno))); } + let n = core::cmp::min(n, buf.len()); + buf[..n].copy_from_slice(&resp[20..20 + n]); + Ok(n) +} + pub fn close_fd(fd: c_int) -> Result<()> { use super::socket_rt as rt; let handle = rt::handle_of(fd).ok_or(Errno(EBADF))?; From 747fd29b3445df92d2b5f065b66ff7c63ff6b57a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 05:47:19 +0600 Subject: [PATCH 56/72] feat(relibc): nonos fcntl O_NONBLOCK for sockets over NSKT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F_SETFL sets the per-fd nonblock bit in socket_rt (consumed by connect → EINPROGRESS) and pushes OP_SETFLAGS to net.sockets with handle@0 / flags@4 where FLAG_NONBLOCK=1 (not POSIX 0x800). F_GETFL returns O_RDWR|O_NONBLOCK when the bit is set. Non-socket fds still return ENOSYS via the same fall-through. --- toolchain/nonos-relibc/platform/nonos/mod.rs | 7 +++++- .../nonos-relibc/platform/nonos/socket.rs | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 3d2a9cec3e..7e6dc7cce0 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -161,7 +161,12 @@ impl Pal for Sys { Ok(()) } fn fstatvfs(_fildes: c_int, _buf: Out) -> Result<()> { Err(Errno(ENOSYS)) } - fn fcntl(_fildes: c_int, _cmd: c_int, _arg: c_ulonglong) -> Result { Err(Errno(ENOSYS)) } + fn fcntl(fildes: c_int, cmd: c_int, arg: c_ulonglong) -> Result { + if socket_rt::is_socket_fd(fildes) { + return socket::fcntl_sock(fildes, cmd, arg); + } + Err(Errno(ENOSYS)) + } fn fpath(_fildes: c_int, _out: &mut [u8]) -> Result { Err(Errno(ENOSYS)) } fn fsync(_fildes: c_int) -> Result<()> { Err(Errno(ENOSYS)) } fn ftruncate(_fildes: c_int, _length: off_t) -> Result<()> { Err(Errno(ENOSYS)) } diff --git a/toolchain/nonos-relibc/platform/nonos/socket.rs b/toolchain/nonos-relibc/platform/nonos/socket.rs index 5c233876b9..fe7424ee69 100644 --- a/toolchain/nonos-relibc/platform/nonos/socket.rs +++ b/toolchain/nonos-relibc/platform/nonos/socket.rs @@ -6,6 +6,7 @@ use crate::{ error::{Errno, Result}, header::{ errno::{EAFNOSUPPORT, EBADF, EINPROGRESS, EINVAL, EMFILE, ENOSYS}, + fcntl::{F_GETFL, F_SETFL, O_NONBLOCK, O_RDWR}, sys_socket::{msghdr, sockaddr, socklen_t}, }, }; @@ -112,3 +113,27 @@ pub fn close_fd(fd: c_int) -> Result<()> { if errno != 0 { return Err(Errno(rt::map_errno(errno))); } Ok(()) } + +pub fn fcntl_sock(fd: c_int, cmd: c_int, arg: c_ulonglong) -> Result { + use super::socket_rt as rt; + match cmd { + F_GETFL => { + let mut fl = O_RDWR; + if rt::nonblock_of(fd) { fl |= O_NONBLOCK; } + Ok(fl) + } + F_SETFL => { + let on = (arg as c_int & O_NONBLOCK) != 0; + let handle = rt::handle_of(fd).ok_or(Errno(EBADF))?; + rt::set_nonblock(fd, on); + let mut payload = [0u8; 8]; + payload[0..4].copy_from_slice(&handle.to_le_bytes()); + payload[4..8].copy_from_slice(&(if on { 1u32 } else { 0u32 }).to_le_bytes()); + let mut resp = [0u8; 24]; + let (errno, _) = rt::nskt_call(rt::OP_SETFLAGS, &payload, &mut resp)?; + if errno != 0 { return Err(Errno(rt::map_errno(errno))); } + Ok(0) + } + _ => Err(Errno(ENOSYS)), + } +} From 44adce51c4332274db7a7d21d3cfee3f48e80a66 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 05:54:32 +0600 Subject: [PATCH 57/72] feat(relibc): nonos epoll registry + epoll_create1/epoll_ctl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit poll/select in relibc route exclusively via PalEpoll — there is no Pal::poll. This builds the first half of that path: a fixed instance registry (epoll_rt.rs, fd base 512, 16 instances × 32 registrations, all Copy for const-init) plus epoll_create1 and epoll_ctl (ADD/MOD via set, DEL via remove). The fd base 512 is disjoint from sockets (256..319) and vfs (3..66). close() dispatches epoll fds to epoll_rt::destroy before the vfs path. epoll_pwait remains ENOSYS; the for_each accessor is defined here for the follow-on 2B.5e-2 task (epoll_pwait→net.sockets OP_POLL). --- toolchain/nonos-relibc/apply.sh | 1 + .../nonos-relibc/platform/nonos/epoll.rs | 27 +++++++- .../nonos-relibc/platform/nonos/epoll_rt.rs | 63 +++++++++++++++++++ toolchain/nonos-relibc/platform/nonos/mod.rs | 5 ++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 toolchain/nonos-relibc/platform/nonos/epoll_rt.rs diff --git a/toolchain/nonos-relibc/apply.sh b/toolchain/nonos-relibc/apply.sh index f65b67cddf..0203547d4d 100755 --- a/toolchain/nonos-relibc/apply.sh +++ b/toolchain/nonos-relibc/apply.sh @@ -21,6 +21,7 @@ cp "$HERE/platform/nonos/socket.rs" "$RELIBC/src/platform/nonos/socket.rs" cp "$HERE/platform/nonos/socket_rt.rs" "$RELIBC/src/platform/nonos/socket_rt.rs" cp "$HERE/platform/nonos/signal.rs" "$RELIBC/src/platform/nonos/signal.rs" cp "$HERE/platform/nonos/epoll.rs" "$RELIBC/src/platform/nonos/epoll.rs" +cp "$HERE/platform/nonos/epoll_rt.rs" "$RELIBC/src/platform/nonos/epoll_rt.rs" cp "$HERE/platform/nonos/ptrace.rs" "$RELIBC/src/platform/nonos/ptrace.rs" cp "$HERE/platform/nonos/fs.rs" "$RELIBC/src/platform/nonos/fs.rs" diff --git a/toolchain/nonos-relibc/platform/nonos/epoll.rs b/toolchain/nonos-relibc/platform/nonos/epoll.rs index 3bf6315258..3f4a089033 100644 --- a/toolchain/nonos-relibc/platform/nonos/epoll.rs +++ b/toolchain/nonos-relibc/platform/nonos/epoll.rs @@ -4,11 +4,32 @@ use super::{ }; use crate::{ error::{Errno, Result}, - header::{bits_sigset_t::sigset_t, errno::ENOSYS, sys_epoll::epoll_event}, + header::{ + bits_sigset_t::sigset_t, + errno::{EINVAL, EMFILE, ENOENT, ENOSYS}, + sys_epoll::epoll_event, + }, }; impl PalEpoll for Sys { - fn epoll_create1(_flags: c_int) -> Result { Err(Errno(ENOSYS)) } - unsafe fn epoll_ctl(_epfd: c_int, _op: c_int, _fd: c_int, _event: *mut epoll_event) -> Result<()> { Err(Errno(ENOSYS)) } + fn epoll_create1(_flags: c_int) -> Result { + super::epoll_rt::create().ok_or(Errno(EMFILE)) + } + unsafe fn epoll_ctl(epfd: c_int, op: c_int, fd: c_int, event: *mut epoll_event) -> Result<()> { + use super::epoll_rt as rt; + use crate::header::sys_epoll::{EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CTL_MOD}; + match op { + EPOLL_CTL_ADD | EPOLL_CTL_MOD => { + if event.is_null() { return Err(Errno(EINVAL)); } + let events = unsafe { (*event).events }; + let data = unsafe { (*event).data.u64 }; + if rt::set(epfd, fd, events, data) { Ok(()) } else { Err(Errno(EINVAL)) } + } + EPOLL_CTL_DEL => { + if rt::remove(epfd, fd) { Ok(()) } else { Err(Errno(ENOENT)) } + } + _ => Err(Errno(EINVAL)), + } + } unsafe fn epoll_pwait(_epfd: c_int, _events: *mut epoll_event, _maxevents: c_int, _timeout: c_int, _sigmask: *const sigset_t) -> Result { Err(Errno(ENOSYS)) } } diff --git a/toolchain/nonos-relibc/platform/nonos/epoll_rt.rs b/toolchain/nonos-relibc/platform/nonos/epoll_rt.rs new file mode 100644 index 0000000000..8b53dd1d99 --- /dev/null +++ b/toolchain/nonos-relibc/platform/nonos/epoll_rt.rs @@ -0,0 +1,63 @@ +use core::cell::SyncUnsafeCell; + +use super::super::types::c_int; + +const EPOLL_FD_BASE: c_int = 512; +const INSTANCES: usize = 16; +const MAX_REGS: usize = 32; + +#[derive(Clone, Copy)] +struct Reg { fd: c_int, events: u32, data: u64, used: bool } + +#[derive(Clone, Copy)] +struct Instance { used: bool, regs: [Reg; MAX_REGS] } + +const EMPTY_REG: Reg = Reg { fd: 0, events: 0, data: 0, used: false }; +const EMPTY_INST: Instance = Instance { used: false, regs: [EMPTY_REG; MAX_REGS] }; + +static EPOLL: SyncUnsafeCell<[Instance; INSTANCES]> = SyncUnsafeCell::new([EMPTY_INST; INSTANCES]); + +fn table() -> &'static mut [Instance; INSTANCES] { unsafe { &mut *EPOLL.get() } } + +pub fn is_epoll_fd(fd: c_int) -> bool { + fd >= EPOLL_FD_BASE && (fd - EPOLL_FD_BASE) < INSTANCES as c_int +} +fn idx(fd: c_int) -> Option { + if is_epoll_fd(fd) { Some((fd - EPOLL_FD_BASE) as usize) } else { None } +} +fn live(fd: c_int) -> Option { + let i = idx(fd)?; + if table()[i].used { Some(i) } else { None } +} + +pub fn create() -> Option { + for (i, inst) in table().iter_mut().enumerate() { + if !inst.used { *inst = Instance { used: true, regs: [EMPTY_REG; MAX_REGS] }; return Some(EPOLL_FD_BASE + i as c_int); } + } + None +} +pub fn destroy(fd: c_int) { + if let Some(i) = idx(fd) { table()[i].used = false; } +} +pub fn set(epfd: c_int, fd: c_int, events: u32, data: u64) -> bool { + let Some(i) = live(epfd) else { return false; }; + for r in table()[i].regs.iter_mut() { + if r.used && r.fd == fd { r.events = events; r.data = data; return true; } + } + for r in table()[i].regs.iter_mut() { + if !r.used { *r = Reg { fd, events, data, used: true }; return true; } + } + false +} +pub fn remove(epfd: c_int, fd: c_int) -> bool { + let Some(i) = live(epfd) else { return false; }; + for r in table()[i].regs.iter_mut() { + if r.used && r.fd == fd { r.used = false; return true; } + } + false +} +pub fn for_each(epfd: c_int, mut f: F) -> bool { + let Some(i) = live(epfd) else { return false; }; + for r in table()[i].regs { if r.used { f(r.fd, r.events, r.data); } } + true +} diff --git a/toolchain/nonos-relibc/platform/nonos/mod.rs b/toolchain/nonos-relibc/platform/nonos/mod.rs index 7e6dc7cce0..9a00421c11 100644 --- a/toolchain/nonos-relibc/platform/nonos/mod.rs +++ b/toolchain/nonos-relibc/platform/nonos/mod.rs @@ -36,6 +36,7 @@ mod ptrace; mod signal; mod socket; mod socket_rt; +mod epoll_rt; fn now_ms() -> i64 { let r = unsafe { lowlevel::syscall0(lowlevel::MK_TIME_MILLIS) }; @@ -111,6 +112,10 @@ impl Pal for Sys { if socket_rt::is_socket_fd(fildes) { return socket::close_fd(fildes); } + if epoll_rt::is_epoll_fd(fildes) { + epoll_rt::destroy(fildes); + return Ok(()); + } let vfs_fd = fs::fd_vfs(fildes).ok_or(Errno(EBADF))?; let pid = Self::getpid() as u32; let mut payload = [0u8; 8]; From 2d104c14d5a7df64c19daa26599ef510d2406ddf Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 06:03:24 +0600 Subject: [PATCH 58/72] feat(relibc): nonos epoll_pwait over net.sockets OP_POLL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each fd registered in the epoll instance, issues an immediate OP_POLL (timeout_ms=0) to net.sockets, translates EPOLLIN/EPOLLOUT ↔ POLLIN(1)/POLLOUT(2) wire bits, and writes typed epoll_event results up to maxevents. The outer wait loop exits when events are ready, timeout==0 (immediate), or the millisecond deadline is reached; negative timeout blocks until ready. Yields between passes via MK_YIELD. Closes the last stub in the relibc epoll surface needed for C poll() and select() to function over NONOS sockets. --- .../nonos-relibc/platform/nonos/epoll.rs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/toolchain/nonos-relibc/platform/nonos/epoll.rs b/toolchain/nonos-relibc/platform/nonos/epoll.rs index 3f4a089033..01a9c53d79 100644 --- a/toolchain/nonos-relibc/platform/nonos/epoll.rs +++ b/toolchain/nonos-relibc/platform/nonos/epoll.rs @@ -6,11 +6,23 @@ use crate::{ error::{Errno, Result}, header::{ bits_sigset_t::sigset_t, - errno::{EINVAL, EMFILE, ENOENT, ENOSYS}, - sys_epoll::epoll_event, + errno::{EBADF, EINVAL, EMFILE, ENOENT}, + sys_epoll::{epoll_data, epoll_event, EPOLLIN, EPOLLOUT}, }, }; +fn poll_one(handle: u32, events: u16) -> u16 { + use super::socket_rt as rt; + let mut payload = [0u8; 12]; + payload[0..4].copy_from_slice(&handle.to_le_bytes()); + payload[4..6].copy_from_slice(&events.to_le_bytes()); + let mut resp = [0u8; 24]; + match rt::nskt_call(rt::OP_POLL, &payload, &mut resp) { + Ok((0, n)) if n >= 2 => u16::from_le_bytes([resp[20], resp[21]]), + _ => 0, + } +} + impl PalEpoll for Sys { fn epoll_create1(_flags: c_int) -> Result { super::epoll_rt::create().ok_or(Errno(EMFILE)) @@ -31,5 +43,36 @@ impl PalEpoll for Sys { _ => Err(Errno(EINVAL)), } } - unsafe fn epoll_pwait(_epfd: c_int, _events: *mut epoll_event, _maxevents: c_int, _timeout: c_int, _sigmask: *const sigset_t) -> Result { Err(Errno(ENOSYS)) } + unsafe fn epoll_pwait(epfd: c_int, events: *mut epoll_event, maxevents: c_int, timeout: c_int, _sigmask: *const sigset_t) -> Result { + use super::epoll_rt as ep; + use super::socket_rt as rt; + let max = if maxevents > 0 { maxevents as usize } else { 0 }; + let deadline = super::now_ms().saturating_add(timeout as i64); + loop { + let mut n = 0usize; + let ok = ep::for_each(epfd, |fd, want, data| { + if n >= max { return; } + let Some(handle) = rt::handle_of(fd) else { return; }; + let mut pe: u16 = 0; + if want & EPOLLIN != 0 { pe |= 1; } + if want & EPOLLOUT != 0 { pe |= 2; } + let revents = poll_one(handle, pe); + if revents != 0 { + let mut e: u32 = 0; + if revents & 1 != 0 { e |= EPOLLIN; } + if revents & 2 != 0 { e |= EPOLLOUT; } + unsafe { + (*events.add(n)).events = e; + (*events.add(n)).data = epoll_data { u64: data }; + } + n += 1; + } + }); + if !ok { return Err(Errno(EBADF)); } + if n > 0 || timeout == 0 || (timeout > 0 && super::now_ms() >= deadline) { + return Ok(n); + } + unsafe { super::lowlevel::syscall0(super::lowlevel::MK_YIELD); } + } + } } From 1e6cadb15a5a62135c8fd48021120fcbaaac2dcb Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 06:07:11 +0600 Subject: [PATCH 59/72] feat(cnet): capsule_c_net exerciser + microkernel-cnet profile wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the C socket exerciser capsule and all kernel wiring needed to embed and spawn it in a microkernel-cnet-smoketest profile. The capsule exercises the full socket→fcntl(O_NONBLOCK)→connect→poll→write→poll→ read→close path against a TCP echo peer at 10.0.2.200:7, proving the relibc socket backend (Phase 2B.5) is reachable from C userland. Cap hand-sync: Capsule.mk CAPSULE_REQUIRED_CAPS=0x119 matches spawn.rs requested_caps() = CoreExec|IPC|Memory|Debug (no Crypto/FS). Ports 4508/4509 are unoccupied. run_c_net() fires after spawn_plan::spawn_network() so net.sockets is live before c_net connects. Signing, keys, boot harness, and QEMU run are deferred to C2. Closes Phase-C1 of the NetSurf-on-NONOS relibc arc. --- Cargo.toml | 18 ++++++ Makefile | 13 +++++ src/userspace/capsule_c_net/embed.rs | 44 +++++++++++++++ src/userspace/capsule_c_net/mod.rs | 20 +++++++ src/userspace/capsule_c_net/spawn.rs | 84 ++++++++++++++++++++++++++++ src/userspace/init/entry.rs | 12 ++++ src/userspace/mod.rs | 1 + userland/capsule_c_net/Capsule.mk | 17 ++++++ userland/capsule_c_net/src/main.c | 43 ++++++++++++++ 9 files changed, 252 insertions(+) create mode 100644 src/userspace/capsule_c_net/embed.rs create mode 100644 src/userspace/capsule_c_net/mod.rs create mode 100644 src/userspace/capsule_c_net/spawn.rs create mode 100644 userland/capsule_c_net/Capsule.mk create mode 100644 userland/capsule_c_net/src/main.c diff --git a/Cargo.toml b/Cargo.toml index cc21ad1fdf..095d70f0e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ nonos-capsule-proof-io = [] nonos-capsule-std-proof = [] nonos-capsule-c-proof = [] nonos-capsule-relibc-test = [] +nonos-capsule-c-net = [] nonos-capsule-ripgrep = [] nonos-capsule-ramfs = [] nonos-capsule-wallpaper = [] @@ -241,6 +242,23 @@ microkernel-relibc-test-smoketest = [ "nonos-capsule-relibc-test", ] +microkernel-cnet-smoketest = [ + "microkernel-core", + "nonos-dev-unverified-capsules", + "nonos-zk-rollout", + "nonos-capsule-proof-io", + "nonos-capsule-driver-virtio-net", + "nonos-capsule-net-l2", + "nonos-capsule-net-ip", + "nonos-capsule-net-udp", + "nonos-capsule-net-dhcp", + "nonos-capsule-net-tcp", + "nonos-capsule-net-dns", + "nonos-capsule-net-sockets", + "nonos-capsule-net-nym", + "nonos-capsule-c-net", +] + microkernel-keyring = [ "microkernel-core", "nonos-production", diff --git a/Makefile b/Makefile index 67360e1589..fcf84f22eb 100644 --- a/Makefile +++ b/Makefile @@ -536,6 +536,7 @@ include userland/capsule_proof_io/Capsule.mk include userland/capsule_std_proof/Capsule.mk include userland/capsule_c_proof/Capsule.mk include userland/capsule_relibc_test/Capsule.mk +include userland/capsule_c_net/Capsule.mk include userland/capsule_ripgrep/Capsule.mk include userland/capsule_ramfs/Capsule.mk include userland/capsule_keyring/Capsule.mk @@ -849,6 +850,18 @@ nonos-mk-relibc-smoke-prod: $(relibc-test_ARTIFACTS) \ nonos-mk-relibc-smoke-test: @CPROOF_PROD_TARGET=nonos-mk-relibc-smoke-prod ./tests/boot/relibc.sh +.PHONY: nonos-mk-cnet-smoke-prod +nonos-mk-cnet-smoke-prod: $(proof-io_ARTIFACTS) $(driver-virtio-net_ARTIFACTS) \ + $(net-l2_ARTIFACTS) $(net-ip_ARTIFACTS) $(net-udp_ARTIFACTS) \ + $(net-dhcp_ARTIFACTS) $(net-tcp_ARTIFACTS) $(net-dns_ARTIFACTS) \ + $(net-sockets_ARTIFACTS) $(net-nym_ARTIFACTS) $(c-net_ARTIFACTS) \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + @echo "Building kernel (microkernel-cnet-smoketest, unverified)..." + @$(SDK_FLAGS) NONOS_SIGNING_KEY=$(KERNEL_SIGNING_KEY) \ + RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + $(CARGO) build $(KERNEL_BUILD_FLAGS) \ + --no-default-features --features microkernel-cnet-smoketest + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." diff --git a/src/userspace/capsule_c_net/embed.rs b/src/userspace/capsule_c_net/embed.rs new file mode 100644 index 0000000000..2c753301b1 --- /dev/null +++ b/src/userspace/capsule_c_net/embed.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 . + +#[cfg(feature = "nonos-capsule-c-net")] +pub(crate) const C_NET_ELF: &[u8] = include_bytes!( + "../../../userland/capsule_c_net/target/x86_64-nonos-user/release/c_net" +); + +#[cfg(feature = "nonos-capsule-c-net")] +pub(crate) const C_NET_NONOS_ID_CERT_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_net.nonos_id_cert.bin"); + +#[cfg(feature = "nonos-capsule-c-net")] +pub(crate) const C_NET_MANIFEST_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_net.manifest.bin"); + +#[cfg(feature = "nonos-capsule-c-net")] +pub(crate) const C_NET_ATTESTATION_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/c_net.zk_trailer.bin"); + +#[cfg(not(feature = "nonos-capsule-c-net"))] +pub(crate) const C_NET_ELF: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-net"))] +pub(crate) const C_NET_NONOS_ID_CERT_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-net"))] +pub(crate) const C_NET_MANIFEST_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-c-net"))] +pub(crate) const C_NET_ATTESTATION_BYTES: &[u8] = &[]; diff --git a/src/userspace/capsule_c_net/mod.rs b/src/userspace/capsule_c_net/mod.rs new file mode 100644 index 0000000000..a0b0dec9a1 --- /dev/null +++ b/src/userspace/capsule_c_net/mod.rs @@ -0,0 +1,20 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod embed; +mod spawn; + +pub use spawn::spawn_c_net_capsule; diff --git a/src/userspace/capsule_c_net/spawn.rs b/src/userspace/capsule_c_net/spawn.rs new file mode 100644 index 0000000000..1f6b73832e --- /dev/null +++ b/src/userspace/capsule_c_net/spawn.rs @@ -0,0 +1,84 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use super::embed::C_NET_ELF; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use super::embed::{ + C_NET_ATTESTATION_BYTES, C_NET_MANIFEST_BYTES, C_NET_NONOS_ID_CERT_BYTES, +}; +use crate::capabilities::Capability; +use crate::kernel_core::process_spawn::capsule_spawn::{self, SpawnError}; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpecVerified; +#[cfg(feature = "nonos-dev-unverified-capsules")] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpec; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_id_cert::IdCertVerifyError; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_trust_anchor::{ + decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY, +}; + +const SERVICE_NAME: &str = "c_net"; +const SERVICE_PORT: u32 = 4508; +const REPLY_INBOX: &str = "endpoint.c_net.reply"; +const REPLY_PORT: u32 = 4509; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +const TARGET_TRIPLE: &str = "x86_64-nonos-user"; + +fn requested_caps() -> u64 { + Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit() + | Capability::Debug.bit() +} + +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +pub fn spawn_c_net_capsule() -> Result<(), SpawnError> { + 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: C_NET_ELF, + nonos_id_cert_bytes: C_NET_NONOS_ID_CERT_BYTES, + manifest_bytes: C_NET_MANIFEST_BYTES, + attestation_trailer: C_NET_ATTESTATION_BYTES, + target_triple: TARGET_TRIPLE, + requested_caps: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + Ok(()) +} + +#[cfg(feature = "nonos-dev-unverified-capsules")] +pub fn spawn_c_net_capsule() -> Result<(), SpawnError> { + let spec = CapsuleSpec { + name: SERVICE_NAME, + service_port: SERVICE_PORT, + reply_inbox: REPLY_INBOX, + reply_port: REPLY_PORT, + elf: C_NET_ELF, + caps_bits: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn(&spec)?; + Ok(()) +} diff --git a/src/userspace/init/entry.rs b/src/userspace/init/entry.rs index 1091a9ead2..31cd6cb0a7 100644 --- a/src/userspace/init/entry.rs +++ b/src/userspace/init/entry.rs @@ -30,6 +30,7 @@ pub fn run_init() -> ! { spawn_plan::spawn_drivers(); spawn_plan::spawn_vfs(); spawn_plan::spawn_network(); + run_c_net(); spawn_plan::spawn_desktop(); spawn_plan::spawn_market(); spawn_plan::spawn_apps(); @@ -81,6 +82,17 @@ fn run_relibc_test() { #[cfg(not(feature = "nonos-capsule-relibc-test"))] fn run_relibc_test() {} +#[cfg(feature = "nonos-capsule-c-net")] +fn run_c_net() { + match crate::userspace::capsule_c_net::spawn_c_net_capsule() { + Ok(()) => boot_log::ok("C-NET", "capsule spawned"), + Err(_) => boot_log::error("C-NET capsule spawn failed"), + } +} + +#[cfg(not(feature = "nonos-capsule-c-net"))] +fn run_c_net() {} + #[cfg(feature = "nonos-capsule-ripgrep")] fn run_ripgrep() { match crate::userspace::capsule_ripgrep::spawn_ripgrep_capsule() { diff --git a/src/userspace/mod.rs b/src/userspace/mod.rs index d521639877..73bb5a4f59 100644 --- a/src/userspace/mod.rs +++ b/src/userspace/mod.rs @@ -62,6 +62,7 @@ pub mod capsule_settings; pub mod capsule_setup_wizard; pub mod capsule_snake; pub mod capsule_c_proof; +pub mod capsule_c_net; pub mod capsule_relibc_test; pub mod capsule_std_proof; pub mod capsule_terminal; diff --git a/userland/capsule_c_net/Capsule.mk b/userland/capsule_c_net/Capsule.mk new file mode 100644 index 0000000000..1b146a4d44 --- /dev/null +++ b/userland/capsule_c_net/Capsule.mk @@ -0,0 +1,17 @@ +CAPSULE_SLUG := c-net +CAPSULE_HANDLE := c_net +CAPSULE_DOMAIN := systems.nonos +CAPSULE_DIR := userland/capsule_c_net +CAPSULE_BIN_NAME := c_net +CAPSULE_FEATURE := nonos-capsule-c-net +CAPSULE_NAMESPACE := systems.nonos.c_net +CAPSULE_SERVICE_ENDPOINT := service:4508:c_net +CAPSULE_REPLY_ENDPOINT := reply:4509:endpoint.c_net.reply +# CoreExec | IPC | Memory | Debug = 0x01 | 0x08 | 0x10 | 0x100 = 0x119 +# Debug is required for the write(1,...) -> MkDebug serial marker. +CAPSULE_REQUIRED_CAPS := 0x119 +CAPSULE_KERNEL_MIRROR := src/userspace/capsule_c_net +CAPSULE_PREBUILT_BIN := userland/capsule_c_net/build/c_net + +include nonos-mk/capsule-c.mk +include nonos-mk/capsule.mk diff --git a/userland/capsule_c_net/src/main.c b/userland/capsule_c_net/src/main.c new file mode 100644 index 0000000000..1eefcd170a --- /dev/null +++ b/userland/capsule_c_net/src/main.c @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define EMIT(s) write(1, s, sizeof(s) - 1) + +int main(void) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { EMIT("[C-NET] FAIL socket\n"); return 1; } + if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) { EMIT("[C-NET] FAIL fcntl\n"); return 1; } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(7); + addr.sin_addr.s_addr = htonl(0x0A0002C8); + + int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc < 0 && errno != EINPROGRESS) { EMIT("[C-NET] FAIL connect\n"); return 1; } + + struct pollfd pfd; + pfd.fd = fd; pfd.events = POLLOUT; pfd.revents = 0; + if (poll(&pfd, 1, 4000) <= 0 || !(pfd.revents & POLLOUT)) { EMIT("[C-NET] FAIL connect-wait\n"); return 1; } + + if (write(fd, "ping", 4) != 4) { EMIT("[C-NET] FAIL send\n"); return 1; } + + pfd.events = POLLIN; pfd.revents = 0; + if (poll(&pfd, 1, 4000) <= 0 || !(pfd.revents & POLLIN)) { EMIT("[C-NET] FAIL recv-wait\n"); return 1; } + + char buf[8]; + for (int i = 0; i < 8; i++) buf[i] = 0; + long n = read(fd, buf, 4); + if (n != 4 || memcmp(buf, "ping", 4) != 0) { EMIT("[C-NET] FAIL echo\n"); return 1; } + + close(fd); + EMIT("[C-NET] PASS\n"); + return 0; +} From 6ed470ef157b520d8adc48433cf13f8c18e81073 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 06:28:01 +0600 Subject: [PATCH 60/72] feat(cnet): connect-retry + nonos-mk-cnet-smoke-test gate The c_net exerciser spawns alongside the net stack, so its first connect races DHCP/net.tcp bring-up. Retry connect (60x, 500ms via poll(NULL,0)) until it returns EINPROGRESS, letting the stack acquire an IP first. Adds the nonos-mk-cnet-smoke-test target driving tests/boot/cnet.sh. make NONOS_DEV=1 nonos-mk-cnet-smoke-test -> [C-NET] PASS: relibc socket->fcntl(O_NONBLOCK)->connect->poll(POLLOUT)->send->poll(POLLIN)-> recv echo->close, proven E2E under QEMU. --- Makefile | 5 ++++- userland/capsule_c_net/src/main.c | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fcf84f22eb..1846fb2f4a 100644 --- a/Makefile +++ b/Makefile @@ -850,7 +850,7 @@ nonos-mk-relibc-smoke-prod: $(relibc-test_ARTIFACTS) \ nonos-mk-relibc-smoke-test: @CPROOF_PROD_TARGET=nonos-mk-relibc-smoke-prod ./tests/boot/relibc.sh -.PHONY: nonos-mk-cnet-smoke-prod +.PHONY: nonos-mk-cnet-smoke-prod nonos-mk-cnet-smoke-test nonos-mk-cnet-smoke-prod: $(proof-io_ARTIFACTS) $(driver-virtio-net_ARTIFACTS) \ $(net-l2_ARTIFACTS) $(net-ip_ARTIFACTS) $(net-udp_ARTIFACTS) \ $(net-dhcp_ARTIFACTS) $(net-tcp_ARTIFACTS) $(net-dns_ARTIFACTS) \ @@ -862,6 +862,9 @@ nonos-mk-cnet-smoke-prod: $(proof-io_ARTIFACTS) $(driver-virtio-net_ARTIFACTS) \ $(CARGO) build $(KERNEL_BUILD_FLAGS) \ --no-default-features --features microkernel-cnet-smoketest +nonos-mk-cnet-smoke-test: + @CNET_PROD_TARGET=nonos-mk-cnet-smoke-prod ./tests/boot/cnet.sh + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." diff --git a/userland/capsule_c_net/src/main.c b/userland/capsule_c_net/src/main.c index 1eefcd170a..60adb72b70 100644 --- a/userland/capsule_c_net/src/main.c +++ b/userland/capsule_c_net/src/main.c @@ -20,7 +20,12 @@ int main(void) { addr.sin_port = htons(7); addr.sin_addr.s_addr = htonl(0x0A0002C8); - int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + int rc = -1; + for (int attempt = 0; attempt < 60; attempt++) { + rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc == 0 || errno == EINPROGRESS) break; + poll(NULL, 0, 500); + } if (rc < 0 && errno != EINPROGRESS) { EMIT("[C-NET] FAIL connect\n"); return 1; } struct pollfd pfd; From 7b0a2a651b894f9bbed57e68d150ec9ecbf1171f Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 09:26:25 +0600 Subject: [PATCH 61/72] fix(net-sockets): free rx-stash on socket close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OP_POLL's POLLIN probe buffers bytes in the per-(pid,handle) rx stash. If a client polls a socket ready then closes it without ever calling recv, the stashed Vec was orphaned in the BTreeMap forever (handles are monotonic, so the key is never revisited) — a bounded-per-socket but unbounded-over-lifetime leak. close now calls stash::clear after SOCKETS.close. Found by the final whole-branch review; no correctness/misdelivery impact, purely the leak. --- userland/capsule_net_sockets/src/server/handlers/close.rs | 3 ++- userland/capsule_net_sockets/src/sockets/stash.rs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/userland/capsule_net_sockets/src/server/handlers/close.rs b/userland/capsule_net_sockets/src/server/handlers/close.rs index 3deb8258bf..6feb9fc3c7 100644 --- a/userland/capsule_net_sockets/src/server/handlers/close.rs +++ b/userland/capsule_net_sockets/src/server/handlers/close.rs @@ -19,7 +19,7 @@ use crate::protocol::{E_NO_HANDLE, E_NO_TRANSPORT, E_OK, OP_CLOSE}; use crate::server::handlers::io::u32_at; use crate::server::parse_req::Request; use crate::server::respond::respond; -use crate::sockets::{Kind, SocketKey, SOCKETS}; +use crate::sockets::{stash, Kind, SocketKey, SOCKETS}; use crate::state; pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { @@ -51,6 +51,7 @@ pub fn handle(pid: u32, req: &Request, body: &[u8], tx: &mut [u8]) { if !SOCKETS.close(key) { return status(pid, req, E_NO_HANDLE, tx); } + stash::clear(pid, handle); status(pid, req, E_OK, tx); } diff --git a/userland/capsule_net_sockets/src/sockets/stash.rs b/userland/capsule_net_sockets/src/sockets/stash.rs index fdccb27d6f..0c6cd010b2 100644 --- a/userland/capsule_net_sockets/src/sockets/stash.rs +++ b/userland/capsule_net_sockets/src/sockets/stash.rs @@ -45,3 +45,7 @@ pub fn put(pid: u32, handle: u32, bytes: &[u8]) { pub fn has(pid: u32, handle: u32) -> bool { STASH.lock().get(&(pid, handle)).is_some_and(|b| !b.is_empty()) } + +pub fn clear(pid: u32, handle: u32) { + STASH.lock().remove(&(pid, handle)); +} From eddc421c99b9cedc6648233e63afa4b47e6819fe Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 09:26:26 +0600 Subject: [PATCH 62/72] fix(relibc): epoll_pwait rejects non-positive maxevents epoll_pwait was safe for every finite timeout, but maxevents<=0 combined with an infinite timeout (-1) could spin forever: max=0 pins the ready count at 0 and neither timeout exit fires. POSIX mandates EINVAL for maxevents<=0; add the guard at entry. No real caller (poll/select pass a positive batch) hit this; found by the final whole-branch review. --- toolchain/nonos-relibc/platform/nonos/epoll.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/toolchain/nonos-relibc/platform/nonos/epoll.rs b/toolchain/nonos-relibc/platform/nonos/epoll.rs index 01a9c53d79..6cbdae28e4 100644 --- a/toolchain/nonos-relibc/platform/nonos/epoll.rs +++ b/toolchain/nonos-relibc/platform/nonos/epoll.rs @@ -46,7 +46,10 @@ impl PalEpoll for Sys { unsafe fn epoll_pwait(epfd: c_int, events: *mut epoll_event, maxevents: c_int, timeout: c_int, _sigmask: *const sigset_t) -> Result { use super::epoll_rt as ep; use super::socket_rt as rt; - let max = if maxevents > 0 { maxevents as usize } else { 0 }; + if maxevents <= 0 { + return Err(Errno(EINVAL)); + } + let max = maxevents as usize; let deadline = super::now_ms().saturating_add(timeout as i64); loop { let mut n = 0usize; From 4f676d0ab14e455ec83f559e0fe99c7943dd6097 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:09:41 +0600 Subject: [PATCH 63/72] feat(netsurf): add nsfb_probe C syscall + surface ABI layer Phase-4 display-shim de-risk begins with a pure-C surface probe that drives the verified surface/compositor ABI before vendoring NetSurf. Hand-rolled inline-asm syscall wrapper marks rdi/rsi/rdx/r10/r8/r9 as in-out operands plus rcx/r11/memory clobbers, because the NONOS kernel does not preserve the arg registers across a syscall (the relibc 39f3aeb3c landmine). SurfaceDescriptor mirrors the 40-byte wire.toml layout (byte_len@16, base_va@24, flags@32). --- userland/capsule_nsfb_probe/.gitignore | 1 + .../capsule_nsfb_probe/src/nonos_surface.h | 37 +++++++++++ userland/capsule_nsfb_probe/src/nonos_sys.h | 61 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 userland/capsule_nsfb_probe/.gitignore create mode 100644 userland/capsule_nsfb_probe/src/nonos_surface.h create mode 100644 userland/capsule_nsfb_probe/src/nonos_sys.h diff --git a/userland/capsule_nsfb_probe/.gitignore b/userland/capsule_nsfb_probe/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/userland/capsule_nsfb_probe/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/userland/capsule_nsfb_probe/src/nonos_surface.h b/userland/capsule_nsfb_probe/src/nonos_surface.h new file mode 100644 index 0000000000..b11c3709f5 --- /dev/null +++ b/userland/capsule_nsfb_probe/src/nonos_surface.h @@ -0,0 +1,37 @@ +#ifndef NONOS_SURFACE_H +#define NONOS_SURFACE_H + +#include "nonos_sys.h" + +#define PROT_RW 0x3 +#define MAP_PRIVATE_ANON 0x22 +#define SURFACE_FORMAT_ARGB8888 1u + +struct surface_descriptor { + u32 width; + u32 height; + u32 stride; + u32 format; + u64 byte_len; + u64 base_va; + u64 flags; +}; + +static inline u64 nonos_mmap(u64 len) { + return (u64)nonos_sys6(SYS_MMAP, 0, len, PROT_RW, MAP_PRIVATE_ANON, + (u64)(-1), 0); +} + +static inline i64 nonos_surface_register(const struct surface_descriptor *d) { + return nonos_sys6(SYS_SURFACE_REGISTER, (u64)d, 0, 0, 0, 0, 0); +} + +static inline i64 nonos_surface_share(u64 sid) { + return nonos_sys6(SYS_SURFACE_SHARE, sid, 0, 0, 0, 0, 0); +} + +static inline i64 nonos_surface_release(u64 handle) { + return nonos_sys6(SYS_SURFACE_RELEASE, handle, 0, 0, 0, 0, 0); +} + +#endif diff --git a/userland/capsule_nsfb_probe/src/nonos_sys.h b/userland/capsule_nsfb_probe/src/nonos_sys.h new file mode 100644 index 0000000000..ead540be56 --- /dev/null +++ b/userland/capsule_nsfb_probe/src/nonos_sys.h @@ -0,0 +1,61 @@ +#ifndef NONOS_SYS_H +#define NONOS_SYS_H + +typedef unsigned int u32; +typedef int i32; +typedef unsigned long u64; +typedef long i64; + +#define FOURCC(a, b, c, d) \ + ((i64)((unsigned char)(a) | ((unsigned char)(b) << 8) | \ + ((unsigned char)(c) << 16) | ((i64)(unsigned char)(d) << 24))) + +#define SYS_MMAP FOURCC('M', 'M', 'A', 'P') +#define SYS_SURFACE_REGISTER FOURCC('M', 'S', 'R', 'G') +#define SYS_SURFACE_SHARE FOURCC('M', 'S', 'S', 'H') +#define SYS_SURFACE_RELEASE FOURCC('M', 'S', 'R', 'L') +#define SYS_SERVICE_LOOKUP FOURCC('M', 'S', 'V', 'L') +#define SYS_IPC_CALL FOURCC('M', 'I', 'C', 'L') +#define SYS_YIELD FOURCC('M', 'Y', 'L', 'D') + +static inline i64 nonos_sys6(i64 num, u64 a1, u64 a2, u64 a3, u64 a4, u64 a5, u64 a6) { + register i64 rax __asm__("rax") = num; + register u64 rdi __asm__("rdi") = a1; + register u64 rsi __asm__("rsi") = a2; + register u64 rdx __asm__("rdx") = a3; + register u64 r10 __asm__("r10") = a4; + register u64 r8 __asm__("r8") = a5; + register u64 r9 __asm__("r9") = a6; + __asm__ volatile("syscall" + : "+r"(rax), "+r"(rdi), "+r"(rsi), "+r"(rdx), + "+r"(r10), "+r"(r8), "+r"(r9) + : + : "rcx", "r11", "memory"); + return rax; +} + +static inline void nonos_yield(void) { + nonos_sys6(SYS_YIELD, 0, 0, 0, 0, 0, 0); +} + +static inline void put_u16(unsigned char *p, u32 off, u32 v) { + p[off] = (unsigned char)(v & 0xFF); + p[off + 1] = (unsigned char)((v >> 8) & 0xFF); +} + +static inline void put_u32(unsigned char *p, u32 off, u32 v) { + put_u16(p, off, v & 0xFFFF); + put_u16(p, off + 2, (v >> 16) & 0xFFFF); +} + +static inline void put_u64(unsigned char *p, u32 off, u64 v) { + put_u32(p, off, (u32)v); + put_u32(p, off + 4, (u32)(v >> 32)); +} + +static inline u32 get_u32(const unsigned char *p, u32 off) { + return (u32)p[off] | ((u32)p[off + 1] << 8) | + ((u32)p[off + 2] << 16) | ((u32)p[off + 3] << 24); +} + +#endif From 7f13b265b99fd30a74cb7556b69855255b58f37f Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:09:56 +0600 Subject: [PATCH 64/72] feat(netsurf): add nsfb_probe compositor NCMP framing NCMP request/reply builders for the four ops the probe needs: OP_DISPLAY_INFO (validates format==1 and nonzero dims before trust), OP_SCENE_SUBMIT (32B body, handle u64@0 so x starts @8), OP_DAMAGE_COMMIT (16B), OP_SCENE_REMOVE (8B). Compositor port resolved by name via mk_service_lookup, never hardcoded. Status checked on every reply. --- .../capsule_nsfb_probe/src/nonos_compositor.h | 75 +++++++++++++++++++ userland/capsule_nsfb_probe/src/nonos_scene.h | 38 ++++++++++ 2 files changed, 113 insertions(+) create mode 100644 userland/capsule_nsfb_probe/src/nonos_compositor.h create mode 100644 userland/capsule_nsfb_probe/src/nonos_scene.h diff --git a/userland/capsule_nsfb_probe/src/nonos_compositor.h b/userland/capsule_nsfb_probe/src/nonos_compositor.h new file mode 100644 index 0000000000..f6087d1b4d --- /dev/null +++ b/userland/capsule_nsfb_probe/src/nonos_compositor.h @@ -0,0 +1,75 @@ +#ifndef NONOS_COMPOSITOR_H +#define NONOS_COMPOSITOR_H + +#include "nonos_sys.h" + +#define NCMP_MAGIC 0x4E434D50u +#define NCMP_VERSION 1 +#define NCMP_HDR_LEN 20 +#define OP_SCENE_SUBMIT 0x0002 +#define OP_DAMAGE_COMMIT 0x0003 +#define OP_SCENE_REMOVE 0x0007 +#define OP_DISPLAY_INFO 0x0008 + +struct display_info { + u32 width; + u32 height; + u32 stride; + u32 format; +}; + +static inline u32 nonos_lookup_port(const char *name, u64 len) { + u32 port = 0; + u32 pid = 0; + i64 rc = nonos_sys6(SYS_SERVICE_LOOKUP, (u64)name, len, (u64)&port, + (u64)&pid, 0, 0); + if (rc < 0 || pid == 0) + return 0; + return port; +} + +static inline void ncmp_header(unsigned char *p, u32 op, u32 rid, u32 plen) { + put_u32(p, 0, NCMP_MAGIC); + put_u16(p, 4, NCMP_VERSION); + put_u16(p, 6, op); + put_u16(p, 8, 0); + put_u16(p, 10, 0); + put_u32(p, 12, rid); + put_u32(p, 16, plen); +} + +static inline i64 ncmp_call(u32 port, const unsigned char *tx, u64 txlen, + unsigned char *rx, u64 rxlen) { + return nonos_sys6(SYS_IPC_CALL, (u64)port, (u64)tx, txlen, (u64)rx, + rxlen, 0); +} + +static inline int ncmp_status_call(u32 port, const unsigned char *tx, u64 txlen) { + unsigned char rx[NCMP_HDR_LEN + 4]; + i64 rc = ncmp_call(port, tx, txlen, rx, sizeof(rx)); + if (rc < (i64)sizeof(rx)) + return -1; + if ((i32)get_u32(rx, NCMP_HDR_LEN) != 0) + return -2; + return 0; +} + +static inline int ncmp_display_info(u32 port, u32 rid, struct display_info *out) { + unsigned char tx[NCMP_HDR_LEN]; + unsigned char rx[NCMP_HDR_LEN + 4 + 16]; + ncmp_header(tx, OP_DISPLAY_INFO, rid, 0); + i64 rc = ncmp_call(port, tx, sizeof(tx), rx, sizeof(rx)); + if (rc < (i64)sizeof(rx)) + return -1; + if ((i32)get_u32(rx, NCMP_HDR_LEN) != 0) + return -2; + out->width = get_u32(rx, NCMP_HDR_LEN + 4); + out->height = get_u32(rx, NCMP_HDR_LEN + 8); + out->stride = get_u32(rx, NCMP_HDR_LEN + 12); + out->format = get_u32(rx, NCMP_HDR_LEN + 16); + if (out->format != 1u || !out->width || !out->height || !out->stride) + return -3; + return 0; +} + +#endif diff --git a/userland/capsule_nsfb_probe/src/nonos_scene.h b/userland/capsule_nsfb_probe/src/nonos_scene.h new file mode 100644 index 0000000000..ab96fba0a0 --- /dev/null +++ b/userland/capsule_nsfb_probe/src/nonos_scene.h @@ -0,0 +1,38 @@ +#ifndef NONOS_SCENE_H +#define NONOS_SCENE_H + +#include "nonos_compositor.h" + +static inline int ncmp_scene_submit(u32 port, u32 rid, u64 handle, u32 x, + u32 y, u32 w, u32 h, u32 z) { + unsigned char tx[NCMP_HDR_LEN + 32]; + ncmp_header(tx, OP_SCENE_SUBMIT, rid, 32); + put_u64(tx, 20, handle); + put_u32(tx, 28, x); + put_u32(tx, 32, y); + put_u32(tx, 36, w); + put_u32(tx, 40, h); + put_u32(tx, 44, z); + put_u32(tx, 48, 0); + return ncmp_status_call(port, tx, sizeof(tx)); +} + +static inline int ncmp_damage_commit(u32 port, u32 rid, u32 x, u32 y, u32 w, u32 h) { + unsigned char tx[NCMP_HDR_LEN + 16]; + ncmp_header(tx, OP_DAMAGE_COMMIT, rid, 16); + put_u32(tx, 20, x); + put_u32(tx, 24, y); + put_u32(tx, 28, w); + put_u32(tx, 32, h); + return ncmp_status_call(port, tx, sizeof(tx)); +} + +static inline int ncmp_scene_remove(u32 port, u32 rid) { + unsigned char tx[NCMP_HDR_LEN + 8]; + ncmp_header(tx, OP_SCENE_REMOVE, rid, 8); + put_u32(tx, 20, 0); + put_u32(tx, 24, 0); + return ncmp_status_call(port, tx, sizeof(tx)); +} + +#endif From cba0101b5152d9895340a81c94e96a3f2b9a5c31 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:10:07 +0600 Subject: [PATCH 65/72] feat(netsurf): add nsfb_probe lifecycle + build manifest main() runs the full surface lifecycle: lookup compositor, OP_DISPLAY_INFO, mmap the ARGB8888 buffer, register+share the surface, paint a known red/green gradient with volatile 32-bit writes, then submit+damage the full-screen scene. Emits [NSFB-PROBE] PASS via write(1,...) -> MkDebug on success or [NSFB-PROBE] FAIL:: on any failed step, then stays idle-resident (yield loop) so the boot harness can screendump the gradient. Capsule.mk caps = 0x1119 (CoreExec|IPC|Memory|Debug|GraphicsSurfaceCreate), hand-synced target for the kernel-side requested_caps() in Task 4.0b. --- userland/capsule_nsfb_probe/Capsule.mk | 19 ++++++ userland/capsule_nsfb_probe/src/main.c | 62 +++++++++++++++++++ .../capsule_nsfb_probe/src/nonos_marker.h | 47 ++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 userland/capsule_nsfb_probe/Capsule.mk create mode 100644 userland/capsule_nsfb_probe/src/main.c create mode 100644 userland/capsule_nsfb_probe/src/nonos_marker.h diff --git a/userland/capsule_nsfb_probe/Capsule.mk b/userland/capsule_nsfb_probe/Capsule.mk new file mode 100644 index 0000000000..c505b2f728 --- /dev/null +++ b/userland/capsule_nsfb_probe/Capsule.mk @@ -0,0 +1,19 @@ +CAPSULE_SLUG := nsfb-probe +CAPSULE_HANDLE := nsfb_probe +CAPSULE_DOMAIN := systems.nonos +CAPSULE_DIR := userland/capsule_nsfb_probe +CAPSULE_BIN_NAME := nsfb_probe +CAPSULE_FEATURE := nonos-capsule-nsfb-probe +CAPSULE_NAMESPACE := systems.nonos.nsfb_probe +CAPSULE_SERVICE_ENDPOINT := service:4512:nsfb_probe +CAPSULE_REPLY_ENDPOINT := reply:4513:endpoint.nsfb_probe.reply +# CoreExec | IPC | Memory | Debug | GraphicsSurfaceCreate +# = 0x01 | 0x08 | 0x10 | 0x100 | 0x1000 = 0x1119 +# Debug(0x100) carries the write(1,...) -> MkDebug serial marker; +# GraphicsSurfaceCreate(0x1000) authorizes surface register/share/release. +CAPSULE_REQUIRED_CAPS := 0x1119 +CAPSULE_KERNEL_MIRROR := src/userspace/capsule_nsfb_probe +CAPSULE_PREBUILT_BIN := userland/capsule_nsfb_probe/build/nsfb_probe + +include nonos-mk/capsule-c.mk +include nonos-mk/capsule.mk diff --git a/userland/capsule_nsfb_probe/src/main.c b/userland/capsule_nsfb_probe/src/main.c new file mode 100644 index 0000000000..7ba821be7b --- /dev/null +++ b/userland/capsule_nsfb_probe/src/main.c @@ -0,0 +1,62 @@ +#include "nonos_marker.h" +#include "nonos_surface.h" +#include "nonos_scene.h" + +#define SCENE_Z 1000u + +static void hang(void) { + for (;;) + nonos_yield(); +} + +static void paint_gradient(u64 base, u32 w, u32 h, u32 stride) { + volatile u32 *px = (volatile u32 *)base; + u32 spx = stride / 4u; + for (u32 y = 0; y < h; y++) { + for (u32 x = 0; x < w; x++) { + u32 r = (x * 255u) / w; + u32 g = (y * 255u) / h; + px[(u64)y * spx + x] = 0xFF000000u | (r << 16) | (g << 8); + } + } +} + +int main(void) { + u32 port = nonos_lookup_port("compositor", 10); + if (!port) { emit_fail("lookup", 0); hang(); } + + struct display_info di; + i64 rc = ncmp_display_info(port, 1, &di); + if (rc != 0) { emit_fail("display_info", rc); hang(); } + + u64 byte_len = (u64)di.stride * (u64)di.height; + u64 base = nonos_mmap(byte_len); + if (!base || (base & 0xFFFu)) { emit_fail("mmap", (i64)base); hang(); } + + struct surface_descriptor desc; + desc.width = di.width; + desc.height = di.height; + desc.stride = di.stride; + desc.format = SURFACE_FORMAT_ARGB8888; + desc.byte_len = byte_len; + desc.base_va = base; + desc.flags = 0; + + i64 sid = nonos_surface_register(&desc); + if (sid < 0) { emit_fail("register", sid); hang(); } + + i64 handle = nonos_surface_share((u64)sid); + if (handle <= 0) { emit_fail("share", handle); hang(); } + + paint_gradient(base, di.width, di.height, di.stride); + + rc = ncmp_scene_submit(port, 2, (u64)handle, 0, 0, di.width, di.height, SCENE_Z); + if (rc != 0) { emit_fail("submit", rc); hang(); } + + rc = ncmp_damage_commit(port, 3, 0, 0, di.width, di.height); + if (rc != 0) { emit_fail("damage", rc); hang(); } + + emit("[NSFB-PROBE] PASS\n"); + hang(); + return 0; +} diff --git a/userland/capsule_nsfb_probe/src/nonos_marker.h b/userland/capsule_nsfb_probe/src/nonos_marker.h new file mode 100644 index 0000000000..aefc49ba1d --- /dev/null +++ b/userland/capsule_nsfb_probe/src/nonos_marker.h @@ -0,0 +1,47 @@ +#ifndef NONOS_MARKER_H +#define NONOS_MARKER_H + +#include + +#include "nonos_sys.h" + +static inline u64 cstr_len(const char *s) { + u64 n = 0; + while (s[n]) + n++; + return n; +} + +static inline void emit(const char *s) { + write(1, s, cstr_len(s)); +} + +static inline u64 fmt_i64(char *buf, i64 v) { + char tmp[24]; + u64 i = 0; + u64 neg = v < 0; + u64 u = neg ? (u64)(-v) : (u64)v; + do { + tmp[i++] = (char)('0' + (u % 10)); + u /= 10; + } while (u); + u64 n = 0; + if (neg) + buf[n++] = '-'; + while (i) + buf[n++] = tmp[--i]; + buf[n] = 0; + return n; +} + +static inline void emit_fail(const char *step, i64 rc) { + char num[24]; + fmt_i64(num, rc); + emit("[NSFB-PROBE] FAIL:"); + emit(step); + emit(":"); + emit(num); + emit("\n"); +} + +#endif From df2c3d816cb72fe7cfd65071a45d388d65c53fcc Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:29:55 +0600 Subject: [PATCH 66/72] feat(netsurf): add capsule_nsfb_probe kernel mirror scaffold Phase 4 Task 4.0b wires the userland nsfb_probe C capsule into the kernel so the framebuffer probe can be embedded and spawned. This adds the kernel-side mirror (embed bytes + verified spawn path), mirroring capsule_c_proof. The unverified smoke path and the init call site land in follow-up commits to keep each file change small. Each file stays within the 75-line per-file change limit; spawn.rs is built across two commits because the full builder exceeds it. --- src/userspace/capsule_nsfb_probe/embed.rs | 44 ++++++++++++++ src/userspace/capsule_nsfb_probe/mod.rs | 20 +++++++ src/userspace/capsule_nsfb_probe/spawn.rs | 70 +++++++++++++++++++++++ src/userspace/mod.rs | 1 + 4 files changed, 135 insertions(+) create mode 100644 src/userspace/capsule_nsfb_probe/embed.rs create mode 100644 src/userspace/capsule_nsfb_probe/mod.rs create mode 100644 src/userspace/capsule_nsfb_probe/spawn.rs diff --git a/src/userspace/capsule_nsfb_probe/embed.rs b/src/userspace/capsule_nsfb_probe/embed.rs new file mode 100644 index 0000000000..a87ae963eb --- /dev/null +++ b/src/userspace/capsule_nsfb_probe/embed.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 . + +#[cfg(feature = "nonos-capsule-nsfb-probe")] +pub(crate) const NSFB_PROBE_ELF: &[u8] = include_bytes!( + "../../../userland/capsule_nsfb_probe/target/x86_64-nonos-user/release/nsfb_probe" +); + +#[cfg(feature = "nonos-capsule-nsfb-probe")] +pub(crate) const NSFB_PROBE_NONOS_ID_CERT_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/nsfb_probe.nonos_id_cert.bin"); + +#[cfg(feature = "nonos-capsule-nsfb-probe")] +pub(crate) const NSFB_PROBE_MANIFEST_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/nsfb_probe.manifest.bin"); + +#[cfg(feature = "nonos-capsule-nsfb-probe")] +pub(crate) const NSFB_PROBE_ATTESTATION_BYTES: &[u8] = + include_bytes!("../../../nonos-data/trust/capsules/nsfb_probe.zk_trailer.bin"); + +#[cfg(not(feature = "nonos-capsule-nsfb-probe"))] +pub(crate) const NSFB_PROBE_ELF: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-nsfb-probe"))] +pub(crate) const NSFB_PROBE_NONOS_ID_CERT_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-nsfb-probe"))] +pub(crate) const NSFB_PROBE_MANIFEST_BYTES: &[u8] = &[]; + +#[cfg(not(feature = "nonos-capsule-nsfb-probe"))] +pub(crate) const NSFB_PROBE_ATTESTATION_BYTES: &[u8] = &[]; diff --git a/src/userspace/capsule_nsfb_probe/mod.rs b/src/userspace/capsule_nsfb_probe/mod.rs new file mode 100644 index 0000000000..b5d7d822b2 --- /dev/null +++ b/src/userspace/capsule_nsfb_probe/mod.rs @@ -0,0 +1,20 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod embed; +mod spawn; + +pub use spawn::spawn_nsfb_probe_capsule; diff --git a/src/userspace/capsule_nsfb_probe/spawn.rs b/src/userspace/capsule_nsfb_probe/spawn.rs new file mode 100644 index 0000000000..bdafa3ad3c --- /dev/null +++ b/src/userspace/capsule_nsfb_probe/spawn.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 . + +use super::embed::NSFB_PROBE_ELF; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use super::embed::{ + NSFB_PROBE_ATTESTATION_BYTES, NSFB_PROBE_MANIFEST_BYTES, NSFB_PROBE_NONOS_ID_CERT_BYTES, +}; +use crate::capabilities::Capability; +use crate::kernel_core::process_spawn::capsule_spawn::{self, SpawnError}; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpecVerified; +#[cfg(feature = "nonos-dev-unverified-capsules")] +use crate::kernel_core::process_spawn::capsule_spawn::CapsuleSpec; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_id_cert::IdCertVerifyError; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +use crate::security::nonos_trust_anchor::{ + decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY, +}; + +const SERVICE_NAME: &str = "nsfb_probe"; +const SERVICE_PORT: u32 = 4512; +const REPLY_INBOX: &str = "endpoint.nsfb_probe.reply"; +const REPLY_PORT: u32 = 4513; +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +const TARGET_TRIPLE: &str = "x86_64-nonos-user"; + +fn requested_caps() -> u64 { + Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit() + | Capability::Debug.bit() + | Capability::GraphicsSurfaceCreate.bit() +} + +#[cfg(not(feature = "nonos-dev-unverified-capsules"))] +pub fn spawn_nsfb_probe_capsule() -> Result<(), SpawnError> { + 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: NSFB_PROBE_ELF, + nonos_id_cert_bytes: NSFB_PROBE_NONOS_ID_CERT_BYTES, + manifest_bytes: NSFB_PROBE_MANIFEST_BYTES, + attestation_trailer: NSFB_PROBE_ATTESTATION_BYTES, + target_triple: TARGET_TRIPLE, + requested_caps: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + Ok(()) +} diff --git a/src/userspace/mod.rs b/src/userspace/mod.rs index 73bb5a4f59..423fa069fb 100644 --- a/src/userspace/mod.rs +++ b/src/userspace/mod.rs @@ -62,6 +62,7 @@ pub mod capsule_settings; pub mod capsule_setup_wizard; pub mod capsule_snake; pub mod capsule_c_proof; +pub mod capsule_nsfb_probe; pub mod capsule_c_net; pub mod capsule_relibc_test; pub mod capsule_std_proof; From 6609dbe38205abfe26edf33cb312e669987a3c0b Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:30:19 +0600 Subject: [PATCH 67/72] feat(netsurf): add nsfb_probe unverified smoke spawn path The smoke profile spawns nsfb_probe through the legacy unverified path (CapsuleSpec) while the NZKCAPS2 capsule-attestation migration is owned elsewhere, matching c_proof/c_net. Completes the spawn builder begun in the scaffold commit. --- src/userspace/capsule_nsfb_probe/spawn.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/userspace/capsule_nsfb_probe/spawn.rs b/src/userspace/capsule_nsfb_probe/spawn.rs index bdafa3ad3c..b0ac64b637 100644 --- a/src/userspace/capsule_nsfb_probe/spawn.rs +++ b/src/userspace/capsule_nsfb_probe/spawn.rs @@ -68,3 +68,18 @@ pub fn spawn_nsfb_probe_capsule() -> Result<(), SpawnError> { capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; Ok(()) } + +#[cfg(feature = "nonos-dev-unverified-capsules")] +pub fn spawn_nsfb_probe_capsule() -> Result<(), SpawnError> { + let spec = CapsuleSpec { + name: SERVICE_NAME, + service_port: SERVICE_PORT, + reply_inbox: REPLY_INBOX, + reply_port: REPLY_PORT, + elf: NSFB_PROBE_ELF, + caps_bits: requested_caps(), + debug_tag: b"", + }; + capsule_spawn::spawn(&spec)?; + Ok(()) +} From 189285181d768bd48b48b775d8b0f8df6808032e Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:30:50 +0600 Subject: [PATCH 68/72] feat(netsurf): spawn nsfb_probe after the driver fleet run_init brings up the compositor via spawn_display_core, then the virtio-gpu driver via spawn_drivers. nsfb_probe registers a surface and reads OP_DISPLAY_INFO, so it must spawn after both the compositor is serving and the GPU framebuffer exists. Placing the gated run_nsfb_probe() right after spawn_drivers satisfies that ordering; it is a no-op when the capsule feature is off. --- src/userspace/init/entry.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/userspace/init/entry.rs b/src/userspace/init/entry.rs index 31cd6cb0a7..6841b8ed6f 100644 --- a/src/userspace/init/entry.rs +++ b/src/userspace/init/entry.rs @@ -28,6 +28,7 @@ pub fn run_init() -> ! { spawn_plan::spawn_core_after_ramfs(); spawn_plan::spawn_display_core(); spawn_plan::spawn_drivers(); + run_nsfb_probe(); spawn_plan::spawn_vfs(); spawn_plan::spawn_network(); run_c_net(); @@ -71,6 +72,17 @@ fn run_c_proof() { #[cfg(not(feature = "nonos-capsule-c-proof"))] fn run_c_proof() {} +#[cfg(feature = "nonos-capsule-nsfb-probe")] +fn run_nsfb_probe() { + match crate::userspace::capsule_nsfb_probe::spawn_nsfb_probe_capsule() { + Ok(()) => boot_log::ok("NSFB-PROBE", "capsule spawned"), + Err(_) => boot_log::error("NSFB-PROBE capsule spawn failed"), + } +} + +#[cfg(not(feature = "nonos-capsule-nsfb-probe"))] +fn run_nsfb_probe() {} + #[cfg(feature = "nonos-capsule-relibc-test")] fn run_relibc_test() { match crate::userspace::capsule_relibc_test::spawn_relibc_test_capsule() { From be57e604dc54e8b9164c6be2443d7bd5e59cb886 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:31:28 +0600 Subject: [PATCH 69/72] feat(netsurf): add nsfb-probe capsule flag and boot profiles Adds the nonos-capsule-nsfb-probe embed flag plus two composing profiles: microkernel-nsfb-probe (verified) and the unverified microkernel-nsfb-probe-smoketest. Both pull in the compositor and virtio-gpu driver so a framebuffer exists for the probe to present to; the smoke profile adds nonos-zk-rollout so the graphics capsules' real ZK trailers stay non-fatal under the unverified spawn path, matching the cnet/relibc smoketests. --- Cargo.toml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 095d70f0e8..d95649cae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ nonos-capsule-std-proof = [] nonos-capsule-c-proof = [] nonos-capsule-relibc-test = [] nonos-capsule-c-net = [] +nonos-capsule-nsfb-probe = [] nonos-capsule-ripgrep = [] nonos-capsule-ramfs = [] nonos-capsule-wallpaper = [] @@ -232,6 +233,33 @@ microkernel-c-proof-smoketest = [ "nonos-capsule-c-proof", ] +# Phase-4 framebuffer probe (verified path). Brings up the compositor +# and the virtio-gpu driver so a real framebuffer exists, then spawns +# nsfb_probe which registers a surface, reads OP_DISPLAY_INFO, paints a +# gradient, and presents it. +microkernel-nsfb-probe = [ + "microkernel-core", + "nonos-production", + "nonos-capsule-proof-io", + "nonos-capsule-compositor", + "nonos-capsule-driver-virtio-gpu", + "nonos-capsule-nsfb-probe", +] + +# Unverified smoke build of the framebuffer probe: spawns nsfb_probe via +# the legacy path while the NZKCAPS2 migration is owned elsewhere. The +# compositor + virtio-gpu carry real ZK trailers that the always-on +# transparent verifier would reject, so `nonos-zk-rollout` keeps a stale +# trailer non-fatal — the same reason the cnet/relibc smoketests set it. +microkernel-nsfb-probe-smoketest = [ + "microkernel-core", + "nonos-dev-unverified-capsules", + "nonos-zk-rollout", + "nonos-capsule-compositor", + "nonos-capsule-driver-virtio-gpu", + "nonos-capsule-nsfb-probe", +] + microkernel-relibc-test-smoketest = [ "microkernel-core", "nonos-dev-unverified-capsules", From b269bf659dcb2d4296beb61201c124b3e60a8037 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 11:53:00 +0600 Subject: [PATCH 70/72] feat(netsurf): retry compositor acquisition in nsfb_probe The probe spawns right after spawn_drivers(), before the compositor is guaranteed to be serving. mk_service_lookup("compositor") returns ERRNO_NOENT immediately when the endpoint is not yet installed, and the first OP_DISPLAY_INFO call blocks only up to the kernel's 5000ms timeout then returns ERRNO_TIMEDOUT while the compositor is still in wait_for_setup(). Both failure modes hit the probe on the first attempt with no retry, so it would FAIL+hang on a transient startup race. Wrap lookup + OP_DISPLAY_INFO in a bounded retry (60 attempts, yielding between tries) mirroring capsule_c_net's connect-retry. Stale replies to timed-out display_info requests are dropped by the kernel's reply no-call guard, so re-issuing the call cannot desync later submit/damage replies. FAIL is emitted only after the bound is exhausted, preserving the original lookup/display_info markers; the success path is unchanged. --- userland/capsule_nsfb_probe/src/main.c | 36 ++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/userland/capsule_nsfb_probe/src/main.c b/userland/capsule_nsfb_probe/src/main.c index 7ba821be7b..cc1676e161 100644 --- a/userland/capsule_nsfb_probe/src/main.c +++ b/userland/capsule_nsfb_probe/src/main.c @@ -3,12 +3,34 @@ #include "nonos_scene.h" #define SCENE_Z 1000u +#define ACQUIRE_ATTEMPTS 60u +#define ACQUIRE_YIELDS 256u static void hang(void) { for (;;) nonos_yield(); } +static void acquire_delay(void) { + for (u32 i = 0; i < ACQUIRE_YIELDS; i++) + nonos_yield(); +} + +static u32 acquire_compositor(struct display_info *di, i64 *last_rc) { + *last_rc = 0; + for (u32 attempt = 0; attempt < ACQUIRE_ATTEMPTS; attempt++) { + u32 port = nonos_lookup_port("compositor", 10); + if (port) { + i64 rc = ncmp_display_info(port, 1, di); + if (rc == 0) + return port; + *last_rc = rc; + } + acquire_delay(); + } + return 0; +} + static void paint_gradient(u64 base, u32 w, u32 h, u32 stride) { volatile u32 *px = (volatile u32 *)base; u32 spx = stride / 4u; @@ -22,12 +44,16 @@ static void paint_gradient(u64 base, u32 w, u32 h, u32 stride) { } int main(void) { - u32 port = nonos_lookup_port("compositor", 10); - if (!port) { emit_fail("lookup", 0); hang(); } - struct display_info di; - i64 rc = ncmp_display_info(port, 1, &di); - if (rc != 0) { emit_fail("display_info", rc); hang(); } + i64 rc = 0; + u32 port = acquire_compositor(&di, &rc); + if (!port) { + if (rc == 0) + emit_fail("lookup", 0); + else + emit_fail("display_info", rc); + hang(); + } u64 byte_len = (u64)di.stride * (u64)di.height; u64 base = nonos_mmap(byte_len); From 5d109ec1e98a3c4deede0298feb59fcc71f1739a Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 12:10:10 +0600 Subject: [PATCH 71/72] feat(netsurf): wire nsfbprobe smoke make targets Add nonos-mk-nsfbprobe-smoke-prod/-test and include the nsfb_probe Capsule.mk so $(nsfb-probe_ARTIFACTS) resolves. The prod target depends on the compositor, virtio-gpu, and nsfb_probe artifacts the microkernel-nsfb-probe-smoketest profile embeds, so a source change to any of them auto-rebuilds and re-signs before the kernel link (the re-sign landmine). The test target hands the harness the prod target. --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index 1846fb2f4a..87872be68d 100644 --- a/Makefile +++ b/Makefile @@ -537,6 +537,7 @@ include userland/capsule_std_proof/Capsule.mk include userland/capsule_c_proof/Capsule.mk include userland/capsule_relibc_test/Capsule.mk include userland/capsule_c_net/Capsule.mk +include userland/capsule_nsfb_probe/Capsule.mk include userland/capsule_ripgrep/Capsule.mk include userland/capsule_ramfs/Capsule.mk include userland/capsule_keyring/Capsule.mk @@ -865,6 +866,19 @@ nonos-mk-cnet-smoke-prod: $(proof-io_ARTIFACTS) $(driver-virtio-net_ARTIFACTS) \ nonos-mk-cnet-smoke-test: @CNET_PROD_TARGET=nonos-mk-cnet-smoke-prod ./tests/boot/cnet.sh +.PHONY: nonos-mk-nsfbprobe-smoke-prod nonos-mk-nsfbprobe-smoke-test +nonos-mk-nsfbprobe-smoke-prod: $(compositor_ARTIFACTS) \ + $(driver-virtio-gpu_ARTIFACTS) $(nsfb-probe_ARTIFACTS) \ + nonos-mk-check-deps nonos-mk-ensure-signing-key + @echo "Building kernel (microkernel-nsfb-probe-smoketest, unverified)..." + @$(SDK_FLAGS) NONOS_SIGNING_KEY=$(KERNEL_SIGNING_KEY) \ + RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + $(CARGO) build $(KERNEL_BUILD_FLAGS) \ + --no-default-features --features microkernel-nsfb-probe-smoketest + +nonos-mk-nsfbprobe-smoke-test: + @NSFBPROBE_PROD_TARGET=nonos-mk-nsfbprobe-smoke-prod ./tests/boot/nsfbprobe.sh + nonos-mk-ramfs-prod: $(proof-io_ARTIFACTS) $(ramfs_ARTIFACTS) \ nonos-mk-check-deps nonos-mk-ensure-signing-key @echo "Building kernel (microkernel-ramfs)..." From 75ef0068f2720a55cda3f23242f4835f6bdc4426 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Tue, 30 Jun 2026 12:33:53 +0600 Subject: [PATCH 72/72] =?UTF-8?q?feat(netsurf):=20hardening=20=E2=80=94=20?= =?UTF-8?q?static=20asserts=20on=20surface=5Fdescriptor=20ABI=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit surface_descriptor is passed by pointer to SYS_SURFACE_REGISTER and is the template Phase 4.2 libnsfb/src/surface/nonos.c will copy; a layout drift would silently mis-deliver the frame buffer address to the kernel. Four _Static_assert checks lock sizeof (40) and the three u64 field offsets (byte_len=16, base_va=24, flags=32) as compile errors. --- userland/capsule_nsfb_probe/src/nonos_surface.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/userland/capsule_nsfb_probe/src/nonos_surface.h b/userland/capsule_nsfb_probe/src/nonos_surface.h index b11c3709f5..cb7146dfc5 100644 --- a/userland/capsule_nsfb_probe/src/nonos_surface.h +++ b/userland/capsule_nsfb_probe/src/nonos_surface.h @@ -1,6 +1,7 @@ #ifndef NONOS_SURFACE_H #define NONOS_SURFACE_H +#include #include "nonos_sys.h" #define PROT_RW 0x3 @@ -16,6 +17,14 @@ struct surface_descriptor { u64 base_va; u64 flags; }; +_Static_assert(sizeof(struct surface_descriptor) == 40, + "surface_descriptor size must be 40 bytes"); +_Static_assert(offsetof(struct surface_descriptor, byte_len) == 16, + "surface_descriptor byte_len must be at offset 16"); +_Static_assert(offsetof(struct surface_descriptor, base_va) == 24, + "surface_descriptor base_va must be at offset 24"); +_Static_assert(offsetof(struct surface_descriptor, flags) == 32, + "surface_descriptor flags must be at offset 32"); static inline u64 nonos_mmap(u64 len) { return (u64)nonos_sys6(SYS_MMAP, 0, len, PROT_RW, MAP_PRIVATE_ANON,