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

agp_amd64_attach error exits leak the GATT and aperture resource and leave a live /dev/agpgart on an unattached device

Summary

After agp_generic_attach has make_dev'd /dev/agpgart and allocated the GATT, three vendor set_aperture failures return ENXIO with no unwind; newbus never detaches a failed attach - the contigmalloc'd GATT (up to 2MiB), the aperture resource, and a live 0600 cdev pointing at an unattached device all leak, and root holding the orphaned node can ALLOCATE/BIND into a GATT never installed in hardware. Trigger: BIOS aperture larger than the vendor sub-table max (ULi >=512MB, nForce3 >=1GB). Same class as DF-1986 (agp_via). Hardware-gated (i440FX guest never probes; trigger built+ran ENOENT). Unwind fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4855 Β· 10 files
FileTypeDescriptionSize
agp_orphan.c β€” 2.4 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 119 B view raw
build.log β€” 267 B view raw
run.log β€” 905 B view raw
env.txt β€” 518 B view raw
VERDICT.md β€” 5.0 KB ↓ raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.4 KB view raw
VERDICT.md
↓ download raw

DF-4855 β€” agp_amd64: attach error paths leak GATT/resources and leave a live /dev/agpgart on an unattached device

What

agp_amd64_attach() (sys/dev/agp/agp_amd64.c:159-243) has three error exits that fire after agp_generic_attach() succeeded at :183:

