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

amdgpu_atif_handler: port gutted the event guard into an empty if (#if 0) β€” unconditional fall-through dereferences adev->atif (NULL+0x12) on ATIF-less systems β†’ kernel panic on first ACPI notify

Summary

The port moved the upstream early-return inside #if 0, leaving the NULL/enabled/type guard with an EMPTY body after preprocessing - control unconditionally falls through to atif->functions.sbios_requests, a kernel-mode load at NULL+0x12 whenever adev->atif==NULL. The notify handler is installed unconditionally even when ATIF probing/kzalloc/verify failed - i.e. on every ATIF-less amdgpu system (all discrete desktop AMD GPUs, the most common deployment). The next firmware Notify on the GPU's ACPI node (hotplug/dock/thermal) panics in ACPICA dispatch context. Secondary: disabled/non-matching notifications are no longer ignored. VERIFIED on guest with a verbatim-extraction control-flow proof (4/4 deterministic: SIGSEGV at exactly &((amdgpu_atif*)NULL)->functions.sbios_requests; fixed variant clean 4/4). Restore-the-early-return fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4605 Β· 13 files
FileTypeDescriptionSize
extract_and_test.sh β€” 6.2 KB view raw
generated_guard_test.c β€” 3.8 KB view raw
guard.inc β€” 344 B ↓ download
deref.inc β€” 39 B ↓ download
guard_fixed.inc β€” 215 B ↓ download
run.log β€” 1.5 KB view raw
run.stress.log β€” 555 B view raw
env.txt β€” 844 B view raw
VERDICT.md β€” 5.8 KB ↓ raw
fix.diff β€” 718 B view raw
build.sh β€” 132 B view raw
run.sh β€” 143 B view raw
verdict.json β€” 6.5 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-4605

Bottom line: CONFIRMED (certain) by verbatim-source control-flow reproduction on the DragonFly guest: the port-time #if 0 gutting of the event guard in amdgpu_atif_handler leaves an empty if body, so control falls through to atif->functions.sbios_requests β€” a load at NULL+0x12 whenever adev->atif == NULL, which is the steady state on every ATIF-less amdgpu system while the ACPI notify handler is installed unconditionally (amdgpu_acpi.c:808-812). Kernel impact: panic (DoS) on the first firmware ACPI_DEVICE_NOTIFY on the GPU node. The proposed one-line fix (restore the early return;) eliminates the fault in the same harness.

1. What was executed on the guest

Guest: DragonFly 6.5-DEVELOPMENT x86_64 (#0 Thu Jul 2 06:02:54 UTC 2026), QEMU/KVM, cc 8.3 [DragonFly]. env.txt records that the guest has no AMD GPU (only virtio-vga 0x1111:1234) and amdgpu is not built into the kernel or module set (sys/conf/files has no amdgpu entries; no /boot/modules). Therefore the in-kernel end-to-end path (firmware notify β†’ AcpiEvNotifyDispatch β†’ amdgpu_acpi_event β†’ panic) cannot be executed on this guest β€” that is a hardware limitation, not a doubt about the code path.

What CAN be executed faithfully is the control flow itself:

  • extract_and_test.sh extracts lines 374–385 (guard) and line 387 (deref) with sed from /usr/src/sys/dev/drm/amd/amdgpu/amdgpu_acpi.c on the guest β€” the exact bytes of the shipped kernel source;
  • compiles them inside a wrapper using the byte-exact struct amdgpu_atif layout from amdgpu_acpi.c:38-75 (verified at runtime: sizeof(struct amdgpu_atif)=40, offsetof(functions.sbios_requests)=18);
  • calls the function with a runtime-produced NULL atif and a SIGSEGV handler that prints the faulting address.

Result (run.log, run.stress.log β€” 4/4 deterministic):

RESULT: SIGSEGV at address 0x12
VERDICT: guard body compiled to NOTHING; control fell through the if() into the NULL deref

The fault address 0x12 (18) matches &((struct amdgpu_atif *)NULL)->functions.sbios_requests exactly. The fixed variant (identical extraction with the #if 0 block replaced by return;) returns cleanly, 4/4.

2. PoC debugging notes (why earlier iterations "passed")

Two harness iterations initially showed no fault, both instructive and neither a refutation:

  1. Literal NULL argument (and later a static ... * volatile initialized to NULL): gcc 8.3 constant-propagated the provable NULL through the noinline static function and deleted the call as UB dead code (verified in the disassembly: main loaded the volatile into %rax and never called). In the kernel this is impossible: atif = adev->atif is an opaque heap field read at notify-dispatch time inside ACPICA β€” not foldable. Fixed by deriving the NULL from getenv() at runtime.
  2. Side-effect-free deref body ((void)type;): gcc dropped the entire if (atif->functions.sbios_requests) statement because an empty-bodied if whose condition is a pure load has no observable effect β€” confirmed even at -O0 (function size 0x4a, no 0x12 displacement anywhere). The kernel body is NOT empty (amdgpu_acpi.c:388-431 calls amdgpu_atif_get_sbios_requests() and the backlight/HPD paths), so the load is emitted in any real build. Fixed by giving the harness body a printf side effect; the fault then appears deterministically.

These are properties of the harness, documented so nobody re-derives them as a refutation. The final harness (both barriers in place) is generated_guard_test.c.

3. Kernel-context reasoning (why 0x12 β‡’ panic, not a benign read)

  • The read executes in ACPICA notify-dispatch context (kernel thread); virtual address 0x12 lies in user VA space whose page 0 is unmapped in the dispatching thread's pmap β‡’ kernel-mode page fault β‡’ DragonFly panics ("fault on non-existent ... / segmentation fault") β€” no SMAP/SMEP needed for this, it is a plain unmapped read.
  • No privilege escalation surface: the faulting address is a fixed NULL+0x12 with no attacker-controlled offset; the value read is never used before the trap. Pure DoS.

4. Realistic trigger population

  • Any discrete desktop AMD GPU system (no ATIF method in DSDT β€” the normal desktop firmware state) + any firmware device-notify on the GPU's ACPI node: display hotplug notifications, dock events, PCIe hotplug/PM events, buggy-BIOS notify storms.
  • Laptops where amdgpu_atif_verify_interface or the atif kzalloc fails: handler still installed (amdgpu_acpi.c:808-812), adev->atif == NULL.
  • Note (functional, not security): on PX/hybrid laptops where ATIF lives in the iGPU namespace, the handler is installed on the dGPU node β€” events may simply never arrive there; the crash population is chiefly ATIF-less desktops, which is also where amdgpu is most common.

5. Fix validation

  • Harness-level: fixed variant returns cleanly 4/4 (baseline faults 4/4).
  • Kernel-build level: not testable on this guest β€” amdgpu is not part of any buildable kernel config here and no AMD GPU exists to probe; fix.diff is authored against the read-only sys/ tree and applies cleanly (git apply --check on the host tree).

6. Relation to prior findings on this file

DF-1749 (stale handler after detach β†’ freed-adev UAF), DF-1750 (4-parser OOB/type-confusion), DF-1751 (allocator mismatch on atif). DF-4605 is independent: live adev, NULL atif, first-notify crash, no teardown required.

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

Kernel-build fix validation not testable on this guest: amdgpu is not part of any buildable kernel config (absent from sys/conf/files; module tree not present) and the guest has no AMD GPU to probe, so no baseline kernel panic and no patched-kernel run are possible there. Validated instead at the verbatim-extraction level inside the same proof: the extracted guard+deref with the fix applied (the #if 0 block replaced by 'return;') returns cleanly in 4/4 runs while the unpatched extraction faults at 0x12 in 4/4 runs. fix.diff additionally passes 'git apply --check' against the unmodified sys/ tree. fix_baseline_reproduced/fix_patched_reproduced are 0 because they describe the kernel-level validation run, which did not occur.

run.log and run.stress.log lines 'fixed exit=0' + 'PROOF-OK: baseline faults at NULL+0x12; fixed variant returns cleanly' (4/4); guard_fixed.inc (extracted guard with fix applied, as compiled); 'git apply --check findings/poc/DF-4605/fix.diff' -> APPLIES-CLEAN (host sys/ tree).
↓ fix.diffper-fix-DF-4605

Confirmed kernel references

Detail

Exploit chain

trigger chain (no privilege escalation β€” DoS only): amdgpu device probes on ATIF-less firmware (any discrete desktop AMD GPU) -> amdgpu_acpi_init installs AcpiInstallNotifyHandler(handle, ACPI_DEVICE_NOTIFY, amdgpu_acpi_event, adev) with adev->atif still NULL (amdgpu_acpi.c:751-753 -> out: -> 808-812) -> firmware issues any device Notify on the GPU ACPI node (hotplug/dock/thermal) -> ACPICA dispatches amdgpu_acpi_event -> amdgpu_atif_handler(adev, type) -> gutted guard is a no-op -> kernel-mode read at vaddr 0x12 (page 0 unmapped) -> trap -> kernel panic.

Evidence (decisive lines)

run.log (decisive run: 'RESULT: SIGSEGV at address 0x12' + 'PROOF-OK: baseline faults at NULL+0x12; fixed variant returns cleanly'); run.stress.log (3 further identical runs, 4/4 total); guard.inc / deref.inc (the sed-extracted kernel lines 374-385 / 387); generated_guard_test.c (final harness as compiled on the guest); extract_and_test.sh (the whole proof, runs on the guest); env.txt (guest has NO AMD GPU β€” virtio-vga 0x1111:1234 β€” and amdgpu not built: in-kernel path not runnable there); VERDICT.md section 2 (why two earlier harness iterations were wrongly clean: gcc-8 UB dead-code elision of a provable-NULL deref and of a side-effect-free if β€” both impossible in the real kernel build); fix.diff (git-apply --check clean against sys/).

PoC changes

No seeded PoC existed (pass-2 finding). The proof was authored during verification; two iterations were silently gutted by gcc 8.3 (call deleted as UB when atif was provably NULL even through a volatile initializer; whole if-statement deleted when its body was side-effect-free β€” confirmed via -O0 disassembly, 0x4a-byte function with no 0x12 displacement). Final harness derives NULL from getenv() at runtime and gives the deref-if a printf side effect mirroring the kernel's real amdgpu_atif_get_sbios_requests() call at :391; fault then appears deterministically at 0x12.

Verified recommended fix

Restore the early return in amdgpu_atif_handler's guard (replace the #if 0 block with 'return;'), mirroring upstream's NOTIFY_DONE early-exit β€” see fix.diff.

Verdict

CONFIRMED by verbatim-source control-flow reproduction on the DragonFly guest (4/4 deterministic): the port-time #if 0 gutting of the event guard in amdgpu_atif_handler (sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:374-385) leaves an if with an EMPTY body, so control falls through to atif->functions.sbios_requests (line 387) β€” a load at NULL+0x12 whenever adev->atif == NULL. The proof sed-extracts lines 374-385 and 387 from the guest's live /usr/src kernel tree, compiles them with the byte-exact struct amdgpu_atif layout (functions.sbios_requests at offset 18, confirmed at runtime) and observes SIGSEGV at address 0x12 exactly as predicted; the fixed variant (same lines, #if 0 block replaced by return;) returns cleanly. adev->atif is NULL on every ATIF-less amdgpu system (amdgpu_acpi.c:751-753 goto out; also :755 kzalloc failure, :763 verify failure) while amdgpu_acpi_init installs the ACPI notify handler unconditionally (amdgpu_acpi.c:808-812) β€” so the first firmware ACPI_DEVICE_NOTIFY on the GPU node panics the kernel (unmapped page-0 read in ACPICA dispatch context). CAVEAT: the in-kernel end-to-end path could not be executed on this guest (no AMD GPU β€” virtio-vga only per env.txt β€” and amdgpu is not built into any guest kernel config); reproduction is at the verbatim-extraction control-flow level, which is deterministic and source-faithful. No escalation surface: fixed NULL+0x12 read, value never consumed β€” pure DoS. Distinct from pass-1 DF-1749 (freed-adev UAF post-detach), DF-1750 (parser OOB/type-confusion), DF-1751 (allocator mismatch).