β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4345

amdgpu atom_context has no bios_size: every VBIOS offset dereference in the ATOM interpreter and master-table parsers is unbounded (OOB heap read up to 128KB; interpreter executes past the image) β€” central hub fix port of DF-4337 to the amdgpu twin

Summary

The amdgpu twin of DF-4337: struct atom_context carries no bios length and amdgpu_atom_parse receives none - although adev->bios_size ALREADY EXISTS and is set by every amdgpu_bios reader (so the port needs no bios.c changes). Every 16-bit VBIOS offset is dereferenced unvalidated: the master cmd/data/IIO pointers, the interpreter base/header and fetch loop (jumps with no upper bound), the ID operand (~128KB reach), calltable/setdatablock, asic_init FWI, and the twin parsers feeding ~54 downstream call sites. CENTRAL FIX PORT (293-line diff, git-apply clean): add bios_size to atom_context, thread adev->bios_size, gate every dereference behind amdgpu_atom_in_bios. Guest harness: ALL 8 OOB cases SIGSEGV baseline vs clean-reject fixed. Neutralization: DF-4104 mostly (residual caller-size checks), DF-4152/4289 partial, DF-4048 unaffected, bios_parser2 paths NOT covered.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4345 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 7.8 KB ↓ raw
atom_harness.c β€” 14.3 KB view raw
atom_harness_fixed.c β€” 11.4 KB view raw
build.sh β€” 151 B view raw
run.sh β€” 232 B view raw
run_fixed.sh β€” 233 B view raw
build.log β€” 26 B view raw
run.log β€” 815 B view raw
fix_run.log β€” 1.1 KB view raw
env.txt β€” 382 B view raw
fix.diff β€” 8.8 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.9 KB view raw

DF-4345 β€” amdgpu ATOM interpreter + master-table parsers dereference unvalidated VBIOS offsets (no bios_size)

sys/dev/drm/amd/amdgpu/atom.c is the amdgpu twin of radeon's atom.c (DF-4337, whose central fix this port mirrors). struct atom_context carries no bios length, so every 16-bit offset taken from VBIOS content is dereferenced unbounded:

  • master table pointers + IIO pointer at attach: atom.c:1323-1334
  • part-number string walk: atom.c:1340-1348 (filed separately as DF-4347)
  • IIO table walk + opcode index: atom.c:1280-1294 (filed as DF-4346)
  • interpreter table base + header: atom.c:1200-1211
  • interpreter fetch loop (jump/switch-movable ptr): atom.c:1228-1249
  • ID operand: atom.c:268
  • calltable / setdatablock master entries: atom.c:619, 869
  • asic_init FWI chain: atom.c:1356-1367
  • amdgpu_atom_parse_data_header / parse_cmd_header: atom.c:1384-1420

Plumbing for the fix already exists on the amdgpu side: adev->bios_size (amdgpu.h:860) is set by every reader in amdgpu_bios.c (:111, :140, :179, :217, :357, :431); only the call at amdgpu_atombios.c:2037 and the atom_context itself ignore it.

Reproduce (logic level β€” guest has no AMD GPU)

cc -O1 -g -o atom_harness atom_harness.c
./atom_harness data_header    # + 7 more OOB cases -> SIGSEGV/139 each
./atom_harness_fixed data_header   # fixed logic -> clean reject, exit 0

run.log (baseline) traps on all 8 OOB cases at the exact kernel expressions; fix_run.log shows the ported-fix logic rejecting every case. Kernel-runtime validation requires an AMD card with a crafted VBIOS (or writable ROM shadow), which QEMU cannot provide.

Fix

fix.diff β€” add bios_size to atom_context, plumb amdgpu_atom_parse(card, bios, bios_size) from adev->bios_size, and gate all bios-offset dereferences behind amdgpu_atom_in_bios() (including the interpreter fetch loop and the part-number string). Neutralizes the outer offsets of the DF-4104/DF-4152/DF-4289 family on the amdgpu side (see VERDICT.md for the per-finding assessment).

