From 8c7c69b216b247c99ee032f3a011dfef5f05dd42 Mon Sep 17 00:00:00 2001 From: Cass Sheng Date: Wed, 26 Aug 2026 22:24:22 -0500 Subject: [PATCH] drivers/gop: Prove console framebuffer ownership via ConOut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lunar Lake firmware (Lenovo 83JT, QPCN13WW) installs GOP on a console aggregate handle: it carries no device path, and no PCI function records an EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER relation naming it. Both existing proofs therefore fail and the framebuffer's source is reported unknown, even though the firmware knows exactly which device scans it out. For that one handle the firmware states the answer in its own ConOut variable. On this machine it reads PciRoot(0x0)/Pci(0x2,0x0)/AcpiAdr(0x80011400), naming 00:02.0 — the Arc 130V/140V function. Add a third proof, gated on the GOP handle being exactly gST->ConsoleOutHandle, that walks that multi-instance path and resolves each instance through EFI_PCI_IO_PROTOCOL. Every instance that resolves must agree on one function; a console scanning out via two devices proves ownership of neither and stays unknown. This remains a firmware-asserted binding, not an inference from display class or framebuffer address. LocateDevicePath stops only at an end-of-entire-path node, so an instance closed by an end-of-instance node would let it read into the next instance. The variable is read into our own buffer, so promote that node for the call and restore it after. Also add revision 1 of the framebuffer-source extension: a per-framebuffer probe mask recording how far each proof got. Rung-level notes can then name the firmware shape that defeated attribution — no device path, a path with no PCI prefix, a ConOut naming two devices — which are indistinguishable from an unknown source alone, and are different bugs. The revision-0 prefix is unchanged, so existing readers are unaffected. Signed-off-by: Cass Sheng --- EXTENSIONS.md | 42 +++++++- common/drivers/gop.c | 208 +++++++++++++++++++++++++++++++++----- common/lib/fb.h | 21 ++++ common/protos/limine.c | 4 + common/protos/limine_mp.h | 19 ++++ test/limine.c | 16 +++ 6 files changed, 280 insertions(+), 30 deletions(-) diff --git a/EXTENSIONS.md b/EXTENSIONS.md index bfa2e441..b2bf050e 100644 --- a/EXTENSIONS.md +++ b/EXTENSIONS.md @@ -32,6 +32,8 @@ struct limine_mp_framebuffer_source_response { uint64_t revision; uint64_t entry_count; struct limine_mp_framebuffer_source **entries; + // Revision >= 1 only. + uint64_t *probe_masks; }; struct limine_mp_framebuffer_source_request { @@ -55,6 +57,40 @@ UEFI GOP handles are first resolved through their device paths to the closest handle supporting `EFI_PCI_IO_PROTOCOL`. If firmware installs GOP on a child whose device path cannot be resolved to PCI I/O, the loader accepts only an explicit `EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER` relationship naming that GOP -handle. The selected PCI I/O protocol's `GetLocation()` method provides the PCI -coordinates. Failure of both positive relationships, a non-PCI provider, and -BIOS VBE all produce an unknown source rather than an inferred identity. +handle. When both relations are absent and the GOP handle is exactly the +system table's `ConsoleOutHandle` — the console-splitter posture, where the +aggregate handle carries no device path and no recorded PCI parent — the +loader resolves the firmware's own `ConOut` variable: every instance of that +multi-instance device path that reaches an `EFI_PCI_IO_PROTOCOL` handle must +agree on one PCI function, which is then the console framebuffer's source. +The selected PCI I/O protocol's `GetLocation()` method provides the PCI +coordinates in every proof. Failure of all three positive relationships, a +non-PCI provider, a `ConOut` naming two PCI devices, and BIOS VBE all produce +an unknown source rather than an inferred identity. + +### Revision 1: probe masks + +A revision >= 1 response adds `probe_masks`: `entry_count` `uint64_t` values, +index-aligned with `entries`, recording how far each source proof advanced +for that framebuffer. A serial-less machine can then name the exact firmware +shape that defeated attribution from the mask alone. + +```c +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_PRESENT (1 << 0) +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_PCI_PREFIX (1 << 1) +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_LOCATED (1 << 2) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CHILD_SCAN (1 << 3) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CHILD_RELATION (1 << 4) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONSOLE_HANDLE (1 << 5) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_VAR (1 << 6) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_PCI (1 << 7) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_UNIQUE (1 << 8) +#define LIMINE_MP_FRAMEBUFFER_PROBE_SOURCE_PCI (1 << 9) +#define LIMINE_MP_FRAMEBUFFER_PROBE_HANDLES_SHIFT 16 +#define LIMINE_MP_FRAMEBUFFER_PROBE_INDEX_SHIFT 24 +``` + +Bits `[23:16]` carry the number of GOP handles the loader enumerated and bits +`[31:24]` this framebuffer's handle index, both saturating at `0xff`; BIOS VBE +framebuffers report a zero mask. Bit meanings are fixed for revision 1; +future proof steps append bits rather than renumbering. diff --git a/common/drivers/gop.c b/common/drivers/gop.c index 31d5e349..0444ea03 100644 --- a/common/drivers/gop.c +++ b/common/drivers/gop.c @@ -131,8 +131,11 @@ static bool mode_to_fb_info(struct fb_info *ret, EFI_GRAPHICS_OUTPUT_PROTOCOL *g bool gop_force_16 = false; -static bool set_pci_framebuffer_source(struct fb_info *fb, - EFI_HANDLE pci_handle) { +// Resolve one EFI_PCI_IO handle to bounded PCI coordinates. The bounds keep +// every reported source inside PCI numbering; a firmware answer outside them +// is treated as no answer at all. +static bool pci_handle_location(EFI_HANDLE pci_handle, + struct fb_pci_source *out) { EFI_GUID pci_io_guid = EFI_PCI_IO_PROTOCOL_GUID; EFI_PCI_IO_PROTOCOL *pci_io = NULL; EFI_STATUS status = gBS->HandleProtocol(pci_handle, &pci_io_guid, @@ -154,15 +157,26 @@ static bool set_pci_framebuffer_source(struct fb_info *fb, return false; } + out->segment = segment; + out->bus = bus; + out->device = device; + out->function = function; + return true; +} + +static bool set_pci_framebuffer_source(struct fb_info *fb, + EFI_HANDLE pci_handle) { + struct fb_pci_source source; + if (!pci_handle_location(pci_handle, &source)) { + return false; + } + fb->source_type = FB_SOURCE_PCI; - fb->pci_source.segment = segment; - fb->pci_source.bus = bus; - fb->pci_source.device = device; - fb->pci_source.function = function; + fb->pci_source = source; printv("gop: Framebuffer source PCI %x:%x:%x.%x\n", - (uint32_t)segment, (uint32_t)bus, (uint32_t)device, - (uint32_t)function); + (uint32_t)source.segment, (uint32_t)source.bus, + (uint32_t)source.device, (uint32_t)source.function); return true; } @@ -176,12 +190,21 @@ static bool get_framebuffer_source_from_device_path(struct fb_info *fb, if (status != EFI_SUCCESS) { return false; } + fb->source_probe |= FB_PROBE_DP_PRESENT; EFI_GUID pci_io_guid = EFI_PCI_IO_PROTOCOL_GUID; EFI_HANDLE pci_handle = NULL; status = gBS->LocateDevicePath(&pci_io_guid, &device_path, &pci_handle); - return status == EFI_SUCCESS - && set_pci_framebuffer_source(fb, pci_handle); + if (status != EFI_SUCCESS) { + return false; + } + fb->source_probe |= FB_PROBE_DP_PCI_PREFIX; + + if (!set_pci_framebuffer_source(fb, pci_handle)) { + return false; + } + fb->source_probe |= FB_PROBE_DP_LOCATED; + return true; } static bool pci_handle_owns_gop_child(EFI_HANDLE pci_handle, @@ -209,17 +232,14 @@ static bool pci_handle_owns_gop_child(EFI_HANDLE pci_handle, return owns; } -static void get_framebuffer_source(struct fb_info *fb, EFI_HANDLE gop_handle) { - if (get_framebuffer_source_from_device_path(fb, gop_handle)) { - return; - } - - // Some firmware installs GOP on a child handle whose device path cannot be - // resolved back to EFI_PCI_IO_PROTOCOL. UEFI bus drivers still record the - // controller relationship by opening the parent PCI I/O protocol with - // EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER and the GOP handle as - // ControllerHandle. Walk only those explicit relationships: guessing from - // framebuffer addresses or display class would not prove boot ownership. +// Some firmware installs GOP on a child handle whose device path cannot be +// resolved back to EFI_PCI_IO_PROTOCOL. UEFI bus drivers still record the +// controller relationship by opening the parent PCI I/O protocol with +// EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER and the GOP handle as +// ControllerHandle. Walk only those explicit relationships: guessing from +// framebuffer addresses or display class would not prove boot ownership. +static bool get_framebuffer_source_from_child_relation(struct fb_info *fb, + EFI_HANDLE gop_handle) { EFI_GUID pci_io_guid = EFI_PCI_IO_PROTOCOL_GUID; EFI_HANDLE tmp_handles[1]; EFI_HANDLE *handles = tmp_handles; @@ -227,7 +247,7 @@ static void get_framebuffer_source(struct fb_info *fb, EFI_HANDLE gop_handle) { EFI_STATUS status = gBS->LocateHandle(ByProtocol, &pci_io_guid, NULL, &handles_size, handles); if (status != EFI_SUCCESS && status != EFI_BUFFER_TOO_SMALL) { - return; + return false; } UINTN handles_alloc = handles_size; @@ -236,17 +256,151 @@ static void get_framebuffer_source(struct fb_info *fb, EFI_HANDLE gop_handle) { &handles_size, handles); if (status != EFI_SUCCESS) { pmm_free(handles, handles_alloc); - return; + return false; } + fb->source_probe |= FB_PROBE_CHILD_SCAN; + bool found = false; size_t handles_count = handles_size / sizeof(EFI_HANDLE); for (size_t i = 0; i < handles_count; i++) { - if (pci_handle_owns_gop_child(handles[i], &pci_io_guid, gop_handle) - && set_pci_framebuffer_source(fb, handles[i])) { - break; + if (pci_handle_owns_gop_child(handles[i], &pci_io_guid, gop_handle)) { + fb->source_probe |= FB_PROBE_CHILD_RELATION; + if (set_pci_framebuffer_source(fb, handles[i])) { + found = true; + break; + } } } pmm_free(handles, handles_alloc); + return found; +} + +// The console-splitter posture: firmware that virtualizes console output +// installs GOP on an aggregate handle with no device path and no recorded +// PCI parent, while the handle IS the system table's ConsoleOutHandle. For +// exactly that handle, firmware's own ConOut variable names the sink device +// paths, and resolving those through EFI_PCI_IO is a firmware-asserted +// binding, not a guess. All resolved instances must agree on one function: +// a console scanning out via two PCI devices proves ownership of neither. +static bool get_framebuffer_source_from_conout(struct fb_info *fb, + EFI_HANDLE gop_handle) { + if (gST->ConsoleOutHandle == NULL + || gop_handle != gST->ConsoleOutHandle) { + return false; + } + fb->source_probe |= FB_PROBE_CONSOLE_HANDLE; + + EFI_GUID global_variable = EFI_GLOBAL_VARIABLE; + UINTN size = 0; + EFI_STATUS status = gRT->GetVariable(L"ConOut", &global_variable, NULL, + &size, NULL); + if (status != EFI_BUFFER_TOO_SMALL || size < 4) { + return false; + } + uint8_t *paths = ext_mem_alloc(size); + UINTN paths_alloc = size; + status = gRT->GetVariable(L"ConOut", &global_variable, NULL, &size, paths); + if (status != EFI_SUCCESS || size != paths_alloc) { + pmm_free(paths, paths_alloc); + return false; + } + + // Walk the multi-instance device path node by node. Node lengths are + // read bytewise: instance boundaries do not keep the header alignment. + bool walked = false; + bool resolved = false; + bool conflict = false; + struct fb_pci_source agreed = {0}; + + UINTN at = 0; + UINTN instance_at = 0; + while (at + 4 <= size) { + uint8_t type = paths[at]; + uint8_t subtype = paths[at + 1]; + UINTN length = (UINTN)paths[at + 2] | ((UINTN)paths[at + 3] << 8); + if (length < 4 || length > size - at) { + break; + } + UINTN node_at = at; + at += length; + if (type != 0x7f) { + continue; + } + // 0x01 ends one instance, 0xff ends the whole list; anything else + // is malformation. Resolve the instance that just closed. + // + // LocateDevicePath stops only at an end-of-ENTIRE-path node, so an + // instance closed by 0x01 would let it read into the next instance. + // The buffer is our own copy of the variable, so promote this end + // node for the call and put it back afterwards. + if (subtype == 0x01) { + paths[node_at + 1] = 0xff; + } + EFI_GUID pci_io_guid = EFI_PCI_IO_PROTOCOL_GUID; + EFI_DEVICE_PATH *remaining = + (EFI_DEVICE_PATH *)(paths + instance_at); + EFI_HANDLE pci_handle = NULL; + instance_at = at; + if (gBS->LocateDevicePath(&pci_io_guid, &remaining, &pci_handle) + == EFI_SUCCESS) { + struct fb_pci_source candidate; + if (pci_handle_location(pci_handle, &candidate)) { + if (!resolved) { + resolved = true; + agreed = candidate; + } else if (agreed.segment != candidate.segment + || agreed.bus != candidate.bus + || agreed.device != candidate.device + || agreed.function != candidate.function) { + conflict = true; + } + } + } + if (subtype == 0x01) { + paths[node_at + 1] = 0x01; + continue; + } + walked = subtype == 0xff && at == size; + break; + } + pmm_free(paths, paths_alloc); + + if (!walked) { + return false; + } + fb->source_probe |= FB_PROBE_CONOUT_VAR; + if (!resolved) { + return false; + } + fb->source_probe |= FB_PROBE_CONOUT_PCI; + if (conflict) { + return false; + } + fb->source_probe |= FB_PROBE_CONOUT_UNIQUE; + + fb->source_type = FB_SOURCE_PCI; + fb->pci_source = agreed; + printv("gop: Framebuffer source PCI %x:%x:%x.%x (ConOut console)\n", + (uint32_t)agreed.segment, (uint32_t)agreed.bus, + (uint32_t)agreed.device, (uint32_t)agreed.function); + return true; +} + +static void get_framebuffer_source(struct fb_info *fb, EFI_HANDLE gop_handle, + size_t handles_count, size_t handle_index) { + if (!get_framebuffer_source_from_device_path(fb, gop_handle) + && !get_framebuffer_source_from_child_relation(fb, gop_handle) + && !get_framebuffer_source_from_conout(fb, gop_handle)) { + printv("gop: No positive framebuffer source (probe %x)\n", + fb->source_probe); + } + if (fb->source_type == FB_SOURCE_PCI) { + fb->source_probe |= FB_PROBE_SOURCE_PCI; + } + fb->source_probe |= (uint32_t)(handles_count > 0xff ? 0xff : handles_count) + << FB_PROBE_HANDLES_SHIFT; + fb->source_probe |= (uint32_t)(handle_index > 0xff ? 0xff : handle_index) + << FB_PROBE_INDEX_SHIFT; } static bool try_mode(struct fb_info *ret, EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, @@ -500,7 +654,7 @@ success:; size_t mode_count; fb->mode_list = get_mode_list(&mode_count, gop); fb->mode_count = mode_count; - get_framebuffer_source(fb, handles[i]); + get_framebuffer_source(fb, handles[i], handles_count, i); fbs_count++; } diff --git a/common/lib/fb.h b/common/lib/fb.h index cb85a3e4..43da8751 100644 --- a/common/lib/fb.h +++ b/common/lib/fb.h @@ -15,6 +15,26 @@ struct resolution { #define FB_SOURCE_NONE 0 #define FB_SOURCE_PCI 1 +// Source-probe outcome bits, recorded per framebuffer while the source +// proofs run. Wire vocabulary: values must match the +// LIMINE_MP_FRAMEBUFFER_PROBE_* constants in protos/limine_mp.h and the +// revision-1 contract in EXTENSIONS.md. +#define FB_PROBE_DP_PRESENT (1 << 0) // GOP handle carries a device path +#define FB_PROBE_DP_PCI_PREFIX (1 << 1) // that path has an EFI_PCI_IO prefix +#define FB_PROBE_DP_LOCATED (1 << 2) // prefix handle yielded PCI coordinates +#define FB_PROBE_CHILD_SCAN (1 << 3) // child-controller relation scan ran +#define FB_PROBE_CHILD_RELATION (1 << 4) // a PCI parent names this GOP handle +#define FB_PROBE_CONSOLE_HANDLE (1 << 5) // GOP handle == gST->ConsoleOutHandle +#define FB_PROBE_CONOUT_VAR (1 << 6) // ConOut variable read and walked +#define FB_PROBE_CONOUT_PCI (1 << 7) // >=1 ConOut instance reached PCI I/O +#define FB_PROBE_CONOUT_UNIQUE (1 << 8) // every resolved instance agrees on one BDF +#define FB_PROBE_SOURCE_PCI (1 << 9) // final verdict: source_type == PCI + +// [23:16] GOP handle count seen by init_gop, [31:24] this framebuffer's +// handle index, both saturating at 0xff. +#define FB_PROBE_HANDLES_SHIFT 16 +#define FB_PROBE_INDEX_SHIFT 24 + struct fb_pci_source { uint16_t segment; uint8_t bus; @@ -44,6 +64,7 @@ struct fb_info { uint8_t source_type; struct fb_pci_source pci_source; + uint32_t source_probe; }; extern struct fb_info *fb_fbs; diff --git a/common/protos/limine.c b/common/protos/limine.c index eb5e2814..c71ed5b4 100644 --- a/common/protos/limine.c +++ b/common/protos/limine.c @@ -1649,6 +1649,7 @@ FEAT_START struct limine_mp_framebuffer_pci_source *pci_sources = ext_mem_alloc_counted(fbs_count, sizeof(struct limine_mp_framebuffer_pci_source)); uint64_t *source_list = ext_mem_alloc_counted(fbs_count, sizeof(uint64_t)); + uint64_t *probe_masks = ext_mem_alloc_counted(fbs_count, sizeof(uint64_t)); for (size_t i = 0; i < fbs_count; i++) { if (fbs[i].source_type == FB_SOURCE_PCI) { @@ -1662,12 +1663,15 @@ FEAT_START } source_list[i] = reported_addr(&sources[i]); + probe_masks[i] = fbs[i].source_probe; } struct limine_mp_framebuffer_source_response *source_response = ext_mem_alloc(sizeof(struct limine_mp_framebuffer_source_response)); + source_response->revision = 1; source_response->entry_count = fbs_count; source_response->entries = reported_addr(source_list); + source_response->probe_masks = reported_addr(probe_masks); source_request->response = reported_addr(source_response); FEAT_END diff --git a/common/protos/limine_mp.h b/common/protos/limine_mp.h index 43b20ad4..8f2f12f0 100644 --- a/common/protos/limine_mp.h +++ b/common/protos/limine_mp.h @@ -13,6 +13,22 @@ #define LIMINE_MP_FRAMEBUFFER_SOURCE_UNKNOWN 0 #define LIMINE_MP_FRAMEBUFFER_SOURCE_PCI 1 +// Revision-1 probe-mask bits: which source proofs ran and how far each got, +// per framebuffer. Values mirror the FB_PROBE_* constants in common/lib/fb.h +// (the loader-side recorder); EXTENSIONS.md is the contract. +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_PRESENT (1 << 0) +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_PCI_PREFIX (1 << 1) +#define LIMINE_MP_FRAMEBUFFER_PROBE_DP_LOCATED (1 << 2) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CHILD_SCAN (1 << 3) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CHILD_RELATION (1 << 4) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONSOLE_HANDLE (1 << 5) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_VAR (1 << 6) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_PCI (1 << 7) +#define LIMINE_MP_FRAMEBUFFER_PROBE_CONOUT_UNIQUE (1 << 8) +#define LIMINE_MP_FRAMEBUFFER_PROBE_SOURCE_PCI (1 << 9) +#define LIMINE_MP_FRAMEBUFFER_PROBE_HANDLES_SHIFT 16 +#define LIMINE_MP_FRAMEBUFFER_PROBE_INDEX_SHIFT 24 + struct limine_mp_framebuffer_pci_source { uint64_t segment; uint64_t bus; @@ -29,6 +45,9 @@ struct limine_mp_framebuffer_source_response { uint64_t revision; uint64_t entry_count; LIMINE_PTR(struct limine_mp_framebuffer_source **) entries; + // Revision >= 1: `entry_count` probe masks, index-aligned with `entries`. + // Readers must gate on `revision` before touching this field. + LIMINE_PTR(uint64_t *) probe_masks; }; struct limine_mp_framebuffer_source_request { diff --git a/test/limine.c b/test/limine.c index 3ad4269c..0499b859 100644 --- a/test/limine.c +++ b/test/limine.c @@ -541,6 +541,22 @@ FEAT_START printf("Source %lu: invalid type %lu\n", i, source->type); } } + if (source_response->revision < 1) { + printf("No probe masks (revision 0)\n"); + break; + } + if (source_response->probe_masks == NULL) { + printf("Revision 1 without probe masks\n"); + break; + } + for (size_t i = 0; i < source_response->entry_count; i++) { + uint64_t probe = source_response->probe_masks[i]; + struct limine_mp_framebuffer_source *source = source_response->entries[i]; + bool source_pci = source->type == LIMINE_MP_FRAMEBUFFER_SOURCE_PCI; + bool probe_pci = (probe & LIMINE_MP_FRAMEBUFFER_PROBE_SOURCE_PCI) != 0; + printf("Probe %lu: %lx%s\n", i, probe, + source_pci == probe_pci ? "" : " (VERDICT MISMATCH)"); + } FEAT_END FEAT_START