parse_csr_fw bounds its stepping-search loop with attacker-controlled package_header->num_entries while the walked array is fixed fw_info[20]: unbounded kernel heap OOB read (panic / OOB dmc_offset)
Summary
The stepping search iterates num_entries (u32, never checked) against the fixed 20-entry fw_info array - a DMC file with a big count walks kernel heap past the struct until an unmapped page panics; short of that the OOB u32 becomes dmc_offset steering further reads (to CSR MMIO, not userspace - DoS ceiling). DISTINCT from DF-1895 (missing fw->size checks): a fully size-checked parse still faults (harness variant B: 1MiB file faults at fw_info[87369]). Harness run ON guest (faults at fw_info[115]/[87369]); kernel path untestable (std-VGA only). Upstream v5.2 carries it; v5.10 sanitizes - DF never got that. num_entries<=20 reject fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4814 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_csr_num_entries.c | β | 5.8 KB | view raw | |
| build.sh | β | 120 B | view raw | |
| run.sh | β | 106 B | view raw | |
| build.log | β | 255 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| env.txt | β | 1.5 KB | view raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| README.md | β | 1.2 KB | β raw | |
| fix.diff | β | 509 B | view raw | |
| evidence/upstream_v5.2_intel_csr.c | β | 16.3 KB | view raw | |
| evidence/upstream_v5.10_intel_csr.c | β | 21.0 KB | view raw |
DF-4814 β parse_csr_fw walks package_header->fw_info[20] with unbounded attacker num_entries: kernel heap OOB read (-> panic / OOB dmc_offset).
BUILD/RUN (guest-compiled logic harness; kernel path itself needs gen9+ HW):
# on the audit guest (or any POSIX box): cc -O2 -o poc4814 poc4814.c ./poc4814 # variant A: minimal 1536-byte crafted DMC file ./poc4814 big # variant B: 1 MiB crafted DMC file
EXPECTED (observed, see run.log): SIGSEGV while reading fw_info[115] (A) / fw_info[87369] (B) β i.e. far past the 20-entry fw_info array, and past the END of the firmware buffer, with the loop still bounded only by the crafted num_entries=0x08000000 (1.5 GiB of address space walked). The harness traps the fault and prints the diagnosis; in-kernel the same walk is a page fault in kernel mode (panic) or an OOB-read dmc_offset feeding &fw->data[readcount+dmc_offset*4].
STATUS: status=untested for the kernel path (no gen9+ Intel display device exists in the audit guest β see env.txt); the parse-loop logic itself is demonstrated by the harness (run.log). Fix validated statically (see VERDICT.md / fix.diff, mirrors upstream v5.10 num_entries sanitization).
DF-4814 VERDICT β intel_csr.c unbounded num_entries walk (OOB heap read)
Bottom line: parse_csr_fw() bounds its stepping-search loop with the
attacker-controlled package_header->num_entries (u32 from the DMC firmware
file) while the walked array is the fixed struct intel_fw_info fw_info[20]
declared inside struct intel_package_header. There is no check of
num_entries against ARRAY_SIZE(fw_info) β for num_entries > 20 the loop
reads kernel heap beyond the array; for large counts it walks off the end of
the request_firmware() buffer entirely. Kernel-mode OOB read β page fault
panic (reliable DoS), and whatever u32 it reads OOB becomes dmc_offset,
which is then used to index fw->data again (compounding DF-1895's unchecked
indexing). Root cause is distinct from DF-1895: even a fully
fw->size-checked parse (the DF-1895 fix) leaves this bug β the missing
bound is the array size (20), not the file size; variant B of the harness
proves a 1 MiB file (far larger than needed) still gets walked off its end.
The defect (path:line, this tree)
sys/dev/drm/i915/intel_csr.c:134-136βnum_entriesu32 +fw_info[20]insidestruct intel_package_header(__packed, total 256 bytes).sys/dev/drm/i915/intel_csr.c:353-365β the loop:for (i = 0; i < package_header->num_entries; i++)readingpackage_header->fw_info[i].substepping/.stepping/.offsetwith noARRAY_SIZE(package_header->fw_info)bound. (Note the'*'/'*'wildcard arm at :362-364 deliberately does NOT break, so the walk continues.)sys/dev/drm/i915/intel_csr.c:372-376β OOB-readoffsetbecomesdmc_offset *= 4; readcount += dmc_offset; dmc_header = &fw->data[readcount].
Trust model / threat
The DMC firmware file is root-placed (loader path /modules/firmware/... via
firmware_get(), or i915_modparams.dmc_firmware_path module param). The
attacker is therefore "root writes a malicious firmware file" / "compromised
firmware distribution" β the same trust model under which DF-1894 (High) and
DF-1895 (Medium) were filed for this file. The load itself is asynchronous
(schedule_work at intel_csr.c:507) from driver load on gen9+ hardware; any
user who can (re)boot/reload the driver triggers the parse. Severity kept at
Medium to stay consistent with DF-1895's calibration.
Verification performed (honest classification)
- Guest kernel runtime: NOT possible. The audit guest's only display
device is QEMU std-VGA
0x1234:1111(see env.txt).parse_csr_fwis reachable only whenHAS_CSR()(gen9+ Intel display) β no such hardware exists in the guest, so the kernel path cannot execute there. Status:untestedper the allowed enum (device absent, not a failed attempt). - Logic replication: REPRODUCED on the audit guest itself.
poc_csr_num_entries.ccopies the packed struct definitions (intel_csr.c:111-137) and the exact loop (intel_csr.c:353-365) verbatim, places the crafted firmware flush against aPROT_NONEguard page (modeling the end of the kernel heap allocation), and runs it:- variant A (1536-byte file, minimal structurally-valid headers):
fault reading
fw_info[115]β the array holds 20 entries; the walk left the file 3 bytes earlier and kept going (run.log). - variant B (1 MiB file): fault reading
fw_info[87369]β proves that no plausible file size bounds the walk; the loop bound alone isnum_entries, here 0x08000000 (1.5 GiB of walk). Compiled and executed on the DragonFly guest (cc -O2, DF 6.5-DEVELOPMENT). In the kernel the identical walk reads past thefirmware_get()buffer until an unmapped page β kernel-mode page fault β panic; short of that it harvests heap bytes intodmc_offset.
- variant A (1536-byte file, minimal structurally-valid headers):
fault reading
- Upstream provenance (verified against tags, saved under evidence/):
upstream v5.2 has the same unbounded loop; upstream v5.10 added the
sanitization β
parse_csr_fw_package():num_entries = package_header->num_entries; if (WARN_ON(package_header->num_entries > max_entries)) return 0;and the walker comment "num_entries is already sanitized". So this is an upstream-inherited bug that upstream later fixed; the DF tree never received that fix.
Why this is NOT a re-report of DF-1895
DF-1895 = "never validates fw->datasize before indexing fw->data" β the fix
is bounds-checking every &fw->data[...] access against fw->size. That fix
does not touch the fw_info[i] loop bound: a 1 MiB file with
num_entries=0x20000 passes every conceivable fw->size check at the
package-header stage yet still reads fw_info[i] for iβ₯21 (intra-file OOB
w.r.t. the struct array) and, for larger counts, past the file end
(harness variant B). Complementary, independent missing check.
Exploit chain (ceiling)
Root-supplied DMC file β async work csr_load_work_fn β parse_csr_fw β
unbounded heap walk β (a) kernel panic (deterministic with unmapped-page
landing) = local DoS at driver load; (b) controlled-ish OOB read: heap bytes
at fw_info[21..] feed dmc_offset (and via '*'-wildcard entries, the
last matching OOB entry wins), steering further reads at
&fw->data[384+dmc_offset*4] β with DF-1895 also unfixed this gives a heap
read primitive whose contents land in dmc_payload and are then written to
CSR_PROGRAM(i) MMIO (not readable back by userspace; so leak ceiling is
low β DoS is the realistic impact). No path to uid=0: reads only, no writes
past the dmc_payload allocation.
Fix
Bound the loop (mirrors upstream v5.10 sanitization):
--- a/sys/dev/drm/i915/intel_csr.c
+++ b/sys/dev/drm/i915/intel_csr.c
@@ -348,6 +348,13 @@ static uint32_t *parse_csr_fw(struct drm_i915_private *dev_priv,
return NULL;
}
+ if (package_header->num_entries > ARRAY_SIZE(package_header->fw_info)) {
+ DRM_ERROR("DMC firmware has wrong package num_entries %u\n",
+ package_header->num_entries);
+ return NULL;
+ }
+
readcount += sizeof(struct intel_package_header);
(see fix.diff β authored against the read-only sys/ tree, never applied)
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_testablefix.diff restores the upstream v5.10 num_entries bound and git-apply --check passes against sys/, but no Vega/gen9+ hardware exists in the audit guest, so a patched-kernel run cannot be performed
fix.diff (git apply --check clean); upstream_v5.10_intel_csr.c:525-526 shows the equivalent upstream sanitization
Confirmed kernel references
Detail
Exploit chain
root-supplied/corrupted DMC fw file -> async csr_load_work_fn at driver load -> parse_csr_fw unbounded fw_info walk -> kernel heap OOB read -> panic (deterministic DoS); OOB-read u32 becomes dmc_offset compounding DF-1895-style unchecked fw->data indexing. Read-only primitive: no uid=0 path (no OOB write).
Evidence (decisive lines)
run.log (harness faults at fw_info[115] / fw_info[87369] past the 20-entry array on the DF guest); VERDICT.md (full trace + upstream provenance); evidence/upstream_v5.2_intel_csr.c:355 vs evidence/upstream_v5.10_intel_csr.c:525-526; env.txt (guest HW absence)
PoC changes
n/a (no orchestrator seed existed for this ID; harness authored during this run; first draft's guard page was not page-aligned for the 1536-byte variant and was fixed)
Verified recommended fix
Reject package headers with num_entries > ARRAY_SIZE(fw_info) before the stepping-search loop (mirrors upstream v5.10 sanitization); see fix.diff
Verdict
parse_csr_fw() bounds its stepping-search loop with the attacker-controlled package_header->num_entries while the walked array is the fixed fw_info[20]; num_entries>20 walks kernel heap beyond the struct, and large counts walk off the request_firmware() allocation entirely (kernel-mode page fault -> panic). Distinct root cause from DF-1895 (missing ARRAY_SIZE bound vs missing fw->size bound): a 1 MiB file still faults at fw_info[87369]. Upstream v5.2 carries the same bug; v5.10 sanitizes num_entries (WARN_ON(num_entries > max_entries)) β the DF tree never got that fix. Root-placed firmware trust model, consistent with DF-1894/1895 calibration.
No comments yet.