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