Skip to content

linux: land the Linux lane on main; terminal linux command, signals to running threads, pipe waits, madvise, VFS store, Process Manager - #593

Open
eKisNonos wants to merge 417 commits into
mainfrom
linux/sync
Open

eKisNonos wants to merge 417 commits into
mainfrom
linux/sync

Conversation

@eKisNonos

@eKisNonos eKisNonos commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Base: main. This is the integration branch for the Linux lane. It merges the open lane and fix PRs, so it builds and boots as one image:

It also records #582's nine newest commits. Four are here as the same commits; five are earlier forms of commits this branch carries in the split form linux/next-waits gave them. So #582, #592 and this one merge into main in any order.

On top of those it carries 76 commits of its own, listed below. Its diff against main shrinks as each of those PRs merges; merging this one lands them all.

Its own commits

Terminal runs Linux programs

  • The kernel lets the terminal run the Linux personality as a tool (tool.linux, not in the embedded tools' registry).
  • linux <prog> [args] runs a program from the Linux tree. A bare name is found along the guest's PATH, and every argument is passed on.
  • The terminal shows the program's own output, not the run's progress. A missing program is told apart from a store in error, and an empty name gets the usage line.
  • A console write longer than 256 bytes reaches the log whole.
  • Only a foreground program's end resets the screen's modes. A background job's end resets them when nothing holds the screen.
  • A baked tool runs as a foreground program, not an install. A tool that fails to start says why, and empty arguments are kept.

Signals to a running thread

  • A thread running its own code is stopped at a tick and takes its handler there.
  • A signal left pending by an answer is delivered, not forgotten.
  • A handler is entered with DF clear and a clean FPU. A frame nested on the alternate stack is bounded.
  • A stop mark lasts until a handler is entered, and is spent when the answer is posted.
  • MkForeignExec refuses a thread stopped at a tick, and a syscall numbered as a stop code gets ENOSYS.
  • A dying supervisor's guests are torn down, not abandoned.

Waits and pipes

  • readv and writev wait on a pipe as read and write do.
  • A write to a blocking pipe answers once all of it is in. After a partial write it answers as Linux does.

Memory

  • madvise does what Linux promises, or refuses. On a span with a hole it advises what is mapped, then returns ENOMEM.
  • A new mapping is the only record of its span. mprotect's commit and mremap's growth keep the mark.
  • A fixed anonymous mapping gives zeros over old frames.
  • fork copies each region a megabyte at a time.

Store and VFS

  • The store holds up to 48 MiB in a 128 MiB heap.
  • Each entry's buffer is sized once, and an entry that won't fit is refused.
  • The status clears once a later attempt stages the whole store.
  • A program's shared libraries are attested, so they may run.

Process Manager

  • A guest's program, calls and pages show in the process table.
  • Share of RAM is shown to the tenth, with who keeps guest pages.
  • Every capability the kernel defines is named.
  • A click on Overview selects the row under it.

Kernel

  • A dead process is reclaimed on a tick that interrupts the idle halt too.
  • With no IOMMU in service, a driver is left unconfined, not refused.

Also

  • The market catalogue gains a curated security-toolset list.
  • The terminal fills block elements as exact parts of the cell.

Proof guests (C)

  • cpipe: bytes through a pipe between two processes.
  • csig: signals for threads that were parked.
  • cpreempt: a handler reaching a spinning thread, with DF set.
  • cmadv: madvise against Linux's rules.
  • cwait: waits, timers, pthread_kill.

Checked


The block was aligned to the layout and the data handed out 32 bytes
into it, after the header, so any request aligned to 64 or more came
back aligned to 32 only. FXSAVE needs 16 and never noticed; XSAVE needs
64 and took a general protection fault on the first switch.

The data now starts at the header size rounded up to the alignment,
with the header directly before it, where free and verify already look.
Free computes the same offset from the same layout.
install pacman:<name> fetches each configured repository's database and
its detached signature, refuses the install if the database is unsigned
or does not verify, and resolves the closure by name and then by what a
package provides. Each package is held to the market's pin when chosen,
to the database's SHA-256, and to its own OpenPGP signature, then
decompressed (zstd, gzip or plain tar; xz is refused by name) and placed
through the same verified path as Alpine's.

Nothing is guessed: the mirror, Host line, path and repositories are
build settings, and the keyring is pinned into the capsule from the
file NONOS_PACMAN_KEYRING names. An image built without them refuses
every pacman install and says which is missing.

RSA goes to the crypto service as an SPKI with the signature padded to
the modulus width. The proof crate runs that exact request through the
checks the service runs, against GnuPG's RSA-3072 signatures, and tests
the desc reader, including file names that would leave the repository.
Streams with padding between them, blocks, the index and the footer,
each held to its CRC-32 and to one another: a block's declared sizes to
what it decoded, the index to every block, the footer to the header and
the index. LZMA2 chunks stored or LZMA, with dictionary, state and
property resets; every check type xz writes (none, CRC-32, CRC-64,
SHA-256). LZMA2 is the one filter read; a block naming BCJ or any other
is refused, never skipped.

The vectors are XZ Utils' output for the zstd vectors' inputs, from -0
to -9e, multi-block, odd lc/lp/pb, a 4 KiB dictionary. The test
regenerates the inputs from the one generator. Breaking the literal
state update or the rep3 shift fails them. A seeded mutation harness
damages six vectors 2000 ways each in a debug build.
A package or database compressed with xz is decoded with nonos_xz
instead of refused; zstd, gzip and plain tar are unchanged. The bytes
are the same verified bytes either way: the checksum and signature are
checked on the compressed file before any decompression.
The base64 between the BEGIN and END lines, armor headers skipped, and
the CRC-24 line checked when there is one. Padding anywhere but the end
and characters outside the alphabet are refused.

The vectors are regenerated with an armored RSA key and an armored
detached signature; tests check the armored key has the exported key's
fingerprints, the armored signature verifies, and a changed checksum
line is refused. A seeded harness damages both 4000 ways each.
FNINIT and LDMXCSR reset the x87 unit and the SSE control word and
nothing else, so a thread with no saved state began with xmm and the
upper ymm halves as the last thread on the CPU left them. The guest
state suite caught it: a forked child read a sibling's bytes in ymm0-15
in 254 of 300 rounds.

init now restores a constant clean area, FCW 0x037F and MXCSR 0x1F80
with every register zero and an XSAVE header marking every component
initial, through XRSTOR, or FXRSTOR where XSAVE is off. The signal path
uses the same init, so a handler starts clean too.
The OpenPGP check, the crypto service request, decompression by magic
and the SHA-256 hex reader move out of pacman into install, where the
Debian backend will use the same code rather than a copy of it. Nothing
changes in what they do; the proof crate mounts them from their new
places and passes unchanged.
install deb:<name> fetches the suite's Release and Release.gpg (armored
or binary) and refuses the install unless it verifies under the archive
key pinned from NONOS_DEB_KEYRING. Each component's Packages.xz or
Packages.gz must match the size and SHA-256 the Release gives it, and
each .deb the SHA-256 its stanza gives, and the market's pin when it is
the one chosen. The data member is unpacked through the same verified
path as Alpine's and pacman's; maintainer scripts are never run. Needs
are groups of alternatives and the first one the index has is taken.
Release's Valid-Until is not checked, for want of a trusted clock, and
the source says so.

