etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

gen_corpus.py (24312B)

      1 #!/usr/bin/env python3
      2 """
      3 gen_corpus.py — generate structured USB/IP protocol seed corpus.
      4 
      5 Produces 80+ binary seeds in ./corpus/, covering:
      6   1. op_common_valid       — well-formed protocol headers
      7   2. op_common_malformed   — bad version / unknown op code
      8   3. devlist_replies       — device-list reply bodies (ndev variations)
      9   4. import_requests       — busid field edge cases
     10   5. urb_submit            — CVE-2016-3955 class (length field variations)
     11   6. urb_unlink            — unlink command edge cases
     12   7. raw_boundary          — size boundary seeds (empty, 1 byte, max)
     13 """
     14 
     15 import os
     16 import struct
     17 import itertools
     18 import hashlib
     19 
     20 OUT = os.path.join(os.path.dirname(__file__), "corpus")
     21 os.makedirs(OUT, exist_ok=True)
     22 
     23 written = 0
     24 
     25 def seed(name: str, data: bytes):
     26     global written
     27     safe = name.replace("/", "_").replace("\x00", "")
     28     path = os.path.join(OUT, safe)
     29     with open(path, "wb") as f:
     30         f.write(data)
     31     written += 1
     32 
     33 
     34 # ── constants ─────────────────────────────────────────────────────────────
     35 
     36 USBIP_VERSION   = 0x0111
     37 OP_REQ_DEVLIST  = 0x8005
     38 OP_REP_DEVLIST  = 0x0005
     39 OP_REQ_IMPORT   = 0x8003
     40 OP_REP_IMPORT   = 0x0003
     41 
     42 CMD_SUBMIT      = 0x00000001
     43 CMD_UNLINK      = 0x00000002
     44 RET_SUBMIT      = 0x00000003
     45 RET_UNLINK      = 0x00000004
     46 
     47 STATUS_OK       = 0x00000000
     48 STATUS_ERROR    = 0x00000001
     49 
     50 SYSFS_BUS_ID    = 32    # bytes
     51 
     52 def op_common(code: int, status: int = STATUS_OK, version: int = USBIP_VERSION) -> bytes:
     53     return struct.pack(">HHI", version, code, status)
     54 
     55 def usbip_usb_device() -> bytes:
     56     """Minimal valid usbip_usb_device struct (312 bytes, all fields safe)."""
     57     path       = b"/sys/devices/pci0000:00/0000:00:01.2/usb1/1-1\x00"
     58     path      += b"\x00" * (256 - len(path))
     59     busid      = b"1-1\x00" + b"\x00" * (SYSFS_BUS_ID - 4)
     60     busnum     = struct.pack(">I", 1)
     61     devnum     = struct.pack(">I", 1)
     62     speed      = struct.pack(">I", 3)       # USB_SPEED_HIGH
     63     idvendor   = struct.pack(">H", 0x1234)
     64     idproduct  = struct.pack(">H", 0x5678)
     65     bcddevice  = struct.pack(">H", 0x0100)
     66     devclass   = struct.pack(">B", 0)
     67     subclass   = struct.pack(">B", 0)
     68     protocol   = struct.pack(">B", 0)
     69     cfg        = struct.pack(">B", 1)
     70     bNumConfs  = struct.pack(">B", 1)
     71     bNumIfaces = struct.pack(">B", 1)
     72     return (path + busid + busnum + devnum + speed +
     73             idvendor + idproduct + bcddevice +
     74             devclass + subclass + protocol +
     75             cfg + bNumConfs + bNumIfaces)
     76 
     77 def usbip_usb_interface() -> bytes:
     78     """Minimal usbip_usb_interface struct."""
     79     return struct.pack(">BBBB", 0, 0, 0, 0)
     80 
     81 
     82 # ══════════════════════════════════════════════════════════════════════════════
     83 # 1. op_common valid
     84 # ══════════════════════════════════════════════════════════════════════════════
     85 
     86 seed("op_common_req_devlist",   op_common(OP_REQ_DEVLIST))
     87 seed("op_common_rep_devlist",   op_common(OP_REP_DEVLIST))
     88 seed("op_common_req_import",    op_common(OP_REQ_IMPORT))
     89 seed("op_common_rep_import",    op_common(OP_REP_IMPORT))
     90 seed("op_common_status_error",  op_common(OP_REP_DEVLIST, STATUS_ERROR))
     91 
     92 # With payload after the header (exercises remainder of the stream)
     93 seed("op_common_devlist_ndev0",
     94      op_common(OP_REP_DEVLIST) + struct.pack(">I", 0))
     95 seed("op_common_devlist_ndev1",
     96      op_common(OP_REP_DEVLIST) + struct.pack(">I", 1) + usbip_usb_device() + usbip_usb_interface())
     97 seed("op_common_import_with_dev",
     98      op_common(OP_REP_IMPORT) + struct.pack(">I", STATUS_OK) + usbip_usb_device())
     99 
    100 
    101 # ══════════════════════════════════════════════════════════════════════════════
    102 # 2. op_common malformed
    103 # ══════════════════════════════════════════════════════════════════════════════
    104 
    105 seed("op_bad_version_0000",     struct.pack(">HHHI", 0x0000, OP_REQ_DEVLIST, STATUS_OK, 0))
    106 seed("op_bad_version_ffff",     struct.pack(">HHHI", 0xFFFF, OP_REQ_DEVLIST, STATUS_OK, 0))
    107 seed("op_bad_version_0100",     struct.pack(">HHHI", 0x0100, OP_REQ_DEVLIST, STATUS_OK, 0))
    108 seed("op_bad_version_0200",     struct.pack(">HHHI", 0x0200, OP_REQ_DEVLIST, STATUS_OK, 0))
    109 seed("op_unknown_code_0001",    op_common(0x0001))
    110 seed("op_unknown_code_0000",    op_common(0x0000))
    111 seed("op_unknown_code_ffff",    op_common(0xFFFF))
    112 seed("op_unknown_code_7fff",    op_common(0x7FFF))
    113 seed("op_unknown_code_8000",    op_common(0x8000))
    114 seed("op_status_max",           op_common(OP_REQ_DEVLIST, 0xFFFFFFFF))
    115 seed("op_status_neg1",          struct.pack(">HHi", USBIP_VERSION, OP_REQ_DEVLIST, -1))
    116 seed("op_all_zeros",            b"\x00" * 8)
    117 seed("op_all_ff",               b"\xff" * 8)
    118 seed("op_just_version",         struct.pack(">H", USBIP_VERSION))  # truncated
    119 seed("op_7_bytes",              op_common(OP_REQ_DEVLIST)[:7])     # one byte short
    120 
    121 
    122 # ══════════════════════════════════════════════════════════════════════════════
    123 # 3. devlist reply bodies
    124 # ══════════════════════════════════════════════════════════════════════════════
    125 
    126 for ndev in [0, 1, 2, 4, 8, 16, 32, 64, 255, 256, 65535, 0xFFFFFFFF]:
    127     name = f"devlist_ndev_{ndev}"
    128     body = struct.pack(">I", ndev)
    129     if ndev <= 2:
    130         for _ in range(min(ndev, 2)):
    131             body += usbip_usb_device() + usbip_usb_interface()
    132     seed(name, body)
    133 
    134 # Device with multiple interfaces
    135 multi_iface = usbip_usb_device()
    136 # Patch bNumInterfaces to 3 (last byte of the struct)
    137 multi_iface = multi_iface[:-1] + b"\x03"
    138 multi_body = struct.pack(">I", 1) + multi_iface
    139 for _ in range(3):
    140     multi_body += usbip_usb_interface()
    141 seed("devlist_multi_iface", multi_body)
    142 
    143 # Truncated device struct
    144 seed("devlist_truncated_dev", struct.pack(">I", 1) + usbip_usb_device()[:64])
    145 
    146 # Device struct with all-ff fields
    147 seed("devlist_dev_all_ff", struct.pack(">I", 1) + b"\xff" * len(usbip_usb_device()))
    148 
    149 
    150 # ══════════════════════════════════════════════════════════════════════════════
    151 # 4. import requests / replies (busid edge cases)
    152 # ══════════════════════════════════════════════════════════════════════════════
    153 
    154 def import_input(busid: bytes, extra: bytes = b"") -> bytes:
    155     """Pack busid to exactly 32 bytes (no forced NUL — let fuzzer explore)."""
    156     bid = busid[:SYSFS_BUS_ID]
    157     bid = bid + b"\x00" * (SYSFS_BUS_ID - len(bid))
    158     return bid + extra
    159 
    160 seed("import_valid_busid",       import_input(b"1-1"))
    161 seed("import_valid_busid_2_1",   import_input(b"2-1"))
    162 seed("import_no_null",           import_input(b"A" * SYSFS_BUS_ID))
    163 seed("import_path_traversal",    import_input(b"../../../etc/passwd"))
    164 seed("import_path_traversal2",   import_input(b"../../proc/self/mem"))
    165 seed("import_format_string",     import_input(b"%s%s%s%s%n%n"))
    166 seed("import_null_bytes",        import_input(b"\x00\x00\x00\x00"))
    167 seed("import_unicode_like",      import_input(b"\xc0\xaf\xc0\xaf\xc0\xaf"))
    168 seed("import_long_busid",        import_input(b"1" * SYSFS_BUS_ID))
    169 seed("import_busid_with_reply",  import_input(b"1-1") + struct.pack(">I", STATUS_OK) + usbip_usb_device())
    170 seed("import_reply_status_err",  import_input(b"1-1") + struct.pack(">I", STATUS_ERROR))
    171 seed("import_reply_truncated",   import_input(b"1-1") + struct.pack(">I", STATUS_OK) + usbip_usb_device()[:32])
    172 seed("import_dev_all_ff",        import_input(b"1-1") + struct.pack(">I", STATUS_OK) + b"\xff" * len(usbip_usb_device()))
    173 seed("import_empty_busid",       import_input(b""))
    174 seed("import_slash_busid",       import_input(b"///" * 10))
    175 seed("import_colon_busid",       import_input(b"0:0"))
    176 seed("import_large_devnum",      import_input(b"255-255"))
    177 seed("import_symbolic_link",     import_input(b"1-1 -> /etc/passwd\x00"))
    178 
    179 
    180 # ══════════════════════════════════════════════════════════════════════════════
    181 # 5. URB submit (CVE-2016-3955 class)
    182 # ══════════════════════════════════════════════════════════════════════════════
    183 
    184 def urb_header(command, seqnum=1, devid=0x10001, direction=0, ep=0,
    185                transfer_flags=0, transfer_buffer_length=0, start_frame=0,
    186                number_of_packets=0, interval=0, setup=b"\x00"*8) -> bytes:
    187     base = struct.pack(">IIIII", command, seqnum, devid, direction, ep)
    188     submit = struct.pack(">IiiiI", transfer_flags, transfer_buffer_length,
    189                          start_frame, number_of_packets, interval)
    190     return base + submit + setup[:8]
    191 
    192 # Normal submit
    193 seed("urb_submit_empty",        urb_header(CMD_SUBMIT, transfer_buffer_length=0))
    194 seed("urb_submit_64b",          urb_header(CMD_SUBMIT, transfer_buffer_length=64) + b"\xaa"*64)
    195 seed("urb_submit_512b",         urb_header(CMD_SUBMIT, transfer_buffer_length=512) + b"\xbb"*512)
    196 
    197 # Length mismatches — CVE-2016-3955 pattern
    198 seed("urb_ret_actual_gt_alloc", urb_header(RET_SUBMIT, transfer_buffer_length=64) + b"\xcc"*512)
    199 seed("urb_ret_len_neg1",        urb_header(RET_SUBMIT, transfer_buffer_length=-1))
    200 seed("urb_ret_len_intmin",      urb_header(RET_SUBMIT, transfer_buffer_length=-2147483648))
    201 seed("urb_ret_len_max",         urb_header(RET_SUBMIT, transfer_buffer_length=65536))
    202 seed("urb_ret_len_overflow",    urb_header(RET_SUBMIT, transfer_buffer_length=0x7FFFFFFF))
    203 
    204 # number_of_packets edge cases (ISO transfers)
    205 # stub_rx.c: kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
    206 # iso_descriptor is 16 bytes; overflow threshold: n > 0x0fffffff
    207 seed("urb_iso_packets_0",           urb_header(CMD_SUBMIT, number_of_packets=0))
    208 seed("urb_iso_packets_max",         urb_header(CMD_SUBMIT, number_of_packets=0x7FFFFFFF))
    209 seed("urb_iso_packets_neg",         urb_header(CMD_SUBMIT, number_of_packets=-1))
    210 seed("urb_iso_wrap_u32",            urb_header(CMD_SUBMIT, number_of_packets=0x10000000))   # n*16 == 0
    211 seed("urb_iso_wrap_u32p1",          urb_header(CMD_SUBMIT, number_of_packets=0x10000001))   # n*16 == 16
    212 seed("urb_iso_wrap_s32",            urb_header(CMD_SUBMIT, number_of_packets=0x08000000))   # n*16 wraps s32
    213 
    214 # transfer_buffer_length near overflow boundaries
    215 seed("urb_tbl_uint_max",            urb_header(CMD_SUBMIT, transfer_buffer_length=0x7FFFFFFF))
    216 seed("urb_tbl_wrap_s32",            urb_header(CMD_SUBMIT, transfer_buffer_length=-2147483648))
    217 
    218 # Invalid endpoint / direction
    219 seed("urb_ep_15",               urb_header(CMD_SUBMIT, ep=15))
    220 seed("urb_ep_overflow",         urb_header(CMD_SUBMIT, ep=0xFFFFFFFF))
    221 seed("urb_dir_in",              urb_header(CMD_SUBMIT, direction=1))
    222 seed("urb_dir_bad",             urb_header(CMD_SUBMIT, direction=0xFFFFFFFF))
    223 
    224 # setup packet edge cases (control transfers)
    225 seed("urb_setup_all_ff",        urb_header(CMD_SUBMIT, setup=b"\xff"*8))
    226 seed("urb_setup_req_clear",     urb_header(CMD_SUBMIT, setup=bytes([0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00])))
    227 seed("urb_setup_get_descriptor",urb_header(CMD_SUBMIT, setup=bytes([0x80,0x06,0x00,0x01,0x00,0x00,0xFF,0x00])))
    228 
    229 
    230 # ══════════════════════════════════════════════════════════════════════════════
    231 # 6. URB unlink
    232 # ══════════════════════════════════════════════════════════════════════════════
    233 
    234 def urb_unlink(seqnum=1, devid=0x10001, direction=0, ep=0, unlink_seqnum=0) -> bytes:
    235     base    = struct.pack(">IIIII", CMD_UNLINK, seqnum, devid, direction, ep)
    236     payload = struct.pack(">I", unlink_seqnum) + b"\x00" * 24  # padding to match size
    237     return base + payload
    238 
    239 seed("urb_unlink_seq0",         urb_unlink(unlink_seqnum=0))
    240 seed("urb_unlink_seq1",         urb_unlink(seqnum=1, unlink_seqnum=1))
    241 seed("urb_unlink_seq_max",      urb_unlink(unlink_seqnum=0xFFFFFFFF))
    242 seed("urb_unlink_seq_neg",      struct.pack(">IiIII", CMD_UNLINK, -1, 0, 0, 0) + b"\x00"*24)
    243 seed("urb_ret_unlink_ok",       urb_header(RET_UNLINK))
    244 seed("urb_all_zeros",           b"\x00" * 48)
    245 seed("urb_all_ff",              b"\xff" * 48)
    246 seed("urb_truncated",           b"\x00" * 20)  # just basic header, no cmd_submit
    247 
    248 
    249 # ══════════════════════════════════════════════════════════════════════════════
    250 # 7. vhci-hcd server-side seeds (for fuzz_vhci_server — two-sided fuzzer)
    251 #
    252 # Each seed is the 28-byte ret_submit union that fuzz_vhci_server overlays onto
    253 # USBIP_RET_SUBMIT responses.  Fields: status(4) actual_length(4) start_frame(4)
    254 # number_of_packets(4) error_count(4) + 8 bytes padding.
    255 # ══════════════════════════════════════════════════════════════════════════════
    256 
    257 def ret_submit_fields(status=0, actual_length=0, start_frame=0,
    258                       number_of_packets=0, error_count=0) -> bytes:
    259     return struct.pack(">iiiii", status, actual_length, start_frame,
    260                        number_of_packets, error_count) + b"\x00" * 8
    261 
    262 INT_MIN = -0x80000000
    263 INT_MAX =  0x7fffffff
    264 
    265 # Normal / status variants
    266 seed("vhci_ret_ok",                    ret_submit_fields(status=0, actual_length=64))
    267 seed("vhci_ret_status_error",          ret_submit_fields(status=-22))   # -EINVAL
    268 seed("vhci_ret_status_epipe",          ret_submit_fields(status=-32))   # -EPIPE
    269 seed("vhci_ret_status_enodev",         ret_submit_fields(status=-19))   # -ENODEV
    270 
    271 # CVE-2016-3955 class: actual_length > transfer_buffer_length
    272 # fuzz_vhci_server will send this as the server's response to any CMD_SUBMIT
    273 seed("vhci_actual_len_overflow",       ret_submit_fields(actual_length=INT_MAX))
    274 seed("vhci_actual_len_intmin",         ret_submit_fields(actual_length=INT_MIN))
    275 seed("vhci_actual_len_neg1",           ret_submit_fields(actual_length=-1))
    276 seed("vhci_actual_len_64k",           ret_submit_fields(actual_length=65536))
    277 seed("vhci_actual_len_64k_plus1",     ret_submit_fields(actual_length=65537))
    278 seed("vhci_actual_len_zero",          ret_submit_fields(actual_length=0))
    279 
    280 # ISO path: number_of_packets > 0 triggers usbip_recv_iso code path
    281 seed("vhci_iso_one_pkt",              ret_submit_fields(actual_length=16, number_of_packets=1))
    282 seed("vhci_iso_many_pkts",            ret_submit_fields(actual_length=0,  number_of_packets=255))
    283 seed("vhci_iso_max_pkts",             ret_submit_fields(number_of_packets=INT_MAX))
    284 seed("vhci_iso_neg_pkts",             ret_submit_fields(number_of_packets=-1))
    285 seed("vhci_iso_overflow",             ret_submit_fields(actual_length=INT_MAX, number_of_packets=INT_MAX))
    286 
    287 # error_count / start_frame edge cases
    288 seed("vhci_start_frame_intmin",       ret_submit_fields(start_frame=INT_MIN))
    289 seed("vhci_error_count_max",          ret_submit_fields(error_count=INT_MAX))
    290 seed("vhci_all_ff",                   b"\xff" * 28)
    291 seed("vhci_all_zeros",                b"\x00" * 28)
    292 
    293 # Integer overflow in kmalloc size calculation:
    294 #   usbip_recv_iso: kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
    295 #   sizeof(usbip_iso_packet_descriptor) == 16 bytes on kernel side
    296 #   Overflow threshold: n * 16 wraps u32 when n > 0x0fffffff (268435455)
    297 #   These values cause the allocated buffer to be smaller than the data being received.
    298 ISO_DESC_SIZE = 16
    299 WRAP_U32 = (1 << 32) // ISO_DESC_SIZE       # 0x10000000 — exactly wraps to 0
    300 WRAP_U32_PLUS1 = WRAP_U32 + 1               # 0x10000001 — wraps to 16 bytes allocated
    301 WRAP_S32 = (1 << 31) // ISO_DESC_SIZE       # 0x08000000 — wraps signed to negative
    302 # WRAP_U32=0x10000000: n*16 == 0x100000000 which wraps to 0 in u32 → kmalloc(0)
    303 # WRAP_U32_PLUS1:      n*16 == 0x100000010 which wraps to 16 → 16-byte alloc but n*16 bytes used
    304 seed("vhci_iso_wrap_u32",     ret_submit_fields(number_of_packets=WRAP_U32,      actual_length=64))
    305 seed("vhci_iso_wrap_u32p1",   ret_submit_fields(number_of_packets=WRAP_U32_PLUS1, actual_length=64))
    306 seed("vhci_iso_wrap_s32",     ret_submit_fields(number_of_packets=WRAP_S32,      actual_length=64))
    307 
    308 # Compound: both actual_length overflow AND iso multiplication overflow
    309 seed("vhci_double_overflow",  ret_submit_fields(actual_length=INT_MAX, number_of_packets=WRAP_U32_PLUS1))
    310 
    311 # actual_length near UINT_MAX (unsigned interpretation — some code paths treat it unsigned)
    312 seed("vhci_actual_uint_max",  struct.pack(">iIiii", 0, 0xFFFFFFFF, 0, 0, 0) + b"\x00" * 8)
    313 
    314 # Borderline: actual_length == transfer_buffer_length (exactly equal — off-by-one test)
    315 # fuzz_vhci_server always sends the response regardless of the original request's tbl,
    316 # but these seed values help AFL++ discover the boundary condition faster.
    317 for tbl in [0, 1, 63, 64, 127, 128, 255, 256, 4095, 4096]:
    318     seed(f"vhci_actual_eq_tbl_{tbl}", ret_submit_fields(actual_length=tbl))
    319 
    320 # ── ISO overflow: targeted values matching the three unpatched kernel bugs ──────
    321 #
    322 # usbip_recv_iso() bug: int size = np * sizeof(*iso)  where sizeof(*iso)==16
    323 #   np=0x08000001 → 0x08000001*16 = 0x80000010 → overflows int32 → -2147483632
    324 #   np=0x10000001 → 0x10000001*16 = 0x100000010 → truncated uint32 = 0x10 = 16
    325 #   np=0x7FFFFFFF/16+1 = 0x08000000 → edge case at INT_MAX/16
    326 #
    327 # With fuzz_vhci_server's ISO descriptor mirroring, the server sends exactly
    328 # the overflowed number of bytes and the kernel's loop reads OOB → KASAN hit.
    329 #
    330 # Seeds named with explicit overflow semantics for clarity in AFL++ crash reports.
    331 seed("vhci_iso_np_overflow_08",
    332      ret_submit_fields(actual_length=64, number_of_packets=0x08000001))
    333 seed("vhci_iso_np_overflow_10",
    334      ret_submit_fields(actual_length=64, number_of_packets=0x10000001))
    335 seed("vhci_iso_np_overflow_7f",
    336      ret_submit_fields(actual_length=64, number_of_packets=0x7FFFFFFF // 16 + 1))
    337 
    338 # usbip_pad_iso() bug: actualoffset underflow → memmove with negative offset
    339 # Trigger: ISO packet with actual_length > accumulated offset in the packet array.
    340 # We send actual_length=0 per ISO packet but a large total actual_length header,
    341 # forcing pad_iso to compute a negative offset for memmove().
    342 # Represent as: number_of_packets=8 (valid), actual_length=INT_MAX (large total)
    343 seed("vhci_iso_pad_underflow",
    344      ret_submit_fields(actual_length=INT_MAX, number_of_packets=8))
    345 
    346 # Per-packet ISO descriptor corruption seeds → usbip_pad_iso() memmove underflow.
    347 #
    348 # struct usbip_iso_packet_descriptor layout (16 bytes each):
    349 #   offset(4BE) actual_length(4BE) status(4BE) padding(4BE)
    350 #
    351 # When the per-packet actual_length inside the ISO descriptor array is corrupted
    352 # to a huge value, usbip_pad_iso() computes a negative actualoffset for its
    353 # memmove(), causing a kernel heap underwrite.  These seeds exercise that path
    354 # directly — prior seeds only corrupted the header-level actual_length field.
    355 for _np in [1, 2, 4, 8]:
    356     for _bad in [0x7FFFFFFF, 0x80000000, 0xFFFFFFFF]:
    357         _iso_desc = struct.pack(">II", 0, _bad) + b"\x00" * 8  # 16 bytes
    358         seed(f"vhci_iso_pkt_corrupt_np{_np}_{hex(_bad)}",
    359              ret_submit_fields(actual_length=0, number_of_packets=_np) + _iso_desc * _np)
    360 
    361 # Stub-side sgl_alloc() boundary seeds (for fuzz_stub_client / corpus/stub/).
    362 #
    363 # stub_recv_cmd_submit() calls sgl_alloc(transfer_buffer_length) with no upper
    364 # bound check.  Values at and above SG_MAX_SINGLE_ALLOC (128 pages = 512 KB on
    365 # x86) exercise the fallback scatter-gather allocation path.  IN transfers
    366 # (direction=1) trigger the alloc without needing the client to send payload.
    367 for _tbl in [0x80000, 0x1000000, 0x40000000, 0x7FFFFFFF, -1]:
    368     seed(f"stub_tbl_in_{hex(_tbl & 0xFFFFFFFF)}",
    369          urb_header(CMD_SUBMIT, direction=1, transfer_buffer_length=_tbl))
    370 
    371 # OUT direction: stub also allocates for the receive path, so exercise that too.
    372 for _tbl in [0x80000, 0x1000000]:
    373     seed(f"stub_tbl_out_{hex(_tbl)}",
    374          urb_header(CMD_SUBMIT, direction=0, transfer_buffer_length=_tbl))
    375 
    376 
    377 # ══════════════════════════════════════════════════════════════════════════════
    378 # 8. Raw boundary seeds
    379 # ══════════════════════════════════════════════════════════════════════════════
    380 
    381 seed("raw_empty",               b"")
    382 seed("raw_1byte",               b"\x01")
    383 seed("raw_7bytes",              b"\x01\x11\x80\x05\x00\x00\x00")
    384 seed("raw_8bytes_op_common",    op_common(OP_REQ_DEVLIST))
    385 seed("raw_max_size",            b"\x01\x11\x80\x05\x00\x00\x00\x00" + b"\xde\xad\xbe\xef" * (65536 // 4))
    386 
    387 
    388 # ══════════════════════════════════════════════════════════════════════════════
    389 # Per-harness subdirs
    390 #
    391 # run-fuzzers.sh points each AFL++ instance at a subdirectory that contains
    392 # only the seeds meaningful for that harness.  This keeps calibration fast and
    393 # map density high from the start.  Cross-pollination between instances still
    394 # happens through the shared output/ directory.
    395 # ══════════════════════════════════════════════════════════════════════════════
    396 
    397 import shutil
    398 
    399 SUBDIRS = {
    400     "protocol": lambda n, _: n.startswith("op_common") or n.startswith("raw_8bytes"),
    401     "devlist":  lambda n, _: "devlist" in n,
    402     "import":   lambda n, _: "import" in n,
    403     "urb":      lambda n, _: "urb" in n or "cmd_" in n or "ret_" in n,
    404     "vhci":     lambda n, _: n.startswith("vhci_"),
    405     # stub seeds: CMD_SUBMIT headers for fuzz_stub_client
    406     # Uses URB submit seeds as the base (same 48-byte CMD_SUBMIT format)
    407     "stub":     lambda n, _: "urb_submit" in n or "urb_iso" in n or "urb_tbl" in n
    408                              or "urb_ep" in n or "urb_dir" in n or "urb_setup" in n
    409                              or "urb_all" in n or "urb_ret_len" in n
    410                              or n.startswith("stub_tbl_"),
    411 }
    412 
    413 for subdir, predicate in SUBDIRS.items():
    414     dest = os.path.join(OUT, subdir)
    415     os.makedirs(dest, exist_ok=True)
    416     count = 0
    417     for fname in os.listdir(OUT):
    418         src = os.path.join(OUT, fname)
    419         if os.path.isfile(src) and predicate(fname, src):
    420             shutil.copy2(src, os.path.join(dest, fname))
    421             count += 1
    422     print(f"[+]   corpus/{subdir}/: {count} seeds")
    423 
    424 print(f"[+] Generated {written} seeds in {OUT}/")