bios_get_image()'s sole bounds check uses wrap-prone 32-bit offset+size arithmetic β the security boundary for ~60 DC atombios parser dereferences, latent only (all current callers are uint16-bounded)
Summary
The only image-bounds check for the ~60 GET_IMAGE atombios dereferences validates firmware-supplied (offset,size) with wrapping uint32 offset+size < bios_size: offset=0xFFFFFFF2/size=16 wraps to 2 and returns a ~4GB-off pointer callers immediately cast+ deref. Trigger set provably empty today (all caller offsets are sums of two le16 fields max ~0x1FFFE; sizes sizeof(struct) or u8 count x2) - hardening only, composing with DF-4168 (bios_size provenance). Overflow-safe two-comparison fix, git-apply-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5224 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| verify.sh | β | 1.6 KB | view raw | |
| run.log | β | 3.5 KB | view raw | |
| fix.diff | β | 551 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-5224 β bios_get_image() bounds check uses wrap-prone offset + size arithmetic (hardening)
File: sys/dev/drm/amd/display/dc/bios/bios_parser_helper.c:40 (pass 2, GLM 5.3)
Severity: Info (hardening) Β· Confidence: speculative (hazard is real at code level; no current caller can trigger it β proven below)
What
uint8_t *bios_get_image(struct dc_bios *bp, uint32_t offset, uint32_t size)
{
if (bp->bios && offset + size < bp->bios_size) /* <-- wrap-prone */
return bp->bios + offset;
else
return NULL;
}
This 90-line helper file's only memory-safety-relevant statement is this
bounds check. It is the security boundary for every GET_IMAGE(type, offset)
dereference in the DC atombios parsers (~60 sites in bios_parser.c +
bios_parser2.c, plus the direct call at bios_parser.c:2022). The addition
offset + size is performed in wrapping 32-bit arithmetic: for
offset = 0xFFFFFFF2, size = 16 the sum wraps to 2 < bios_size and the
function returns bp->bios + 0xFFFFFFF2 β a wild kernel pointer that callers
immediately cast to an atom-table struct and dereference.
Why it is NOT reachable today (killed, with evidence)
Every offset expression reaching this function is bounded by two
little-endian uint16_t VBIOS fields summed, plus small constants:
- data-table offsets:
bp->master_data_tbl_offset + tbl->list.X.data_offsetβ both sides uint16 (atomfirmware.h:381-424,atom_master_list_of_data_tables_v2_1is alluint16_t) - object/record offsets:
bp->object_info_tbl_offset + le16(field)(bios_parser.c:137,164,234,297,887,1527,1988,2015) - ROM bootstrap offsets: fixed constants (
OFFSET_TO_ATOM_ROM_HEADER_POINTER = 0x48,atomfirmware.h:210) oruint16_timage reads (bios_parser.c:2884-2923)
So offset β€ 0xFFFF + 0xFFFF β 0x1FFFE. Sizes are sizeof(struct) (largest
atom struct here ~4 KB) or, for the one direct call
(bios_parser.c:2022), *number * sizeof(uint16_t) where number is a
uint8_t image byte (max 255, product computed in 64-bit via sizeof, max
510). Worst case offset + size β 0x21000 β cannot wrap uint32_t.
bios_size itself is bios[2] * 512 β€ 130560 (bios_parser.c:2878).
The < comparator is conservative-correct (admits only strictly-interior
windows), and bp->bios is NULL-guarded.
NOT a duplicate of DF-4168
DF-4168 (Medium, bios_parser2.c:1930) covers the reference point being
wrong: bios_size is derived from the image's self-declared size byte
(bios[2]*512) instead of the allocated buffer length, so even
non-wrapping offsets can read past the real allocation. DF-5224 is about the
check's own arithmetic: with DF-4168 fixed (honest bios_size), the
wrap-prone form still silently admits offset near UINT32_MAX from any
future/wrong caller. The two compose; they are distinct defects at distinct
sites. (Also distinct from DF-3942/DF-1299, which are missing NULL/limit
checks in the callers, not in this helper.)
Fix (hardening, upstream-portable)
See fix.diff β rewrite in overflow-safe form. Behavior is byte-identical
for every in-range call that passes today; only wrap-candidate inputs change
from "wild pointer" to "NULL".
Reproduce
Not runtime-reproducible on the audit guest: the trigger requires an AMD
ASIC whose VBIOS content produces a 32-bit offset near UINT32_MAX, and no
current parser expression can emit such an offset. Verified at source level
only; that is appropriate for an Info hardening finding per the audit
contract. Run verify.sh for the mechanical caller-bounds audit backing the
negative reachability claim.
DF-5224 β VERDICT
Status: untested (Info hardening β source-level verification only, per audit contract for Low/Info findings). Reproduced: no. Impact: none (latent).
What I verified, line by line
bios_parser_helper.c is compiled into the live amdgpu.ko module
(sys/dev/drm/amd/amdgpu/Makefile:208, .PATH includes ../display/dc/bios;
CONFIG_DRM_AMD_DC 1 in sys/dev/drm/kconfig.h:23). It is reached at GPU
attach; its BIOS image is adev->mode_info.atom_context->bios
(amdgpu_dm.c:410-411 β dc.c:639 β bios_parser.c:2877 /
bios_parser2.c:1929), i.e. firmware/hardware-supplied data β the attacker
model for this whole file family is a crafted or corrupt VBIOS on the card
(same precondition class as DF-5005/DF-5163/DF-5164/DF-4168).
The helper's check itself (bios_parser_helper.c:40)
offset + sizewraps mod 2^32 foroffset β₯ 2^32 - size. With a wrapped sum belowbios_size, the function returnsbp->bios + offsetβ a kernel pointer ~4 GB away from the image buffer that callers immediately dereference as an atom table struct. The arithmetic hazard is real.- Reachability: killed. All ~60
GET_IMAGEsites plus the one direct call feed offsets that are the sum of at most twouint16_tLE VBIOS fields (+ small constants): max β 0x1FFFE. Sizes:sizeof(struct)β€ ~4 KB, or β€ 510 (64-bit-computed,bios_parser.c:2022). Max sum β 0x21000 βͺ 2^32. No wrap is producible by any caller in this tree. (Mechanical audit of every call site: see README.md andverify.shoutput inrun.log.) <(vs<=) is the conservative direction β safe.bp->biosNULL-guarded (:40).bios_sizeprovenance is DF-4168's finding (bios_parser2.c:1930,bios_parser.c:2878: content-derivedbios[2]*512vs real allocation); explicitly not re-reported here β this finding is only about the check's arithmetic and is meaningful after DF-4168 is fixed.
The rest of the file (hunt items aβe, all clean)
- (a) GET_* helpers / index bounds / casts:
bios_get_imageis the only image helper;GET_IMAGEmacro (bios_parser_helper.h:39) adds nothing. No index arithmetic lives in this file. - (b) shifts/masks/sign:
FN(:54-55) expands toATOM_S6_ACC_MODE_SHIFT=4 / ATOM_S6_ACC_MODE=0x10andATOM_S6_CRITICAL_STATE_SHIFT=8 / ATOM_S6_CRITICAL_STATE=0x100(atombios.h:6959,6963,7031,7035) β mask == 1<<shift for both, coherent pairs, single-bit fields, unsigned throughout. No sign confusion. - (c) callers: both parsers (
bios_parser.cv1 path viadal_bios_parser_createfallback,bios_parser2.cfirmware-parser path,bios_parser_interface.c:41-48) β firmware-table-driven, as expected. - (d) build/users: amdgpu.ko only;
bios_is_accelerated_mode,bios_set_scratch_*,bios_get_vga_enabled_displaysconsumed by DC init/HPD paths. - (e) register helpers:
bios->regs->BIOS_SCRATCH_6is defined non-zero on every constructible DC: dce80/100/110/112 (*_resource.c: .BIOS_SCRATCH_6 = 0x05CF), dce120/dcn10 (NBIO segment macros). So the unguardedREG_*SCRATCH_6 uses are safe on all shipped ASICs. Theif (bios->regs->BIOS_SCRATCH_3)guard at:86exists precisely because pre-SOC15bios_regsstructs only populate SCRATCH_6 β deliberate, and0xFFFFmasking of the SCRATCH_3 count matches the 16-bit field. - Command-table struct consistency: the file includes
command_table.hbut consumes nothing from it; no struct skew possible from this file.
Fix
fix.diff (authored post-verification, never applied to sys/):
- if (bp->bios && offset + size < bp->bios_size)
+ if (bp->bios && offset < bp->bios_size &&
+ size <= bp->bios_size - offset)
Overflow-safe, behavior-identical for all honest inputs, converts wrap-candidate inputs into NULL (which callers such as DF-3942's NULL-deref sites already expect β the caller-side NULL-check gap is separately tracked by DF-3942 and is unaffected in the safe direction).
Why no QEMU run
The audit guest has no AMD GPU and no path by which an unprivileged local
user can supply a VBIOS; the finding's trigger set is empty on this tree
(proven above). A runtime "verification" would be theater. verify.sh
reruns the mechanical caller-bounds audit so the negative claim is
reproducible by anyone.
Bottom line
File the Info hardening note: the DC's single image-bounds check is written
in wrap-prone arithmetic; fix it while fixing the DF-4168 family so the
eventually-honest bios_size is checked with eventually-honest arithmetic.
No exploitable defect exists in this file today.
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
not_testableNo behavioral delta to validate: the finding has no runtime trigger on this tree (no caller can wrap the sum) and the guest has no AMD GPU. The one-line fix is behavior-identical for every honest input (all currently-passing calls still pass: offset<size and size<=size-offset holds whenever offset+size<size in true arithmetic), converts wrap-candidate inputs from wild pointer to NULL; git apply --check clean.
fix.diff
Confirmed kernel references
- sys/dev/drm/amd/display/dc/bios/bios_parser_helper.c:40
- sys/dev/drm/amd/display/dc/bios/bios_parser_helper.h:39
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:2878
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:2022
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1930
- sys/dev/drm/amd/include/atomfirmware.h:381
Detail
Evidence (decisive lines)
README.md (wrap math + caller-by-caller bound proofs); VERDICT.md (full narrative incl. register-helper and shift/mask audits); verify.sh + run.log (mechanical caller-bounds audit, incl. BIOS_SCRATCH_6 nonzero on all six constructible DCs); fix.diff (git apply --check clean)
PoC changes
not applicable β no PoC (Info hardening; trigger set provably empty on this tree; guest has no AMD GPU)
Verified recommended fix
bios_parser_helper.c:40 β rewrite as if (bp->bios && offset < bp->bios_size && size <= bp->bios_size - offset) (see fix.diff).
Verdict
Info hardening, verified at source level only (per contract for Low/Info findings; the QEMU guest has no AMD GPU and no caller can produce the trigger). bios_get_image() (bios_parser_helper.c:36-44) is this file's sole memory-safety boundary and its check offset + size < bp->bios_size is written in wrap-prone 32-bit arithmetic: a wrapped sum below bios_size returns bp->bios+offset as a wild kernel pointer that ~60 GET_IMAGE sites immediately dereference as atom-table structs. Reachability was hunted and KILLED: every caller offset is the sum of at most two little-endian uint16_t VBIOS fields plus small constants (max ~0x1FFFE; atomfirmware.h:381-424 master-data-table offsets are all uint16_t; bios_parser.c:137/164/234/297/887/1527/1988/2015/2884-2923), and every size is sizeof(struct) (<= ~4KB) or numbersizeof(uint16_t) with number a uint8_t image byte (<=510, product computed in 64-bit via sizeof) at bios_parser.c:2022 β so offset+size (~0x21000 max) cannot wrap uint32. Distinct from DF-4168 (bios_parser2.c:1930: bios_size is content-derived bios[2]*512 instead of the allocated buffer length β wrong reference point, not wrong arithmetic) and composes with its fix. Recommended rewrite is overflow-safe: offset < bios_size && size <= bios_size - offset.
No comments yet.