The tar reader now strips the ./ dpkg writes before every member name
and hard-link target. Without it every file in a .deb was refused as a
dot name and nothing was placed; the three .deb tests fail when the
stripping is removed. Symlink targets are left as written.

The family is now chosen in family.rs, so run.rs is Alpine's alone.

Vectors are made with Debian's tools: .debs from dpkg-deb with xz, zstd
and gzip data members, a symlink, alternatives and a virtual package,
indexed by dpkg-scanpackages, and a Release signed with a throwaway
GnuPG key. The proof crate walks the chain an install walks, checks a
changed Release fails, refuses pool paths that leave the root, and
damages the .debs, Packages, Release and pacman desc records 1500 ways
each in a debug build.
The kernel was compiled with SSE and SSE2, so compiled kernel code on the
interrupt and syscall paths used xmm registers before the switch saved
the thread's state: the timer handler overwrote a guest's xmm0-15 and the
save that followed stored the kernel's values. The guest state suite
lost every one of 300 rounds in both processes.

The kernel target now matches x86_64-unknown-none: every SIMD feature
off and the soft-float ABI. Floating point in the kernel becomes soft
float; the only vector instructions left are the FPU save and restore
and the SIMD exception handler's MXCSR access, all written by hand.
Userland keeps its own target, SSE included. The kernel's warnings are
the same 47 either way.
The soft-float target covers Rust, but the kernel also links C and
assembly compiled apart from it. PQClean's ML-DSA, ML-KEM and SHAKE were
built with the compiler's default SSE, and BLAKE3 linked its SSE2 and
SSE4.1 assembly: 1548 vector moves in kernel text, the PQClean ones on
paths a syscall reaches, such as checking a capsule's signature.

The kernel's C and assembly are built with -mno-mmx -mno-sse -mno-sse2
-mno-avx, and BLAKE3 with its portable implementation on every target.
The aarch64 core profile (microkernel-core + nonos-arch-preview) had not
compiled at any commit in this history: 4 errors at the root commit, 59
here, with codegen and link failures behind them. It now builds and links.

- process::foreign is x86_64 only. Elsewhere foreign_absent.rs answers
  every SYS_FOREIGN_* and SYS_PEER_* call with ERRNO_NOSYS: no other
  architecture parks an unknown syscall for a supervisor.
- memory::mmu stays x86_64 only. Its users are gated; on aarch64 the boot
  log states PAN is not enabled and the hardening entry point refuses by
  name.
- mapping_in_asid masks interrupts through the helper its siblings use,
  AddressSpace::kernel reads the root through arch::paging::read_root,
  and the aarch64 descriptor backend exports is_executable.
- STZG takes its tag from a register holding 0; XZR is not encodable in
  that field.
- linker_aarch64.ld defines the eight __kernel_* section bounds that
  layout::kernel_sections reads on the boot path.
- The lib.rs comment states what the arch gate does and nothing more.

x86_64 microkernel-core: all three PT_LOAD segments are byte identical
before and after. Only a ThinLTO .llvm symbol suffix in .strtab moves.
The FEAT_PAN probe read ID_AA64MMFR1_EL1[3:0], which is HAFDBS. PAN is
[23:20]. A core with HAFDBS and no PAN would have executed MSR PAN and
taken an undefined instruction on the first user copy; a core with PAN
and no HAFDBS would never have opened the window.

