Skip to content

feat(netsurf): NetSurf-on-NONOS foundation — toolchain, relibc Pal, capsule ops, display de-risk gate - #259

Open
senseix21 wants to merge 72 commits into
mainfrom
feat/netsurf-relibc-fork
Open

feat(netsurf): NetSurf-on-NONOS foundation — toolchain, relibc Pal, capsule ops, display de-risk gate#259
senseix21 wants to merge 72 commits into
mainfrom
feat/netsurf-relibc-fork

Conversation

@senseix21

@senseix21 senseix21 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

NetSurf-on-NONOS foundation — toolchain, relibc Pal, capsule ops, display de-risk gate

Brings up the foundation for running NetSurf as a signed NONOS capsule via a relibc nonos Pal backend + clang x86_64-unknown-nonos toolchain (not a hand-rolled engine). Long-lived branch built across multiple sessions; the latest arc (Phase 4 Task 4.0) is freshly landed and pixel-proven. 96 commits off main.

Phase status

Phase What Proof
0 — toolchain clang x86_64-unknown-nonos + relibc compiles; signed clang/relibc C capsule boots, enters CPL=3, runs mainwrite→serial [C-PROOF] PASS
1 — relibc Pal de-stub full relibc startup (malloc/stdio/environ); time/mem/getrandom/fs real over IPC [RELIBC-TEST] PASS (fs+malloc+clock)
2 — capsule ops VFS random-access (OP_SEEK/PREAD/PWRITE) + net.sockets nonblock/poll/timeout server + full relibc socket+epoll backend [C-NET] PASS (E2E TCP echo over SLIRP)
3 — futex determined a no-op (single-threaded runtime already proven; NetSurf dep shelf is single-threaded) — confirmed at the Phase-5 link ✅ decision recorded
4 — display shim Task 4.0 (de-risk gate, this arc): pure-C capsule paints a gradient through the real compositor path, presented to screen [NSFB-PROBE] PASS + screendump

This arc — Phase 4 Task 4.0 (display-shim de-risk gate)

Before vendoring NetSurf + libnsfb, the verified surface/compositor contract is proven end-to-end. capsule_nsfb_probe (pure C) does mmap → mk_surface_register → mk_surface_share → OP_DISPLAY_INFO → paint ARGB gradient → OP_SCENE_SUBMIT → OP_DAMAGE_COMMIT, and the gradient reaches the screen.

  • Gate: make NONOS_DEV=1 nonos-mk-nsfbprobe-smoke-test → serial [NSFB-PROBE] PASS and a QMP screendump whose four corners are channel-correct (black / red / green / yellow) — first boot, under hvf.
  • Implementation: hand-rolled C inline-asm syscall wrappers (clobber set defuses the NONOS rdi/rsi/rdx convention), SurfaceDescriptor 40B byte-exact (_Static_assert-locked), NCMP framing per the audited contract, caps 0x1119 hand-synced (Capsule.mk ↔ spawn.rs), kernel mirror of the capsule_c_proof pattern, microkernel-nsfb-probe-smoketest profile (core + compositor + virtio-gpu + probe).
  • Confirmed-real compositor-readiness race fixed (mk_service_lookup NOENT + OP_DISPLAY_INFO timeout while the compositor is in wait_for_setup) via a bounded retry.
  • Genuine, non-vacuous boot gate: a black / inverted / channel-swapped / missing screendump all FAIL closed; PASS requires both the serial marker and all four corner thresholds.

Built under subagent-driven-development: each sub-task implement → independent review → fix → complete; a final whole-arc review came back 0 Critical / 0 Important with the four-layer contract source-verified byte-exact.

Test state

  • The per-phase boot smoketests are the verification harness (no cargo test for the kernel image). Phase gates [C-PROOF] / [RELIBC-TEST] / [C-NET] / [NSFB-PROBE] all pass.
  • The host cargo check --features std (#[panic_handler] not found) is pre-existing on the base tree (reproduces without this branch's changes) — tracked separately, not a regression introduced here.

Not in this PR (follow-on)

Phase 4.1+ (vendor netsurf-all-3.11 + the nonos libnsfb backend nonos.c delegating to the proven surface path) and Phase 5 (cross-build OpenSSL/curl/freetype + link netsurf-fb + package).

senseix21 added 30 commits June 28, 2026 21:25
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.
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.
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.
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.
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.
faccessat/openat/read/write-family/dirents/links/stat + mmap-family
(mlock/mprotect/mremap/msync/madvise/nanosleep) + getpagesize=4096.
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.
All of PalSocket(15)/PalSignal(12)/PalEpoll(3)/PalPtrace(1) stubbed to
ENOSYS (sigsuspend returns Errno directly). Completes the sub-trait surface.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
… init wiring

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).
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).
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.
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.
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.
…c_crt0

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.
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).
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.
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.
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.
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.
senseix21 added 30 commits June 29, 2026 16:52
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.
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.
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).
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.
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.
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.
… 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).
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.
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<u8> 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<u8> 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.
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).
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
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
39f3aeb landmine). SurfaceDescriptor mirrors the 40-byte
wire.toml layout (byte_len@16, base_va@24, flags@32).
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.
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:<step>:<rc> 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.
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.
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.
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.
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.
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.
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.
…ayout

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant