DF-3536 / ngcisco7_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 | /* * DF-3536 -- netgraph7 ng_cisco: disconnecting the downstream hook with the * 10s keepalive ng_callout armed leaks the timer ng_item and its * node reference (ng_cisco twin of DF-3531 / ng_tty). * * Root cause: * cisco_newhook("downstream") arms the keepalive timer: * sys/netgraph7/cisco/ng_cisco.c:219-220 * ng_callout(&sc->handle, node, NULL, hz*KEEPALIVE_SECS, * &cisco_keepalive, (void *)sc, 0); * ng_callout() (netgraph7/netgraph/ng_base.c:3248-3268) allocates a * struct ng_item carrying one NG_NODE_REF (ng_base.c:3257) and stores it * as the callout argument. * cisco_disconnect() cancels it: * sys/netgraph7/cisco/ng_cisco.c:418 * ng_uncallout(&sc->handle, NG_HOOK_NODE(hook)); * ng_uncallout() (ng_base.c:3273-3296) frees the item only under * `if ((rval > 0) && ...)`. On this kernel callout_stop() of an ARMED, * not-yet-fired callout completes the cancel inline but returns 0 * (DF-3515 kern_timeout contract violation: _callout_cancel_or_stop() * early `return 0`, sys/kern/kern_timeout.c:886-890), so the gate never * passes: the ng_item (with its NG_NODE_REF) is orphaned forever. * The leaked reference pins the ng_node in the ID hash and pins the * module: kldunload ng_cisco stays "Device busy"; repeating the cycle * grows the M_NETGRAPH zone without bound. * * Cycle (leak mode): * 1. ng7 control socket (its node is auto-created) * 2. NGM_MKPEER type="cisco" ourhook="hN" peerhook="downstream" * -> cisco_newhook -> ng_callout ARMED * 3. NGM_RMHOOK "hN" * -> cisco_disconnect -> ng_uncallout -> item + node ref LEAKED * 4. close socket * * Control mode ("inet" arg): identical except peerhook="inet" * (no callout is armed for non-downstream hooks) -> allocation count must * stay flat. The ONLY difference between leak and control runs is the * armed-then-cancelled ng_callout. * * Build: cc -O2 -o ngcisco7_leak ngcisco7_leak.c * Run: ./ngcisco7_leak <cycles> [inet] (measure: vmstat -m | grep netgraph) */ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> /* ---- ng7 netgraph user ABI (values from sys/netgraph7/ng_message.h) ---- */ #define AF_NG 32 #define NG_CONTROL 2 #define NG_VERSION 8 #define NGM_GENERIC_COOKIE 1137070366 #define NGM_MKPEER 2 #define NGM_RMHOOK 5 #define NG_TYPESIZ 32 #define NG_HOOKSIZ 32 struct ngm_hdr_ { u_char version; u_char spare; u_short spare2; u_int arglen; u_int cmd; u_int flags; u_int token; u_int typecookie; char cmdstr[32]; }; struct ng_mesg_ { struct ngm_hdr_ header; char data[1024]; }; struct ngm_mkpeer_ { char type[NG_TYPESIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; }; struct ngm_rmhook_ { char ourhook[NG_HOOKSIZ]; }; struct sockaddr_ng_ { u_char sg_len; u_char sg_family; /* sa_family_t is __uint8_t on DragonFly */ char sg_data[64]; }; static u_int32_t tok = 0x7000; static int ng_send_generic(int cs, u_int cmd, const void *arg, size_t arglen) { struct ng_mesg_ m; struct sockaddr_ng_ sa; ssize_t n; memset(&m, 0, sizeof(m)); m.header.version = NG_VERSION; m.header.arglen = arglen; m.header.cmd = cmd; m.header.token = ++tok; m.header.typecookie = NGM_GENERIC_COOKIE; if (arglen) memcpy(m.data, arg, arglen); memset(&sa, 0, sizeof(sa)); sa.sg_family = AF_NG; sa.sg_data[0] = '\0'; /* own node */ sa.sg_len = 2 + 1; n = sendto(cs, &m, sizeof(m.header) + arglen, 0, (struct sockaddr *)&sa, sa.sg_len); if (n < 0) return (-1); /* MKPEER/RMHOOK have no reply; drain any stray async data */ (void)recv(cs, &m, sizeof(m), MSG_DONTWAIT); return (0); } int main(int argc, char **argv) { long cycles = (argc > 1) ? atol(argv[1]) : 2000; int inet_mode = (argc > 2 && strcmp(argv[2], "inet") == 0); const char *peerhook = inet_mode ? "inet" : "downstream"; long i; for (i = 0; i < cycles; i++) { int cs; struct ngm_mkpeer_ mk; struct ngm_rmhook_ rm; char hook[NG_HOOKSIZ]; if ((cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL)) < 0) { perror("socket(AF_NETGRAPH, NG_CONTROL)"); return (1); } /* mkpeer cisco downstream (or inet for the control run) */ memset(&mk, 0, sizeof(mk)); snprintf(mk.type, sizeof(mk.type), "cisco"); snprintf(mk.ourhook, sizeof(mk.ourhook), "h%ld", i); snprintf(mk.peerhook, sizeof(mk.peerhook), "%s", peerhook); if (ng_send_generic(cs, NGM_MKPEER, &mk, sizeof(mk)) < 0) { fprintf(stderr, "cycle %ld: mkpeer: %s\n", i, strerror(errno)); close(cs); return (1); } /* immediately tear the hook down with the timer armed */ memset(&rm, 0, sizeof(rm)); snprintf(rm.ourhook, sizeof(rm.ourhook), "h%ld", i); if (ng_send_generic(cs, NGM_RMHOOK, &rm, sizeof(rm)) < 0) { fprintf(stderr, "cycle %ld: rmhook: %s\n", i, strerror(errno)); close(cs); return (1); } close(cs); if ((i % 500) == 499) { fprintf(stderr, "... %ld rounds (%s)\n", i + 1, inet_mode ? "inet/control" : "downstream/leak"); } } fprintf(stderr, "done: %ld rounds (%s)\n", cycles, inet_mode ? "inet/control" : "downstream/leak"); return (0); } |