The comment on allow() and deny() said MSR PAN was encoded by hand. It
was not, and the target spec has no +pan, so LLVM rejects both on the
first concrete caller of with_user_access. They are now .inst 0xd500409f
and 0xd500419f, which llvm-objdump decodes as msr PAN, #0 and #1.
Seven static_mut_refs warnings: the table getters formed &mut and & to
whole static mut items. The address getters now use addr_of!, which
creates no reference, and the &'static mut getters borrow through
addr_of_mut! with the aliasing rule written as a # Safety contract. The
secondary entry is cast through its fn pointer type rather than as a
function item.
cause.rs imported two contract types it never names, pl011 re-exported
two config types no path reaches, the AP idle path read its CPU id and
discarded it, and FP_SIMD_CONTEXT_BYTES had no user.
Add a build-aarch64 lane that runs make nonos-mk-arm, the target a
developer runs, then checks the output with check_kernel_elf.py: an
AArch64 static executable whose entry is _start inside an executable
PT_LOAD, no segment both writable and executable, and the signed
manifest and signature sections present.

nonos-mk-arm now asks only for the Ed25519 seed that build.rs reads,
not an ML-DSA keypair it never uses. nonos-setup skips the aarch64
JSON targets the same way it skips the x86_64 ones.
"not ready, refused" named no reason: the transport error, or which of
the seven readiness checks failed, or a release that did not resolve.
Each is now printed with the listing, so a refusal says what to fix.
bind, listen and accept4 under a NetBind capability carrying an explicit
bind set, loopback and external named apart, no shared ports, accepted
sockets confined like their listener, and raw sockets refused by name.
With the five properties it must be proved against.
The store asks to install a listing's default release with an empty id.
Readiness read that as the first release; get_release looked for a
release named "" and found none. Init was told the package was ready,
got no release to pin, and refused every store install.

get_release now resolves through the same find_release, and the proof
crate pins it: an empty id is the first release, a named id is that one,
an unknown listing or release is nothing.
The standing pane assumed the gate list was six rows tall, but it also
paints the verdict and a note line above them, so the measurement label
was drawn over the attestation row. gates::paint now returns the y it
ended at and the pane carries on from there.
The personality is spawned as app.linux.install and app.linux.run on
endpoints 4938/4939 and 4942/4943, and its manifest listed only
app.linux's. The spawn gate refuses an endpoint the manifest does not
declare, so every store install stopped at capsule_verify_fail and
every run of an installed package would have too.

init also prints the spawn error when the installer is refused, which
is how this was found.
The installer exits with a code that names why it stopped: the index did
not verify, something is in no index, the closure is too large, a package
did not download or verify, or the image has no mirror or key for the
family. init records every asked-for install as queued, running, refused
or, once the installer has ended, installed or failed with that code,
read without reaping from the process table or the reap log.

MkAppInstallStatus returns that stage for a listing to any caller that
holds AppInstall, the capability MkAppInstall already needs; asking
changes nothing. It is in the syscall registry, the capability table,
the router, the ABI description and libc as mk_app_install_status.
A request no longer ends at "install requested". The store asks the
system where each install stands every half second while one is moving,
and not at all when none is. The card's one button reads Install,
Queued, Installing, then Open once installed, or Retry after a failure,
and the detail pane says what happened in a sentence, naming the reason
the installer gave. Enter does the next thing: install, wait, or open.
On refresh every Linux listing is asked about once, so one installed
earlier in the session reads as installed.

The pane no longer repeats the verdict as a separate Install label, and
the status bar's Enter hint is the button's own word.
The detail pane asks the market for the default release and shows its
version and the host its bytes come from under the publisher, and the
operator's validation note, such as the size it hashed, below the
gates.
The line being typed opened with ">~", which read like a redirect into a
file named "~". It is "~ $ " now, path first, the mark in the last
command's colour, and a space before the cursor; echoed commands carry
the same "$ ".

The window opens at 960 by 540, centred on a 1280 by 720 screen, where
760 by 460 was filled by help alone.

Bare help was nineteen lines of commands, key bindings and syntax at
once. It is now the command groups, with the group names in the accent
colour, and three ways to go deeper: help keys, help shell, and help
<command> as before. Each page only lists what the shell does: the
separators and the alias form were checked against the parser.
The x86_64 backend picks the IOMMU from the firmware tables instead of
assuming VT-d: a DMAR remapping unit selects VT-d, else an IVRS table
selects AMD-Vi, else none. CPUID is never read, so QEMU's intel-iommu
under KVM on an AMD host stays VT-d.

select_vendor, capabilities and report_posture state only guarantees in
force now. VT-d reports enforcing only while translation is live and the
firmware described exactly one unit, because bring-up programs the first
unit only. Snoop control is read from ECAP.SC through the new decoder
regs::cap::snoop_control, not from the leaf flag. AMD-Vi and machines
with no IOMMU report nothing in force.

Domain calls are routed by the selected vendor. On VT-d, and before
selection has run, they reach the unchanged VT-d functions. Otherwise
each is refused with a log line and IommuError::AmdViNotDriven or
IommuError::NoIommu. The VT-d tree gains only the decoder.

No live DMA outcome changes: MkDmaMap and the direct memory::dma
allocators still hand out host-physical addresses and IommuDomain still
has no caller. The Cargo.toml comment that claimed otherwise is fixed.

x86_64 behaviour changes:
B1 init_dma_protection selects first. The VT-d probe and bring-up run
   only when DMAR described a remapping unit, as the same two calls in
   the same order.
B2 without a DMAR unit, "[VT-D] no remapping units in DMAR; DMA is
   unrestricted" is no longer printed. An [AMD-VI] or [IOMMU] selection
   line naming the missing backend replaces it.
B3 every boot prints "[IOMMU] vendor= enforcing= aw= ir= snoop= pages=
   domains=" after the VT-d lines, which are unchanged.
B4 after selection on AMD-Vi or IOMMU-less machines the six domain
   calls return AmdViNotDriven or NoIommu with a log line instead of
   NotInitialized. Nothing calls them today.

