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

radeon_acpi: ACPI_OBJECT type confusion and missing Buffer.Length validation in all four ATIF/ATCS return parsers β€” firmware-controlled kernel wild-pointer read and heap OOB read (panic)

Summary

All four return parsers read size=*(u16*)Buffer.Pointer and memcpy from it without checking Type==BUFFER, Pointer!=NULL, Length>=2, or Length>=size. ACPICA externalizes any return type on AE_OK and Buffer.Pointer aliases Integer.Value at offset 8, so an SBIOS method returning an INTEGER makes the kernel dereference the firmware-chosen 64-bit value verbatim; a short/lying buffer yields a 1..13-byte heap OOB read; zero-length faults at NULL+0 - all in notify-dispatch context = panic. Radium twin of DF-1750 (identical defect, different file; this file was pass-1 clean). VERIFIED on guest via verbatim-extraction control-flow proof (sha-gated extraction, actypes-faithful union shim: firmware-integer deref'd verbatim 16x, all length fixtures fault exactly at the boundary; fix validated rejecting all five fixtures with the control still parsing). Firmware-control threat model, DoS ceiling. Validate-type+bound-every-parse fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4636 Β· 9 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 6.1 KB ↓ raw
extract_and_test.sh β€” 12.5 KB view raw
fix.diff β€” 2.9 KB view raw
build.log β€” 547 B view raw
run.log β€” 27.1 KB view raw
env.txt β€” 259 B view raw
verdict.json β€” 8.1 KB view raw
manifest.json β€” 1.1 KB view raw

DF-4636 β€” radeon_acpi: ACPI_OBJECT type confusion + missing Buffer.Length validation in all 4 ATIF/ATCS return parsers

File: sys/dev/drm/radeon/radeon_acpi.c (pass-2 finding) Verification: verbatim-extraction control-flow proof on the DragonFly QEMU guest (methodology identical to accepted finding DF-4605) β€” BASELINE-CORRUPTS 4/4 rounds, FIXED-CLEAN 4/4 rounds.

What the bug is

All four ATIF/ATCS return-buffer parsers in radeon_acpi.c:

parser u16 size read memcpy
radeon_atif_verify_interface radeon_acpi.c:212 radeon_acpi.c:220
radeon_atif_get_notification_params radeon_acpi.c:259 radeon_acpi.c:267
radeon_atif_get_sbios_requests radeon_acpi.c:318 radeon_acpi.c:326
radeon_atcs_verify_interface radeon_acpi.c:494 radeon_acpi.c:502

read size = *(u16 *) info->Buffer.Pointer; and then memcpy out of info->Buffer.Pointer without ever checking:

  1. info->Type == ACPI_TYPE_BUFFER β€” if the SBIOS method returns an ACPI_TYPE_INTEGER (perfectly legal for ACPICA β€” AcpiEvaluateObject externalizes any object type on AE_OK), info->Buffer.Pointer is a union alias of the firmware-chosen 64-bit Integer.Value β†’ wild-pointer kernel read at a firmware-chosen address (verified: si_addr is the integer, verbatim).
  2. info->Buffer.Pointer != NULL β€” a 0-length buffer β†’ NULL+0 fault.
  3. info->Buffer.Length >= 2 before the u16 size read β†’ 1-byte OOB read past the ACPICA allocation.
  4. info->Buffer.Length >= size before the memcpy β†’ up to sizeof(struct atif/atcs_*output) (8–13) bytes heap OOB read past the ACPICA allocation when the buffer's own size header lies.

Trigger: any SBIOS/firmware that implements ATIF/ATCS with a malformed return (bug or malice). Parsed values feed internal notification config and radeon_set_backlight_level() (arithmetic only β€” no indexing), so the impact ceiling is kernel-mode wild/OOB read β†’ panic (DoS), not escalation.

How to reproduce

scp -F dfbsd-qemu/config extract_and_test.sh fix.diff dfbsd:/tmp/df4636_dir/
./dfbsd-qemu/vm.sh run_root 'sh /tmp/df4636_dir/extract_and_test.sh'

The script (single-tenant guest must be up: dfbsd-qemu/vm.sh status):

  1. sha256-gates the guest's /usr/src/sys/dev/drm/radeon/radeon_acpi.c and .h against the audited tree (byte-identical),
  2. awk-extracts the four parsers verbatim (plus *_call and the *_parse_* helpers),
  3. compiles them against a faithful ACPICA shim (union layout printed at runtime: offsetof(Integer.Value)==offsetof(Buffer.Pointer)==8) with a mock AcpiEvaluateObject returning firmware-chosen objects,
  4. drives 5 fault fixtures + 1 well-formed control, 4 iterations each, in forked children with a SIGSEGV tracer printing exact si_addr,
  5. re-extracts from a copy patched with fix.diff and repeats everything.

Expected output (decisive)

PROOF-RESULT: BASELINE-CORRUPTS (5/5 fixtures SIGSEGV 4/4 iterations, control parses)
PROOF-RESULT: FIXED-CLEAN (no faults, control still parses)

with fault-address distribution over 4 rounds (see run.log):

fixture si_addr meaning
integer_wild_atif 0x414141414100 (16Γ—) SBIOS integer value dereferenced verbatim
integer_wild_atcs 0x424242424200 (16Γ—) same, ATCS parser
null_buffer 0x0 (16Γ—) NULL+0 fault
shortbuf_hdr guard page (16Γ—) u16 size read 1 byte past 1-byte ACPI buffer
lying_size guard page (16Γ—) memcpy reads 8 bytes past 4-byte ACPI buffer
control_wellformed no fault, err=0 parser logic intact (both variants)

Honest scope caveat

The in-kernel end-to-end path could not run on this guest: it has no AMD GPU (virtio-vga) and radeon is not built into any guest kernel config. The proof is at the verbatim-extraction level against the byte-identical shipped source (deterministic, source-faithful). In the kernel, every fault observed here is a kernel-mode fault in ACPICA notify/init context β†’ panic.

Fix

fix.diff (git-apply --check clean against sys/): add a Type/NULL/Length>=2 guard after each if (!info) and require info->Buffer.Length >= size before each memcpy/parse. Validated in the same harness: FIXED variant rejects all malformed fixtures with -EINVAL while the well-formed control still parses.

VERDICT.md
↓ download raw

DF-4636 VERDICT β€” radeon_acpi ATIF/ATCS parser type-confusion / OOB read

Status: reproduced (verbatim-extraction control-flow proof on the guest) Impact: dos (firmware-controlled kernel wild-pointer/OOB read β†’ panic) Confidence: certain

1. Root cause (source-faithful)

sys/dev/drm/radeon/radeon_acpi.c has four parsers of ACPI method return buffers β€” radeon_atif_verify_interface (:198-231), radeon_atif_get_notification_params (:245-294), radeon_atif_get_sbios_requests (:307-334), radeon_atcs_verify_interface (:480-512). Each does, with zero validation of the returned ACPI_OBJECT:

size = *(u16 *) info->Buffer.Pointer;   /* :212, :259, :318, :494 */
...
size = min(sizeof(output), size);
memcpy(&output, info->Buffer.Pointer, size);   /* :220, :267, :326, :502 */

radeon_atif_call/radeon_atcs_call (:103-138, :414-449) return buffer.Pointer whenever AcpiEvaluateObject does not fail. ACPICA (nsxfeval.c:536-599) externalizes any return object type on AE_OK β€” an SBIOS ATIF/ATCS method that returns an Integer, a zero-length Buffer, or a Buffer whose in-band size header exceeds the allocation reaches the parsers unchoked. In union acpi_object (actypes.h:1077-1098) Buffer.Pointer and Integer.Value share offset 8, so a returned INTEGER makes the parser dereference the firmware-chosen 64-bit value as a kernel pointer. This is the radeon twin of pass-1 amdgpu finding DF-1750 (same defect, different file β€” this file was clean in pass 1).

Threat model: the input is platform firmware, not an unprivileged user. A malicious or buggy SBIOS (or crafted ACPI tables, which require root/loader control to install) gets a kernel-mode read at a chosen 64-bit address, or a 1..13-byte heap OOB read past an M_ACPICA allocation; any fault lands in ACPICA notify-dispatch or driver-init context β†’ kernel panic. Parsed output feeds internal notification config masks and radeon_set_backlight_level() (atombios arithmetic only, atombios_encoders.c:87-112 β€” no indexing), so there is no escalation sink; the impact ceiling is DoS. Severity Medium (matches DF-1750's calibration for the identical amdgpu defect).

2. Proof (what was run, what was seen)

Methodology identical to accepted DF-4605: verbatim extraction + faithful shim.

  • Guest /usr/src/sys/dev/drm/radeon/radeon_acpi.c sha256 794cb6c29cb9e9fd17d4c826e86389ecb44a778e1f80ace15f7cc3e1a3410cb1 β€” byte-identical to the audited tree (script aborts otherwise); same for radeon_acpi.h. The four parsers + 5 helpers are awk-extracted verbatim (258 lines) and compiled on the guest (cc 8.3) against an actypes.h-faithful union acpi_object (aliasing confirmed at runtime: offsetof(Integer.Value)=8 offsetof(Buffer.Pointer)=8) with a mock AcpiEvaluateObject that returns AE_OK with firmware-chosen objects, exactly as ACPICA does after externalization.

Result (run.log, 4 rounds, 5 fault fixtures Γ— 4 iterations each + control):

fixture expected fault observed
ATIF returns INTEGER 0x414141414100 *(u16*)0x414141414100 16Γ— SIGSEGV si_addr=0x414141414100
ATCS returns INTEGER 0x424242424200 same shape 16Γ— SIGSEGV si_addr=0x424242424200
ATIF returns Buffer{len 0, NULL} *(u16*)NULL 16Γ— SIGSEGV si_addr=0x0
ATIF returns 1-byte buffer u16 header read +1 OOB 16Γ— SIGSEGV si_addr=0x80047d000 (guard page = buffer_end+1)
ATIF returns 4-byte buffer claiming 12 memcpy 12 from 4 16Γ— SIGSEGV si_addr=0x80047d000 (guard page = buffer_end+4..11)
control: well-formed 13-byte buffer parses, err=0 4Γ—/round exit=0, no fault

PROOF-RESULT: BASELINE-CORRUPTS in 4/4 full rounds. Every fault address is exactly the firmware-supplied value / exactly one guard page past the ACPI allocation β€” deterministic, no heuristics.

Iterations fixed during verification (all mine, not the kernel's): 1. sha256 typo in the gate (my transcription), 2. extraction bugs: indented # define lines in radeon_acpi.h missed by the grep; a line-range sed of radeon.h swallowed a trailing /* β€” both replaced by pattern-bounded extraction, 3. first wild-address values (0x4141414100000000) were non-canonical x86-64 addresses β†’ #GP reports si_addr=0x0; switched to canonical values so si_addr prints the dereferenced pointer verbatim.

3. Fix validation

fix.diff (git-apply --check clean against the unmodified sys/ tree): after each if (!info) the parsers now require info->Type == ACPI_TYPE_BUFFER, info->Buffer.Pointer != NULL, info->Buffer.Length >= 2, and each parse is bounded by info->Buffer.Length >= size before any memcpy. The same harness compiled from the fix.diff-patched extraction returns -EINVAL for all five malformed fixtures with no fault in 4/4 rounds, while the well-formed control still parses (PROOF-RESULT: FIXED-CLEAN Γ—4 in run.log).

Kernel-level build/run validation is not possible on this guest (no AMD GPU; radeon absent from the guest kernel config and module set), so fix_status is inconclusive with the harness-level validation as evidence β€” same posture as DF-4605.

4. Why this is a real finding and not noise

  • The identical defect in the sibling amdgpu driver is an accepted pass-1 finding (DF-1750); this file was audited clean in pass 1 and the pass-2 deeper look found the radeon twin.
  • ACPICA externalizes non-BUFFER returns on success (nsxfeval.c:536-599 read line-by-line during this verification) β€” the "firmware will always return a buffer" assumption is unenforced by anything in the path.
  • Every code path is firmware-data-driven with zero validation before the first dereference; the control fixture proves the extraction reproduces the real parser semantics (well-formed input parses to err=0).

Guest stayed up throughout (userland harness only); no reset 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 possible on this guest: no AMD GPU (virtio-vga) and radeon is absent from the guest kernel config and module set, so no in-kernel baseline or patched run exists there. Validated instead at the verbatim-extraction level inside the same proof: the identical harness compiled from the fix.diff-patched extraction returns -EINVAL for all five malformed fixtures with no fault in 4/4 rounds (PROOF-RESULT: FIXED-CLEAN x4 in run.log) while the unpatched extraction faults deterministically (BASELINE-CORRUPTS x4), and the well-formed control still parses after the fix. 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 (same posture as DF-4605).

run.log: 'FIX-APPLIED-CLEAN', 'PROOF-RESULT: FIXED-CLEAN (no faults, control still parses)' x4 rounds vs 'PROOF-RESULT: BASELINE-CORRUPTS (5/5 fixtures SIGSEGV 4/4 iterations, control parses)' x4; host: 'git apply --check findings/poc/DF-4636/fix.diff' -> clean.
↓ fix.diffper-fix-DF-4636

Confirmed kernel references

Detail

Exploit chain

trigger chain (no privilege escalation β€” firmware-controlled DoS): platform firmware implements ATIF/ATCS on the radeon device's ACPI node with a malformed return (buggy or malicious SBIOS, or crafted ACPI tables injected with root/loader access) -> radeon_acpi_init (radeon_acpi.c:707/713, runs after successful GPU init on Avivo+ ASICs with a bios) evaluates ATCS/ATIF -> AcpiEvaluateObject succeeds and externalizes the object (any type) -> parser reads (u16)info->Buffer.Pointer at radeon_acpi.c:212/259/318/494 -> with Type=INTEGER this is a load at the firmware-chosen 64-bit address; with a short/lying buffer it is a 1..13-byte heap OOB read past the M_ACPICA allocation -> unmapped/guarded address -> kernel fault in ACPICA context -> panic. The notify path (radeon_atif_handler:346 -> radeon_atif_get_sbios_requests:307) re-parses on every matching ACPI notify, so a persistent malicious SBIOS re-triggers at will.

Evidence (decisive lines)

run.log (decisive: 'PROOF-RESULT: BASELINE-CORRUPTS (5/5 fixtures SIGSEGV 4/4 iterations, control parses)' x4 rounds; si_addr histogram 16x 0x414141414100, 16x 0x424242424200, 16x 0x0, 32x 0x80047d000 guard page; 'offsetof(Integer.Value)=8 offsetof(Buffer.Pointer)=8 (union aliasing: YES)'; 'FIX-APPLIED-CLEAN'; 'PROOF-RESULT: FIXED-CLEAN (no faults, control still parses)' x4); extract_and_test.sh (whole proof, sha256-gated to the audited tree, runs on the guest); fix.diff (git apply --check clean vs sys/); VERDICT.md (full narrative incl. 3 harness iterations fixed); env.txt (guest uname, cc 8.3, radeon absent from guest kernel); build.log.

PoC changes

No seeded PoC existed (pass-2 finding); the proof was authored during verification. Three authoring iterations were fixed: a sha256 typo in the source-fidelity gate; two extraction bugs (indented '# define' capability-mask lines in radeon_acpi.h missed by grep; line-range sed of radeon.h swallowed a trailing '/*' β€” both replaced with pattern-bounded extraction); non-canonical wild-address values (0x4141414100000000 -> #GP reports si_addr=0x0) switched to canonical 0x414141414100/0x424242424200 so the fault address prints the dereferenced firmware value verbatim.

Verified recommended fix

In all four ATIF/ATCS parsers, validate info->Type==ACPI_TYPE_BUFFER, info->Buffer.Pointer!=NULL, info->Buffer.Length>=2 right after the !info check, and require info->Buffer.Length>=size before the memcpy/parse β€” see fix.diff (mirrors the DF-1750 amdgpu fix).

Verdict

CONFIRMED by verbatim-source control-flow reproduction on the DragonFly guest (4/4 deterministic rounds): all four ATIF/ATCS return parsers in sys/dev/drm/radeon/radeon_acpi.c (radeon_atif_verify_interface:212/220, radeon_atif_get_notification_params:259/267, radeon_atif_get_sbios_requests:318/326, radeon_atcs_verify_interface:494/502) dereference info->Buffer.Pointer and memcpy from it without checking info->Type==ACPI_TYPE_BUFFER, Pointer!=NULL, Buffer.Length>=2 before the u16 size read, or Buffer.Length>=size before the memcpy. ACPICA externalizes ANY return object type on AE_OK (nsxfeval.c:536-599), and in union acpi_object Buffer.Pointer aliases Integer.Value at offset 8 (verified at runtime), so an SBIOS ATIF/ATCS returning an INTEGER makes the kernel dereference the firmware-chosen 64-bit value verbatim (observed SIGSEGV si_addr=0x414141414100 == the returned integer, 16/16 runs); short/lying buffers fault exactly 1..8 bytes past the ACPI allocation on a guard page (32/32 runs); zero-length buffer faults at NULL+0 (16/16); well-formed control parses to err=0 in both variants. In-kernel impact is a kernel-mode wild/OOB read in ACPICA notify-dispatch/driver-init context -> panic; parsed values feed notification config and radeon_set_backlight_level() which is arithmetic-only (atombios_encoders.c:87-112), so no escalation sink β€” firmware-trust DoS, Medium. This is the radeon twin of pass-1 amdgpu finding DF-1750 (same defect, different file; this file was clean in pass 1). In-kernel end-to-end not runnable on this guest (no AMD GPU, radeon not built) β€” reproduction is at the verbatim-extraction level against byte-identical source (sha256-gated), same accepted methodology as DF-4605. fix.diff adds Type/NULL/Length validation to all four parsers; FIXED variant rejects all malformed fixtures with -EINVAL, no fault, 4/4 rounds, control still parses.