โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0593

Latent UAF: fairq_class_destroy does not clear dangling pif_default pointer (currently unreachable via pf ioctls)

Field Value
ID DF-0593
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/net/altq/altq_fairq.c
Lines 428, 488-526, 570-578
Area net/altq (FAIRQ scheduler)
Confidence speculative
Discovered 2026-07-02
Reported pending

Summary

fairq_class_destroy resets pif_classes[pri], pif_poll_cache, and pif_maxpri when destroying a class, but does not clear pif->pif_default if the destroyed class was the default. After destruction, pif_default is a dangling pointer to freed heap. The next fairq_enqueue that falls through to the default class (line 571) would pass the non-NULL dangling-pointer check and write to freed memory. This is currently unreachable from userspace ioctls (all pf callers gate on qname[0] == 0 before calling altq_remove, so per-class destroy is never dispatched), but the defect is real in the code and the function is public API (altq_var.h:105); it is a latent regression / hardening hazard if a per-class removal path is added in the future (e.g. wiring up the reserved DIOCCHANGEALTQ placeholder).

Root cause

fairq_class_destroy (sys/net/altq/altq_fairq.c:488-529) clears:

499:    pif->pif_classes[cl->cl_pri] = NULL;
500:    if (pif->pif_poll_cache == cl)
501:        pif->pif_poll_cache = NULL;

but there is no equivalent if (pif->pif_default == cl) pif->pif_default = NULL;. pif_default is only ever assigned in fairq_class_create at line 428 (if (flags & FARF_DEFAULTCLASS) pif->pif_default = cl;).

After kfree(cl, M_ALTQ) at line 526, if cl was the default class, pif->pif_default is a dangling pointer to freed heap memory (sizeof(struct fairq_class) โ‰ˆ 200 bytes on amd64).

The next fairq_enqueue (altq_fairq.c:570-577) executes:

570:    if (cl == NULL) {
571:        cl = pif->pif_default;
572:        if (cl == NULL) { ... }
573:    }
...
578:        cl->cl_flags |= FARF_HAS_PACKETS;     /* WRITE to freed memory */
...
581:        fairq_addq(cl, m, hash);              /* DEREF cl->cl_buckets etc. */

โ€” the dangling pointer is non-NULL so the NULL-check passes, then cl->cl_flags |= FARF_HAS_PACKETS; writes to freed memory, and fairq_addq(cl, m, hash) dereferences cl->cl_head (altq_fairq.c:728), cl->cl_buckets (:731, :734), cl->cl_nbucket_mask (:733) โ€” all freed-heap reads, plus _addq writes to the freed bucket queue. This is a controllable UAF: the freed ~200-byte fairq_class can be reallocated by spraying same-sized allocations, turning the flag-write and bucket-deref into arbitrary-field corruption.

The sibling function sys/net/altq/altq_priq.c:priq_class_destroy (:402-436) has the identical omission for priq_if->pif_default.

Threat model & preconditions

  • Attacker position: currently no userspace trigger path exists. fairq_remove_queue (:259) โ†’ fairq_remove_queue_locked (:248) โ†’ fairq_class_destroy is the only per-class destroy entry point. But altq_remove (sys/net/altq/altq_subr.c:566-571) dispatches to altq_remove_queue only when a->qname[0] != 0, and all three pf callers (pf_begin_altq sys/net/pf/pf_ioctl.c:590, pf_rollback_altq :615, pf_commit_altq :663) explicitly check altq->qname[0] == 0 before calling altq_remove, so altq_remove is only ever called with discipline-type altqs. There is no DIOCREMOVEALTQ ioctl (DIOCCHANGEALTQ returns ENODEV at pf_ioctl.c:2094). Therefore, individual class destruction while the discipline remains live does not occur via the standard interface.
  • Privileges gained or impact: currently zero (no trigger). If a per-class removal path is wired up later (e.g. implementing DIOCCHANGEALTQ or DIOCREMOVEALTQ, or an in-kernel module calling fairq_remove_queue/altq_remove_queue directly), the exploit becomes: 1. Configure a fairq discipline with a default class (FARF_DEFAULTCLASS). 2. Remove only the default class via the new path. 3. Groom the kernel heap: spray ~200-byte allocations (same slab as struct fairq_class) to reclaim the freed default class's memory with a controlled object. 4. Send a UDP packet via the interface that doesn't match any classifier (so fairq_enqueue falls through to pif_default). 5. fairq_enqueue writes FARF_HAS_PACKETS to the reclaimed object's cl_flags field offset, and fairq_addq reads cl_head/cl_buckets from the reclaimed object โ€” if the reclaimed object is a fake fairq_class pointing to attacker-controlled memory, this yields arbitrary kernel read/write.
  • Required config or capabilities: if a trigger were added, root + ALTQ config (which already requires root to set up).
  • Reachability: currently not reachable. This finding documents the latent code defect so it is not re-introduced as a live bug by a future per-class-removal patch.

Proof of concept

No PoC can be built today โ€” the trigger path is unreachable from userspace. The findings/poc/DF-0593/ directory contains only a README.md documenting the latent defect and the conditions under which it would become exploitable.

Impact

  • Blast radius: currently zero (unreachable).
  • Severity rationale: Info โ€” hardening opportunity / defense-in-depth. The code defect is certain (verified by reading fairq_class_destroy vs fairq_class_create); the exploitability is purely speculative (depends on a future patch wiring up per-class removal). The CVSS vector above reflects the hypothetical impact if a trigger were added.
  • Reliability: not currently triggerable.

Add pif_default reset in fairq_class_destroy, mirroring the existing pif_poll_cache reset:

--- a/sys/net/altq/altq_fairq.c
+++ b/sys/net/altq/altq_fairq.c
@@ -498,6 +498,8 @@ fairq_class_destroy(struct fairq_class *cl)
    pif = cl->cl_pif;
    pif->pif_classes[cl->cl_pri] = NULL;
    if (pif->pif_poll_cache == cl)
        pif->pif_poll_cache = NULL;
+   if (pif->pif_default == cl)
+       pif->pif_default = NULL;
    if (pif->pif_maxpri == cl->cl_pri) {

The same fix should be applied to sys/net/altq/altq_priq.c:priq_class_destroy (after line 413).

References

  • sys/net/altq/altq_priq.c:priq_class_destroy โ€” the sibling function with the identical defect.
  • sys/net/altq/altq_subr.c:566-571 (altq_remove dispatch) and the three pf ioctl callers (pf_ioctl.c:590, 615, 663) that gate on qname[0] == 0, making per-class destroy currently unreachable.

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending) as a hardening/latent-defect item.