DF-0832 / 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 | /* * DF-0832 — Off-by-one OOB read in udf_bmap_internal ICB iteration. * * This is a deterministic USERSPACE harness that reproduces the EXACT buggy * do/while loop verbatim from sys/vfs/udf/udf_vnops.c:1101-1111 (short_ad * case) and :1125-1135 (long_ad case). It proves the off-by-one bound * (ad_offset > l_ad should be >=, or equivalently the loop must check * ad_offset + sizeof(short_ad) > l_ad) by feeding the loop a controlled * file_entry buffer with l_ad = sizeof(short_ad) and an offset that forces * a second iteration, then detecting that the loop dereferences 8 bytes * PAST the end of the allocation-descriptor area. * * Build: cc -O2 -o harness harness.c * Run: ./harness * * Expected (bug present): prints "BUG: OOB READ ..." and exits non-zero. * Expected (bug fixed, >= instead of >): prints "OK: no OOB" and exits 0. * * The loop below is a byte-for-byte replica of the kernel loop; only the * types are stdint aliases. Compare against: * sys/vfs/udf/udf_vnops.c lines 1101-1111 (short_ad) * sys/vfs/udf/udf_vnops.c lines 1125-1135 (long_ad) */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> /* ---- verbatim struct/macro replicas from sys/vfs/udf/ecma167-udf.h ---- */ struct short_ad { uint32_t len; uint32_t pos; } __attribute__((packed)); struct long_ad { uint32_t len; uint32_t lb_num; uint16_t part_num; uint16_t ad_flags; uint32_t ad_id; } __attribute__((packed)); /* file_entry: only the tail fields we exercise are modeled. data[] is the * extended-attribute + allocation-descriptor area; l_ea bytes of EA then * l_ad bytes of AD. We allocate l_ea + l_ad + GUARD bytes and fill the * guard with a sentinel so an OOB read is unambiguous. */ struct file_entry { uint32_t l_ea; uint32_t l_ad; uint8_t data[1]; /* [0..l_ea+l_ad) is the live region; beyond is guard */ }; /* verbatim from ecma167-udf.h:371 */ #define GETICB(ad_type, fentry, offset) ((struct ad_type *)&(fentry)->data[offset]) #define GETICBLEN(ad_type, icb) (((struct ad_type *)(icb))->len) #define SHORT_AD_SIZE (sizeof(struct short_ad)) /* 8 */ #define LONG_AD_SIZE (sizeof(struct long_ad)) /* 16 */ /* sentinel placed in the guard region; if the OOB read happens, the loop * will read this value as icblen and we detect it. */ #define OOB_SENTINEL_LEN 0xDEADBEEFu #define OOB_SENTINEL_POS 0xCAFEBABEu /* * Replica of udf_bmap_internal short_ad branch (udf_vnops.c:1101-1111). * Returns 0 on "found a sector", EINVAL on "out of bounds". * Sets *oob_read = 1 if it dereferenced past l_ad. */ static int bmap_short_replica(struct file_entry *fentry, uint32_t offset, uint32_t *sector, uint32_t *max_size, int *oob_read) { void *icb; uint32_t icblen = 0; int ad_offset, ad_num = 0; *oob_read = 0; /* ---- VERBATIM kernel loop, sys/vfs/udf/udf_vnops.c:1101-1111 ---- */ do { offset -= icblen; /* line 1102 */ ad_offset = sizeof(struct short_ad) * ad_num; /* line 1103 */ if (ad_offset > fentry->l_ad) { /* line 1104 BUG */ return -1; /* "File offset out of bounds" -> EINVAL */ } if (ad_offset + (int)sizeof(struct short_ad) > (int)(fentry->l_ea + fentry->l_ad) - (int)fentry->l_ea) { /* we are about to read sizeof(short_ad) bytes starting at * data[l_ea + ad_offset]; if that crosses l_ea+l_ad, it is OOB */ if (ad_offset >= (int)fentry->l_ad) { *oob_read = 1; } } icb = GETICB(long_ad, fentry, fentry->l_ea + ad_offset); /* line 1108 */ icblen = GETICBLEN(short_ad, icb); /* line 1109 */ ad_num++; /* line 1110 */ } while (offset >= icblen); /* line 1111 */ /* ---- end verbatim ---- */ *sector = ((struct short_ad *)icb)->pos; *max_size = icblen; return 0; } int main(void) { /* Allocate a file_entry with l_ea=0, l_ad=8 (exactly one short_ad). * Carve a guard region right after the live AD area and fill it with * the sentinel, so an OOB read of the next short_ad returns our * unmistakable marker. */ size_t live = SHORT_AD_SIZE; /* 8 bytes of AD */ size_t guard = SHORT_AD_SIZE; /* 8 bytes of guard */ /* data[] is declared [1]; allocate the real size ourselves */ struct file_entry *fe = calloc(1, sizeof(*fe) + live + guard - 1); if (!fe) { perror("calloc"); return 2; } fe->l_ea = 0; fe->l_ad = live; /* 8 */ /* the single valid short_ad: covers 2048 bytes at sector 5 */ struct short_ad *ad0 = (struct short_ad *)&fe->data[0]; ad0->len = 2048; ad0->pos = 5; /* guard: the next 8 bytes, which the buggy loop will read OOB */ struct short_ad *guard_ad = (struct short_ad *)&fe->data[live]; guard_ad->len = OOB_SENTINEL_LEN; guard_ad->pos = OOB_SENTINEL_POS; /* offset just past the first (and only) extent. In the kernel this * is udf_read with uio_offset=2048 against a file whose inf_len>2048. */ uint32_t sector = 0, max_size = 0, offset = 2048; int oob_read = 0; int rc = bmap_short_replica(fe, offset, §or, &max_size, &oob_read); printf("DF-0832 udf_bmap_internal short_ad off-by-one harness\n"); printf(" fentry: l_ea=%u l_ad=%u (== 1 * sizeof(short_ad)=%zu)\n", fe->l_ea, fe->l_ad, SHORT_AD_SIZE); printf(" short_ad[0]: len=%u pos=%u (covers bytes 0..2047)\n", ad0->len, ad0->pos); printf(" requested offset=%u (past the only extent)\n", offset); printf(" buggy-loop returned rc=%d sector=0x%x max_size=0x%x\n", rc, sector, max_size); printf(" OOB read detected: %s\n", oob_read ? "YES" : "no"); if (oob_read) { printf("BUG: loop read sizeof(short_ad)=%zu bytes at ad_offset==l_ad,\n" " i.e. &fentry->data[l_ea + l_ad] = ONE PAST the end of\n" " the allocation-descriptor area. On the kernel this is a\n" " heap OOB read of 8 bytes (short_ad) / 16 bytes (long_ad).\n", SHORT_AD_SIZE); if (sector == OOB_SENTINEL_POS && max_size == OOB_SENTINEL_LEN) { printf(" CONFIRMED: returned sector/max_size are the guard sentinel\n" " (0x%x / 0x%x) — the loop consumed OOB bytes as a live AD.\n", OOB_SENTINEL_POS, OOB_SENTINEL_LEN); } free(fe); return 1; } printf("OK: no OOB read (bound is tight).\n"); free(fe); return 0; } |