DF-0797 / 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 | /* * harness.c - Deterministic DF-0797 primitive proof. * * Transcribes the EXACT vulnerable lines from * sys/vfs/hammer/hammer_ondisk.c:210 volume->vol_no = ondisk->vol_no * sys/vfs/hammer/hammer_ondisk.c:234 hammer_volume_number_add(hmp, volume) * sys/vfs/hammer/hammer.h:1579-1584 hammer_volume_number_add() inline * sys/vfs/hammer/hammer.h:1567-1571 __hammer_vol_index() inline * sys/vfs/hammer/hammer.h:1573-1577 __hammer_vol_low() inline * * struct hammer_mount carries `uint64_t volume_map[4]` as its LAST field * (hammer.h:875). __hammer_vol_index does `vol_no >> 6` with NO mask, so any * vol_no outside 0..255 indexes past volume_map[]: * * vol_no = 0x100 (256) i = 4 -> 8 bytes past volume_map (small OOB) * vol_no = 0x140 (320) i = 5 -> 16 bytes past volume_map * vol_no = 0x7FFFFFFF i = 0x1FFFFFF -> ~256 MiB past (fatal) * * The write is `volume_map[i] |= __hammer_vol_low(vol_no)` -- a SINGLE-BIT OR * at byte offset (i*8 + bitpos). Bit position within the qword is * `vol_no & 0x3F`, so for vol_no = 256+64*K the OR sets bit K*8 (within the * K-th qword past volume_map). This is an attacker-controllable-offset * single-bit heap write on production kernels. * * This harness reproduces BOTH variants against a poisoned allocation so the * OOB extent is observable WITHOUT needing to corrupt a live kernel. * * On GENERIC (INVARIANTS ON): the live trigger (crafted image mount) panics * immediately at the page fault from the unmapped write (for vol_no=0x7FFFFFFF) * OR silently corrupts adjacent heap (for vol_no=256). See panic.txt for the * captured fatal trap. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <stddef.h> /* ---- HAMMER constants (hammer.h / hammer_disk.h) ---- */ #define HAMMER_MAX_VOLUMES 256 #define VOLUME_MAP_QWORDS 4 #define HAMMER_MOUNT_SIZE 16384 /* generous model: hammer_mount is huge */ /* ---- verbatim transcription of hammer.h:1567-1598 ---- */ static __inline int __hammer_vol_index(int vol_no) { return(vol_no >> 6); } static __inline uint64_t __hammer_vol_low(int vol_no) { return((uint64_t)1 << (vol_no & ((1 << 6) - 1))); } struct hammer_mount { uint8_t body[HAMMER_MOUNT_SIZE - sizeof(uint64_t) * VOLUME_MAP_QWORDS]; uint64_t volume_map[VOLUME_MAP_QWORDS]; /* last field -- hammer.h:875 */ }; static void hammer_volume_number_add(struct hammer_mount *hmp, int vol_no) { int i = __hammer_vol_index(vol_no); /* In the real kernel this is the OOB write: * hmp->volume_map[i] |= __hammer_vol_low(vol_no); * We emulate the write into a wider "heap" so we can SEE the OOB byte. */ extern uint64_t *g_heap; extern int g_heap_qwords; if (i >= 0 && i < g_heap_qwords) { uint64_t bit = __hammer_vol_low(vol_no); printf(" volume_map[%d] |= 0x%016llx", i, (unsigned long long)bit); if (i >= VOLUME_MAP_QWORDS) { ptrdiff_t off = (i - VOLUME_MAP_QWORDS) * 8; printf(" *** OOB +%ld bytes past volume_map (bit %d of qword +%d)", (long)off, vol_no & 0x3F, i - VOLUME_MAP_QWORDS); } printf("\n"); g_heap[i] |= bit; } else if (i >= g_heap_qwords) { printf(" volume_map[%d] |= ... *** FATAL: +%d MiB past volume_map " "-> unmapped page fault in kernel (panic)\n", i, (i * 8) / (1024 * 1024)); } } /* poisoned heap model: volume_map[] sits inside a bigger heap region */ uint64_t *g_heap; int g_heap_qwords; struct hammer_mount *g_hmp; static void test_vol_no(const char *tag, int vol_no) { int i = __hammer_vol_index(vol_no); printf("---- %s: vol_no=%d (0x%x) -> __hammer_vol_index=%d ----\n", tag, vol_no, (unsigned)vol_no, i); if (i >= 0 && i < VOLUME_MAP_QWORDS) { printf(" IN-BOUNDS (volume_map[%d] valid)\n", i); } else if (i >= VOLUME_MAP_QWORDS && (size_t)i * 8 < (size_t)g_heap_qwords * 8) { printf(" OUT-OF-BOUNDS by %ld bytes (single-bit OR heap write)\n", (long)(i - VOLUME_MAP_QWORDS) * 8); } else { printf(" FAR-OOB (~%d MiB past volume_map) -> kernel page fault / panic\n", (i * 8) / (1024 * 1024)); } /* emulate the vulnerable call path: * hammer_ondisk.c:210 volume->vol_no = ondisk->vol_no; * hammer_ondisk.c:234 hammer_volume_number_add(hmp, volume); */ hammer_volume_number_add(g_hmp, vol_no); printf("\n"); } int main(void) { /* model hammer_mount embedded in a much larger heap region so we can * observe small OOB writes land in "adjacent heap". */ g_heap_qwords = 8192; /* 64 KiB heap model */ g_heap = (uint64_t *)calloc(g_heap_qwords, sizeof(uint64_t)); g_hmp = (struct hammer_mount *)g_heap; /* volume_map at end of struct */ if (!g_heap) { perror("calloc"); return 1; } /* poison: the qwords AFTER volume_map represent adjacent kernel heap */ for (int q = VOLUME_MAP_QWORDS; q < g_heap_qwords; q++) g_heap[q] = 0xDEADBEEFCAFEBABEULL; printf("==== DF-0797 deterministic OOB-write primitive proof ====\n"); printf("struct hammer_mount { ...; uint64_t volume_map[4]; } (hammer.h:875)\n"); printf("__hammer_vol_index(v) = v >> 6 (hammer.h:1568 -- NO mask)\n"); printf("__hammer_vol_low(v) = 1 << (v & 63)\n"); printf("volume_map valid qwords: 0..3 (covers vol_no 0..255)\n"); printf("hammer_mount model size = %d bytes; heap model = %d qwords\n\n", HAMMER_MOUNT_SIZE, g_heap_qwords); /* (1) baseline: legitimate vol_no=0 -> volume_map[0] (in-bounds) */ test_vol_no("baseline (legitimate root volume)", 0); /* (2) controllable-offset OOB: vol_no = 256 -> i=4 -> 8 bytes past */ test_vol_no("smallest OOB (vol_no=256)", 0x100); /* (3) controllable-offset OOB: vol_no = 256+64*4 = 512 -> i=8 -> 32 bytes past */ test_vol_no("mid OOB (vol_no=0x200, bit 6 of qword +4)", 0x200); /* (4) the panic trigger: vol_no = 0x7FFFFFFF -> i = 0x1FFFFFF -> ~256 MiB past */ test_vol_no("panic trigger (vol_no=0x7FFFFFFF)", 0x7FFFFFFF); /* (5) negative vol_no: vol_no = -1 = 0xFFFFFFFF -> i = 0x3FFFFFF -> ~512 MiB */ test_vol_no("negative vol_no (-1)", (int)0xFFFFFFFF); /* show the OOB byte pattern that the small OOB writes left */ printf("---- resulting heap state: qwords 0..7 (volume_map is [0..3]) ----\n"); for (int q = 0; q < 8; q++) { const char *tag = (q < VOLUME_MAP_QWORDS) ? "volume_map" : "ADJACENT HEAP"; printf(" %s[%d] = 0x%016llx", tag, q, (unsigned long long)g_heap[q]); if (q >= VOLUME_MAP_QWORDS && g_heap[q] != 0xDEADBEEFCAFEBABEULL) printf(" *** MODIFIED by OOB OR-write ***"); printf("\n"); } /* Summary of the controllable-offset primitive */ printf("\n==== controllable-offset primitive summary ====\n"); printf("vol_no = 256 + 64*K + N (N=0..63, K=0..) => OOB OR-write of bit N\n"); printf(" at qword (4+K), i.e. byte offset (K*8 + N/8) past volume_map end.\n"); printf("Both the qword offset AND the bit-within-qword are attacker-controlled.\n"); printf("On production (INVARIANTS-OFF): single-bit OR at attacker-controlled\n"); printf(" byte offset in adjacent kernel heap -- a constrained heap-write primitive.\n"); printf("On GENERIC (INVARIANTS-ON): mount-time; small OOB may corrupt heap\n"); printf(" silently, far-OOB (vol_no=0x7FFFFFFF) panics at the unmapped write.\n"); free(g_heap); return 0; } |