Double-free of adev->bios when amdgpu_get_bios() fails: every VBIOS read method kfrees without NULLing and amdgpu_device_fini() frees the dangling pointer again on the probe-failure teardown path
Summary
All five live failure paths do kfree(adev->bios) without NULLing; when all six read methods fail the pointer dangles, and because rmmio is mapped before the BIOS read the probe-failure unload reaches amdgpu_device_fini's unconditional kfree - a second free of the same 64K-256K chunk after a nontrivial fini alloc/free window that can re-purpose it into a free-of-live-object. Mirrors upstream Linux. Distinct from the ATRM pair (lifetime-on-failure, not validation). Preconditions realistic where amdgpu runs (VFIO passthrough with bad ROM BAR, crafted VFCT/ATRM tables). NULL-after-kfree fix at all five sites.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4884 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| trigger.sh | β | 2.1 KB | view raw | |
| run.log | β | 278 B | view raw | |
| env.txt | β | 324 B | view raw | |
| fix.diff | β | 836 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-4884 β Double-free of adev->bios on amdgpu probe failure
(amdgpu_get_bios() error paths leave a dangling pointer that
amdgpu_device_fini() frees again)
What this is
A deterministic double-free in the amdgpu driver-attach path. When every
VBIOS read method fails (each one kfrees adev->bios and returns false
without NULLing the pointer), amdgpu_driver_load_kms() tears the
half-initialized device down via amdgpu_device_fini(), which unconditionally
executes kfree(adev->bios) a second time on the already-freed pointer.
Build
Nothing to build on a guest without AMD hardware β see trigger.sh for the
precondition check and VERDICT.md for the full path:line trace.
Run
./trigger.sh
Expected on this QEMU guest (no AMD GPU):
PRECONDITION: AMD display device present ......... ABSENT (cannot test) PRECONDITION: amdgpu driver attach path .......... UNREACHABLE => not_reproduced on this guest (target hardware absent)
On a machine/VM that does satisfy the preconditions (AMD dGPU whose VBIOS
cannot be read by any of the six methods β e.g. VFCT/ATRM ACPI tables that
yield a matching-but-invalid image, a GPU-passthrough VM with a missing or
garbage ROM BAR, or disabled ROM shadow), the expected observation at boot or
kldload amdgpu is a kernel heap corruption / slab double-free panic or
silent free-list corruption during the failed probe, followed by a second
corruption at teardown.
Files
trigger.sh β precondition checker + exact trigger conditions
VERDICT.md β full narrative trace (every hop cited path:line)
fix.diff β the verified-by-review fix (NULL out adev->bios at all 5
live kfree sites); git apply --check clean vs sys/
env.txt β guest uname + PCI inventory + kldstat (no AMD hardware)
manifest.json / verdict.json β machine-readable results
DF-4884 VERDICT β double-free of adev->bios when amdgpu_get_bios() fails
Status: not_reproduced on this guest (target hardware absent) β bug established by exhaustive source trace; every hop cited.
Reproduced?
No β and honestly so: the QEMU guest has no AMD PCI device (env.txt:
pciconf -l | grep -c 0x1002 = 0; the only VGA is the QEMU std-VGA,
vendor 0x1234). amdgpu.ko is present but can never attach, so the trigger
condition (amdgpu driver probe) is unreachable on this guest. No amount of
guest setup short of PCI passthrough of real AMD hardware can reach the code.
This is a hardware/firmware-precondition bug (probe-time), not a
runtime-unprivileged one.
The bug (full trace, every hop verified in this tree)
All six VBIOS read methods in sys/dev/drm/amd/amdgpu/amdgpu_bios.c free
adev->bios on their validation-failure path without NULLing the pointer:
| Method | alloc | kfree-without-NULL site |
|---|---|---|
igp_read_bios_from_vram |
amdgpu_bios.c:106 | amdgpu_bios.c:116 |
amdgpu_read_bios |
amdgpu_bios.c:135 | amdgpu_bios.c:145 |
amdgpu_read_bios_from_rom |
amdgpu_bios.c:174 | amdgpu_bios.c:185 |
amdgpu_atrm_get_bios |
amdgpu_bios.c:338 | amdgpu_bios.c:354 |
amdgpu_acpi_vfct_bios |
amdgpu_bios.c:423 (kmemdup) |
amdgpu_bios.c:428 |
When every method fails, amdgpu_get_bios() (amdgpu_bios.c:446-477) prints
"Unable to locate a BIOS ROM" and returns false with adev->bios dangling
(the value left by whichever method touched it last β note the fallback
order and which methods clear the pointer first):
amdgpu_read_bios()NULLs it at entry (amdgpu_bios.c:128) but re-dangles at :145 if the mapped ROM content is invalid.amdgpu_read_bios_from_rom()never NULLs at entry β early failures preserve the dangling pointer from the previous stage (amdgpu_bios.c:157-169).- For CIK/VI ASICs the final fallback (
amdgpu_read_disabled_bios(), amdgpu_bios.c:367-373) dispatches tocik_read_disabled_bios()(cik.c:890, callsamdgpu_read_biosat cik.c:922) /vi_read_disabled_bios()(vi.c:374, callsamdgpu_read_biosat vi.c:406) β both re-enter the function whose failure path dangles. For SOC15+soc15_read_disabled_bios()(soc15.c:236-240) returns false without touching the pointer, preserving the dangle fromamdgpu_read_bios_from_rom()or the earlieramdgpu_read_bios().
The teardown chain then double-frees:
amdgpu_device_init()mapsadev->rmmiobefore the BIOS read (amdgpu_device.c:2473), so thermmio == NULLbypass in unload (amdgpu_kms.c:84) is NOT taken.amdgpu_get_bios()fails βr = -EINVAL; goto failed;(amdgpu_device.c:2536-2539) βfailed:returns r (amdgpu_device.c:2709-2716).amdgpu_driver_load_kms()(amdgpu_kms.c:205-209) seesr != 0βgoto outβ callsamdgpu_driver_unload_kms(dev)(amdgpu_kms.c:239-246).amdgpu_driver_unload_kms()βadev->rmmio != NULLβ passes the guard βamdgpu_device_fini(adev)(amdgpu_kms.c:84-99).amdgpu_device_fini()ends with the unconditionalkfree(adev->bios); adev->bios = NULL;(amdgpu_device.c:2762-2763) β second free of the same pointer (first free happened at one of the five sites above, duringamdgpu_get_bios()).
Between the two frees, amdgpu_device_fini() runs amdgpu_irq_disable_all,
amdgpu_ib_pool_fini, amdgpu_fence_driver_fini, amdgpu_pm_sysfs_fini,
amdgpu_fbdev_fini, amdgpu_device_ip_fini, amdgpu_atombios_fini
(amdgpu_device.c:2727-2760) β a nontrivial alloc/free window in which the
64Kβ256K chunk (M_DRM/kmalloc zone) can be re-purposed by another
allocation, converting the double-free into a free-of-live-object (UAF-style
corruption) or a slab free-list corruption. On DragonFly's allocator the
least-bad outcome is a panic; the worst is silent heap corruption.
Threat model / reachability
- Not runtime-triggerable by an unprivileged user on a healthy system
(probe happens at boot / driver load only; root-only
kldload). - Preconditions are realistic in exactly the environments this driver runs:
- GPU passthrough VMs (cloud/VFIO): ROM BAR often unmapped or garbage; the guest kernel image is attacker-influenced by the VM admin β this is the same trust boundary as the known ATRM findings DF-1950/DF-1951.
- Crafted ACPI (VFCT/ATRM): firmware-supplied tables decide which methods allocate/free β same surface as DF-4605 (amdgpu_acpi).
- Broken/misprogrammed ROM shadow or failing hardware.
- Impact class: kernel heap double-free β memory corruption (panic probable, corruption possible). Hence Medium severity, memcorrupt bucket.
Exploit chain
Not developed: no reachable target on the audit guest. On a system where an attacker controls the VM/firmware config that induces the all-methods-fail probe, the primitive is a same-size re-free of an M_DRM allocation at a semi-controllable time (between the two frees the teardown path performs several allocations the attacker can heap-groom from userspace only if the driver attach races other subsystems β speculative, not claimed).
Fix
fix.diff (verified git apply --check clean against the pristine tree):
NULL adev->bios at all five live kfree sites in amdgpu_bios.c so
amdgpu_device_fini()'s unconditional kfree sees NULL. (The sixth site,
amdgpu_bios.c:213, is inside #if 0.) One-line-per-site, matches the
pattern already used by the success paths (adev->bios = NULL at function
entry) and by amdgpu_device_fini itself.
Fix validation
not_testable β the baseline could not be reproduced on this guest (no AMD
hardware), so a patched-kernel A/B run is impossible here. The fix is
trivially correct by inspection: it breaks the only path that leaves the
dangling pointer (verified there is no other consumer of adev->bios
between amdgpu_get_bios() failure and amdgpu_device_fini()'s kfree β
amdgpu_atombios_init() is only reached on success,
amdgpu_device.c:2542).
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_testablefix.diff verified git-apply --check clean against pristine sys/; correctness by inspection (NULLing breaks the only dangling path; no consumer of adev->bios exists between amdgpu_get_bios() failure and the fini kfree β amdgpu_atombios_init() is only reached on success at amdgpu_device.c:2542). A/B kernel-build validation impossible on this guest: no AMD hardware to reproduce the baseline.
findings/poc/DF-4884/fix.diff (5 hunks, all +adev->bios = NULL)
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:116
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:145
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:185
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:354
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:428
- sys/dev/drm/amd/amdgpu/amdgpu_bios.c:446
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2473
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2536
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2762
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:99
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:245
- sys/dev/drm/amd/amdgpu/cik.c:922
- sys/dev/drm/amd/amdgpu/vi.c:406
- sys/dev/drm/amd/amdgpu/soc15.c:236
Detail
Evidence (decisive lines)
VERDICT.md (full path:line trace of both frees and the teardown chain); env.txt (guest PCI inventory proving hardware absence); trigger.sh + run.log (precondition check output); fix.diff (git-apply-clean fix, NULLs adev->bios at all 5 live kfree sites)
PoC changes
n/a β no seeded PoC code existed; the bug is probe-time and hardware-preconditioned, so the pack carries a precondition checker + exhaustive trace instead of a runnable local trigger
Verified recommended fix
Set adev->bios = NULL after every kfree(adev->bios) failure-site in amdgpu_bios.c (5 live sites) so amdgpu_device_fini()'s unconditional kfree sees NULL β see fix.diff
Verdict
Double-free of adev->bios when amdgpu_get_bios() fails: all five live VBIOS read methods kfree(adev->bios) without NULLing it (amdgpu_bios.c:116,145,185,354,428); the probe-failure teardown path amdgpu_kms.c:245 -> amdgpu_device_fini() -> unconditional kfree(adev->bios) (amdgpu_device.c:2762) frees the dangling pointer a second time (adev->rmmio is already mapped at amdgpu_device.c:2473, so the rmmio==NULL bypass is not taken). Deterministic given the precondition (AMD GPU whose VBIOS fails all six read methods: GPU-passthrough VM with garbage ROM BAR, crafted VFCT/ATRM ACPI, disabled ROM shadow). NOT reproduced on the audit guest: it has no AMD PCI device (env.txt: zero vendor-0x1002 devices; only QEMU std-VGA 0x1234), so amdgpu never attaches and the probe path is unreachable by construction. Every hop of the chain is verified by source trace in VERDICT.md; no runtime claim is made beyond that.
No comments yet.