VERDICT.md
↓ download raw

DF-4345 VERDICT β€” amdgpu atom.c has no bios_size (DF-4337 central fix port)

Status: inconclusive (kernel runtime hardware-gated β€” guest has no AMD GPU; logic-level OOB reproduced on the audit guest, 8/8 cases). Defect: certain. This is the amdgpu twin of DF-4337 (radeon), which this port mirrors 1:1.

Root cause (code-proven)

struct atom_context (amd/amdgpu/atom.h:125-143) has no bios length member, and amdgpu_atom_parse(struct card_info *, void *) (atom.h:147) receives no size β€” even though struct amdgpu_device already carries bios_size (amdgpu.h:860) and every amdgpu_bios.c reader sets it (VRAM 256KB at amdgpu_bios.c:111, pci_map_rom size at :140, ROM-reader length at :179, platform ROM at :217, ATRM 256KB at :357, VFCT ImageLength at :431). The one-line consumer at amdgpu_atombios.c:2037 passes only adev->bios.

Meanwhile every ATOM dereference uses unvalidated 16-bit offsets taken from image content:

Site (sys/dev/drm/amd/amdgpu/atom.c) Expression
1323-1334 base=CU16(0x48); CU16(base+0x1E/0x20); CU16(data_table+0x32) β€” cmd/data/IIO pointers
1340-1347 idx=CU16(0x6E); str=CSTR(idx); %s + strlcpy walk
1287-1293 IIO walk CU8(base), atom_iio_len[CU8(base)]
1200, 1209-1211 base=CU16(cmd_table+4+2*index); header reads
1228-1249 interpreter fetch op=CU8(ptr++) β€” ptr moved by jumps (atom.c:756 *ptr = ctx->start + target) and switch walks (atom.c:1004-1022) with no upper bound
268 val = U32(idx + gctx->data_block) β€” up to 0x1FFFE (~128KB)
619 calltable master entry U16(cmd_table+4+2*idx)
869 setdatablock master entry U16(data_table+4+2*idx)
1356-1367 asic_init FWI chain CU32(hwi+8/0xC)
1388-1401 amdgpu_atom_parse_data_header: idx=CU16(data_table+offset), *size=CU16(idx), *data_start=idx β€” unvalidated
1408-1418 amdgpu_atom_parse_cmd_header: same

Note check_atom_bios (amdgpu_bios.c:48-75) only proves bios_header_start+8 <= size ("ATOM"/"MOTA" magic); it does NOT bounds any of the pointers above. (It also has its own off-by-one β€” reads bios[0x49] after checking only size < 0x49, i.e. size==0x49 β†’ one byte OOB; amdgpu_bios.c:52-64 β€” adjacent-file observation, folded into the port notes.) Unlike radeon, where a 512-byte image is accepted from bios[2]*512, the smallest amdgpu-sourced image is the VFCT ImageLength (attacker-influenced only via platform firmware); the 256KB VRAM/ATRM readers make offsets up to 0xFFFC fully reachable in-allocation, but nothing stops a short VFCT/ROM image (e.g. 4KB) from coexisting with a 16-bit offset of 0x8000 β†’ OOB read of a heap kzalloc(size) allocation.

Reproduction (logic level, on the guest)

Guest: DragonFly 6.5-DEVELOPMENT, gcc 8.3 (env.txt). No AMD GPU (pciconf -l: QEMU 0x1234:1111 only) β†’ amdgpu never attaches β†’ kernel path unreachable, identical to the DF-4337 situation on the radeon side.

atom_harness.c copies the exact expressions (verbatim amdgpu_atom_parse_data_header/parse_cmd_header, ID operand, calltable/setdatablock entries, IIO walk, part-string, asic_init FWI reads, interpreter fetch loop) into a userland mirror whose bios buffer ends at a 16MB PROT_NONE guard region. Result (run.log):

