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

Uninitialized kernel stack leaked to userspace via fairq_getqstats copyout of struct fairq_classstats

Field Value
ID DF-0592
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N
CWE CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
File sys/net/altq/altq_fairq.c
Lines 282, 306, 312, 974-1001
Area net/altq (FAIRQ scheduler)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

fairq_getqstats declares struct fairq_classstats stats on the kernel stack without zeroing (line 282). The helper get_class_stats (altq_fairq.c:974-1001) only populates a subset of the struct fields: compiler-inserted padding bytes (between qlimit and xmit_cnt, and between qtype and red[0]), plus the entire red[3] array when qtype != Q_RIO (i.e. for the default Q_DROPTAIL configuration), remain uninitialized. copyout(&stats, ubuf, sizeof(stats)) at line 312 then leaks up to ~176 bytes of stale kernel stack to userspace per DIOCGETQSTATS call.

Root cause

The struct layout on amd64 (computed from sys/net/altq/altq_fairq.h:68-79 and sys/net/altq/altq_red.h:50-57):

struct fairq_classstats {
    uint32_t         class_handle;   // off  0, size 4
    u_int            qlength;        // off  4, size 4
    u_int            qlimit;         // off  8, size 4
    /* 4 bytes compiler-inserted padding here (align pktcntr.uint64) */
    struct pktcntr   xmit_cnt;       // off 16, size 16
    struct pktcntr   drop_cnt;       // off 32, size 16
    int              qtype;          // off 48, size 4
    /* 4 bytes compiler-inserted padding here (align redstats) */
    struct redstats  red[3];         // off 56, size 168 (3 ร— ~56)
};                                   // total sizeof = 224

At sys/net/altq/altq_fairq.c:282, struct fairq_classstats stats; is declared on the kernel stack with no memset/initializer.

get_class_stats (altq_fairq.c:974-1001) writes: - sp->class_handle (978) - sp->qlimit (979) - sp->xmit_cnt (980) - sp->drop_cnt (981) - sp->qtype (982) - sp->qlength (983, then accumulated :988) - conditionally sp->red[0] via red_getstats (995) only if Q_RED - conditionally sp->red[0..2] via rio_getstats (999) only if Q_RIO

It never touches: - (a) 4 bytes of padding at struct offset 12-15 (between qlimit and xmit_cnt); - (b) 4 bytes of padding at struct offset 52-55 (between qtype and red[0]); - (c) when qtype == Q_DROPTAIL (the default), the entire red[3] array (168 bytes at offset 56-223); - (d) when qtype == Q_RED, red[1] and red[2] (112 bytes), plus 4 bytes of internal padding inside red[0] between q_avg and xmit_cnt (red_getstats at sys/net/altq/altq_red.c:252-260 does not fill that padding).

copyout((caddr_t)&stats, ubuf, sizeof(stats)) at altq_fairq.c:312 copies the full 224 bytes unconditionally, leaking all uninitialized regions.

The same pattern exists in sys/net/altq/altq_priq.c:priq_getqstats and sys/net/altq/altq_hfsc.c:hfsc_getqstats (identical stack-declared, partially-populated *_classstats struct).

Threat model & preconditions

  • Attacker position: privileged local user. /dev/pf is make_dev'd at sys/net/pf/pf_ioctl.c:3360 with mode 0600, UID root, GID wheel โ€” so this requires either genuine root or a process that has been granted /dev/pf access (e.g. a jail with /dev/pf delegated).
  • Privileges gained or impact: disclosure of up to ~176 bytes of stale kernel stack per DIOCGETQSTATS call when the class uses Q_DROPTAIL (the default). The leaked bytes may include kernel text/data pointers from prior call frames (useful for KASLR bypass), credential structure pointers, or other sensitive locals. No code execution.
  • Required config or capabilities: root or /dev/pf access. A fairq discipline with a class using Q_DROPTAIL (the default) configured.
  • Reachability: open("/dev/pf") โ†’ DIOCBEGINALTQS โ†’ DIOCADDALTQ (scheduler=ALTQT_FAIRQ) โ†’ DIOCADDALTQ (class with qname="def", default Q_DROPTAIL โ†’ no FARF_RED/FARF_RIO) โ†’ DIOCCOMMITALTQS โ†’ DIOCGETQSTATS with pq.buf=<heap buffer>, pq.nbytes=sizeof(struct fairq_classstats).

Proof of concept

PoC source: findings/poc/DF-0592/fairq_leak.c (sketch โ€” full driver to be materialized by the per-PoC verifier using <net/pf/pfvar.h> pf ioctls).

Build & run

cc -O2 -o fairq_leak fairq_leak.c
sudo ./fairq_leak em0       # must run as root or with /dev/pf access

Expected output

Hex dump of the returned struct fairq_classstats showing non-zero bytes at struct offsets 12-15, 52-55, and 56-223 (the uninitialized regions). Look for values in the kernel text/data range (e.g. 0xffffffff8xxxxxxx on amd64) โ€” those are stale kernel pointers recovered from the stack.

class_handle = 0x...
qlimit       = ...
qtype        = 0  (Q_DROPTAIL)
stack leak:
  off 12-15 : 0x <possibly kernel pointer fragment>
  off 52-55 : 0x <possibly kernel pointer fragment>
  off 56-223: 168 bytes, of which:
    <hex dump showing stale kernel stack>

Impact

  • Blast radius: any DragonFly system where a privileged process can open /dev/pf and configure a FAIRQ class. Realistic in VPN concentrators, routers, and jails with delegated /dev/pf. The same defect affects priq and hfsc disciplines (identical code pattern).
  • Severity rationale: Low. Deterministic and repeatable leak, but the attacker is already privileged (/dev/pf mode 0600 root:wheel). Primary impact is KASLR bypass and credential-pointer disclosure for an already-privileged process in a constrained environment. No code execution.
  • Reliability: 100% โ€” straight-line copyout of partially-initialized struct, no race.

Zero the stats struct before populating. Apply at the top of fairq_getqstats:

--- a/sys/net/altq/altq_fairq.c
+++ b/sys/net/altq/altq_fairq.c
@@ -279,6 +279,7 @@ fairq_getqstats(struct pf_altq *a, void *ubuf, int *nbytes)
    struct fairq_classstats stats;
    struct ifaltq *ifq;
    int error = 0;

+   memset(&stats, 0, sizeof(stats));
    if (*nbytes < sizeof(stats))
        return (EINVAL);

(Or initialize at declaration: struct fairq_classstats stats = {0};.)

The same fix should be applied to sys/net/altq/altq_priq.c:priq_getqstats and sys/net/altq/altq_hfsc.c:hfsc_getqstats, which have the identical pattern (stack-declared, partially-populated *_classstats struct).

References

  • The classic pattern: kernel code that copyouts a stack-declared struct partially filled by a helper. The historical fix in many BSD subsystems is memset/= {0} at declaration.
  • sys/net/pf/pf_ioctl.c:3360 โ€” /dev/pf make_dev with 0600 root:wheel (the privilege gate that bounds the impact here).

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).