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)
PoC verification
Evidence pack
findings/poc/DF-4337 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_biosallocatesbios[2]*512bytes (512..256KB; a 512-byte image is accepted ifbios[2]==1);radeon_acpi_vfct_biosallocatesvhdr->ImageLength;radeon_read_biosallocates 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/*sizeare 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):baseand the interpreter fetch loop itself (op = CU8(ptr++)) never checkptragainst 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).
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_sizeandsize >= 4; recommend the port also enforcesize >= 0x2Cfor 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_sizebounds. - 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
inconclusivefix.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
Confirmed kernel references
- sys/dev/drm/radeon/atom.h:125
- sys/dev/drm/radeon/atom.c:1193
- sys/dev/drm/radeon/atom.c:1298
- sys/dev/drm/radeon/atom.c:1366
- sys/dev/drm/radeon/atom.c:1387
- sys/dev/drm/radeon/atom.c:279
- sys/dev/drm/radeon/atom.c:633
- sys/dev/drm/radeon/atom.c:848
- sys/dev/drm/radeon/radeon_bios.c:146
- sys/dev/drm/radeon/radeon_bios.c:717
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).
No comments yet.