The x86_64 core image changes: .text grows by 1408 bytes and .rodata by
448. PT_LOAD count, addresses and flags are unchanged.
…i-up

Cell.iommu now names the device QEMU presents: "" for none,
"intel-iommu" or "amd-iommu", and qemu.py maps each to its -device
argument. Every cell that reaches readiness must print the [IOMMU]
posture line with the vendor and enforcing value its device calls for:
none 0, intel-vt-d 1, amd-vi 0.

The intel-iommu cells keep both existing checks: no "DMA is
unrestricted", and the "[VT-D] enumerated devices identity mapped;
others denied" line present. The new q35-amdvi-up cell boots the
single-CPU image with an amd-iommu and must also carry the [AMD-VI]
line that names the hardware the kernel does not drive, so the refusal
is asserted rather than assumed.

No kernel change; the image is untouched by this commit.
The rule a trust anchor is held to (three independent sources agreeing,
recorded with where and when), the two claims a pinned keyring can and
cannot make, and the BlackArch finding of 2026-09-27: the keyring tarball
is signed by a key its own trusted list does not include, the key
strap.sh pins is a different one, and no source outside blackarch.org
was reachable. The backend stays keyless and refuses every install.
table() took its second argument as _user_accessible and dropped it, while
table_grants_user reads APTable[0] (bit 61), a bit table() never set. Every
interior entry the aarch64 backend wrote therefore granted EL0, including
the kernel-half entries seed_kernel_half_pdpts builds with false. A table
built without user access now sets APTABLE_NO_EL0, as x86_64 clears U/S.

- arch_paging_proofs: table_grants_user(table(pa, user)) == user, as a host
  test on both backends and a Kani harness over every pa and both values.
  The host test fails on the previous encoder and passes on this one.
- Paging.lean is regenerated by the pinned Charon and Aeneas; the only
  change is table's body. Regenerating from the parent is byte stable.
- PagingRefinement: the_aarch64_table_honours_the_request and
  the_backends_agree_on_tables on the extracted code. The old body is kept
  as oldAarch64Table, and the_aarch64_table_ignores_the_request and
  the_backends_disagree_on_tables are restated on it rather than deleted.
  Checked with Lean 4.30.0-rc2 and the pinned Aeneas: lake build of the
  extraction corpus completes, 1720 jobs, and each new theorem depends on
  propext, Classical.choice and Quot.sound only.

x86_64 microkernel-core: all three PT_LOAD segments byte identical.
unreachable_pub reported 106 items on the aarch64 build. None hides dead
code: every one is used inside the crate. This applies rustc's own
suggestion (pub(crate) or pub(super)) to the 56 in the aarch64 tree.

Kept pub on purpose: the ten aarch64_exc_* handlers, which vectors.S calls
by symbol and so form the assembly ABI. The 40 remaining are in shared
files. unreachable_pub on aarch64 goes from 106 to 50. No file this touches
is compiled for x86_64.
Off x86_64, clear_low_half returned Ok having done nothing and init then
printed "[VM-INIT] low half cleared". aarch64 still executes from that
identity map, so it is kept and the log now says "low half kept"; the
"cleared" line prints only on x86_64, where the teardown happens.

The timer line said "TSC freq" on aarch64, where the rate is CNTFRQ_EL0.
The label is a per-architecture constant placed after every panic site,
so the call keeps its line and x86_64 keeps its bytes.

x86_64 microkernel-core: all three PT_LOAD segments byte identical.
deliver.rs said any thread answered without a handler is marked for its
next tick. Only a futex wake and rt_sigreturn do that (Guest::rearm); a
thread whose frame could not be placed or written is answered bare and
keeps the signal queued for its next answer. The doc now names the two.
Since 6331bf6 keeps empty arguments, `linux ""` made the empty token
the program. The PATH search resolved each directory for it, and the
launch reported "linux: /bin is a directory". An empty name now gets the
usage line, as a missing one does, and as it did before.
6f1f990 ended a thread's stop mark in settle(), when the thread takes
a handler answer. Between the post and the take the thread does not read
as parked, since parked_nr looks for one with no answer, so a
MkForeignInterrupt for another signal marks it and returns 0. settle()
then dropped that newer mark with the old one, and the second signal
waited for the thread's next call: a handler that spins without calls
never got it.

answer_raw now drops the mark as it posts a handler answer, under the
parked table's lock, and a mark set after that stops the thread at its
next tick. Other answers leave the mark as before.

The window was read from the code. No guest reaches it yet: a signal
does not end a futex wait here, and the personality keeps no signal
mask, so no boot shows the second signal lost before this change or
delivered after it.
Each part passed once its handler had run, on any thread, so a signal
delivered to the wrong thread of the process passed too. The worker and
each handler now record their thread id, and a part passes only when its
handlers ran on the worker; a handler run elsewhere prints "ran on
another thread". Checked on Linux both ways: as written all three parts
pass, and with the first signal sent to main instead all three fail so.
The spinning thread set the direction flag after publishing `spinning`,
so main could raise the signal before `std` ran, and the handler's check
that DF is clear on entry held whatever the kernel did. DF is now set
first.
Two more parts, for what 38af0e4 and 314495b change:

- A PROT_NONE reservation with a read-write MAP_FIXED mapping over half
  of it, as Go makes its heap, is written, given MADV_DONTNEED, and then
  read(2) fills one of its pages from a pipe. The pages must read zero
  and the read must land.
- The ENOMEM part now also checks that the mapped pages of the span
  with a hole read zero afterwards.

