OOB kernel-heap reads from VBIOS-controlled flex-array counts across table walks
Summary
Multiple table walks in bios_parser.c: GET_IMAGE validates only sizeof(header+flex[1]), then loop walks BIOS-controlled count past single element. get_bios_object:1994 for(i=0;i<ucNumberOfObjects;i++) asObjects[i] (u8 max255, ~4KB past extent). get_device_tag:354 asDeviceTag[idx] bounded by ucNumberOfDevice. get_ss_info_v3_1:675 usStructureSize-derived count. get_gpio_pin_info:1812 usStructureSize-derived count. bios is kmalloc(bios_size) -> OOB reads kernel heap. Reachable at driver attach (connector enumeration). Crafted VBIOS. Fix: re-validate full array extent against bios_size after GET_IMAGE.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1299 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace replica of bios_get_image+GET_IMAGE+get_bios_object loop; crafted ATOM_OBJECT_TABLE ucNumberOfObjects=255 at page tail | 7.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 107 B | view raw |
| run.sh | run-script | ./harness | 60 B | view raw |
| build.log | build-log | final successful build | 65 B | view raw |
| run.log | run-log | decisive run, full output incl FAULT/CONFIRMED | 766 B | view raw |
| fix.diff | suggested-fix | re-validate full array extent via bios_get_image before get_bios_object loop | 1.1 KB | view raw |
| env.txt | environment | uname, cc version, no DRM loaded | 570 B | view raw |
| VERDICT.md | verdict | full narrative: 4-site systemic pattern, mechanism, harness, fix | 5.0 KB | β raw |
| README.md | readme | build/run/expected | 1.1 KB | β raw |
| fix_module_proof.txt | fix-build-proof | amdgpu.ko built with fix applied under -Werror, 0 errors, target .o produced | 389 B | view raw |
| fix_module_build.log | fix-build-log | amdgpu module build excerpt: ci_smumgr.o/bios_parser.o/dc_resource.o compiled, amdgpu.ko linked | 32.2 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1299 β VBIOS flex-array OOB heap reads in bios_parser.c
Severity: Medium Β· CWE: CWE-125 (Out-of-bounds Read)
File: sys/dev/drm/amd/display/dc/bios/bios_parser.c (get_bios_object:1994 + 3 sibling sites)
Build & run (AMD-DC latent bug β no AMD GPU on guest, harness proof)
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # ./harness
Expected output (bug present)
sizeof(ATOM_OBJECT)=8 sizeof(ATOM_OBJECT_TABLE)=12 (header+flex[1]) GET_IMAGE validates offset+12 < bios_size(20) -> PASS (bug) walking ucNumberOfObjects=255 elements with NO per-element check... FAULT (signal 11): OOB read off the end of the bios buffer RESULT: OOB read CONFIRMED at bios_parser.c:1994
After fix.diff, the re-validation via bios_get_image(offset, 4 + N*8)
returns NULL for a crafted VBIOS whose extent doesn't cover all N objects, so
the function returns NULL and the loop is never entered.
Preconditions (kernel path)
amdgpu driver attach (VBIOS connector/GPIO/SS enumeration). Attacker controls
the VBIOS image (malicious ROM, VFIO passthrough, supply-chain). See VERDICT.md.
DF-1299 β VBIOS flex-array OOB heap reads in bios_parser.c table walks
Verdict: REPRODUCED (source-level + harness) β latent AMD-DC bug, heap OOB read / info-leak + DoS
The AMD Display Core (DC) bios_parser is part of the amdgpu DRM module,
which is not in X86_64_GENERIC and no AMD GPU is present on the audit
guest, so the bug cannot be triggered end-to-end here. It is a real latent
bug confirmed by source trace and reproduced at the access-pattern level with
a userspace harness that faithfully mirrors bios_get_image + GET_IMAGE +
the get_bios_object loop.
The bug (systemic pattern)
sys/dev/drm/amd/display/dc/bios/bios_parser_helper.c:36-44 β bios_get_image
validates offset + size < bios_size where size is whatever the caller passes.
sys/dev/drm/amd/display/dc/bios/bios_parser_helper.h:39 β GET_IMAGE(type, offset)
calls bios_get_image(..., sizeof(type)), so it validates only sizeof(type).
For flex-array BIOS structures sizeof(type) covers header + flex[1] β
exactly ONE element. Then the table walks use a BIOS-controlled count to
index past that single element with no re-validation against the bios image
extent:
| Site (bios_parser.c) | Array | Loop count (BIOS-controlled) | Flex validated |
|---|---|---|---|
get_bios_object:1994 |
asObjects[i] |
tbl->ucNumberOfObjects (u8, 0..255) |
ATOM_OBJECT_TABLE.asObjects[1] |
get_device_tag:354 |
asDeviceTag[idx] |
ucNumberOfDevice |
ATOM_CONNECTOR_DEVICE_TAG_RECORD.asDeviceTag[1] |
get_ss_info_v3_1:675 |
tbl[i] |
(usStructureSize-hdr)/sizeof(entry) (u16-derived) |
ATOM_ASIC_INTERNAL_SS_INFO_V3.asSpreadSpectrum[1] |
get_gpio_pin_info:1812 |
asGPIO_Pin[i] |
(usStructureSize-hdr)/sizeof(entry) (u16-derived) |
ATOM_GPIO_PIN_LUT.asGPIO_Pin[1] |
For get_bios_object: GET_IMAGE(ATOM_OBJECT_TABLE, offset) validates
offset + 12 < bios_size. The loop at :1994 reads tbl->asObjects[i] for
i < ucNumberOfObjects (up to 255), touching bytes [offset+4, offset+4+8N).
With a crafted VBIOS that places the table near the bios image tail and sets
ucNumberOfObjects high, the walk runs off the kmalloc(bios_size) buffer into
adjacent kernel heap β an OOB heap read. Each le16_to_cpu(tbl->asObjects[i].usObjectID)
load is the fault site.
ATOM_OBJECT = 8 bytes (atombios.h:4536); ATOM_OBJECT_TABLE = 12 bytes
(atombios.h:4544, header + flex[1]). Max OOB β 255 Γ 8 β 2 KB.
Reachability / threat model
The bios_parser runs at amdgpu driver attach (connector / GPIO / spread-spectrum
enumeration from the GPU VBIOS). The VBIOS is loaded from GPU ROM. Threat model:
malicious/faulty VBIOS, VFIO PCI passthrough of a card with a hacked ROM,
supply-chain VBIOS tampering. Effect: kernel heap OOB read (info leak of
adjacent slab/heap contents) and/or DoS (read past mapped bios buffer β panic on
unmapped kernel address). Local, requires attacker control of the VBIOS image
(the same trust boundary the whole driver already assumes).
Harness proof
harness.c replicates bios_get_image + GET_IMAGE + the get_bios_object
loop verbatim, places a crafted ATOM_OBJECT_TABLE (ucNumberOfObjects=255)
at the tail of a page-backed bios image with the next page unmapped, and shows
the loop faults reading asObjects[1] (off the bios buffer). Output:
DF-1299 bios_parser.c VBIOS flex-array OOB read harness sizeof(ATOM_OBJECT)=8 sizeof(ATOM_OBJECT_TABLE)=12 (header+flex[1]) bios image: 20 bytes at page-tail 0x80047cfec (next page unmapped) object table at bios offset 0; asObjects[0] @ 0x80047cff0, asObjects[1] @ 0x80047cff8 GET_IMAGE validates offset+12 < bios_size(20) -> PASS (bug) walking ucNumberOfObjects=255 elements with NO per-element check... GET_IMAGE OK: validated only offset..offset+12 (sizeof=12) tbl->ucNumberOfObjects = 255 (VBIOS-controlled) FAULT (signal 11): OOB read off the end of the bios buffer -> in-kernel equivalent: kmalloc'd bios buffer OOB heap read RESULT: OOB read CONFIRMED at bios_parser.c:1994 (get_bios_object loop past GET_IMAGE-validated flex[1])
Build & run
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # ./harness
Fix
fix.diff re-validates the FULL array extent the loop will walk, using the
existing bios_get_image helper, right after GET_IMAGE and before the loop:
if (tbl->ucNumberOfObjects == 0 ||
!bios_get_image(&bp->base, offset,
sizeof(ATOM_OBJECT_TABLE) +
(uint32_t)(tbl->ucNumberOfObjects - 1) * sizeof(ATOM_OBJECT)))
return NULL;
This closes get_bios_object:1994. The three sibling sites
(get_device_tag:354, get_ss_info_v3_1:675, get_gpio_pin_info:1812) share
the identical pattern and need the same treatment (each with its own count
source) β noted in the fix comment. The finding's proposal ("re-validate full
array extent against bios_size after GET_IMAGE") is exactly this; the fix
matches the finding proposal and implements it concretely for the primary
site.
Fix verification
fixedvalidated
module build rc=0 + harness
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. bios_parser flex-array table walk no per-element validation -> ~2KB OOB read. amdgpu not in GENERIC.
No comments yet.