183:    if ((error = agp_generic_attach(dev)))
184:        return (error);
...
204:    switch (pci_get_vendor(dev)) {
205:    case 0x10b9:    /* ULi */
206:        agp_amd64_uli_init(dev);
207:        if (agp_amd64_uli_set_aperture(dev, sc->initial_aperture))
208:            return (ENXIO);            /* <-- no unwind */
...
211:    case 0x10de:    /* nVidia */
212:        agp_amd64_nvidia_init(dev);
213:        if (agp_amd64_nvidia_set_aperture(dev, sc->initial_aperture))
214:            return (ENXIO);            /* <-- no unwind */
...
217:    case 0x1106:    /* VIA */
218:        sc->via_agp = agp_amd64_via_match();
219:        if (sc->via_agp) {
220:            agp_amd64_via_init(dev);
221:            if (agp_amd64_via_set_aperture(dev, sc->initial_aperture))
222:                return (ENXIO);        /* <-- no unwind */

agp_generic_attach() (agp.c:196-245) has already: - bus_alloc_resource'd the aperture BAR (agp.c:209), - make_dev(&agp_ops, 0, UID_ROOT, GID_WHEEL, 0600, "agpgart") with si_drv1 = dev (agp.c:240-242).

And sc->gatt was already allocated via agp_alloc_gatt() (agp_amd64.c:189, stored at :202; up to entries * 4 = 2 MiB of contigmalloc, agp.c:146).

When these return (ENXIO) fire, newbus never calls agp_amd64_detach() (detach is only invoked for successfully-attached devices), so nothing ever runs agp_free_cdev / agp_free_gatt / agp_free_res.

Consequences

  1. Leaked kernel memory: the GATT (contiguous, up to 2 MiB) plus the aperture bus resource leak on every failed attach.
  2. Orphaned live cdev: /dev/agpgart (0600 root:wheel) remains registered pointing at a half-initialized, unattached device: agp_open/agp_ioctl never check device_is_attached(). Root holding the node can AGPIOC_ALLOCATE + AGPIOC_BIND; the bind lands in sc->gatt β€” which is set (allocated at :202 before the failing switch) β€” but the GART was never installed: the ATTBASE/APCTRL-GARTEN config-write loop at agp_amd64.c:229-238 never ran, so the hardware ignores every entry written. (No memory-safety violation occurs in the driver itself; the GATT writes are in-bounds β€” ag_entries is u_int32_t, agppriv.h:83, so the ag_entries << AGP_PAGE_SHIFT bound at :333/:347 promotes correctly to 64-bit.)
  3. Same defect class as DF-1986 (agp_via.c:180-183, filed Low) β€” this is the agp_amd64.c instance.

Trigger (hardware-gated)

sc->initial_aperture comes from the AMD64 GART APCTRL register (agp_amd64.c:186, table at :265-273: 32M..2G). The vendor sub-tables are strictly smaller: - ULi M1689 accepts 32/64/128/256 MB (agp_amd64.c:398-406) β†’ BIOS aperture β‰₯ 512 MB β‡’ EINVAL β‡’ ENXIO. - nForce3 accepts 32..512 MB (agp_amd64.c:433-441) β†’ BIOS aperture β‰₯ 1 GB β‡’ EINVAL β‡’ ENXIO. - VIA: aperture math (agp_amd64.c:470-472) validates every power-of-two, so the VIA exit needs an actual config-write anomaly.

So: boot or kldload agp on an AMD-8151/SiS-75x/ULi-M1689/nForce3/VIA-K8 board whose BIOS programmed a larger-than-vendor-max aperture. Requires matching 2003-2005-era hardware; the cdev is 0600, so the ioctl surface is root-only regardless.

Guest verification (honest negative)

The QEMU/i440FX guest has exactly one host bridge, 0x12378086 (Intel 82441) β€” no id in agp_amd64_match()'s table (agp_amd64.c:87-112) can match, agp_amd64_probe returns ENXIO for every device, /dev/agpgart does not exist, and the trigger program (built clean on the guest) gets ENOENT from both root and nobody. The failure path cannot execute on this guest; the defect is proven statically by the line trace above. Verdict: not_reproduced (hardware absent), confidence certain in the code defect.

Fix

Unwind in the error paths (authoritative fix.diff next to this file):

--- a/sys/dev/agp/agp_amd64.c
+++ b/sys/dev/agp/agp_amd64.c
@@ -204,10 +204,13 @@ agp_amd64_attach(device_t dev)
    switch (pci_get_vendor(dev)) {
    case 0x10b9:    /* ULi */
        agp_amd64_uli_init(dev);
-       if (agp_amd64_uli_set_aperture(dev, sc->initial_aperture))
-           return (ENXIO);
+       if (agp_amd64_uli_set_aperture(dev, sc->initial_aperture)) {
+           error = ENXIO;
+           goto err;
+       }
        break;

(with matching goto err at the nVidia and VIA sites, and err: agp_free_gatt(sc->gatt); agp_generic_detach(dev); return (error); after the switch.)

Alternatively (defense in depth, fixes every chipset driver at once): move the make_dev() out of agp_generic_attach() into a post-attach step, or make agp_open/agp_ioctl reject unattached devices.

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 is a compile-pattern-correct unwind authored against the read-only tree and validated with git apply --check; kernel build validation is hardware-gated (attach cannot be exercised on the QEMU guest with or without the fix)

fix.diff (git apply --check OK)
↓ fix.diffper-fix-DF-4855

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (line-trace + trigger conditions); run.log (guest: ENOENT root+nobody, single host bridge 0x12378086, agp in-kernel but /dev/agpgart absent); fix.diff (git-apply-able unwind, validated with git apply --check)

PoC changes

agp_bind field corrected for DragonFly sys/sys/agpio.h:149-152 (pg_start, not offset); guest heredoc re-formatting

Verified recommended fix

Convert the three vendor set_aperture failures in agp_amd64_attach to goto err; unwind with agp_free_gatt(sc->gatt) + agp_generic_detach(dev); see fix.diff

Verdict

Code defect is certain by line trace: agp_amd64_attach calls agp_generic_attach (agp.c:196) which make_dev()s /dev/agpgart at agp.c:240-242, then three vendor set_aperture failures (agp_amd64.c:207-208 ULi, :213-214 nVidia, :221-223 VIA) return ENXIO with no unwind - newbus never calls detach for a failed attach, so the GATT contigmalloc (up to 2MiB, agp.c:146), the aperture bus resource and the live 0600 /dev/agpgart node on an unattached device all leak. Trigger is hardware-gated (ULi M1689 with >=512MB or nForce3 with >=1GB BIOS-set aperture, since the vendor sub-tables at agp_amd64.c:398-406/:433-441 reject those sizes that the host table :265-273 accepts); the ioctl surface is root-only (0600). Runtime reproduction was impossible on the audit guest: QEMU i440FX presents only host bridge 0x12378086, which matches no id in agp_amd64_match (agp_amd64.c:87-112), so the driver never attaches - trigger builds clean and open() returns ENOENT for root and nobody. No memory-safety violation in the driver itself: the orphaned-node binds write in-bounds into a GATT that is allocated (sc->gatt set at :202) but never installed (ATTBASE/GARTEN loop :229-238 never ran), so impact is resource leak + stale-cdev functional corruption, Low.