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)
PoC verification
Evidence pack
findings/poc/DF-4636 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
info->Type == ACPI_TYPE_BUFFERβ if the SBIOS method returns an ACPI_TYPE_INTEGER (perfectly legal for ACPICA βAcpiEvaluateObjectexternalizes any object type on AE_OK),info->Buffer.Pointeris a union alias of the firmware-chosen 64-bitInteger.Valueβ wild-pointer kernel read at a firmware-chosen address (verified: si_addr is the integer, verbatim).info->Buffer.Pointer != NULLβ a 0-length buffer β NULL+0 fault.info->Buffer.Length >= 2before the u16 size read β 1-byte OOB read past the ACPICA allocation.info->Buffer.Length >= sizebefore the memcpy β up tosizeof(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):
- sha256-gates the guest's
/usr/src/sys/dev/drm/radeon/radeon_acpi.cand.hagainst the audited tree (byte-identical), - awk-extracts the four parsers verbatim (plus
*_calland the*_parse_*helpers), - compiles them against a faithful ACPICA shim (union layout printed at
runtime:
offsetof(Integer.Value)==offsetof(Buffer.Pointer)==8) with a mockAcpiEvaluateObjectreturning firmware-chosen objects, - drives 5 fault fixtures + 1 well-formed control, 4 iterations each, in
forked children with a SIGSEGV tracer printing exact
si_addr, - re-extracts from a copy patched with
fix.diffand 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.
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.csha256794cb6c29cb9e9fd17d4c826e86389ecb44a778e1f80ace15f7cc3e1a3410cb1β byte-identical to the audited tree (script aborts otherwise); same forradeon_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-faithfulunion acpi_object(aliasing confirmed at runtime:offsetof(Integer.Value)=8 offsetof(Buffer.Pointer)=8) with a mockAcpiEvaluateObjectthat 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
inconclusiveKernel-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.
Confirmed kernel references
- sys/dev/drm/radeon/radeon_acpi.c:212
- sys/dev/drm/radeon/radeon_acpi.c:220
- sys/dev/drm/radeon/radeon_acpi.c:259
- sys/dev/drm/radeon/radeon_acpi.c:267
- sys/dev/drm/radeon/radeon_acpi.c:318
- sys/dev/drm/radeon/radeon_acpi.c:326
- sys/dev/drm/radeon/radeon_acpi.c:494
- sys/dev/drm/radeon/radeon_acpi.c:502
- sys/dev/drm/radeon/radeon_acpi.c:103
- sys/dev/drm/radeon/radeon_acpi.c:414
- sys/contrib/dev/acpica/source/components/namespace/nsxfeval.c:536
- sys/contrib/dev/acpica/source/include/actypes.h:1077
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.
No comments yet.