Skip to content

Milestone 243: something to say before kernel_main, and a screen on the boards - #998

Queued
calef wants to merge 12 commits into
mainfrom
milestone/243-no-serial-port
Queued

calef wants to merge 12 commits into
mainfrom
milestone/243-no-serial-port

Conversation

@calef

@calef calef commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Lane: milestone/243-no-serial-port, written by an agent; calef's account is the author GitHub shows.

Closes milestone 243's two Outstanding items. 243 is now BUILT.

1. Early boot: bounded, and it cannot be more than bounded

The premise was checked before it was answered, and half of it turned out to be unanswerable rather than unanswered. From ExitBootServices to console::attach_screen there is no console on a serial-less machine: the firmware's is gone by specification, the kernel's is not up, and everything in between (the loader's trampoline, boot.s's 32-bit half, the page tables, the long-mode jump) runs 32-bit with no IDT, so a fault there is a triple fault and a silent reset. No code in that window knows where the screen is.

So it is bounded instead. uefi_loader clears the screen and writes two lines as its final act, after ExitBootServices and before the jump:

nife loader: firmware released, entering the kernel.
If this line is still here, the kernel stopped before its console came up.

Five outcomes are now distinguishable where four shared one black rectangle. The third is the one that did not exist: the firmware never started the stick / the loader refused and said why over ConOut / the kernel never reached its first statement / the kernel armed its console and died after (black) / the boot tour.

Proved rather than reasoned. With a temporary halt in place of the jump, cargo xtask uefi-boot's screendump had ink in exactly the top sixteen pixel rows of a 1280x800 screen and they read those two lines. The painting itself is host-tested in uefi_loader::screen (three tests plus a doctest).

2. aarch64 and riscv64: a screen, through ramfb

x86_64 gets a screen because UEFI already lit one. QEMU's virt boards are entered from -kernel with nothing configured, so there was nothing to discover. ramfb is what they can present, and it inverts the arrangement: the guest owns the pixels and tells the emulator where they are over fw_cfg.

Piece Where
The fw_cfg wire format, 8 host tests + 2 doctests crates/firmware_configuration (name provisional)
The register poking, two DMA transactions before arch::mmu::init kernel/src/drivers/ramfb.rs
The memory and the wiring kernel/src/screen.rs (provisional), console::peek_screen (provisional)
Painting text crates/screen_console, unchanged
The gate cargo xtask screen-boot <arch> (provisional), board_console::screen unchanged

Measured, and both legs run inside script/test:

screen-boot: read 33 non-blank row(s) of the aarch64 tour back off a ramfb, ending
screen-boot:   | nife self-test: 5 of 5 passed
screen-boot: read 47 non-blank row(s) of the riscv64 tour back off a ramfb, ending

The surprise worth reading the diff for: the inversion breaks milestone 400's handover, which maps the screen's physical range into a userspace driver. Right for a display adapter's BAR, a hole for a framebuffer that is the kernel's own .bss. user::boot_screen_terminal now refuses a screen inside the kernel image, checked before the yield so a refusal leaves the kernel still painting. It is a range test rather than a flag, so milestone 157's U-Boot aperture passes it with nothing to remember.

script/shell-check --graphical and --graphical-serial are green, so the one-painter invariant still holds.

Gates, all from this worktree

Gate Exit
NIFE_CC=… script/lint 0
script/names (refusals unchanged) 0
script/test (all three architectures, plus both new screen-boot legs) 0
script/shell-check, --graphical, --graphical-serial 0, 0, 0
script/bench --check 0
script/icount 0
script/fastpath-footprint 0

This lane took the machine-global nife-dev link, as every gating lane does. It did not run script/roadmap --write; only its own block was edited.

