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

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 for the DF-4104/4152/4289 family

Summary

struct atom_context carries bios with NO length. The allocation is the self-declared image size while every ATOM dereference uses raw u16 offsets from image content: the rom/cmd/data/iio/msg pointers, both master-table header parsers whose unvalidated *data_start/*size feed ~40 caller walks, the interpreter fetch loop (jumps can drive out of the image so the kernel executes adjacent heap as ATOM bytecode), the ID operand reaching 0x1FFFE, and the calltable/setdatablock reads. CENTRAL HUB FIX (the amdgpu twin is byte-identical): add bios_size to atom_context, plumb the allocation length from every radeon_bios reader, gate every dereference behind atom_in_bios(). NEUTRALIZATION ASSESSMENT: DF-4104 killed; DF-4152/DF-4289 partial (inner walks still need per-site bounds); DF-4168 and DF-4048 unaffected (different parser / not a bounds bug). Guest logic-level PoC: verbatim data_header with a 512B image -> SIGSEGV at the CU16 read; fixed-logic rejects. Fix.diff git-apply clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4337 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
atom_harness.c β€” 7.5 KB view raw
atom_harness_fixed.c β€” 5.2 KB view raw
build.sh β€” 115 B view raw
run.sh β€” 107 B view raw
run_fixed.sh β€” 171 B view raw
build.log β€” 7 B view raw
run_cases.log β€” 507 B view raw
fix_run.log β€” 488 B view raw
env.txt β€” 467 B view raw
fix.diff β€” 9.4 KB view raw
verdict.json β€” 3.7 KB view raw

DF-4337 β€” atom.c has no bios_size: every VBIOS offset dereference in the ATOM interpreter/parsers is unbounded

Severity: High Confidence: certain File: sys/dev/drm/radeon/atom.c

What

struct atom_context (atom.h:125-143) carries void *bios but no length. The bios heap allocation is exactly as large as the source image β€” and the sources self-declare sizes that can be far smaller than the 16-bit offset space the ATOM tables use:

  • radeon_bios.c:radeon_read_platform_bios allocates bios[2]*512 bytes (512..256KB; a 512-byte image is accepted if bios[2]==1);
  • radeon_acpi_vfct_bios allocates vhdr->ImageLength;
  • radeon_read_bios allocates the vga_pci ROM mapping size.

Every ATOM dereference uses raw 16-bit offsets from the image with no check against that allocation:

  • atom_parse (atom.c:1298-1326): ROM-table ptr, cmd/data table ptrs, IIO ptr, message-string ptr are all dereferenced unvalidated;
  • atom_parse_data_header (atom.c:1366-1385) / atom_parse_cmd_header (atom.c:1387-1402): master-table entry and table header read unvalidated; *data_start/*size are handed to dozens of callers (radeon_atombios.c, atombios_encoders.c, *_dpm.c) who then walk structs at that offset;
  • atom_execute_table_locked (atom.c:1163-1214): base and the interpreter fetch loop itself (op = CU8(ptr++)) never check ptr against the image β€” a crafted jump/switch-walk walks the execution pointer out of the image and the kernel keeps executing whatever heap bytes follow as ATOM opcodes;
  • ATOM_ARG_ID (atom.c:279) val = U32(idx + gctx->data_block): idx(u16) + data_block(u16) reaches 128KB past the buffer base;
  • atom_op_calltable (atom.c:633), atom_op_setdatablock (atom.c:848), atom_asic_init (atom.c:1334-1341), atom_allocate_fb_scratch (atom.c:1411-1418): same class.

How to reproduce (logic-level; kernel runtime needs ATOM hardware)

The guest has no ATI/AMD GPU (env.txt), so the kernel path cannot be exercised. The exact dereference expressions were compiled verbatim into a userland mirror with the bios buffer ending at a PROT_NONE guard region; any read past the allocation faults deterministically.

./build.sh   # on the audit guest (or any DF box): cc -O1 -g ...
./run.sh     # case data_header -> Segmentation fault (exit 139)
./run_fixed.sh  # fixed logic -> rejected cleanly, exit 0

Fix

fix.diff (validated git apply --check): adds bios_size to atom_context, plumbs the allocation length from every radeon_bios.c reader through radeon_device into atom_parse, and gates every offset dereference behind a new atom_in_bios() helper β€” including the interpreter fetch loop, both master-table header parsers, and the ID/calltable/ setdatablock reads.

The amdgpu twin (sys/dev/drm/amd/amdgpu/atom.c, amdgpu_atom_parse*) is byte-for-byte the same code and needs the same port (see VERDICT.md for the downstream-neutralization analysis: DF-4104 neutralized; DF-4152/DF-4289 partially (inner walks still need per-site bounds); DF-4168 and DF-4048 not affected by this fix).

VERDICT.md
↓ download raw

DF-4337 VERDICT

Status: inconclusive (kernel runtime hardware-gated; logic-level OOB reproduced on the audit guest). Defect: certain.

Root cause (code-proven)

struct atom_context has no bios length member (atom.h:125-143). The bios buffer is allocated to the actual image size β€” which is self-declared: radeon_read_platform_bios uses bios[2]*512 (radeon_bios.c:146-157, accepts 512-byte images), radeon_acpi_vfct_bios uses vhdr->ImageLength (radeon_bios.c:717), radeon_read_bios uses the vga_pci ROM size (radeon_bios.c:98). Meanwhile every ATOM dereference uses unvalidated 16-bit offsets from image content:

Site Expression
atom.c:1307-1309 cmd_table/data_table = CU16(base+0x1E/0x20), IIO ptr, msg ptr deref'd
atom.c:1366-1385 idx = CU16(data_table+offset), *size=CU16(idx), *data_start=idx unvalidated
atom.c:1387-1402 same for the command master table
atom.c:1165/1193 base = CU16(cmd_table+4+2*index), then op = CU8(ptr++) in an unbounded fetch loop
atom.c:279 val = U32(idx + gctx->data_block) β€” up to 0x1FFFE (~128KB)
atom.c:633, 848 calltable/setdatablock master-entry reads
atom.c:1334-1341, 1411-1418 asic_init FWI chain, VRAM_UsageByFirmware struct

With a 512-byte allocation, an offset of 0x8000 reads 32KB past the heap object. The fetch loop is the worst instance: ATOM jumps (atom_op_jump: *ptr = ctx->start + target, atom.c:749) and the atom_op_switch case walk (atom.c:983-1001) move ptr with no upper bound, so the interpreter keeps fetching "opcodes" from adjacent kernel heap until an EOT byte (or an unmapped page β†’ kernel page fault β†’ panic).

Reproduction

Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (env.txt), no ATI/AMD GPU (pciconf -l shows only QEMU 0x1234:1111), radeon/amdgpu not loaded β€” the kernel path is unreachable on this guest (QEMU cannot emulate an ATI card with a custom VBIOS ROM).

Logic-level PoC (run ON the guest): atom_harness.c copies the exact expressions (atom_parse_data_header verbatim, atom.c:1366-1385) into a userland mirror whose bios buffer ends at a 16MB PROT_NONE guard region.

Result (run_cases.log): data_header case β€” 512-byte image, master entry 0x8000 β†’ Segmentation fault, exit 139: the *size = CU16(idx) read at atom.c:1378 lands 32KB past the allocation. Identical expression, identical index arithmetic, identical result class in-kernel (heap OOB read).

Fix validation (logic-level)

fix.diff adds bios_size plumbing + atom_in_bios() gates at every site above (incl. the fetch loop). Validated: 1. git apply --check passes against the pristine tree (all 5 files). 2. atom_harness_fixed.c re-runs the same cases with the fixed logic on the guest: data_header β†’ ok=0 (entry rejected vs bios_size=512, exit 0, no trap) β€” see fix_run.log.

A kernel-build + on-hardware validation was not possible: the guest has no ATOM hardware to attach the patched driver to (hence fix_status inconclusive, not fixed).

Downstream neutralization assessment (the central-fix question)

Porting this fix to the amdgpu twin (amd/amdgpu/atom.c:1296-1419, same verbatim functions amdgpu_atom_parse_data_header/cmd_header) plus amdgpu_bios.c/amdgpu_atombios.c:2037 plumbing:

  • DF-4104 (atombios_encoders LVDS_Info fixed 0x2C reads at unvalidated data_offset): neutralized β€” parse_data_header then guarantees data_offset + size <= bios_size and size >= 4; recommend the port also enforce size >= 0x2C for that caller (or a caller-side check).
  • DF-4152 (amdgpu_atombios object-table walks): partially β€” the outer data-table offsets become validated; the inner walks (object header β†’ path arrays with their own strides/counts) still need per-site bios_size bounds.
  • DF-4289 (ppatomctrl SS walk bounded by its own usStructureSize): partially β€” the table entry/offset becomes validated; the walk itself must be bounded by bios end (per-site fix still required).
  • DF-4168 (bios_parser2.c DC parser bounds by bios[2]*512): not neutralized β€” bios_parser2 never calls atom_parse_data_header; it needs its own image-length fix.
  • DF-4048 (command_table ulSymClock truncation): not affected β€” not a bounds defect.

radeon-side callers (radeon_atombios.c, atombios_encoders.c, ci/ni/rv770/ kv/rs* _dpm.c β€” ~40 atom_parse_data_header/cmd_header call sites) get the same outer-offset guarantee from this diff alone.

Bottom line

Certain, unbounded OOB-read primitive (up to 128KB reach on the ID path, unbounded on the fetch-loop path) at driver attach, enabler of the whole DF-4104/4152/4289 family. Kernel-runtime escalation was not demonstrable on the audit guest because the trigger hardware does not exist there; the primitive itself is proven at the expression level on the guest, and the fix is validated to close it.

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 passes git apply --check against the pristine tree; the fixed-logic mirror on the guest rejects the malicious master entry (ok=0, exit 0) where the baseline traps (139). A patched-kernel run is impossible on this guest (no ATOM hardware), so fixed-at-kernel-runtime is not demonstrable here.

fix_run.log (data_header fixed case), fix.diff, DF-4337/VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-4337

Confirmed kernel references

Detail

Evidence (decisive lines)

run_cases.log [data_header] Segmentation fault exit=139; VERDICT.md table of unbounded sites with path:line; fix_run.log fixed case ok=0 exit 0; fix.diff (git apply --check OK)

PoC changes

no seed; harness written from scratch: verbatim atom_parse_data_header + atom-bits.h macros, bios buffer placed at the end of a 16MB PROT_NONE region so OOB reads trap deterministically

Verified recommended fix

Add bios_size to atom_context, plumb the allocation length from every radeon_bios.c reader, and gate every bios-offset dereference (both master-table header parsers, the interpreter fetch loop, ID/calltable/setdatablock reads) behind an atom_in_bios(ctx, off, len) helper; port the same to amd/amdgpu/atom.c

Verdict

Code defect certain: atom_context carries no bios length, so every 16-bit VBIOS offset dereference in atom.c (master/data/cmd table parsers at atom.c:1366/1387, interpreter fetch loop at atom.c:1193, ID operand at atom.c:279, calltable/setdatablock at atom.c:633/848, atom_parse itself at atom.c:1298-1326) is an unbounded OOB read up to 128KB past a heap allocation that can be as small as 512 bytes (radeon_bios.c self-declared bios[2]*512). Kernel runtime could not be exercised: the audit guest has no ATI/AMD GPU (QEMU bochs vga only), so the radeon driver never attaches β€” the primitive was instead proven on the guest by compiling the verbatim expressions into a guard-page userland mirror: the data_header case (master entry 0x8000 vs 512-byte image) deterministically SIGSEGVs (exit 139) exactly at atom.c:1378's CU16(idx). The fix (bios_size plumbing + atom_in_bios gates, incl. the fetch loop) passes git apply --check and the fixed-logic mirror rejects the same input cleanly (exit 0).