On Linux all nine parts pass.
Run from the terminal on 79d6a6a, cwait failed one part, "absolute
timer through epoll (1, 76)", where the same image's boot run gave 103
ms. The absolute deadline was taken from the clock before settime,
epoll_create1 and epoll_ctl, and the stopwatch started after them; with
the desktop running those three calls took about 24 ms, so a timer that
fired on time measured 76. The one-shot read started its stopwatch after
settime in the same way.

Both now time from before the timer is armed: the one-shot from before
settime, the absolute one from the clock reading its deadline is made
from. A timer firing early still fails. On Linux all 18 parts pass, the
two waits at 100 ms each.
A Linux guest was listed as foreign:linux or foreign:fork whatever it
ran, with 0 in its syscall and memory columns: its calls trap to the
supervisor before the syscall entry counts them, and its pages come from
peer calls that never touched its resident count.

- MkForeignStart and MkForeignExec name the guest after the last part of
  argv[0] on its new stack, as Linux's comm, at most 15 bytes of printable
  ASCII behind a "foreign:" the guest cannot remove. The stack is read
  under the peer lock and only in the guest's own half.
- A forked child takes its parent's name, as a Linux child its comm.
- A trapped call counts as one of the guest's syscalls.
- MkPeerMap and MkPeerUnmap add and subtract the pages they map and
  unmap in the guest's resident count. A page filled on first touch of a
  reservation is not counted yet.

exec.rs's context helpers move to exec_swap.rs, keeping it within the
line limit. All 11 guest boots pass on this change.
The detail pane wrapped a process's grant chips downward with no bound,
while End Process and Force Quit sit at the bottom of the pane. A process
with several grants had its chips drawn under Force Quit, whose tinted
ground let them show through the label.

The chips now stop above the actions. Grants without room are counted in
a last "+N more" chip on the final row that fits, so none goes missing
unsaid. The chip painters move to insp_chip.rs.

For a Linux guest the Mmapped field said "0 KB in 0 regions": its
mappings are kept by its supervisor, and the kernel lists none. It now
says so in words.
Overview draws its table below the four cards, but the hit test measured
rows from the top of the pane. A click on a process row resolved to a slot
past the end of the list and selected nothing, and a click on the cards
could resolve as a sort. The arrow keys also clamped against a table as
tall as the whole pane.

The table's rect now comes from one function, table_rect, which the
painter, the hit test and the row budget all read.
Share of RAM was a whole percent printed with a ".0" after it, so every
process under a hundredth of RAM read 0.0%. It is now computed in tenths:
a guest with 11.6 MB of 1580.6 MB reads 0.7%.

The Memory screen printed "0 KB" mmapped for a Linux guest, whose mappings
its supervisor keeps and the kernel does not list. The cell now says
"supervisor", as the detail pane already says "kept by its supervisor".
The grant table stopped at ProcessControl (bit 25). A grant from bit 26 to
bit 33 was counted in a process's Authority but drawn as no chip at all:
the terminal holds AttestRead and showed "Authority 9" over eight chips.
The table now runs through LocalSign, in the kernel's enum order, and
moves out of format.rs into format_caps.rs.
When the selected process ended, the selection fell back to the first
process in the whole list, even one the filter hides. With the table
filtered to "foreign" and every Linux guest gone, the detail pane named
app.about while the table was empty, and End Process would have aimed at
it.

The fallback is now the first row the table shows. When the filter
matches none, nothing is selected: the pane reads "No process selected"
and End Process asks for a selection first.
The half block U+2580 was drawn from the face, whose glyph stops about a
pixel short of the cell's edges. Between rows of half blocks a one-pixel
line of the other colour showed, as thin stripes across the flat colours
of pictures drawn with them.

Block elements (the halves, eighths and quadrants from U+2580 to U+259F,
without the shades) are now filled as exact spans of the cell, as the
box-drawing lines already run to its edges.
security-packages.txt lists 26 security and networking tools from Alpine
main and community (nmap, tcpdump, tshark, masscan, radare2, john, hydra,
aircrack-ng, nikto, openssl and more), grouped by use, with the ones that
need a live network target marked so an operator knows which want the
network capability granted before they reach anything.

SECURITY-TOOLSET.md records how a tool reaches the machine through the
existing verified pipeline (catalogue measures its BLAKE3, the market
serves a signed index, install holds the bytes to the distribution's own
signature, the tool runs as a foreign guest at Authority 0), and what each
still needs: its measurement enrolled, and the network capability for the
tools that reach a target. The catalogue was run against radare2, nmap,
openssl and john: it fetched and hashed the real signed bytes and listed
each, held back until enrolled.
A dynamically linked tool maps its shared libraries executable, and the
personality proves every executable file before any of its pages run, so
an unproven library is refused: openssl and file, brought in with their
Alpine closure as data, failed at load with "Operation not permitted" on
the first library.

