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

gen8 3-level PPGTT insert walk advances pdpe with a 4-level wrap constant and no runtime bound: latent OOB walk of the 4-entry pdp->page_directory array (GEM_BUG_ON-only guard is a production no-op)

Summary

gen8_ppgtt_insert_pte_entries() hand-rolls the walk; its pdpe wrap check compares against GEN8_PML4ES_PER_PML4 (512) - correct only for 4-level VMs. On a 3-level VM (Broadwell default), pdp->page_directory has GEN8_3LVL_PDPES=4 entries. If idx->pdpe reached 4, :1082 would read page_directory[4] OOB, then pd->page_table[pde] and kmap+PTE-write through the garbage pointer. The only bound, GEM_BUG_ON, is BUILD_BUG_ON_INVALID (compile-time no-op) in production. Not triggerable in this tree: sg page count == drm_mm node page count within vm->total and the sg-exhaustion break precedes the advance, so the walk can never pass pdpe 3. Filed as defense-in-depth so refactors do not inherit an unguarded 4-entry array walk (upstream fixed it only by retiring the walker in v5.x).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3769 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
verdict.json β€” 2.3 KB view raw
manifest.json β€” 505 B view raw
fix.diff β€” 632 B view raw

DF-3769 β€” gen8 3-level PPGTT insert walk: no effective runtime bound on pdpe advance

Finding

gen8_ppgtt_insert_pte_entries() (sys/dev/drm/i915/i915_gem_gtt.c:1037-1092) hand-rolls the page-table walk instead of using the bounded gen8_for_each_pdpe macro. Its pdpe wrap check at i915_gem_gtt.c:1075 compares against GEN8_PML4ES_PER_PML4 (512), which is only correct for 4-level VMs. On a 3-level VM (Broadwell default when 48-bit PPGTT is off; i915_pdpes_per_pdp() == 4), pdp->page_directory is a 4-entry kmalloc_array (__pdp_init, i915_gem_gtt.c:763-776). If idx->pdpe ever reached 4, the code would read pdp->page_directory[4] (i915_gem_gtt.c:1082) out of bounds, then pd->page_table[idx->pde] (another OOB deref of attacker-shaped heap data), then kmap_atomic_px() it and write 8-byte PTEs.

The only guard is GEM_BUG_ON(idx->pdpe >= i915_pdpes_per_pdp(&ppgtt->vm)) (i915_gem_gtt.c:1081), and GEM_BUG_ON is a compile-time no-op in production kernels (i915_gem.h:60, #define GEM_BUG_ON(expr) BUILD_BUG_ON_INVALID(expr) unless CONFIG_DRM_I915_DEBUG_GEM).

Why this is latent (not exploitable today)

For idx->pdpe to advance past 3, the scatterlist must supply more than 4 GiB of pages to a single VMA bound in a VM whose total is exactly 2^32. The node/sg invariants forbid that: - vma_create() (i915_vma.c:151) sets vma->size = obj->base.size, and i915_vma_insert() allocates node.size == round_up(vma->size) inside [0, vm->total) via drm_mm; - vma->pages is the object's sg table, whose page count equals round_up(obj->base.size)/PAGE β€” never more than the node; - the walk breaks out at !iter->sg (i915_gem_gtt.c:1059) before the pde/pdpe advance whenever the sg ends exactly at the 4 GiB boundary.

So the bug needs a future invariant break (e.g. a badly reworked view/sg path) to become live memory corruption. Defense-in-depth / hardening finding β€” same code shape as upstream Linux v4.19 (fixed upstream only by the v5.x rewrites that retired this walker).

Make the wrap check level-aware and clamp, e.g. if (++idx->pdpe == i915_pdpes_per_pdp(&ppgtt->vm)) { idx->pdpe = 0; ret = true; break; } β€” for 3lvl the caller (gen8_ppgtt_insert_3lvl) then relies on the existing sg termination. See fix.diff.

Reproduction

Not reproducible at runtime: requires invariant break + Broadwell-class hardware; audit guest is virtio-only (no i915 device, no i915kms.ko). Source-level verification only. Verification skipped per contract (Info hardening finding).

VERDICT.md
↓ download raw

DF-3769 β€” VERDICT

Status: untested (runtime) β€” Info hardening finding; PoC phase skipped per contract (Low/Info). Source analysis below is complete.

Trace (path:line)

  • i915_gem_gtt.c:1050 β€” entry guard GEM_BUG_ON(idx->pdpe >= i915_pdpes_per_pdp(&ppgtt->vm)): no-op in production (i915_gem.h:60).
  • i915_gem_gtt.c:1068-1083 β€” advance: ++idx->pte == GEN8_PTES β†’ ++idx->pde == I915_PDES β†’ ++idx->pdpe == GEN8_PML4ES_PER_PML4 (512). For a 3lvl VM the pdpe array has 4 entries (__pdp_init/i915_pdpes_per_pdp, i915_gem_gtt.h:512-519), so a pdpe of 4..511 would be an OOB read at i915_gem_gtt.c:1082 (pd = pdp->page_directory[idx->pdpe]) β†’ wild pointer deref chain into kmap_atomic_px + PTE writes.
  • Why it cannot fire today: sg page count == round_up(obj->size)/PAGE == node pages ≀ vm->total = 2^32 (i915_vma.c:151-178 vma_create, i915_vma.c:306 GEM_BUG_ON(vma->size > vma->node.size), drm_mm bounds); and the sg-exhausted break at i915_gem_gtt.c:1057-1062 precedes the index advance, so a full 4 GiB VMA starting at 0 exits cleanly at pdpe 3.
  • Contrast: the allocation-side walkers use the correctly bounded macros (gen8_for_each_pdpe, i915_gem_gtt.h:533-539, iter < i915_pdpes_per_pdp(vm)).

Bottom line

Certain that the runtime guard is absent and the wrap constant is wrong for 3lvl; speculative on any path to triggering it (none exists in this tree). Report as hardening so a future refactor doesn't inherit an unguarded 4-entry array walk. fix.diff makes the wrap level-aware.

Artifacts

  • fix.diff β€” one-line-equivalent hardening (never applied to sys/).

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored by inspection; no runtime validation possible/needed for this latent hardening item

findings/poc/DF-3769/fix.diff
↓ fix.diffper-fix-DF-3769

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (trace + invariant proof), VERDICT.md, fix.diff

PoC changes

n/a

Verified recommended fix

Make the pdpe wrap level-aware: wrap (or WARN-and-stop) at i915_pdpes_per_pdp(vm) instead of GEN8_PML4ES_PER_PML4 β€” see fix.diff

Verdict

Hardening gap confirmed in source: gen8_ppgtt_insert_pte_entries (i915_gem_gtt.c:1071-1082) advances idx->pdpe with a wrap check against GEN8_PML4ES_PER_PML4 (512) and its only 3lvl-appropriate bound is GEM_BUG_ON, which is a compile-time no-op in production (i915_gem.h:60). On a 3-level VM pdp->page_directory has 4 entries (i915_gem_gtt.c:768-769), so a pdpe of >=4 would be an OOB heap read feeding kmap_atomic + PTE writes. Not triggerable in this tree: the vma->pages sg count equals the drm_mm node page count within vm.total=2^32 (i915_vma.c:151-178, i915_vma.c:306), and the sg-exhaustion break at i915_gem_gtt.c:1057-1062 precedes the index advance, so the walk can never legitimately pass pdpe 3. Runtime verification skipped per contract (Info finding) and impossible on the virtio-only guest (no i915 device/module). Report so future refactors do not inherit an unguarded 4-entry array walk; upstream only fixed this by retiring the walker in v5.x.