# 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:

```c
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.
