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

agp_i915_write_gtt programs GTT PTEs into the MMADR register BAR (sc_res[0]) instead of GTTADR (sc_res[1]): GPU-register corruption incl. PGTBL_CTL hijack at PTE index 0x808, and OOB writes past the mapped MMADR resource for PTE indices >= 32768

Summary

The i915 write_gtt uses sc_res[0] (MMADR, the 128KB register window) while the GTT lives in GTTADR (sc_res[1]) - proven by the file's own read path using res[1] and by FreeBSD upstream using res[1]; only the DragonFly copy uses res[0]. The i965 and g4x pairs are internally consistent; the i915 pair alone is mismatched (transcription divergence). Every GGTT bind writes PTE values into GPU register space: PTE index 0x808 lands exactly on PGTBL_CTL, reprogramming the GTT base to a GEM page phys|1 - a user who gets a page installed there can author GTT entries via mmap, giving the GPU arbitrary physical mappings (full compromise chain). With a 256MB aperture, indices 32768-65535 write 128KB past the mapped MMADR into adjacent kernel virtual memory. Hardware-gated (gen3 graphics). One-token BAR-index fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4255 Β· 10 files
FileTypeDescriptionSize
trigger.c β€” 3.1 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 150 B view raw
build.log β€” 65 B view raw
run.log β€” 65 B view raw
env.txt β€” 1007 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
fix.diff β€” 406 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.6 KB view raw
VERDICT.md
↓ download raw

DF-4255 VERDICT β€” agp_i915_write_gtt writes GTT PTEs to the wrong BAR (MMADR, the register window) instead of GTTADR

Status: inconclusive on the audit guest (hardware-gated). Root cause certain at source level.

The bug

DragonFly sys/dev/agp/intel-gtt.c:998-1004:

static void
agp_i915_write_gtt(device_t dev, u_int index, uint32_t pte)
{
    struct agp_i810_softc *sc;

    sc = device_get_softc(dev);
    bus_write_4(sc->sc_res[0], index * 4, pte);      /* <-- WRONG BAR */
}

agp_i915_res_spec (intel-gtt.c:139-143) maps two BARs for the CHIP_I915 driver family (915G 0x2582, E7221 0x258A, 915GM 0x2592, 945G 0x2772, 945GM 0x27A2, 945GME 0x27AE β€” intel-gtt.c:318-347):

  • sc_res[0] = AGP_I915_MMADR (BAR 0x10) β€” the memory-mapped register window (128 KB on 915G/945G).
  • sc_res[1] = AGP_I915_GTTADR (BAR 0x1C) β€” the GTT itself.

The file's own agp_i915_sync_gtt_pte (intel-gtt.c:1092-1098) reads the GTT back from sc_res[1], and FreeBSD upstream β€” the direct ancestor of this file β€” programs the PTE through sc_res[1] as well (freebsd-src sys/dev/agp/agp_i810.c, agp_i915_write_gtt: bus_write_4(sc->sc_res[1], index * 4, pte);). Only the DragonFly copy writes sc_res[0]. This is a transcription divergence with security consequences, not an intentional change: nothing else in the file treats MMADR as containing the GTT for gen3.

Consequences on affected hardware

