boot: a framebuffer address is sixty-four bits, and there is more than one GPU
The first piece of real-hardware work, and it is three bugs that QEMU cannot show you. The report was an ASUS laptop -- Ryzen 7000-series APU, Radeon integrated, RTX discrete -- booting to a white underscore on a black screen, and unable to be offered 1920x1080. **The framebuffer address was truncated.** The loader handed over `(UINT32)gop->Mode->FrameBufferBase` and the kernel read a 32-bit field out of a VBE mode-info block -- a *BIOS* structure the UEFI path had been filling in because the kernel already knew how to read one. A machine that puts its aperture above 4 GB, which is ordinary on a laptop with a discrete GPU, got a plausible-looking address pointing at nothing. The splash went into it, the kernel was handed it, and the firmware's own cursor kept blinking on an otherwise black screen. Every step in that sequence succeeded on a number that was wrong, which is why there is no error message anywhere in it. SCHBOOT_FB_BASE is the loader's own display block: 64-bit address, 64-bit size, 32-bit width, height and stride, and the pixel format. The VBE block is still written for the BIOS loader, but anything that does not fit is left at zero rather than wrapped -- a truncated address is this bug, and a zero is at least honest. The kernel then has a problem the loader does not: its direct map covers the low 4 GB, so a framebuffer above it needs ioremap(), which needs kpdpt, which kvmalloc() has not allocated when the early console starts. A low framebuffer is attached exactly as before; a high one is attached immediately after kvmalloc(), still thousands of lines before anything else. **There was more than one adapter, and the first one was taken.** LocateProtocol answers with *a* GOP and the specification says nothing about which. QEMU with a stdvga and a virtio-gpu has **three** handles and two of them are Blt-only with a framebuffer address of zero -- so this is not a hardware-only hazard, it was a one-in-three chance of drawing into address 0 that happened to keep coming up heads. Every handle is scored now: no aperture, no candidacy; a readable current mode beats one that needs changing; size breaks the tie. **mode_usable() rejected modes real machines have.** A padded scanline (every GPU that aligns its rows -- and very likely the reason 1920x1080 could not be offered), a width not divisible by eight (1366x768, which is half the laptops in the world), and anything below 800x600 (the only mode some firmware gives over a KVM). All three passed on QEMU, which is why they survived. The mode list now offers 1366x768 and 640x480 where it did not. The loader also prints what it found, to the firmware's text console, which is cleared before the splash: invisible on a boot that works, and the only thing on the screen on a boot that dies before the splash. One process note, recorded because it cost a wrong test: `make UEFI_KEEP=1` ships the loader that is already built. Its own comment says "a build that quietly ships a stale bootloader is how you spend an evening debugging code that is not running", and that is exactly what happened -- a regression run that looked clean had tested the old loader. Loader work gets a full build. Both supported configurations verified after a clean build: one boot each, no traps, no panics, socktest and memfdtest green, and the adapter report shows the right one being chosen out of three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c84d06c5b1
commit
35ea398334
4 changed files with 358 additions and 24 deletions
|
|
@ -68,6 +68,74 @@ a missing table.
|
|||
**The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more
|
||||
descriptors than QEMU's, and the space was already there.
|
||||
|
||||
### Choosing a display, on a machine that has more than one
|
||||
|
||||
`LocateProtocol` answers with *a* GOP, and the specification says nothing about
|
||||
which. On QEMU with a stdvga and a virtio-gpu there are **three** handles, and
|
||||
two of them report `PixelBltOnly` with a framebuffer address of zero:
|
||||
|
||||
```
|
||||
schos: 3 display adapter(s)
|
||||
gop: 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000
|
||||
gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0
|
||||
gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0
|
||||
schos: display 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000
|
||||
```
|
||||
|
||||
A laptop with switchable graphics is the same shape: an integrated adapter, a
|
||||
discrete one, and often a console splitter over both. Taking the first handle
|
||||
means a one-in-three chance of drawing into an adapter with no aperture — a
|
||||
black screen with the firmware's own cursor still blinking on it, which looks
|
||||
exactly like a loader that crashed.
|
||||
|
||||
So every handle is examined and scored: an adapter with no framebuffer address
|
||||
is not a candidate at all, one whose current mode is readable beats one that
|
||||
would need changing, and size breaks the tie. The report above is printed to
|
||||
the firmware's text console, which is cleared a few lines later before the
|
||||
splash — so on a boot that works nobody sees it, and on a boot that dies before
|
||||
the splash it is the only thing on the screen.
|
||||
|
||||
### A framebuffer address is sixty-four bits
|
||||
|
||||
The loader used to hand over `(UINT32)gop->Mode->FrameBufferBase`, and the
|
||||
kernel used to read a 32-bit field out of a *VBE mode-info block* — a BIOS
|
||||
structure the UEFI path had been filling in because the kernel already knew how
|
||||
to read one.
|
||||
|
||||
Any machine that puts its aperture above 4 GB — which is ordinary on a laptop
|
||||
with a discrete GPU — got a truncated address that pointed at nothing. The
|
||||
splash was drawn into it, the kernel was handed it, and the screen kept the
|
||||
firmware's cursor. There is no error anywhere in that sequence; every step
|
||||
succeeded on a number that was wrong.
|
||||
|
||||
`SCHBOOT_FB_BASE` is the fix: the loader's own display block, with a 64-bit
|
||||
address, a 64-bit size, 32-bit width, height and stride, and the pixel format.
|
||||
The VBE block is still written for the BIOS loader and for anything that has
|
||||
not been taught to look at the new one, but anything that does not fit is left
|
||||
at **zero** rather than wrapped — a truncated address is the bug above, and a
|
||||
zero is at least honest about it.
|
||||
|
||||
The kernel then has a second problem the loader does not: it runs on its own
|
||||
page tables, and its direct map covers the low 4 GB. A framebuffer above that
|
||||
needs `ioremap()`, which needs `kpdpt`, which `kvmalloc()` has not allocated
|
||||
when the early console is set up. So a low framebuffer is attached exactly as
|
||||
before, and a high one is attached a few dozen instructions later, immediately
|
||||
after `kvmalloc()` — still thousands of lines before anything else happens.
|
||||
|
||||
### Modes a real machine actually has
|
||||
|
||||
`mode_usable()` rejected three things it should not have:
|
||||
|
||||
| Was rejected | Why it was wrong |
|
||||
| --- | --- |
|
||||
| `PixelsPerScanLine != HorizontalResolution` | A GPU is entitled to pad each row, and 1920×1080 arriving with a 1984- or 2048-pixel scanline is ordinary. The stride is carried through to the kernel, so this excluded nothing but every machine that pads. |
|
||||
| `HorizontalResolution % 8` | Excludes 1366×768, the panel on a very large number of laptops. |
|
||||
| resolutions below 800×600 | 640×480 is a legitimate fallback and the only mode some firmware offers over a KVM. |
|
||||
|
||||
All three passed on QEMU, which is why they survived. None of them protected
|
||||
anything: the drawing code steps by the stride and has done for as long as
|
||||
there has been a stride to step by.
|
||||
|
||||
### The entry point
|
||||
|
||||
The kernel publishes a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within
|
||||
|
|
|
|||
Loading…
Reference in a new issue