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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4884 Β· 8 files
FileTypeDescriptionSize
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

VERDICT.md
↓ download raw

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 to cik_read_disabled_bios() (cik.c:890, calls amdgpu_read_bios at cik.c:922) / vi_read_disabled_bios() (vi.c:374, calls amdgpu_read_bios at 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 from amdgpu_read_bios_from_rom() or the earlier amdgpu_read_bios().

The teardown chain then double-frees:

  1. amdgpu_device_init() maps adev->rmmio before the BIOS read (amdgpu_device.c:2473), so the rmmio == NULL bypass in unload (amdgpu_kms.c:84) is NOT taken.
  2. amdgpu_get_bios() fails β†’ r = -EINVAL; goto failed; (amdgpu_device.c:2536-2539) β†’ failed: returns r (amdgpu_device.c:2709-2716).
  3. amdgpu_driver_load_kms() (amdgpu_kms.c:205-209) sees r != 0 β†’ goto out β†’ calls amdgpu_driver_unload_kms(dev) (amdgpu_kms.c:239-246).
  4. amdgpu_driver_unload_kms() β†’ adev->rmmio != NULL β†’ passes the guard β†’ amdgpu_device_fini(adev) (amdgpu_kms.c:84-99).
  5. amdgpu_device_fini() ends with the unconditional kfree(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, during amdgpu_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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4884

Confirmed kernel references

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.