From ff0acfc91357d86f766b0c080f2ae25a0fad84b1 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 22 Sep 2026 14:58:21 +0200 Subject: [PATCH] gfx: a framebuffer at physical zero is not a framebuffer Booting with a virtio-gpu as the *only* display reboot-looped. Not a hang, not a panic: the firmware came back round to the boot menu, started the loader again, and did it again, for ever, with nothing to say why. virtio-gpu has no linear aperture, so OVMF's virtio GOP implements Blt and nothing else. On a machine with one and nothing else, the firmware reports a width and a height and a FrameBufferBase of **zero**: schos: framebuffer 0x0000000000000000 0x500x0x320 Two places took that at face value. **kmain() tested `if (bi.ptr && ...)` with bi.ptr already run through P2V()**, and P2V(0) is KERNBASE, which is not zero. So the guard could not fail -- and its own comment, three lines above, said "drawing into address zero is a worse failure than staying quiet". The intent was exactly right and the test could never enforce it. It reads the physical value first now and tests that. **vga_fb_init() attached the scanout regardless**, one layer down, so fixing kmain alone just moved the same write. Either one had gfx_clear() write four megabytes over physical 0: the real-mode IVT, the BIOS data area, the AP trampoline at 0x7000, the early page tables. Hence a reset with no output -- there was nothing left to run. Both refuse now, and the refusal says what it is: vga: firmware reported a 1280x800 display with no framebuffer address; no graphical console A machine with no usable linear framebuffer gets no graphical console. That is the truth about it, and the serial line still has everything. **It still does not reach a shell on such a machine**, and that is recorded rather than implied: the console path assumes a framebuffer -- vt_init() reports a terminal of `0 by 0` cells -- and that path has never been exercised. What this fixes is a machine that destroyed low memory and reset; what it does not do is make a headless boot work. Supported remains stdvga, with virtio-gpu optional beside it, which is what docs/architecture/graphics.md already said. Both supported configurations verified: stdvga alone and stdvga with virtio-gpu, one boot each, no traps, socktest and memfdtest green. Co-Authored-By: Claude Opus 5 --- docs/architecture/graphics.md | 31 +++++++++++++++++++++++++++++++ kernel/core/main.c | 20 ++++++++++++++++++-- kernel/drivers/gpu/vga/vga_fb.c | 17 +++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index 429b1f6..11127c4 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -67,6 +67,37 @@ splash is drawn on — so `-vga std` must stay on the QEMU command line even whe adding `-device virtio-gpu-pci`, because the loader's splash goes through VBE and there is no virtio path that early. +### A framebuffer at physical zero is not a framebuffer + +That last sentence used to be advice. It is now enforced, because ignoring it +did not produce a machine with no splash — it produced a **reboot loop**. + +virtio-gpu has no linear aperture, so OVMF's virtio GOP implements `Blt` and +nothing else. On a machine whose only display is one, the firmware reports a +width and a height and a `FrameBufferBase` of **zero**. Two places took that at +face value: + +- `kmain()` tested `if (bi.ptr && …)` — with `bi.ptr` already run through + `P2V()`, and `P2V(0)` is `KERNBASE`, which is not zero. The guard could not + fail, and its own comment said *"drawing into address zero is a worse failure + than staying quiet"*. It now tests the physical value before translating it. +- `vga_fb_init()` attached the scanout regardless, one layer down. + +Either one wrote four megabytes over physical 0 — the real-mode IVT, the BIOS +data area, the AP trampoline at `0x7000`, the early page tables — and the +machine reset before it could print anything, came back to the firmware, and +did it again. + +Both refuse now and say so. A machine with no usable linear framebuffer gets no +graphical console, which is the truth about it; the serial line still has +everything. + +**A virtio-gpu-only machine still does not reach a shell.** It no longer +corrupts memory and it explains itself, but the console path assumes a +framebuffer — `vt_init()` reports a terminal of `0 by 0` cells — and that path +has never been exercised. Supported today is stdvga, with virtio-gpu optional +beside it. + `/dev/fb0` speaks the Linux fbdev interface: geometry through the fbdev ioctls, and `mmap` of the real framebuffer at its real physical address. diff --git a/kernel/core/main.c b/kernel/core/main.c index 9494343..6097b27 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -59,7 +59,23 @@ kmain(uint32 fb_addr) // the only time anyone sees this is the only time anyone needs it. { struct fb_info bi; - bi.ptr = (volatile uchar *)P2V((uint64)*(uint32*)P2V(SCHBOOT_VBE_LFB)); + // The *physical* address, tested before it is translated. + // + // This used to be `if (bi.ptr && ...)` with bi.ptr already run through + // P2V -- and P2V(0) is KERNBASE, which is not zero, so the guard could + // not fail. The comment below was right and the test could never enforce + // it. + // + // It matters on any machine whose only display is a virtio-gpu. OVMF's + // virtio GOP implements Blt and nothing else: there is no linear + // aperture to report, so it publishes width and height and a + // FrameBufferBase of **zero**. The kernel took that at face value, and + // gfx_clear() then wrote four megabytes over physical address 0 -- the + // real-mode IVT, the BIOS data area, the AP trampoline at 0x7000 and the + // early page tables. The machine reset before printing anything and came + // back round to the firmware, for ever. + uint32 fb_phys = *(uint32*)P2V(SCHBOOT_VBE_LFB); + bi.ptr = (volatile uchar *)P2V((uint64)fb_phys); bi.width = *(uint16*)P2V(SCHBOOT_VBE_WIDTH); bi.height = *(uint16*)P2V(SCHBOOT_VBE_HEIGHT); bi.stride = *(uint16*)P2V(SCHBOOT_VBE_STRIDE); @@ -67,7 +83,7 @@ kmain(uint32 fb_addr) // Only if the loader actually described something. A zero here means the // BIOS path or a firmware that gave no GOP, and drawing into address zero // is a worse failure than staying quiet. - if (bi.ptr && bi.width && bi.height && bi.stride && bi.bpp >= 16) { + if (fb_phys && bi.width && bi.height && bi.stride && bi.bpp >= 16) { gfx_attach_scanout(&bi); // Before vt_init, which reads the cell size to work out how many rows // and columns there are. The font is in the kernel image, so this needs diff --git a/kernel/drivers/gpu/vga/vga_fb.c b/kernel/drivers/gpu/vga/vga_fb.c index b9b0a22..b26f0fd 100644 --- a/kernel/drivers/gpu/vga/vga_fb.c +++ b/kernel/drivers/gpu/vga/vga_fb.c @@ -83,6 +83,23 @@ vga_fb_init(uint64 phys_addr) if (fb_height == 0) fb_height = 600; if (fb_stride == 0) fb_stride = fb_width * (fb_bpp / 8); + // A framebuffer at physical zero is not a framebuffer. + // + // The loader publishes what the firmware's GOP reported, and on a machine + // whose only display is a virtio-gpu that is a width, a height and a + // FrameBufferBase of **zero**: OVMF's virtio GOP implements Blt and nothing + // else, because virtio-gpu has no linear aperture to hand out. Attaching it + // anyway pointed the scanout at P2V(0) and every subsequent draw wrote over + // the real-mode IVT, the BIOS data area and the AP trampoline. + // + // Refusing leaves the machine with no graphical console, which is the truth + // about it. The serial line still has everything. + if (phys_addr == 0) { + cprintf("vga: firmware reported a %dx%d display with no framebuffer " + "address; no graphical console\n", fb_width, fb_height); + return; + } + kinfo("vga: %dx%d, %d bpp, stride %d, LFB: 0x%x\n", fb_width, fb_height, fb_bpp, fb_stride, (uint32)phys_addr);