Stolen reserved-base above stolen_top u64-wraps g4x/bdw reserved size, bypasses resource_contains and inflates stolen_usable_size up to 96x β OOB physical allocations beyond stolen
Summary
g4x/bdw compute *size = stolen_top - *base from the BIOS-programmed register with no validation; a base above top wraps the u64, making resource_contains PASS (end==stolen_top-1==dsm.end) and inflating usable up to 96x (measured 85x/96x in the harness) - drm_mm then hands out offsets beyond stolen and the GPU DMAs adjacent physical memory (kernel RAM). The only sanity check (:475) covers just base==0. Firmware/root-MMIO gated; upstream carries the identical code. Extend-the-guard fix (harness-validated fixed build).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4679 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| i915_stolen_harness.c | β | 8.5 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 133 B | view raw | |
| fix.diff | β | 455 B | view raw | |
| build.log | β | 116 B | view raw | |
| run.log | β | 1.6 KB | view raw | |
| fix_run.log | β | 1.4 KB | view raw | |
| env.txt | β | 977 B | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-4679 β i915 stolen reserved-base above stolen_top: u64 size wrap
bypasses the containment check and inflates stolen_usable_size
sys/dev/drm/i915/i915_gem_stolen.c β g4x_get_stolen_reserved()
(:201-229) and bdw_get_stolen_reserved() (:349-366) compute
*size = stolen_top - *base from the BIOS-programmed STOLEN_RESERVED
register; i915_gem_init_stolen() (:470-503) then trusts it.
Build (on the DragonFly guest)
cd /root/df4679 && sh build.sh
The guest has no Intel GPU, so the driver-attach path cannot run; instead the
harness transcribes the exact inline helpers from
include/linux/pci.h:621-642 and both register masks, and build.sh
grep-verifies every transcribed kernel line against the guest's /usr/src
(SOURCE_LINES_VERIFIED in build.log) before compiling.
Run
./i915_stolen_harness # BASELINE (pristine :475 logic) ./i915_stolen_harness fixed # with fix.diff's guard active
Expected (baseline)
== G4X garbage reserved base (0xABC -> 0x0ABC0000) == resource_contains : PASS <- containment check bypassed stolen_usable_size : 2867855360 (85.47x the real 33554432) worst-case alloc phys : 0xabbfffff (dsm.end=0x2cfffff => OUTSIDE stolen) DF-4679 REPRODUCED: ... == BDW garbage reserved base (0xFFF -> 0xFFF00000) == (95.97x)
A reserved base above stolen_top makes stolen_top - *base wrap mod 2^64
(resource_size_t = uint64_t); DEFINE_RES_MEM then computes
dsm_reserved.end == stolen_top - 1 == dsm.end, so
resource_contains(&dsm, &dsm_reserved) at :485 passes even though the
reservation starts above stolen β and stolen_usable_size (:499-500)
inflates up to ~96x the real region, so drm_mm_init(0, usable) (:503)
hands out offsets beyond dsm.end and
i915_pages_create_for_stolen (:516,:536) points the GPU at physical memory
outside stolen (the GEM_BUG_ON(range_overflows(...)) at :516 is a no-op in
release builds β i915_gem.h:38-61).
The sane-firmware control case behaves normally (usable 0.97x real), isolating the wrap as the cause. Threat model: malicious/buggy BIOS (or root pre-attach) β not reachable by an unprivileged local user.
Fix validation
fix.diff extends the existing :475 sanity check to
if (!reserved_base || reserved_base > stolen_top). In the FIXED run both
garbage cases produce usable == real with allocations inside stolen; the
control case is unchanged.
DF-4679 β VERDICT
Status: REPRODUCED (verbatim-arithmetic level on the guest; full kernel path not runnable β the QEMU guest has no Intel GPU) Β· impact: dos/corruption on affected firmware Β· confidence: likely Β· fix: fixed (same level)
Root cause
Two of the stolen-reserved probes compute the reservation size as
top - base in u64:
g4x_get_stolen_reserved()i915_gem_stolen.c:228 β*base = (reg_val & G4X_STOLEN_RESERVED_ADDR2_MASK) << 16(ADDR2 = 0xFFF<<4, i915_reg.h:3507 β base up to 0x0FFF0000)bdw_get_stolen_reserved():365 β*base = reg_val & GEN6_STOLEN_RESERVED_ADDR_MASK(0xFFF<<20, i915_reg.h:401 β base up to 0xFFF00000)
Both registers are chipset config space programmed by firmware. If the base
lands above stolen_top = dsm.end + 1, stolen_top - *base wraps mod
2^64 (resource_size_t = uint64_t, linux/types.h:35). The wrapped size then
defeats the only downstream sanity check:
dev_priv->dsm_reserved = DEFINE_RES_MEM(reserved_base, reserved_size); /* :482 */
if (!resource_contains(&dev_priv->dsm, &dev_priv->dsm_reserved)) return 0; /* :485 */
reserved_total = stolen_top - reserved_base; /* :493 wraps */
dev_priv->stolen_usable_size = resource_size(&dsm) - reserved_total; /* :499 */
drm_mm_init(&dev_priv->mm.stolen, 0, stolen_usable_size); /* :503 */
DEFINE_RES_MEM(start, size) computes end = start + size - 1
(pci.h:626-630); with the wrapped size, end β‘ stolen_top - 1 == dsm.end
(mod 2^64), and resource_contains(a,b) = a->start <= b->start && a->end >=
b->end (pci.h:638-642) passes although b->start is above a->end.
(The !reserved_base check at :475 only catches base==0; the
WARN_ON(ADDR1 < base) at :226 is a warning.) The other probes are safe:
gen6/gen7/chv use fixed size fields and a masked base (containment fails
normally when base is out of range), vlv uses top - size, icl reads a 64-bit
masked base with fixed sizes β checked, none can wrap.
Consequence chain after :503: the drm_mm stolen allocator returns offsets up
to usable-1 far beyond resource_size(dsm); i915_pages_create_for_stolen
(:508-540) builds sg_dma_address = dsm.start + offset (:536) β the GPU DMAs
to physical memory outside stolen (kernel RAM adjacent to the stolen hole).
The GEM_BUG_ON(range_overflows(offset, size, resource_size(dsm))) at :516
is compiled out in release (CONFIG_DRM_I915_DEBUG_GEM undefined,
i915_gem.h:38-61).
Verification
Guest /usr/src md5-matched to the audit tree. Harness transcribes the exact
inline helpers and size formulas; build.sh grep-verifies each kernel line
(SOURCE_LINES_VERIFIED). Baseline run (run.log):
- G4X, 32 MiB stolen [0xD00000..0x2CFFFFF], ADDR2 field 0xABC β base 0x0ABC0000: containment PASS (bypass), usable = 2,867,855,360 = 85.47x real, worst-case allocation phys 0xABBFFFFF (0x7F000000 past dsm.end).
- BDW, 32 MiB at 0x40000000, ADDR 0xFFF β base 0xFFF00000: usable 3,220,176,896 = 95.97x, phys 0xFFEFFFFF.
- Control (sane BIOS base 0x2C00000): usable 0.97x real, allocations inside stolen β isolates the wrap as the cause.
Fixed run (fix_run.log): with the reserved_base > stolen_top guard, both
garbage cases yield usable == real and inside-stolen allocations; control
unchanged.
Limits of this verification (why "likely")
The wrap arithmetic, containment bypass, and usable-size inflation are proven exactly; what could not run on this guest is the physical driver attach (no i915 hardware) β i.e., the final "GPU DMAs to the inflated offsets" step is traced by code reading (:503β:516/:536), not observed. The trigger is firmware-gated (malicious/buggy BIOS, or root MMIO before attach), so this is not an unprivileged-user bug; severity Medium (memcorrupt bucket), exploitability ceiling on affected firmware: OOB physical memory access by the GPU.
Fix
One-line extension of the existing :475 sanity check (fix.diff, git-apply
clean, validated at the same arithmetic level): also ignore the reservation
when reserved_base > stolen_top. Note upstream Linux carries the identical
g4x/bdw code β the fix applies there verbatim.
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
fixedValidated at the same verbatim-arithmetic level on the guest: fix.diff extends the existing :475 sanity check to 'if (!reserved_base || reserved_base > stolen_top)' (falling back to the existing ignore-reservation semantics). Baseline build inflates usable_size 85-96x and places worst-case allocations outside stolen; FIXED build (same guard logic compiled in via ./i915_stolen_harness fixed) yields usable == real and allocations inside stolen for both garbage cases, control case unchanged. fix.diff is git-apply-able against the pristine tree (git apply --check clean) and was never applied to sys/. Full kernel-build validation is not possible on this guest (no Intel GPU to attach; the guard is a 2-line branch in driver-init arithmetic already covered exactly by the harness).
run.log vs fix_run.log (BASELINE vs FIXED, same three register cases); fix.diff in pack.
Confirmed kernel references
- sys/dev/drm/i915/i915_gem_stolen.c:201-229
- sys/dev/drm/i915/i915_gem_stolen.c:349-366
- sys/dev/drm/i915/i915_gem_stolen.c:470-503
- sys/dev/drm/i915/i915_gem_stolen.c:516
- sys/dev/drm/i915/i915_gem_stolen.c:536
- sys/dev/drm/include/linux/pci.h:626-642
- sys/dev/drm/include/linux/types.h:35
- sys/dev/drm/i915/i915_gem.h:38-61
- sys/dev/drm/i915/i915_reg.h:3507
- sys/dev/drm/i915/i915_reg.h:401
Detail
Exploit chain
buggy/malicious BIOS (or root via /dev/mem pre-attach) programs STOLEN_RESERVED base above stolen_top on G4X/ILK or non-LP BDW -> u64 size wrap -> containment check bypassed at :485 -> stolen_usable_size inflated (measured 85-96x) -> drm_mm stolen allocator returns offsets beyond dsm.end -> stolen objects get sg_dma_address outside stolen (GEM_BUG_ON inert) -> GPU DMA reads/writes arbitrary adjacent physical memory (kernel memory included) -> silent corruption or panic. No unprivileged trigger; no uid0 route.
Evidence (decisive lines)
run.log: 'DF-4679 REPRODUCED' x2 with 85.47x/95.97x inflation, 'worst-case alloc phys ... OUTSIDE stolen', control case normal; fix_run.log (FIXED build): 'FIX VERIFIED ... usable (33554432) <= real (33554432)' for both garbage cases; build.log SOURCE_LINES_VERIFIED; fix.diff (git apply --check clean).
PoC changes
Self-authored (no seed): logic-level harness transcribing the two size formulas and the init tail; a first draft had a control-case register-value slip (0x02C00<<16 vs intended 0x2C0<<16) that was corrected -- the kernel-modeled arithmetic itself never misbehaved.
Verified recommended fix
Extend the :475 sanity check to also reject reserved_base > stolen_top before computing dsm_reserved/reserved_total (fix.diff).
Verdict
REPRODUCED at the verbatim-arithmetic level on the DragonFly guest (no Intel GPU exists on this QEMU guest, so the driver-attach path itself cannot run -- the exact inline helpers from include/linux/pci.h:621-642 and both register masks are transcribed and grep-verified against the guest's /usr/src at build time, see SOURCE_LINES_VERIFIED in build.log): when the BIOS-programmed CTG/ELK (g4x_get_stolen_reserved, i915_gem_stolen.c:228) or GEN6 (bdw_get_stolen_reserved, :365) STOLEN_RESERVED register yields a reserved base ABOVE stolen_top, 'size = stolen_top - base' wraps mod 2^64 (resource_size_t is uint64_t, linux/types.h:35). DEFINE_RES_MEM then computes dsm_reserved.end = base + wrapped_size - 1 == stolen_top - 1 == dsm.end, so resource_contains(&dsm, &dsm_reserved) at :485 PASSES despite the reservation starting above stolen -- the only sanity check between the register read and drm_mm_init. reserved_total = stolen_top - reserved_base (:493) then wraps huge and stolen_usable_size (:499-500) INFLATES far above the real stolen size: measured 2867855360 bytes = 85.47x a real 32 MiB region (G4X, base 0x0ABC0000) and 3220176896 = 95.97x (BDW, base 0xFFF00000), worst-case allocation physical address dsm.start+usable-1 landing 0x7F000000 past dsm.end. drm_mm_init(0, usable) at :503 then hands out offsets beyond stolen, and i915_pages_create_for_stolen (:516,:536) builds sg_dma_address = dsm.start + offset pointing at physical memory outside stolen -- the GEM_BUG_ON(range_overflows(...)) at :516 is a no-op in release builds (CONFIG_DRM_I915_DEBUG_GEM undefined, i915_gem.h:38-61). A sane-firmware control case behaves normally (usable 0.97x real), isolating the wrap as the cause. Fix validated at the same level: with fix.diff's 'reserved_base > stolen_top' extension of the existing :475 sanity check, both garbage cases produce usable == real and allocations stay inside stolen. Threat model: requires malicious/buggy firmware programming the MCHBAR/chipset register (or root writing it before driver load) -- not reachable by an unprivileged local user on a correct BIOS; hence 'likely' overall confidence in kernel manifestation and dos/corruption framing rather than certain.
No comments yet.