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)
PoC verification
Evidence pack
findings/poc/DF-4605 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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.shextracts lines 374β385 (guard) and line 387 (deref) withsedfrom/usr/src/sys/dev/drm/amd/amdgpu/amdgpu_acpi.con the guest β the exact bytes of the shipped kernel source;- compiles them inside a wrapper using the byte-exact
struct amdgpu_atiflayout 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
atifand 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:
- Literal NULL argument (and later a
static ... * volatileinitialized to NULL): gcc 8.3 constant-propagated the provable NULL through thenoinlinestatic function and deleted the call as UB dead code (verified in the disassembly: main loaded the volatile into%raxand never called). In the kernel this is impossible:atif = adev->atifis an opaque heap field read at notify-dispatch time inside ACPICA β not foldable. Fixed by deriving the NULL fromgetenv()at runtime. - Side-effect-free deref body (
(void)type;): gcc dropped the entireif (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, no0x12displacement anywhere). The kernel body is NOT empty (amdgpu_acpi.c:388-431 callsamdgpu_atif_get_sbios_requests()and the backlight/HPD paths), so the load is emitted in any real build. Fixed by giving the harness body aprintfside 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_interfaceor the atifkzallocfails: 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.diffis authored against the read-onlysys/tree and applies cleanly (git apply --checkon 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
inconclusiveKernel-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).
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:374
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:379
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:387
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:751
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:808
- sys/dev/drm/amd/amdgpu/amdgpu_acpi.c:811
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:215
- sys/dev/drm/amd/amdgpu/amdgpu.h:1045
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).
No comments yet.