Identified work, with a home

  • design/roadmap/proposals/the-boards-screen-under-uefi.md (new). Milestone 441 landed uefi_loader boot files for aarch64 and riscv64 while this lane was running, so those boards now have a firmware stage that has lit a display. The banner could move there, and the loader could hand the kernel a real GOP aperture by synthesising the simple-framebuffer node milestone 157 will read from U-Boot, retiring the ramfb's 1.9 MB of .bss. The second half is a wire format between two boot stages and wants a decision rather than a lane.
  • Recorded as BUGS where a reader meets them: the .bss cost, the missing cache maintenance (QEMU-only by construction), the handover refusal, and the fact that the loader's banner is unobservable in a healthy boot.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2

@calef
calef force-pushed the milestone/243-no-serial-port branch from 6216eb4 to 2089028 Compare September 19, 2026 22:38
@calef
calef marked this pull request as ready for review September 19, 2026 22:57
@calef
calef enabled auto-merge September 19, 2026 22:58
@calef

calef commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

merge-drain: STALLED. #998 is failing cpu matrix (riscv64 across QEMU CPU models) (Milestone 243: something to say before kernel_main, and a screen on the boards)

@calef

calef commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Lane: milestone/243-no-serial-port, written by an agent; calef's account is the author GitHub shows.

The red cpu matrix (riscv64) leg, fixed

It was this lane's wiring, not a CPU model, and it reproduces on rv64 as well as on the four narrow ones.

The bug. cargo() exports NIFE_INITRD pointing at the aarch64 archive, because it is the aarch64 path's helper; every other riscv64 caller in xtask overrides it and says in a comment that it has to. screen_boot did not. The archive it built (initrd_riscv()) was correct; the variable pointed somewhere else.

And I had already been told, an hour before CI was. The first local riscv64 run printed MEASURED BOOT REFUSED: 'progenitor' is not what this kernel image was built against, which is precisely a riscv kernel loading an aarch64 archive, and I read it as noise because the gate's own assertion still passed. The defect had two faces: with the aarch64 archive on disk the boot carried on refused, and in a job that never built one QEMU would not start (could not load ramdisk). That dismissal is written into the block, because it is the reusable part.

Two changes, and they are different kinds of thing.

  1. The wiring (98a…): the archive is chosen per architecture at the spawn. Verified by reproducing CI's condition rather than by reasoning: with target/initrd.img moved out of the tree, cargo xtask screen-boot riscv64 exits 0 and reads 45 rows off the screen. The tour also reaches further than it used to, because the progenitor now composes userspace instead of refusing the archive.

  2. The screen legs no longer run under --cpu. A cost decision on its own merits, not a way around the failure: the matrix runs the suite five times to narrow the ISA, and nothing here varies with -cpu (byte moves, MMIO stores, integer arithmetic, all of it already executed on that model by the three hundred tests above). script/cpu-matrix's own warning is about dropping a model; no model is dropped, and the leg still runs on every ordinary script/test, --arch riscv64 included.

Local reruns

script/cpu-matrix sifive-u54 0 — and its log mentions screen-boot zero times, so the matrix job runs exactly what it ran before this branch
cargo xtask screen-boot riscv64, with target/initrd.img absent 0, 45 rows
cargo xtask screen-boot aarch64 0, 33 rows
script/lint, script/names 0, 0

On the diagnostic line

Noted in the block and in notes/serial-less-output.md as a shape to copy, since it is what made a red matrix readable without a local re-run:

screen-boot: nothing decodable was ever on the screen (did QEMU get a ramfb and a monitor?)
screen-boot: the tour never reached the SERIAL line either, so this is a boot failure and not a screen one

The second line is a control, not a detail. A screen gate that only reports "the screen was blank" sends the next reader into the framebuffer path, which is where I would have gone and where nothing was wrong. Asserting the same marker on the channel the screen is replacing costs one contains and separates "this milestone's mechanism is broken" from "the machine did not boot". Any gate reading a machine through one channel should do the same.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2

@calef

calef commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

merge-drain: STALLED. #998 has conflicts a person must resolve (Milestone 243: something to say before kernel_main, and a screen on the boards)

