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)
PoC verification
Evidence pack
findings/poc/DF-3769 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
Recommended fix
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).
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 guardGEM_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 ati915_gem_gtt.c:1082(pd = pdp->page_directory[idx->pdpe]) β wild pointer deref chain intokmap_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-178vma_create,i915_vma.c:306GEM_BUG_ON(vma->size > vma->node.size), drm_mm bounds); and the sg-exhausted break ati915_gem_gtt.c:1057-1062precedes 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_testablefix.diff authored by inspection; no runtime validation possible/needed for this latent hardening item
findings/poc/DF-3769/fix.diff
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.
No comments yet.