case data_header  exit=139   (CU16(idx)        @ atom.c:1396)
case cmd_header   exit=139   (CU8(idx+2)       @ atom.c:1416)
case id_operand   exit=139   (U32(idx+db)      @ atom.c:268)
case master_entry exit=139   (U16(ct+4+2*idx)  @ atom.c:619/869)
case iio          exit=139   (atom_iio_len[CU8]@ atom.c:1291)  -> DF-4346
case partstr      exit=139   (%s/strlcpy walk  @ atom.c:1346-47)-> DF-4347
case asic_fwi     exit=139   (CU32(hwi+8)      @ atom.c:1362)
case fetchloop    exit=139   (CU8(ptr++)       @ atom.c:1229)
case shift        exit=0     (UB divergence report)             -> DF-4348

8/8 OOB cases deterministically SIGSEGV at the exact kernel expression; identical arithmetic, identical result class in-kernel (heap OOB read / unbounded interpreter fetch past the allocation).

Fix (the DF-4337 port) + validation

fix.diff (293 lines, git apply --check OK) adds int bios_size to amd/amdgpu/atom.h's atom_context, threads it through amdgpu_atom_parse(card, bios, bios_size) from amdgpu_atombios.c:2037 (adev->bios_size β€” already maintained by all six amdgpu_bios.c readers, so no amdgpu_bios.c changes are needed, unlike the radeon fix), and gates every site above behind amdgpu_atom_in_bios(ctx, off, len) including the interpreter fetch loop and the part-number string (NUL must exist inside the image).

Validated like DF-4337: 1. git apply --check passes against the pristine tree (3 files). 2. atom_harness_fixed.c re-runs all 9 cases with the fixed logic on the guest: every OOB case cleanly rejected (ok=0 / ret=1 / ret=-EINVAL / entry-skip), fix_run.log, all exit 0.

A kernel-build + on-hardware validation is impossible on this guest (no AMD GPU to attach the patched driver to) β†’ fix_status inconclusive.

Downstream neutralization assessment (amdgpu side)

~38 amdgpu_atom_parse_data_header + ~16 amdgpu_atom_parse_cmd_header call sites (amdgpu_atombios.c Γ—22, amdgpu_atomfirmware.c Γ—9, dpm: ci/kv/si Γ—6, atombios_encoders/crtc, ppatomctrl via smu_helper) get the outer-offset guarantee (entry offset + declared table size within the image) from this diff alone:

  • DF-4104 (atombios_encoders.c LVDS_Info fixed 0x2C reads at unvalidated data_offset, atombios_encoders.c:2074-2081): mostly neutralized β€” after the gate, data_offset+size βŠ† image; the residual is a table whose declared size < 0x2C while data_offset+0x2C still ends within the image (read stays in-allocation, merely past the table body β€” info disclosure within the bios buffer, not heap OOB). Recommend the caller-side size >= 0x2C check as noted in DF-4337's verdict.
  • DF-4152 (amdgpu_atombios.c object-table walks): partially β€” outer data-table offsets validated; the inner walks (object header β†’ connector/path arrays with their own strides and counts) still need per-site bios_size bounds.
  • DF-4289 (ppatomctrl.c SS walk bounded only by its own usStructureSize): partially β€” same shape: entry offset validated, the walk itself needs a per-site bound.
  • DF-4048 (command_table.c ulSymClock 16-bit truncation): not affected β€” not a bounds defect.
  • amdgpu-side bios_parser2.c (DC): not neutralized β€” never calls amdgpu_atom_parse_data_header (same as the radeon-side DF-4168 note).

Not fixed by this port (orthogonal, already known, do-not-re-report): DF-1542 (calltable recursion), DF-1543 (FB scratch integer overflow, atom.c:273/529 β€” note fb_base + idx*4 still overflows), DF-1544 (PS operand index, atom.c:218/491), DF-1545 (WS operand index + ws=NULL, atom.c:256/523). New twins filed with this batch: DF-4346 (IIO index), DF-4347 (part string), DF-4348 (shifts).

