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

Missing cleanup on agp_via_attach failure leaks /dev/agpgart with freed softc -> NULL-deref kernel panic

  • File: sys/dev/agp/agp_via.c
  • Lines: 175–183 (leaked cleanup at 180–183)
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-416 Use After Free, CWE-476 NULL Pointer Dereference
  • Confidence: likely
  • Status: new

Summary

agp_via_attach calls agp_generic_attach (which creates the /dev/agpgart cdev and allocates the aperture resource) but if the subsequent initial_aperture==0 check fails, it returns ENXIO without calling agp_generic_detach.

The new-bus framework then frees the softc (subr_bus.c:1944), while the /dev/agpgart device node persists with si_drv1 pointing at the device whose softc is now NULL. Any process that opens /dev/agpgart triggers a NULL pointer dereference in agp_open (agp.c:781: sc->as_isopen) β†’ kernel panic.

Root cause

In agp_via_attach (agp_via.c:175-183):

error = agp_generic_attach(dev);   /* line 175: creates /dev/agpgart via
                                    * make_dev (agp.c:240), allocates aperture
                                    * resource (agp.c:209) */
if (error)
    return error;                    /* line 177: OK -- generic_attach failed,
                                      * nothing leaked */

sc->initial_aperture = AGP_GET_APERTURE(dev);  /* line 179 */
if (sc->initial_aperture == 0) {                /* line 180 */
    device_printf(dev, "bad initial aperture size, disabling\n");
    return ENXIO;                               /* line 182: BUG -- returns
                                                 * without agp_generic_detach() */
}

agp_generic_attach (agp.c:195-245) has already: - (a) bus_alloc_resource_any'd the aperture (agp.c:209), - (b) make_dev'd /dev/agpgart with si_drv1=dev (agp.c:240-242).

Neither is cleaned up. The other failure path β€” the for(;;) GATT loop at lines 185-197 β€” correctly calls agp_generic_detach(dev) before returning (line 195), proving the omission at line 182 is an oversight.

After agp_via_attach returns ENXIO, device_doattach (subr_bus.c:2108-2124) calls device_set_driver(dev, NULL) at line 2121, which in turn calls kfree(dev->softc) and sets dev->softc = NULL (subr_bus.c:1943-1945).

The /dev/agpgart cdev is never destroyed (destroy_dev / dev_ops_remove_minor only happens in agp_free_cdev, which was never called).

When any process subsequently opens /dev/agpgart, agp_open (agp.c:775-787) does:

struct agp_softc *sc = device_get_softc(dev);   /* returns NULL -- softc freed */
if (!sc->as_isopen) {                            /* NULL deref -> page fault -> panic */

Trigger precondition

The initial_aperture==0 precondition is reached for AGP 3.0 (v3) VIA chipsets when the APSIZE register (agp_via.c:255) has a value not in the switch table (any value other than 0x800/0xc00/0xe00/0xf00/0xf20/0xf30/ 0xf38/0xf3c/0xf3e/0xf3f). The default case at agp_via.c:277-280 returns 0.

For v2 chipsets, get_aperture always returns β‰₯1MB, so this path is v3-only.

Threat model

Attacker position: any local user with read/write access to /dev/agpgart (root:wheel 0600 by default per agp.c:241; may be relaxed via devfs rules for video group on systems running Xorg/DRM, which is the intended use case for AGP hardware).

Precondition: a VIA AGP 3.0 host bridge whose APSIZE register value is unrecognized by the driver's switch table β€” this is a hardware/BIOS condition that persists for the lifetime of the system once triggered at boot; the attacker cannot create it but inherits it.

Once the precondition exists, the attack is trivial and deterministic: a single open("/dev/agpgart", O_RDWR) call triggers a NULL pointer dereference in kernel context, causing an immediate kernel panic (system-wide DoS).

No race, no grooming, no special privileges beyond device-node access.

Impact: local denial of service (kernel panic). Not privilege escalation or info leak.

Proof of concept

/* poc_open_agpgart.c
 * Build: cc -o poc_open_agpgart poc_open_agpgart.c
 * Run:   ./poc_open_agpgart
 *
 * Triggers NULL-deref panic on systems where VIA AGP 3.0 attach failed
 * (check dmesg for "bad initial aperture size, disabling" before running).
 *
 * agp_open dereferences device_get_softc(dev) which is NULL after
 * device_doattach freed the softc via device_set_driver(dev, NULL).
 *
 * Expected: kernel panic "Fatal trap 12: page fault while in kernel mode"
 * on address 0x0 (or small offset for as_isopen field).
 */
#include <fcntl.h>
#include <unistd.h>
int main(void) {
    int fd = open("/dev/agpgart", O_RDWR);
    if (fd >= 0) close(fd);
    return 0;
}

Success: immediate kernel panic (page fault at NULL). The dmesg/boot log will show the earlier "agp0: Invalid aperture setting 0x..." and "bad initial aperture size, disabling" messages from the failed attach, followed by the panic stack trace through agp_open.

The precondition (failed attach) can be verified pre-exploit by checking dmesg for the "bad initial aperture size" message and confirming /dev/agpgart exists (ls -la /dev/agpgart) despite the AGP driver not being attached.

Call agp_generic_detach(dev) before returning ENXIO, matching the cleanup pattern already used in the for(;;) loop failure path at line 195.

This destroys the leaked /dev/agpgart cdev (via agp_free_cdev) and releases the aperture resource (via agp_free_res).

--- a/sys/dev/agp/agp_via.c
+++ b/sys/dev/agp/agp_via.c
@@ -179,6 +179,7 @@
    sc->initial_aperture = AGP_GET_APERTURE(dev);
    if (sc->initial_aperture == 0) {
        device_printf(dev, "bad initial aperture size, disabling\n");
+       agp_generic_detach(dev);
        return ENXIO;
    }

agp_generic_detach (agp.c:263-269) calls agp_free_cdev (removes the /dev/agpgart device node) and agp_free_res (releases the aperture bus resource), ensuring no dangling cdev persists after the failed attach.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1986 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.1 KB ↓ raw
fix.diff suggested-fix Fix: Call agp_generic_detach(dev) before returning ENXIO. 334 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1986 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/agp/agp_via.c:180-182

Mechanism: agp_via_attach returns ENXIO on initial_aperture==0 without calling agp_generic_detach (unlike the for-loop path at L195). Leaks /dev/agpgart cdev + freed softc access.

Hardware dependency: Requires VIA AGP bridge with zero initial aperture.

Fix: Call agp_generic_detach(dev) before returning ENXIO.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/agp/agp_via.c:180-182 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/agp/agp_via.c:180-182. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: ENXIO without agp_generic_detach β†’ leaks cdev + freed softc. Call detach.

Verified recommended fix

See fix.diff. ENXIO without agp_generic_detach β†’ leaks cdev + freed softc. Call detach.

Verdict

REPRODUCED (source-only). sys/dev/agp/agp_via.c:180-182: ENXIO without agp_generic_detach β†’ leaks cdev + freed softc. Call detach.