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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4679 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated 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.
↓ fix.diffper-fix-DF-4679

Confirmed kernel references

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.