Reachable from unprivileged userspace: any GGTT bind on a gen3 part goes i915_ggtt_insert_entries (i915_gem_gtt.c:2703-2713) β†’ intel_gtt_insert_sg_entries (intel-gtt.c:1415) β†’ agp_i915_install_gtt_pte (intel-gtt.c:985) β†’ agp_i915_write_gtt. Same for i915_ggtt_insert_page β†’ intel_gtt_insert_page (a distinct function from the known DF-1476/DF-2401 argument swap; the argument order here is correct β€” the destination BAR is wrong).

  1. Register-space corruption (any aperture size): PTE index i is written at MMADR + i*4, clobbering live GPU registers. PTE index 0x808 lands exactly on MMADR+0x2020 == AGP_I810_PGTBL_CTL (agpreg.h:198) β€” the GTT base pointer β€” reprogramming it to (GEM page phys | 1). A user that can get its GEM page installed as the GTT base effectively gets to author GTT entries from userspace (mmap'd GEM contents), i.e. arbitrary GPU-visible physical mappings β†’ GPU DMA anywhere β†’ full system compromise chain on real hardware.
  2. Out-of-bounds write past the mapped resource (aperture > 128 MB): bus_space handles are KVA mappings sized to the BAR. With the common 256 MB aperture, gtt_mappable_entries = 65536, and PTE indices 32768..65535 write MMADR+128 KB .. MMADR+256 KB β€” the first 128 KB of which lies entirely past the end of the mapped MMADR resource, into whatever kernel virtual memory adjoins it. Contents are attacker-influenced (page frame numbers of user GEM pages | PTE flags).

Why inconclusive on this guest

The audit guest is QEMU/KVM without any Intel integrated graphics: pciconf -l shows no 8086:2582/258A/2592/2772/27A2/27AE device and agp/drm/i915 are not loaded (env.txt). The trigger cannot execute; the code path is exclusively hardware-driven (device attach β†’ DRM ioctls).

Fix

One-line: restore the upstream BAR index in agp_i915_write_gtt (fix.diff). Not validated on hardware for the same reason; the patched line matches FreeBSD upstream exactly.

References

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 restores the upstream BAR index; cannot be validated on this guest for the same hardware-gating reason (no Intel GTT device)

findings/poc/DF-4255/fix.diff (one-line sc_res[0] -> sc_res[1] in agp_i915_write_gtt)
↓ fix.diffper-fix-DF-4255

Confirmed kernel references

Detail

Exploit chain

On affected hardware: unprivileged GEM create + execbuf -> bind at rising GGTT offsets -> PTE writes clobber MMADR register space -> index 0x808 reprograms PGTBL_CTL to attacker GEM page -> user-authored GTT (GEM mmap) maps arbitrary physical pages for the GPU -> GPU DMA read/write anywhere -> uid=0. First stage (register clobber) is unconditional on use; not exercised here for lack of hardware.

Evidence (decisive lines)

VERDICT.md (full trace + upstream contrast); build.log/run.log (guest build succeeds, /dev/dri/card0 absent -> hardware-gated); trigger.c (bind walker); fix.diff (one-line BAR restore)

PoC changes

Vendored the uapi structs/ioctl numbers directly into trigger.c (guest kernel headers pull in linux/types.h -> asm/int-ll64.h which does not exist for userland); exact layouts taken from the guest's /usr/src i915_drm.h

Verified recommended fix

agp_i915_write_gtt: write the PTE through sc->sc_res[1] (GTTADR) as FreeBSD upstream does

Verdict

agp_i915_write_gtt (sys/dev/agp/intel-gtt.c:998-1004) writes GTT PTEs via bus_write_4(sc->sc_res[0], index*4, pte) β€” the MMADR register BAR β€” while the GTT actually lives in GTTADR (sc_res[1], mapped by agp_i915_res_spec at intel-gtt.c:139-143). The file's own agp_i915_sync_gtt_pte reads the GTT from sc_res[1] (intel-gtt.c:1092-1098) and FreeBSD upstream programs res[1], proving a transcription divergence. On the six CHIP_I915 chipsets (915G/E7221/915GM/945G/945GM/945GME) every GGTT bind from unprivileged GEM use (i915_ggtt_insert_entries -> intel_gtt_insert_sg_entries -> agp_i915_install_gtt_pte) therefore writes PTEs into GPU register space: index 0x808 lands exactly on MMADR+0x2020 == AGP_I810_PGTBL_CTL, handing the GTT base register a (GEM page phys | 1) value β€” a GPU-side arbitrary-physical-mapping primitive; with a 256MB aperture, indices 32768-65535 write 128KB past the end of the mapped MMADR resource into adjacent kernel virtual memory. Could not be executed: the audit guest is QEMU without any Intel graphics (env.txt: no 8086 display device, agp/drm/i915 not loaded) and the trigger fails at open('/dev/dri/card0') as expected (run.log). Source-level certainty; hardware-gated verification.