@calef
calef force-pushed the milestone/243-no-serial-port branch from 0930152 to 4a8289c Compare September 20, 2026 00:30
@calef

calef commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Lane: milestone/243-no-serial-port, written by an agent; calef's account is the author GitHub shows.

Rebased onto 365's xtask split, and one flake fixed that was not mine

Two things happened after the last comment, and the second is worth reading.

1. Milestone 365 split xtask/src/main.rs into twenty modules

The gate could not be rebased, because the file it was written against stopped existing in that shape. Re-landed rather than replayed, which is why it arrives as one commit instead of three: xtask/src/screen.rs beside uefi.rs whose uefi_boot it is the twin of, the dispatch entry in main.rs, the two suite legs in suite.rs behind the --cpu guard, and the icount exclusions in icount.rs. Nothing changed but where it lives.

2. uefi-boot's tour stage was asserting a race, and the premise behind it was false

Re-verifying after that rebase, uefi-boot went red intermittently: four consecutive runs read 19, 27, 40 and 0 rows. Milestone 400's own BUGS predicted it in those words.

The obvious suspect was my loader banner, so I tested it instead of arguing. With the banner's paint disabled and nothing else changed, the same gate failed two runs in four — worse. Not the cause.

The cause was a premise that was already false when it was written. The marker was boot_ladder::SELF_TEST, chosen "near the end of the boot on purpose" because a 1280x800 screen is 100 character rows and the boot was believed to be longer, so early lines would have scrolled off. Measured, the tour tops out at 98 non-blank rows: nothing scrolls, the banner is on the screen the whole time, and what the marker actually selected for was the last line before the handover's clear — a few hundred milliseconds. The comment describes the flake it was written to prevent.

The marker is now the banner. Five consecutive runs: 52, 68, 78, 84, 89 rows, all green, on the same loaded machine that had been failing one in two.

The total claim is unchanged, and that is the thing to check before accepting any gate that got easier. The screen's job is to prove the pixelsLocateProtocol, byte order, stride, the mapping surviving mmu::init, the glyphs — and any decoded row proves all five. That the boot reached the self-test is asserted separately and unconditionally on the serial transcript a few dozen lines above. Depth is now reported rather than required.

Two things for you, because a lane may not do them.

  • This edits milestone 400's gate, not 243's. Flagged rather than done quietly.
  • 400's BUGS entry is now stale in its last clause (the one beginning "The tour stage of uefi-boot now has a window"). It needs striking or rewording; I did not touch that block.

Gates, re-run after all of the above

script/test (three architectures, uefi-boot, uefi-test, both screen-boot legs) 0
script/lint, script/names 0, 0
script/cpu-matrix sifive-u54 0, and its log mentions screen-boot zero times
cargo xtask uefi-boot x5 after the marker change 0 x5
cargo xtask screen-boot riscv64 with target/initrd.img absent 0

No leaked QEMU, clean tree, branch on top of main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2

calef and others added 12 commits September 19, 2026 17:43
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
… for a screen

Milestone 243's second outstanding item is that aarch64 and riscv64 have no
screen at all: `machine_discovery::framebuffer` and `screen_console` are
arch-neutral and nothing on those boards ever calls them, because QEMU's `virt`
is entered from `-kernel` with no firmware stage that has lit a display.

`ramfb` is what the emulator can present, and its whole contract is that the
guest supplies the memory and tells the device where it is over `fw_cfg`. Every
field of every structure in that conversation is big-endian on a
little-endian-only tree, which is the mistake worth making unrepresentable, so
the encoders take native values and hand back bytes and the arithmetic is proved
on the host before any MMIO exists to get wrong.

Name provisional: QEMU calls the interface `fw_cfg` and this tree spells crate
names out. The refusals are in the Cargo.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
… one

