feat(netsurf): NetSurf-on-NONOS foundation — toolchain, relibc Pal, capsule ops, display de-risk gate - #259
Open
senseix21 wants to merge 72 commits into
Open
feat(netsurf): NetSurf-on-NONOS foundation — toolchain, relibc Pal, capsule ops, display de-risk gate#259senseix21 wants to merge 72 commits into
senseix21 wants to merge 72 commits into
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
nonosPal backend + clangx86_64-unknown-nonostoolchain (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 offmain.Phase status
x86_64-unknown-nonos+ relibc compiles; signed clang/relibc C capsule boots, enters CPL=3, runsmain→write→serial[C-PROOF] PASS[RELIBC-TEST] PASS(fs+malloc+clock)OP_SEEK/PREAD/PWRITE) +net.socketsnonblock/poll/timeout server + full relibc socket+epoll backend[C-NET] PASS(E2E TCP echo over SLIRP)[NSFB-PROBE] PASS+ screendumpThis 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) doesmmap → mk_surface_register → mk_surface_share → OP_DISPLAY_INFO → paint ARGB gradient → OP_SCENE_SUBMIT → OP_DAMAGE_COMMIT, and the gradient reaches the screen.make NONOS_DEV=1 nonos-mk-nsfbprobe-smoke-test→ serial[NSFB-PROBE] PASSand a QMP screendump whose four corners are channel-correct (black / red / green / yellow) — first boot, under hvf.SurfaceDescriptor40B byte-exact (_Static_assert-locked), NCMP framing per the audited contract, caps0x1119hand-synced (Capsule.mk ↔ spawn.rs), kernel mirror of thecapsule_c_proofpattern,microkernel-nsfb-probe-smoketestprofile (core + compositor + virtio-gpu + probe).mk_service_lookupNOENT +OP_DISPLAY_INFOtimeout while the compositor is inwait_for_setup) via a bounded retry.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
cargo testfor the kernel image). Phase gates[C-PROOF]/[RELIBC-TEST]/[C-NET]/[NSFB-PROBE]all pass.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+ thenonoslibnsfb backendnonos.cdelegating to the proven surface path) and Phase 5 (cross-build OpenSSL/curl/freetype + linknetsurf-fb+ package).