DF-3161 / umount_fifoleak_scmrights.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 | /* * DF-3161 PoC (deterministic variant): umount -f force-close loop never * passes FREAD -> fifo_close() cannot decrement fi_readers -> the * fifoinfo + connected AF_LOCAL socket pair are leaked permanently. * * Making the force-close path actually run requires an "open" fifo that no * process owns, so the umount retry loop's SIGINT/SIGKILL protocol * (sys/kern/vfs_syscalls.c:929-946) cannot close it and the retries * exhaust (50 x 0.25s); the mount is then ripped with the fifo still * open and vclean_vxlocked(DOCLOSE) runs its force-close loop * (sys/kern/vfs_subr.c:1341-1352), which never passes FREAD. * * Trick: send the fifo fd over a unix-domain socket (SCM_RIGHTS) and never * receive it - the fp (and its namecache ref onto the mount) is pinned by * the socket buffer, owned by no process. * * Must run as root. Expected: netstat -f unix stream-socket count stays +2 * above baseline after umount -f + socket close (leak); a control fifo * closed normally returns to baseline. */ #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/uio.h> #include <sys/wait.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static void count_sockets(const char *tag) { char cmd[128]; FILE *f; int c; snprintf(cmd, sizeof(cmd), "netstat -f unix 2>/dev/null | awk '$2==\"stream\"{n++}END{print n+0}'"); f = popen(cmd, "r"); if (!f || fscanf(f, "%d", &c) != 1) c = -1; if (f) pclose(f); printf("%s: %d stream sockets\n", tag, c); } int main(void) { const char *fifo = "/mnt/df3161b/f"; const char *mp = "/mnt/df3161b"; struct msghdr msg; struct cmsghdr *cmsg; struct iovec iov; char buf[CMSG_SPACE(sizeof(int))], dbuf[1]; int sv[2], fd, n; if (getuid() != 0) { fprintf(stderr, "must be root\n"); exit(2); } count_sockets("baseline"); if (mkdir(mp, 0755) < 0 && errno != EEXIST) { perror("mkdir"); exit(2); } if (system("mount -t tmpfs tmpfs /mnt/df3161b") != 0) { fprintf(stderr, "mount failed\n"); exit(2); } unlink(fifo); if (mkfifo(fifo, 0600) < 0) { perror("mkfifo"); exit(2); } /* * O_RDWR: fi_readers=1, fi_writers=1, v_writecount=1. * After the force-close loop, fi_writers will be decremented * (FWRITE|FNONBLOCK pass) but fi_readers never is (no FREAD bit * ever passed) -> fip leaks. */ fd = open(fifo, O_RDWR); if (fd < 0) { perror("open fifo"); exit(2); } count_sockets("fifo-open (+2 expected)"); /* hide the fd inside a unix socket buffer */ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); exit(2); } memset(&msg, 0, sizeof(msg)); iov.iov_base = dbuf; iov.iov_len = 1; dbuf[0] = 'x'; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); if (sendmsg(sv[0], &msg, 0) < 0) { perror("sendmsg"); exit(2); } close(fd); /* fp now pinned only by the socket buffer */ printf("fifo fd hidden in unix socket buffer; nobody owns it\n"); /* umount -f: 50 retries * 0.25s = ~12.5s, then mount is ripped */ printf("umount -f %s (takes ~13s while retries exhaust)...\n", mp); n = system("umount -f /mnt/df3161b"); printf("umount rc=%d\n", WEXITSTATUS(n)); /* drop the socket holding the fp */ close(sv[0]); close(sv[1]); sleep(2); count_sockets("after umount -f + close (leak: stays +2)"); if (system("umount /mnt/df3161b 2>/dev/null") != 0) ; /* already unmounted */ printf("done\n"); return (0); } |