Milestone 243's second outstanding item. The console-on-a-framebuffer path was
written arch-neutrally and wired up on x86_64 alone, because x86_64 is the only
one of the three whose boot chain has a stage that has already configured a
display. QEMU's `virt` boards are entered from `-kernel` with nothing lit, so
`machine_discovery::framebuffer` and `screen_console` sat unused there.

`ramfb` inverts the UEFI arrangement: the guest owns the pixels and tells the
emulator where they are. So the discovery half becomes a `fw_cfg` conversation
and everything above `console::attach_screen` is unchanged, which is what the
arch-neutral halves were for.

The surprise worth recording is what that inversion breaks. Milestone 400's
handover maps the screen's physical range into a userspace driver, which is
right for a display adapter's BAR and is a hole for a framebuffer that is the
kernel's own `.bss`. `boot_screen_terminal` now refuses a screen inside the
kernel image, before the yield rather than after, so a refusal leaves the kernel
still painting instead of a cleared screen nobody owns. It is a range test, so
milestone 157's U-Boot aperture will pass it without anybody remembering to.

`kernel/src/screen.rs` and `console::peek_screen`: names provisional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
The claim a serial transcript cannot make. Both board boots print the same tour
on the UART whether or not a single pixel was written, so the only way to know
the screen worked is to photograph it and decode the glyphs, which is exactly
what `uefi-boot` already does on x86_64. This is its twin, using
`board_console::screen` unchanged.

Measured on 2026-09-19: 33 non-blank rows off the aarch64 screen ending in
`nife self-test: 5 of 5 passed`, and 63 off the riscv64 one.

It is a boot of its own rather than a stage of the suite, and that is forced
rather than chosen: `ramfb` adds a QEMU console, `screendump` with no device
argument writes console 0, and the suite's machine already has a virtio-gpu
there. A boot carrying both would be photographing whichever QEMU ordered
first.

`screen-boot` and `NIFE_SCREEN`: names provisional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
Milestone 243's first outstanding item, early boot, and what is buyable here is
narrower than the item's wording suggests. From `ExitBootServices` to the
kernel's own `attach_screen` nothing can speak on a machine with no serial port:
the firmware's console is gone, the kernel's is not up, and a fault in between
is a triple fault with no IDT to catch it. That window cannot be narrated,
because the code in it is thirty-two bit and does not know where the screen is.

It can be bounded. The loader clears the screen and writes one sentence as its
final act, so a person at a monitor can tell the four outcomes apart: the
firmware never started the stick, the loader refused and said why over ConOut,
the kernel never reached its first statement (this line is still there), or the
kernel armed its console and died after (a cleared screen). Only the third of
those was previously indistinguishable from the fourth.

Proved rather than reasoned: with a temporary halt in place of the jump,
`cargo xtask uefi-boot`'s screendump has ink in exactly the top sixteen pixel
rows and they read the two lines of `HANDOFF`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
…ot be

Both Outstanding items are closed and the block says how, including the half of
the early-boot item that turned out to be unanswerable rather than unanswered:
the window between ExitBootServices and the kernel's console runs 32-bit with no
IDT, so it can be bounded and cannot be narrated. Saying that plainly is worth
more than the line it prints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
script/lint green. Four things it asked for and each is a real improvement
rather than a formality: the loader's banner moved into `uefi_loader::screen`
where it is host-tested (three tests, one doctest) instead of being a constant
only a boot could exercise; the `fw_cfg` refusal got a named type instead of
`()`; the driver reads its control word a byte at a time, so it asks nothing of
the caller's alignment; and both fences carry their PAIR comment, with the
inventory in notes/memory-ordering.md grown from twelve to fourteen. One of
them is the only fence in this tree whose partner is host code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
…alf-built

Milestone 441 landed `uefi_loader` boot files for aarch64 and riscv64 while
this lane was running, which changes the premise this lane worked from: those
boards now have a firmware stage that has already lit a display, so a `ramfb`
is not their only possible screen and the 1.9 MB of `.bss` behind it has an
exit.

