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/pfismake_dev'd at sys/net/pf/pf_ioctl.c:3360 with mode0600, UID root, GID wheel โ so this requires either genuine root or a process that has been granted/dev/pfaccess (e.g. a jail with/dev/pfdelegated). - Privileges gained or impact: disclosure of up to ~176 bytes of stale
kernel stack per
DIOCGETQSTATScall when the class usesQ_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/pfaccess. A fairq discipline with a class usingQ_DROPTAIL(the default) configured. - Reachability:
open("/dev/pf")โDIOCBEGINALTQSโDIOCADDALTQ(scheduler=ALTQT_FAIRQ) โDIOCADDALTQ(class withqname="def", defaultQ_DROPTAILโ noFARF_RED/FARF_RIO) โDIOCCOMMITALTQSโDIOCGETQSTATSwithpq.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/pfand configure a FAIRQ class. Realistic in VPN concentrators, routers, and jails with delegated/dev/pf. The same defect affectspriqandhfscdisciplines (identical code pattern). - Severity rationale: Low. Deterministic and repeatable leak, but the
attacker is already privileged (
/dev/pfmode 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.
Recommended fix
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 ismemset/= {0}at declaration. sys/net/pf/pf_ioctl.c:3360โ/dev/pfmake_dev with0600root: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).