amdgpu vs radeon atom.c differences relevant to the port

Diffed the two files: the interpreter core is byte-identical modulo printk renames (kprintf→printk, DRM_INFO→pr_info) and the RV515 ioreg-write quirk (radeon-only). amdgpu adds atom_op_mul32/div32 (64-bit math, benign) and replaces radeon's name[512] message loop with the vbios_version[20] part-number string (atom.c:1340-1348) — which is why DF-4339's fix shape does not apply here and DF-4347 exists. All DF-4337 gate sites map 1:1; the only plumbing difference is that amdgpu already tracks the allocation length in the device struct.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

inconclusive
baseline no→ patch + rebuild →patched clean

fix.diff (293 lines, 3 files) passes git apply --check against the pristine tree; the fixed-logic mirror on the guest rejects all 8 baseline-trapping inputs (ok=0/ret=-EINVAL, exit 0) where the baseline traps (139). A patched-kernel run is impossible on this guest (no AMD/ATOM hardware), so fixed-at-kernel-runtime is not demonstrable; unlike the radeon fix, no amdgpu_bios.c plumbing is required because adev->bios_size already exists.

fix.diff; fix_run.log (all 9 cases exit 0 with clean rejections); VERDICT.md 'Fix (the DF-4337 port) + validation'
↓ fix.diffper-fix-DF-4345

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 8/8 OOB cases Segmentation fault exit=139; fix_run.log: all cases rejected exit 0; fix.diff (3 files, git apply --check OK); VERDICT.md site table with path:line + downstream neutralization assessment (DF-4104 mostly, DF-4152/DF-4289 partial, DF-4048 unaffected)

PoC changes

harness adapted from the DF-4337 radeon mirror to the amdgpu twins (renamed functions, vbios_version part-string instead of name[512], asic_init FWI, fetch-loop NOP walk); fixed one comment self-termination bug and EINVAL include on the guest build

Verified recommended fix

Port DF-4337's central fix: add bios_size to amd/amdgpu/atom.h's atom_context, pass adev->bios_size at amdgpu_atombios.c:2037, and gate every bios-offset dereference (both header parsers, interpreter fetch loop, ID/calltable/setdatablock reads, IIO walk, asic_init FWI, part-number string) behind amdgpu_atom_in_bios()

Verdict

Code defect certain: the amdgpu atom_context (amd/amdgpu/atom.h:125-143) has no bios length and amdgpu_atom_parse (amdgpu_atombios.c:2037) passes none -- even though adev->bios_size already exists (amdgpu.h:860) and every amdgpu_bios.c reader sets it (bios_size at :111/:140/:179/:217/:357/:431). Every 16-bit VBIOS offset dereference is unbounded: master/cmd/data/IIO table pointers at atom.c:1323-1334, interpreter base+fetch loop at atom.c:1200/1228-1249 (ptr moved by jumps atom.c:756 and switch walks atom.c:1004-1022), ID operand U32(idx+data_block) at atom.c:268 (reach ~128KB), calltable/setdatablock master entries at atom.c:619/869, asic_init FWI chain at atom.c:1356-1367, and the verbatim twin parsers amdgpu_atom_parse_data_header/amdgpu_atom_parse_cmd_header at atom.c:1384-1420 (idx/size/data_start unvalidated; ~38+16 downstream call sites). Kernel runtime could not be exercised (no AMD GPU on the guest, QEMU bochs vga only); proven instead on the guest by a guard-page userland mirror of the exact expressions: ALL 8 OOB cases (data_header, cmd_header, id_operand, master_entry, iio, partstr, asic_fwi, fetchloop) SIGSEGV deterministically (exit 139, run.log); the DF-4337 port fix logic (bios_size + amdgpu_atom_in_bios gates incl. the fetch loop) rejects every case cleanly (exit 0, fix_run.log).