linux: finish the stack (#532 #535 #512 + install queue) - #559
Merged
Merged
Conversation
Capability was written out three times. bits_to_caps filters over all(), so an entry missing there is grantable in a signed manifest and resolves to nothing at spawn. That is how ForeignExec went missing. capability_table! generates the enum, bit(), all() and count() from defs.rs. bit.rs and all.rs are deleted. guard.rs fails the build if two capabilities share a bit. IO and Hardware enforce nothing: their only readers are can_read, can_write and can_hardware, none of which is called. Both documented as such, naming what really gates each thing. cap-audit counted any mention of Capability::X as a consultation, including inside dead checkers, which is why it passed. It resolves each checker to whether anything calls it now, with IO and Hardware as a recorded baseline so the list can only shrink.
MkLocalSign was gated on can_admin. Nothing requests Admin and it is in FORBIDDEN_AMBIENT, so vouch always failed: every package the Linux installer wrote had no trailer, and the exec gate then refused it. LocalSign gets its own bit. Admin is keys to everything including MkCapGrant, EnrolDevRoot is a different job, and AppInstall would let a capsule decide what the machine runs as a side effect of installing. Five of the seven places that must agree are here. The Linux manifest and its spawn mirror land with the personality. Trailer verification dispatches on the trailer's magic rather than nonos-stark-attest. Following the flag meant a local mint produced NZKCAPS2 while the verifier expected NZKSTRK1, and it surfaced as a malformed trailer rather than a configuration error. cap_table's doc says it is an entry gate, not the authority map. Two reviews have read it the other way and called the handlers unguarded.
Zero capabilities was being treated as containment. brk, munmap and the wayland shm path took a guest number and acted on it, so a guest could unmap the personality or ask for a buffer no machine has. Limits come from one address plan in guest/layout.rs now. They had already drifted: only map.rs knew the mapping cursor's neighbour is EXEC_BASE and not the stack. Paths resolve to a Key only file::resolve can mint and the store wrappers take nothing else, so /linux is the root by construction. PT_INTERP and library mappings were loaded unproven, which left the attestation gate on the main image doing nothing useful. Both proven, ELF arithmetic checked. argv was read under MAX_PATH, so long arguments failed execve with nothing saying why. The resolver hands out 100.64/10 addresses and maps them back at connect, and the installer runs over the mixnet. resolve_host in net_sockets still does a clearnet lookup and is not fixed here. Package bytes are still unauthenticated, so place_entry refuses to vouch for them and the exec gate refuses what lands. tar::entries stopped at the first end-of-archive block, which in an apk is the end of the signature stream, so unpack had never written a file. Mirrored in python: one stream, sig+ctl+data, unterminated middle, garbage tail.
A runtime installs handlers before main and checks the return. Answering ENOSYS made programs abort at startup that would otherwise have run to completion, because most of them never raise anything. rt_sigaction, rt_sigprocmask and sigaltstack now succeed and record what was asked. Nothing is ever raised. Delivery means pushing a frame onto a guest thread's stack and redirecting it, and the trap mechanism hands out a register frame without any way to rewrite one, so it cannot be done from here yet. That limit is written in the file rather than hidden behind the success: a program that depends on SIGALRM will hang rather than misbehave quietly, which is the failure that can be diagnosed. SIGKILL and SIGSTOP are refused as uncatchable, as Linux refuses them.
net.sockets offers socket, connect, send, recv, close and a readiness poll, keyed by the caller's pid, which maps onto the Linux calls almost one to one. A guest's descriptor now holds a handle that service issued to this capsule, so a guest reaches only the sockets opened for it. read and write route by descriptor kind, so a program that treats a socket as a file, which most do, works without knowing the difference. Closing one closes the handle behind it. poll answers for every descriptor. A file or a console is always ready, which is what Linux reports too. A socket is asked one handle at a time, because that is the shape of the readiness call the service serves, and inventing a batched form here would mean a second protocol with nobody on the other end. The opcodes are transcribed rather than imported: the server is a binary and its protocol module is not a library. The file they came from is named beside them, since a number that changes there and not here is a wrong operation rather than a failed one.
supervised_asid returned an asid and dropped the lock that made it true, so peer map, unmap, copy and protect all ran against an asid the supervisor no longer held. It returns the guard with it now. The entry stub saves the callee-saved five plus a pad so a forked child resumes on the parent's register state. Nothing else on the path writes them to memory and a handler's prologue may already be using them, so it happens in the stub or not at all. Six slots keeps the frame 16-byte aligned and leaves existing offsets alone. libc gains wrappers for foreign exec, fork and resume, peer TLS and unmap, and the local signing and consent calls.
# Conflicts: # src/capabilities/types/as_str.rs # src/capabilities/types/bit.rs # src/capabilities/types/defs.rs
bit.rs and all.rs are gone: every capability's bit now sits beside its name in types/defs.rs, and table.rs generates bit(), all() and count() from it. Three consumers still read the deleted files. The two ABI checkers parse defs.rs's `Name = 1 << n` entries instead of bit.rs's match arms; attest_receipt includes table.rs and guard.rs in place of bit.rs and all.rs; and kernel_proofs binds by value in the loops over all(), which now yields a static slice rather than an owned array. Refs #532
pr535 is a single commit made on a stale tree: taken whole it re-added the five signing calls 1aeddc6 removed (CEDV/CEDS/CEDP/CSKS/CSPB), dropped CryptoMachineKey, and reverted comment work in the syscall tables. The syscall-table files are resolved as main plus only the new numbers (MPTL MFFK MPUN MFEX MLSG MLVF MAIN MDRO), their dispatch, their gates and can_local_sign/can_app_install. MPTL..MFEX resolve once #512 lands.
de5a310 carried stale copies of abi/syscalls.toml and the libc export lists: taken whole they re-added the removed ed25519/secp256k1 calls, dropped CryptoMachineKey and undid the per-call capabilities of ac000a8 and a0d26d0. Those three files are resolved as main plus the new calls only (MPTL MFFK MPUN MFEX MLSG MLVF MAIN MDRO, their libc wrappers and numbers); the toml now also describes the four #535 numbers.
MkAppInstall (#535) hands a package name to init::request_install, which only #545 defined, alongside the App Store capsule and a marketplace index that must be signed into nonos-data. Only the queue is taken: a bounded, deduplicated list that init's supervisor loop drains through capsule_linux::spawn_install. #545's wake/priority rework is left out; the loop already parks for at most 20 ms, which bounds how long a request waits.
LocalSign (bit 33) is enforced by the kernel but was missing from abi/caps.toml, and MDRO published EnrolDevRoot while the cap table routes it through the same arm as MDRQ/MDRC, which the syscall-ABI gate reads as valid_token. The authority check stays inside the handler.
The kernel defines 34 capabilities after LocalSign landed, but the shell and terminal name tables, nonos_cap and the manifest parser stopped at ForeignExec, so a manifest could not name it and the UIs printed an unnamed bit.
The stub gate reads 'entry stub' as an admission of unsupported work. The assembly entry is complete; say what it is so the baseline need not grow.
The linux merges add userland/capsule_linux_proofs, so the regenerated evidence counts 55 runnable proof crates and CI's drift check fails against the committed 54.
syscall.S saves 17 words and frame_snapshot read 16, so a forked child got a kernel stack word in rbx and lost r15. The size now lives in syscall_frame.inc; syscall.S checks its saves against it and Rust reads it. Every foreign handler takes its pid through pid_arg, which refuses a value past u32 instead of letting 2^32 + n name process n.
The one-list table computes each bit as a shift, so per-case rfl no longer unfolds has_capability and add_capability to a literal. Both now take the bit's value from bit_spec, which still closes per case, and are uniform over the variants instead of enumerating them.
Collaborator
Author
|
@eKisNonos Review before merge (update) Scope Our changes
Findings
Verdict: holding for boot verification. |
Collaborator
Author
|
@eKisNonos Boot verification of head
Verdict: merging. CI was green on this head: ci, verify, lean and boot-smoke. |
eKisNonos
added a commit
that referenced
this pull request
Sep 27, 2026
#562's init boost is kept in the branch's instance_spawn::priority form, which masks interrupts around the lock; main's entry.rs guard is kept. The ABI table takes main's caps_any order; every ABI check passes.
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.
Finishes the Linux stack.
#532, #535 and #512 each depend on another in a cycle, so none of them can merge on its own:
LocalSign(bit 33) is defined in capabilities: generate the enum, bit and all from one list #532.spawn_install, the libc wrappers and theN_MK_*numbers are in linux: signals recorded, sockets and poll served #512.request_install, which lives in store: the app store capsule #545.This branch merges them in that order on top of main. Each one is a real merge commit, so GitHub closes #532, #535 and #512 as merged.
Also in this PR
f9aa0e2cd: only the install queue (request_install) from store: the app store capsule #545. The App Store capsule and market: one release codec, and check a release before offering it #544's marketplaceindex.binare left out because that index still has to be signed. They land separately.5883548b2: restoresrelease_new. It was moved out of process: purge the reap log too when a pid is reused #533, which had no caller; its first caller is linux: signals recorded, sockets and poll served #512'sstart_context.cd02d652c: restoresregistry::is_foreignforpark()'s recheck. process: run foreign binaries without teaching the kernel about them #506 deferred it to its first caller.c9798ad02/320012545: publishLocalSignand the MDRO cap row, and mirrorLocalSigninto the four userland capability tables.54ba01172: stubs gate.a0c610085: evidence count.Local gates