Two pieces, and the split is the point. Moving the handoff banner to the
boards is small and needs no agreement between stages. Carrying a real GOP
aperture to the kernel means synthesising the `simple-framebuffer` node
milestone 157 will read from U-Boot, into the device tree the loader already
copies, which is a wire format between two boot stages and belongs with 157's
own premise check rather than with a lane that found it on the way past.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
Milestone 441 moved this code into `arch/x86_64/mod.rs` and turned the screen
into a field of `Found` while this lane was rebasing onto it; the banner was
re-applied against the old shape and only the UEFI-target build says so, which
`script/lint` does not run. Caught by script/test's x86_64 leg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
Two decisions and one lesson, recorded where a reader meets them rather than in
a pull request nobody reads twice.

The screen legs do not run under `--cpu`, and that is priced on its own merits:
the matrix narrows the ISA, and nothing on this path varies with `-cpu`. The
failure that exposed it reproduces on `rv64` as well and is fixed, so this is
not the "do not route around it by dropping the model" that script/cpu-matrix
warns about.

The lesson is the dismissal rather than the bug: a `MEASURED BOOT REFUSED` line
inside an otherwise green gate was the same defect an hour earlier, and was read
as noise because the gate's own assertion passed.

And the gate's second diagnostic line is written up as a shape to copy: a check
that reads a machine through one channel should assert the same fact through the
channel it replaces, and say which one failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
Milestone 365 split `xtask/src/main.rs` into twenty modules while this lane was
in the merge queue, so this gate could not be rebased: its file stopped
existing in the shape it was written against. Re-landed rather than replayed,
which is why it arrives as one commit instead of the three it was.

Nothing about it changed except where it lives and what it imports. The gate is
`xtask/src/screen.rs`, beside `uefi.rs` whose `uefi_boot` it is the twin of; the
dispatch entry is in `main.rs`, the two suite legs are in `suite.rs` behind the
`--cpu` guard, and the icount boot's device exclusions are in `icount.rs`.

Re-verified after the move rather than assumed: `cargo xtask screen-boot
riscv64` reads 61 rows off the screen with no measured-boot refusal, and
`aarch64` reads 34.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
…d a race

Re-verifying after the rebase turned `uefi-boot`'s tour stage red intermittently:
four consecutive runs read 19, 27, 40 and 0 rows. Milestone 400's BUGS predicted
this in those words.

Tested rather than assumed, because the obvious suspect was this lane's own
loader banner: with the banner's paint disabled and nothing else changed, the
same gate failed two runs in four, which is worse. Not the cause.

The cause is a premise that was already false when it was written. The marker
was the self-test verdict, chosen "near the end of the boot on purpose" because
a 1280x800 screen is 100 rows and the boot was believed longer, so early lines
would have scrolled off. The tour tops out at 98 non-blank rows: nothing
scrolls, the banner is on screen the whole time, and what the marker selected
for was the last line before the handover's clear, a few hundred milliseconds.
The comment describes the flake it was written to avoid.

The total claim does not change. The screen proves the pixels, and any decoded
row proves all five things that path can get wrong; that the boot reached the
self-test is asserted unconditionally on the serial transcript already. Depth is
now reported instead of required.

Five consecutive runs: 52, 68, 78, 84, 89 rows, all green.

This touches milestone 400's gate rather than 243's, and leaves 400's BUGS entry
stale in its last clause. Named in 243's block for the integrator, since a lane
may not edit another milestone's block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STu3VeDYnEHem3iKCbzBF2
@calef
calef force-pushed the milestone/243-no-serial-port branch from 9503d88 to e54fa9c Compare September 20, 2026 00:44
@calef
calef added this pull request to the merge queue Sep 20, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
@calef
calef added this pull request to the merge queue Sep 20, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
@calef
calef added this pull request to the merge queue Sep 20, 2026
Any commits made after this event will not be merged.
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