LINUX_GUEST_LIBS names a library and its path under /linux, the way
LINUX_GUEST_PROGRAMS names a program, and gives each a certificate,
manifest and trailer so the machine has agreed to run it. With
libcrypto, libssl and libmagic proved this way, openssl 3.3.7 and file
5.45 run: openssl prints its version and hashes stdin to the same
SHA-256 as the host, and file reads its magic database and names an ELF
by the same build id.
A dynamically linked musl guest ran correctly but took a SIGSEGV at
exit: openssl 3.3.7 and file 5.45 printed the host's exact output, then
ended with status 139. musl loads a library by reserving its whole file,
then laying the segments over that reservation. The data segment's file
mapping reaches past its filesz into the pages musl then covers with a
fixed anonymous mapping to zero the bss. That overlay did nothing here,
because peer_map leaves a page already present in place, so the bss kept
the file bytes read past the segment (libcrypto's section-header table),
and a library global that should be null held .rodata's address 0x30e000.
Its teardown read of that global faulted.

A fixed anonymous span is zeroed pages by definition, even where frames
are already there, so drop those frames first and let the map fill
fresh, zeroed ones. A reservation has no frames to drop, so the
commit-over-reservation path a runtime uses is unchanged. On a boot,
openssl and file now exit 0 with the same output, and no fault is
logged.
Guest TCP leaves through the mixnet and nowhere else. When the mixnet holds no
gateway, net.sockets answers the connect with E_NO_TRANSPORT (6); the
personality turned every non-zero status into ECONNREFUSED, so a program was
told a peer had answered and refused it when no packet could have left the
machine. Status 6 is now ENETUNREACH, the errno Linux gives when there is no
route. Every other refusal stays ECONNREFUSED, and a call that got no reply
stays EIO.

The mixnet stays the only way out: nothing here opens a direct path.
…ishes them

virtio-net wrote descriptors, avail ring slots and the frame itself, then the
avail index, with no fence anywhere. The descriptor and ring stores are
volatile, but the frame bytes on the TX side are a plain copy, and Rust only
orders volatile accesses against each other: the compiler is free to sink the
copy past the index store that tells the device to read it. On the RX side the
used element and the frame were read after the used index with nothing tying
them to it. x86 hardware keeps these in order today; the language does not, and
a weakly ordered CPU would not either.

A release fence now sits before every avail index store (prime, refill, TX
post) and an acquire fence after the used index is seen to move, the same
pairing virtio-gpu already used.

virtio-gpu had the acquire in the wrong place: after reading the used entry
rather than before, so it ordered nothing the entry depended on. It moves ahead
of the reads.
Both capsules re-ran their whole setup in a yield loop for ten seconds when no
device matched, then exited 0. The broker is filled from PCI during kernel
init, before the first capsule starts, so discovery gives the same answer on
every pass; the loop only burned the CPU through that part of boot. On a PC,
which has neither device, that was ten seconds of every boot. While
virtio-net sat in it, net_core's link probes to driver.virtio_net0 went
unanswered and each one waited out its timeout.

A failed discovery now exits 2, as e1000, rtl8139 and rtl8169 do for an
absent chip. The bounded retry stays for a device that is present and fails a
later step.

Booted with an e1000 on plain VGA, so with neither device present, both now
leave with code 2 as they start and the desktop comes up on the GOP
framebuffer. With no virtio-net present, net_core had logged "ipc.call
unanswered driver.virtio_net0" five times in a boot; with this it logs it
none.
…al, take short frames

The driver never answered the stack. It tagged replies with its own magic
(0x4E45_3130) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read
since the stack moved into capsules. net_core and net_l2 speak NNET
(0x4E4E_4554) and wait for the answer to their own call, which is how
virtio-net replies. Requests are now taken with mk_ipc_recv_from and every
answer goes back to the capsule that asked, with mk_ipc_reply, under the
NNET tag. The ops and payload layouts already matched.

Booted as the only NIC, the e1000 came up and net_core's probe got nothing
("link probe driver.e1000_0 no-answer"). With this it binds and takes the
lease itself: "bind: interface up on driver.e1000_0", "lease 10.0.2.15/24 gw
10.0.2.2", and the mixnet directory fetch connects and handshakes over it as
it does over virtio-net.

TX posted every frame unconditionally and then spun on DD. When the spin ran
out the handler answered E_IO but left the descriptor live with TDT already
moved, and the next call wrote over the same slot without looking. On silicon
the 8254x sets no DD while the link is down (auto-negotiation after SLU, a
pulled cable, a blocking switch port), so every send timed out; after 31 of
them TDT caught up with TDH, which the part reads as an empty ring, and later
posts overwrote buffers it might still be fetching. A frame answered E_IO was
also still sent once the link came up, so a retry went out twice. QEMU's model
sets DD with the link down, which is why it never showed. TX now keeps a clean
index: reclaim walks it over finished descriptors, a ring with no free slot
answers E_AGAIN (one slot always stays empty), and a frame is answered once it
is queued.

The reset wrote CTRL and the receive address within microseconds of RST
clearing. A global reset starts an EEPROM auto-load that rewrites RAL0/RAH0 and
parts of CTRL, so the drawn station address could be replaced by the factory
one afterwards and unicast would go to the wrong filter. The sequence now
follows e1000_reset_hw: mask, stop RX and TX, wait 10 ms for bus-master cycles
in flight, reset, keep off the part for the first millisecond, poll RST clear
against a time bound rather than a spin count, then wait 20 ms for the
auto-load (5 ms on 82540/5/6, 20 on 82541/7) before anything is written.

Frames under 60 bytes were refused. TCTL.PSP has the part pad them, and ARP
(42) and a bare TCP ACK (54) are shorter than that, so IPv4 stopped right after
DHCP. A bare header is now the minimum.

The driver bound its INTx line and never used it: it polls and IMS is never
set. The discovery filter also skipped the NIC when firmware left Interrupt
Line at 0xFF, which UEFI commonly does, and a bound line is masked until acked,
so a shared GSI stayed masked for any other device on it. No line is bound
now, and the capsule gives up the Irq capability in its manifest and in the
kernel's spawn spec.

Release fences sit before the TDT and RDT writes and the ring hand-over, and an
acquire follows the DD reads, as in virtio-net.

e1000_proofs: 14 passed. The host shim gains Deadline, the fixture drops the
IRQ grant, and the request fixture carries the NNET tag. The no-entropy test
still holds the transmitter and receiver to never having been written, so
the quiesce writes TCTL as 0.
…the ring

The driver never answered the stack. It tagged replies with its own magic
(0x4E52_3839) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read
since the stack moved into capsules. net_core and net_l2 speak NNET
(0x4E4E_4554) and wait for the answer to their own call, which is how
virtio-net replies. Requests are now taken with mk_ipc_recv_from and every
answer goes back to the capsule that asked, with mk_ipc_reply, under the
NNET tag. The ops and payload layouts already matched.

Discovery skipped any card whose PCI Interrupt Line read 0xFF. OVMF leaves
it there, as UEFI firmware commonly does, so booted under OVMF with an
RTL8139 as its only NIC the driver exited 2, absent. The line was only ever
bound to be acked; the driver polls. It binds none now, sets IMR to 0 so an
unserviced source cannot hold a shared INTx asserted for another device,
and gives up the Irq capability.

The station address was the factory one, read out of IDR: the identifier
nonos_mac exists to keep off the wire, where e1000 and rtl8169 already drew
theirs. It is now drawn through nonos_mac too, written as dwords
with the config lock open as 8139too writes it, read back, and fails
closed. The capsule gains Crypto, which CryptoRandom is gated on, in its
manifest and in the kernel's spawn spec.

Booted under OVMF as the only NIC, it now binds and takes the lease itself:
"bind: interface up on driver.rtl8139_0", "lease 10.0.2.15/24 gw 10.0.2.2",
and the mixnet directory fetch connects and handshakes over it. QEMU's
`info network` shows the card at b6:54:0b:29:cc:83 rather than its
configured 52:54:00:12:34:56, so the lease was taken under the drawn
address.

RCR set no RBLEN bits, so the chip used an 8K+16 ring while every offset here
was taken against 32K. After about 8 KB received the chip wrote at the start of
the ring and the driver read zeros at 8K: ROK clear, an error, and every later
receive failed the same way. QEMU honours RBLEN too (8192 << RBLEN), so this
was reachable in emulation. RBLEN is now 10, the 32K+16 ring the allocation was
already sized for.

With RCR.WRAP set, a frame that crosses the end of the ring is written on past
it into the slack, not back at the start, but reads wrapped modulo the ring and
took the tail of that frame from offset 0: stale bytes once a lap. Reads are
linear now, and a read past the allocation returns zero. The ring proofs held
the modulo behaviour; they now hold the linear one, over a buffer the size of
the real allocation, and still check that no read leaves it.

The chip does not pad short frames and frames under 60 bytes were refused, so
ARP (42) and bare TCP ACKs (54) were never sent. A bare header is the minimum
now and send zero-pads to 60.

A bad header (ROK clear, or a length no good frame carries) returned without
moving, so the same header was read forever. Receive now restarts from the top
of the ring the way 8139too's rx_err does. A good frame too big for the caller
(a tagged full-size one) is skipped instead of stalling, and 0xFFF0 (still
arriving) is left for the next poll.

TX walked TSD0-3 with a cursor that stayed put when a send failed, while the
chip's own pointer moved on, so every later send waited on a descriptor the
chip would never look at again. TX is now four slots with a cursor and a
reclaim index, as 8139too keeps cur_tx and dirty_tx: a slot is finished on
TOK, TUN or TABT, an abort writes TCR.CLRABT to restart the transmitter, a
full ring answers E_AGAIN, and the early-TX threshold starts at 256 bytes
rather than 8.

Rx and Tx are enabled before RCR and TCR are written, as 8139too and the BSD
drivers do.

rtl8139_proofs: 7 passed.
The driver never answered the stack. It tagged replies with its own magic
(0x4E52_3639) and sent them to KERNEL_REPLY_ENDPOINT, an inbox nothing has read
since the stack moved into capsules. net_core and net_l2 speak NNET
(0x4E4E_4554) and wait for the answer to their own call, which is how
virtio-net replies. Requests are now taken with mk_ipc_recv_from and every
answer goes back to the capsule that asked, with mk_ipc_reply, under the
NNET tag. The ops and payload layouts already matched.

It could not have come up on any card in any case. The station address is
drawn with CryptoRandom, which is gated on the Crypto capability, and neither
the manifest nor the kernel's spawn spec granted it: the draw failed closed
and bring-up stopped with "no entropy for station address". Both grant it
now, as they already did for e1000 and rtl8821ce.

Discovery skipped any card whose PCI Interrupt Line read 0xFF, which is what
UEFI firmware commonly leaves there, so on those PCs a present RTL8111/8168
was reported absent. The line was only ever bound to be acked; the driver
polls. It binds none now, sets IMR to 0 so an unserviced source cannot hold
a shared INTx asserted for another device, and gives up the Irq capability.

The TX doorbell wrote TPPoll bit 7, which polls the high-priority ring. Only
the normal-priority ring (TNPDS) is ever set up; THPDS is never written. On a
real card the part fetched a descriptor from whatever THPDS held after reset,
the real descriptor kept OWN, the first send timed out and every later one saw
"busy". No frame was ever transmitted. The doorbell is bit 6, NPQ, as in Linux
r8169.

Both rings could fall a slot behind the part for good. On an RX descriptor
error the slot was re-armed but the cursor stayed, while the part had already
moved on; low-rate traffic like ARP then sat unread until fifteen more frames
wrapped the ring. On TX the cursor moved only after a clean completion, so a
TER or a slow completion (link still negotiating) left it on a slot the part
had left. The RX cursor now advances past a bad descriptor, and the TX cursor
advances the moment OWN is handed over; the OWN check on the next slot is the
full-ring test, answered with E_AGAIN.

The station address went into IDR with byte writes. The part takes IDR as
dwords; byte writes are dropped on silicon, the readback check failed, and
bring-up stopped there. It is now two 32-bit writes, high dword first, each
read back to post it, as rtl_rar_set does.

RxConfig and TxConfig were written with the receiver and transmitter off. On
the 8169 and 8168B-F those writes can be lost and the accept bits never take.
TE|RE now go on first, then the two configs, still after the station address,
so the part is never enabled under the factory one (the bring-up proof that
watches for that still passes).

Frames under 60 bytes were refused. ARP (42) and a bare TCP ACK (54) are
shorter than that; they are now zero-padded to 60, which also covers the 8168
revisions that pad short frames wrong.

No QEMU model exists for this chip. rtl8169_proofs: 8 passed; the bring-up
proof now holds IMR at 0.
"bind: interface up" did not say which interface. With a wired port and a WiFi
link both present the choice is the first thing to know, and on a boot with
one NIC it is what ties the lease that follows to a driver. The line now names
the candidate, for example "bind: interface up on driver.e1000_0".
No profile carried e1000, rtl8139 or rtl8169, so none of them had ever been
booted with the stack. This target is the desktop profile plus those three,
under the same attestation. QEMU models the e1000 and the RTL8139, so each can
be booted as the only NIC and has to take the lease itself; the RTL8169 has no
QEMU model and shows that a driver whose chip is absent exits and the boot
goes on.
…yout

Setup polled CSR_GP_CNTRL bit 1 for MAC clock ready. In the v1 CSR layout
that every family probed here uses, bit 1 is undefined and the flag is bit 0,
so the poll timed out on every card and the capsule exited before serving
anything. RF-kill was read from INIT_DONE, the bit setup had just set itself,
so the airplane-mode switch was never reported; it is HW_RF_KILL_SW, bit 27,
set while the radio is allowed on. Bz-family parts (BE200) use the v2 layout
and are still not handled.

The legacy firmware parser read the version at offset 8, which is the start of
the 64-byte name ("Core", "rele", "jenk" in the bundled images), so every image
was refused as an unknown API. The header is 88 bytes: version at 72, build at
76, TLVs from 88. The section types were one off (20/21/33): 19 is the runtime
image, 20 INIT, 32 paging, so the INIT and WoWLAN images were staged and the
runtime one never was. The gen3 path already had 19. The API ceiling moves to
86 to take the newest bundled image.

Checked against the six bundled .ucode files: the version at 72 is the API in
each file name (29, 36, 46, 77, 84, 86) and the TLV walk from 88 ends exactly
at the end of every file. The blob test written for this was never declared in
the proofs crate; it is wired in now and runs against the real 7265D-29 image
(four runtime sections, 364400 bytes). iwlwifi_proofs: 76 passed.

This gets the capsule past its first register and its firmware file. It does
not boot the firmware: the per-family load paths, the RX and TX queues and the
post-ALIVE command sequence are still to be written.
The GTK KDE in message 3 carries the key's index, and find_gtk threw it away.
The driver then installed every group key in CAM slot 1. With the sec engine
looking group-addressed frames up by the CCMP KeyID, that works until the AP
rekeys: hostapd starts at 1 and swaps between 1 and 2 on every rekey, so after
the first one every broadcast frame (ARP requests, DHCP, router adverts) came
in under index 2, found an empty slot, and was dropped. The index now travels
from the supplicant through the MLME and the join outcome to install_gtk and
the session that removes it.

TX data frames were also tagged SEC_TYPE_CCMP in the descriptor after the
station had already encrypted them in software (header, CCMP header, MIC).
rtw88 sets the security type only for frames with a hardware key; a frame
tagged for hardware CCMP after software encryption risks being encrypted
twice and failing the AP's MIC check. The tag is now zero.

nonos_wifi_core_proofs gains a handshake test that drives the real supplicant
with the KDE naming index 1, 2 and 3 (and with the Tx bit set) and holds it to
the index sent: 14 passed. rtl8821ce_proofs does not build on main (sec.rs
wants a crate::constants the proofs crate never declares, and its tests look
for the firmware under the capsule directory rather than
nonos-bootloader/firmware/realtek, where it is), so these edits are checked by
the capsule build.
#582 gained nine commits on 30 September. Four are already here as the
same commits. The other five are earlier forms of 07d9c92, d7b2019,
f5fe356, 2b5c726 and 5b40bbc, which this branch carries in the split
form linux/next-waits gave them. The tree here already holds all of it, so
the merge keeps it as it is, and #582 and this branch now merge into main
in either order.
Brings the wired NIC, virtio and wifi fixes into the integration branch,
and with them main as of eb2c0cc.

Two files met both lines. The drivers branch maps a connect with no
transport to ENETUNREACH against main's connect.rs, which still builds the
host body inline; this branch moved that into host_body.rs. The resolution
keeps host_body.rs and routes both connect paths through the new
outcome(). errno.rs keeps this branch's constants and adds ENETUNREACH.
With this recorded here, the drivers PR and this one merge into main in
either order.
Main gained #591 (dceba0e). Its new kernel_proofs modules and this
branch's ipc_peers_tests extend the same module list in
userland/kernel_proofs/src/lib.rs; the list keeps both. kernel_proofs:
89 passed; the kernel checks clean.
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