Target 4 from the fuzzing plan (the alkcall-bug-class hunt): a #[derive(Arbitrary)] op sequence drives the producer session pump (drive_session_pre_negotiated) over a duplex pair against a fuzz-local mock backend. - fuzz/shared/src/session_opseq.rs: op vocabulary (client writes incl. invalid-type/oversize-length barrier probes, backend production/EOF, exit resolve/fail/drop, control messages, bounded reads, yields), kill-guard exit future (ADR-005 observability), delivery model, and the invariants: kill-on-Drop (kill_fired == !exit_resolved), exit chunk is last with first-issued code -1 on wait-failure (ADR-004), lossless per-stream FIFO + single stdout sentinel, stdin prefix-losslessness (post-shutdown chunks never delivered), control dispatch bounds, take-once allocation, teardown termination. - Harness-model correction caught by a 60s smoke run before the campaign (the alkcall §7.7 pattern, third time): a failed client write can mean the session completed and dropped the server duplex half while the write was in flight - the harness now drains and requires the exit chunk on any write/shutdown error (premature close without it is a finding). No adapter code changed. - 12 committed seeds via the deterministic generator, hand-encoded against the arbitrary 1.4.x derive layout; the encoding is pinned by a seed-decode test. Existing chunk_frame/negotiation_frame/ control_json seed corpora are byte-identical to before. - fuzz/.gitignore: the per-target seed negations never matched (corpus/* excludes the parent dir; git cannot re-include beneath an excluded dir). Fixed with the !corpus/*/ + corpus/*/* + !corpus/*/seed-* recipe - new seed files were silently ignored until now. - Campaign: 10 min detached via run-detached.sh, exited 0, artifact dir empty - 42.3k execs across 33 fork jobs, 0 crash/oom/timeout, cov 3934 -> 4075 edges. Grown corpus excised per the corpus policy. Verification: corpus replay green on stable (9 tests), cargo test 113 + --all-features 156, clippy stable/wasm/fuzz-shared, fmt, doc, publish dry-run, wasm check - all pass.
519 lines
13 KiB
Python
519 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Regenerate the committed seed corpora for alktty's fuzz targets.
|
|
|
|
Writes into fuzz/corpus/<target>/. Deterministic: fixed inputs only, no
|
|
randomness. Run from the repo root:
|
|
|
|
python3 fuzz/gen_fuzz_seeds.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import struct
|
|
|
|
MAX_CHUNK_LEN = 16 * 1024 * 1024
|
|
|
|
STREAM_TYPES = [0, 1, 2, 3, 4, 5, 255]
|
|
|
|
CHUNK_PAYLOADS = [
|
|
b"",
|
|
b"x",
|
|
b"hello chunk",
|
|
b"\x00\x01\x02\x03",
|
|
b'{"type":"eof"}',
|
|
b'{"type":"resize","cols":80,"rows":24}',
|
|
b'{"type":"signal","name":"INT"}',
|
|
b'{"type":"exit","code":0}',
|
|
b'{"type":"exit","code":-1}',
|
|
b'{"type":"unknown-control"}',
|
|
b'{"type":',
|
|
b"\xff\xfe invalid utf8 \xb0",
|
|
b"y" * 4096,
|
|
b'{"type":"resize","cols":65535,"rows":0,"pixel_width":1,"pixel_height":2}',
|
|
b'{"type":"signal","name":"NOT_A_SIGNAL"}',
|
|
b"{}}",
|
|
b"[]",
|
|
b"null",
|
|
b"\x00" * 64,
|
|
]
|
|
|
|
CONTROL_SAMPLES = [
|
|
b'{"type":"resize","cols":80,"rows":24}',
|
|
b'{"type":"resize","cols":0,"rows":65535,"pixel_width":800,"pixel_height":600}',
|
|
b'{"type":"resize"}',
|
|
b'{"type":"resize","cols":-1,"rows":24}',
|
|
b'{"type":"resize","cols":70000,"rows":24}',
|
|
b'{"type":"resize","cols":"80","rows":24}',
|
|
b'{"type":"signal","name":"TERM"}',
|
|
b'{"type":"signal","name":"HUP"}',
|
|
b'{"type":"signal","name":"KILL"}',
|
|
b'{"type":"signal","name":"USR1"}',
|
|
b'{"type":"signal","name":"USR2"}',
|
|
b'{"type":"signal","name":"TSTP"}',
|
|
b'{"type":"signal","name":"CONT"}',
|
|
b'{"type":"signal","name":"QUIT"}',
|
|
b'{"type":"signal","name":"int"}',
|
|
b'{"type":"signal","name":""}',
|
|
b'{"type":"signal"}',
|
|
b'{"type":"signal","name":123}',
|
|
b'{"type":"eof"}',
|
|
b'{"type":"EOF"}',
|
|
b'{"type":"exit","code":0}',
|
|
b'{"type":"exit","code":-9}',
|
|
b'{"type":"exit"}',
|
|
b'{"type":"exit","code":null}',
|
|
b'{"type":"exit","code":"0"}',
|
|
b'{"type":"bogus"}',
|
|
b'{"type":123}',
|
|
b'{}',
|
|
b'{"type":"eof","extra":[1,2,3]}',
|
|
b'{"type":"resize","cols":80,"rows":24,"type":"signal","name":"KILL"}',
|
|
b'{"type":"resize","cols":80,"rows":24',
|
|
b'',
|
|
b'{',
|
|
b'null',
|
|
b'"resize"',
|
|
b'[]',
|
|
b'{"type":"resize","cols":80,"rows":24}}',
|
|
b'\xff\xfe',
|
|
b'{"type":"\xc3\xa9of"}',
|
|
]
|
|
|
|
NEGOTIATION_SAMPLES = [
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/bash","-l"]}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/bash","-l"],'
|
|
b'"tty":{"term":"xterm-256color","cols":80,"rows":24,"pixel_width":0,'
|
|
b'"pixel_height":0,"modes":{}}}',
|
|
b'{"carriage":"raw","backend":"docker","cmd":["sh"],"container":"abc123",'
|
|
b'"image":"ubuntu:22.04"}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["sh"],"cwd":"/tmp",'
|
|
b'"env":{"FOO":"bar","BAZ":"qux"}}',
|
|
b'{"carriage":"pipe","backend":"local","cmd":["cat"]}',
|
|
b'{"carriage":"","backend":"","cmd":[]}',
|
|
b'{"carriage":"raw","backend":"docker","cmd":["bash"],"container":123}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"tty":{"cols":-1,"rows":-1}}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"tty":{"cols":99999,"rows":0}}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"tty":null,"cwd":null,"env":null}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"tty":{"modes":{"deep":{"nested":[1,2,3,{"x":None}]}}}}'.replace(b"None", b"null"),
|
|
b'{"carriage":"raw"}',
|
|
b'{"backend":"local"}',
|
|
b'{"cmd":["sh"]}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"env":{"KEY":"\\u00e9value"},"cwd":"/w\\u00f6rld"}',
|
|
b'{"error":"malformed_negotiation"}',
|
|
b'{"error":"unknown_backend","backend":"docker"}',
|
|
b'{"error":"allocate_failed","reason":"spawn failed"}',
|
|
b'{"error":"spoofed","error2":"real"}',
|
|
b'{"error":"malformed_negotiation","detail":"carriage must be raw"}',
|
|
b'{"error":123}',
|
|
b'{"error":null}',
|
|
b'{"error":"x","nest":{"a":{"b":{"c":[1,{"d":None}]}}}}'.replace(b"None", b"null"),
|
|
b'{"error":"' + b"z" * 4096 + b'"}',
|
|
b'{"error":"\\ud83d\\ude00"}',
|
|
b'\xff\xfe{"error":"bad"}',
|
|
b'{"error":"trailing"} trailing',
|
|
b'{"error":"unicode \\u0000 control"}',
|
|
b'{"type":"resize"}',
|
|
b'{"carriage":"raw","backend":"local","cmd":["/bin/sh"],'
|
|
b'"tty":{"cols":80,"rows":24},"cmd_extra":["ignored"]}',
|
|
]
|
|
|
|
ERROR_RESPONSE_CASES = [
|
|
("malformed_negotiation", []),
|
|
("unknown_backend", [("backend", "docker")]),
|
|
("allocate_failed", [("backend", "local"), ("reason", "spawn failed")]),
|
|
("spoof", [("error", "ignored"), ("detail", "x")]),
|
|
]
|
|
|
|
|
|
def chunk_header(stream_type, length):
|
|
return bytes([stream_type]) + struct.pack(">I", length)
|
|
|
|
|
|
def write_seeds(target, seeds):
|
|
corpus_dir = os.path.join(os.path.dirname(__file__), "corpus", target)
|
|
os.makedirs(corpus_dir, exist_ok=True)
|
|
for name, blob in seeds:
|
|
with open(os.path.join(corpus_dir, name), "wb") as f:
|
|
f.write(blob)
|
|
|
|
|
|
def chunk_frame_seeds():
|
|
seeds = []
|
|
n = 0
|
|
|
|
def add(blob):
|
|
nonlocal n
|
|
seeds.append((f"seed-{n:03d}", blob))
|
|
n += 1
|
|
|
|
add(b"")
|
|
for st in STREAM_TYPES:
|
|
add(bytes([st]))
|
|
add(bytes([st]) + b"\x00")
|
|
for payload in CHUNK_PAYLOADS:
|
|
add(chunk_header(st, len(payload)) + payload)
|
|
add(chunk_header(0, 1))
|
|
add(chunk_header(1, 1) + b"ab")
|
|
add(chunk_header(4, 16) + b"short")
|
|
add(chunk_header(1, 0) + b"trailing-after-sentinel")
|
|
add(chunk_header(2, 0) + chunk_header(1, 3) + b"abc")
|
|
add(chunk_header(0, MAX_CHUNK_LEN + 1))
|
|
add(chunk_header(1, MAX_CHUNK_LEN + 1))
|
|
add(chunk_header(5, MAX_CHUNK_LEN + 1))
|
|
add(chunk_header(255, MAX_CHUNK_LEN + 1))
|
|
add(chunk_header(0, 0xFFFFFFFF))
|
|
add(chunk_header(1, 0xFFFFFFFF))
|
|
add(chunk_header(4, 0xFFFFFFFF))
|
|
add(chunk_header(3, MAX_CHUNK_LEN))
|
|
add(chunk_header(1, MAX_CHUNK_LEN))
|
|
add(b"\x01\x01\x00\x00\x00")
|
|
add(b"\x02\x00\x00\x00\x00" + b"")
|
|
add(b"\x04" + b'{"type":"exit","code":0}' + b"")
|
|
truncated = chunk_header(1, 64) + b"partial"
|
|
for i in range(1, len(truncated)):
|
|
add(truncated[:i])
|
|
valid = chunk_header(1, 5) + b"hello" + chunk_header(2, 3) + b"err"
|
|
for i in range(1, min(len(valid), 12)):
|
|
add(valid[:i])
|
|
peekable = chunk_header(1, 4) + b"data"
|
|
for lead in (b"", b"\x01", b"\x00"):
|
|
add(lead + peekable)
|
|
add(chunk_header(1, 0) + chunk_header(1, 0) + chunk_header(4, 2) + b"{}")
|
|
return seeds
|
|
|
|
|
|
def negotiation_frame_seeds():
|
|
seeds = []
|
|
n = 0
|
|
|
|
def add(blob):
|
|
nonlocal n
|
|
seeds.append((f"seed-{n:03d}", blob))
|
|
n += 1
|
|
|
|
add(b"")
|
|
add(b"\x00")
|
|
add(b"\x00\x00")
|
|
add(b"\x00\x00\x00")
|
|
for body in NEGOTIATION_SAMPLES:
|
|
add(struct.pack(">I", len(body)) + body)
|
|
add(struct.pack(">I", len(body)) + body[: len(body) // 2])
|
|
add(struct.pack(">I", 0))
|
|
add(struct.pack(">I", 1) + b"{}")
|
|
add(struct.pack(">I", MAX_CHUNK_LEN + 1)[:4])
|
|
add(struct.pack(">I", 0xFFFFFFFF))
|
|
add(struct.pack(">I", 0xFFFFFFFF) + b"x" * 4)
|
|
add(struct.pack(">I", MAX_CHUNK_LEN))
|
|
add(struct.pack(">I", MAX_CHUNK_LEN - 1) + b"x" * 16)
|
|
valid = struct.pack(">I", len(b'{"error":"x"}')) + b'{"error":"x"}'
|
|
for i in range(1, len(valid)):
|
|
add(valid[:i])
|
|
return seeds
|
|
|
|
|
|
def control_json_seeds():
|
|
seeds = []
|
|
n = 0
|
|
|
|
def add(blob):
|
|
nonlocal n
|
|
seeds.append((f"seed-{n:03d}", blob))
|
|
n += 1
|
|
|
|
for blob in CONTROL_SAMPLES:
|
|
add(blob)
|
|
add(b'{"type":"resize","cols":80,"rows":24' + b" " * 8)
|
|
add(b' {"type":"eof"} ')
|
|
add(b'{"type":"resize","cols":1e2,"rows":24}')
|
|
add(b'{"type":"exit","code":1e400}')
|
|
add(b'{"type":"exit","code":2147483648}')
|
|
add(b'{"type":"exit","code":-2147483649}')
|
|
add(b'{"type":"signal","name":"SIG' + b"T" * 64 + b'"}')
|
|
add(b'{"type":"resize","cols":80,"rows":24,"pixel_width":-5}')
|
|
add(b'{"type":["resize"],"cols":80}')
|
|
add(b'{"TYPE":"eof"}')
|
|
add(b'{"type":"resize","cols":80,"rows":24,"type":"resize"}')
|
|
return seeds
|
|
|
|
|
|
# session_opseq seeds are arbitrary-encoded `SessionSequence` inputs, so
|
|
# they are hand-encoded against the `arbitrary` 1.4.x derive layout the
|
|
# same way alkcall's manager_routing seeds are: each element of a Vec is
|
|
# gated by a keep-going byte (odd = another element follows; the final
|
|
# gated read before data exhaustion yields true via zero-fill), enum
|
|
# variant tags are 4-byte LE u32s with `(tag * 12) >> 32` selecting by
|
|
# declaration order, and fields follow in declaration order
|
|
# little-endian, zero-filled on short data. The encoding is pinned by
|
|
# the seed-decode test in fuzz/shared/src/session_opseq.rs.
|
|
SESSION_OP_VARIANTS = [
|
|
"ClientWrite",
|
|
"ClientCloseWrite",
|
|
"Stdout",
|
|
"StdoutEof",
|
|
"Stderr",
|
|
"StderrEof",
|
|
"Exit",
|
|
"ExitFail",
|
|
"DropExitTx",
|
|
"CtrlIn",
|
|
"ClientReadAll",
|
|
"Yield",
|
|
]
|
|
|
|
|
|
def _le32(v):
|
|
return struct.pack("<I", v & 0xFFFFFFFF)
|
|
|
|
|
|
class OpSeqEncoder:
|
|
def __init__(self, stderr_enabled):
|
|
self.buf = bytearray()
|
|
self.buf.append(1 if stderr_enabled else 0)
|
|
|
|
def op(self, name):
|
|
idx = SESSION_OP_VARIANTS.index(name)
|
|
self.buf += _le32((idx << 32) // 12)
|
|
|
|
def u8(self, v):
|
|
self.buf.append((v if isinstance(v, int) else v[0]) & 0xFF)
|
|
|
|
def u16(self, v):
|
|
self.buf += struct.pack("<H", v & 0xFFFF)
|
|
|
|
def i16(self, v):
|
|
self.buf += struct.pack("<h", v)
|
|
|
|
def end(self):
|
|
self.buf.append(0)
|
|
return bytes(self.buf)
|
|
|
|
|
|
def session_opseq_seeds():
|
|
seeds = []
|
|
n = 0
|
|
|
|
def add(blob):
|
|
nonlocal n
|
|
seeds.append((f"seed-{n:03d}", blob))
|
|
n += 1
|
|
|
|
add(
|
|
bytes(
|
|
[
|
|
0x00, 0x01, 0xAB, 0xAA, 0xAA, 0x2A, 0x10, 0x00, 0x61, 0x01,
|
|
0xAB, 0xAA, 0xAA, 0xEA, 0x04,
|
|
]
|
|
)
|
|
)
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("Stdout")
|
|
e.u16(32)
|
|
e.u8(b"a")
|
|
e.op("Stderr")
|
|
e.u16(8)
|
|
e.u8(b"e")
|
|
e.op("Yield")
|
|
e.u8(8)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(3)
|
|
e.u8(b"x")
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(0)
|
|
e.u8(0)
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(1)
|
|
e.u8(b"y")
|
|
e.op("CtrlIn")
|
|
e.u8(2)
|
|
e.u16(0)
|
|
e.u16(0)
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("Exit")
|
|
e.i16(3)
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("Exit")
|
|
e.i16(7)
|
|
e.op("Exit")
|
|
e.i16(43)
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("Stderr")
|
|
e.u16(4)
|
|
e.u8(b"e")
|
|
e.op("StderrEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=False)
|
|
e.op("ExitFail")
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("ClientWrite")
|
|
e.u8(200)
|
|
e.u16(0)
|
|
e.u8(0)
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("ClientWrite")
|
|
e.u8(5)
|
|
e.u16(0)
|
|
e.u8(0)
|
|
e.op("StdoutEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
e.op("Exit")
|
|
e.i16(-1)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=False)
|
|
for cols, rows in [(80, 24), (0, 0), (65535, 65535), (120, 40)]:
|
|
e.op("CtrlIn")
|
|
e.u8(0)
|
|
e.u16(cols)
|
|
e.u16(rows)
|
|
e.op("CtrlIn")
|
|
e.u8(1)
|
|
e.u16(0)
|
|
e.u16(0)
|
|
e.op("ClientReadAll")
|
|
e.u8(4)
|
|
e.op("Exit")
|
|
e.i16(0)
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=False)
|
|
e.op("CtrlIn")
|
|
e.u8(3)
|
|
e.u16(99)
|
|
e.u16(0)
|
|
e.op("CtrlIn")
|
|
e.u8(4)
|
|
e.u16(0)
|
|
e.u16(0)
|
|
e.op("ClientWrite")
|
|
e.u8(4)
|
|
e.u16(2)
|
|
e.u8(b"z")
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("Exit")
|
|
e.i16(5)
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(2)
|
|
e.u8(b"w")
|
|
e.op("ClientCloseWrite")
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("Stderr")
|
|
e.u16(4)
|
|
e.u8(b"e")
|
|
e.op("StderrEof")
|
|
e.op("Exit")
|
|
e.i16(0)
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=False)
|
|
e.op("DropExitTx")
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=True)
|
|
e.op("Stderr")
|
|
e.u16(4)
|
|
e.u8(b"e")
|
|
e.op("StderrEof")
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("Exit")
|
|
e.i16(9)
|
|
e.op("StdoutEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
e = OpSeqEncoder(stderr_enabled=False)
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(2)
|
|
e.u8(b"a")
|
|
e.op("ClientWrite")
|
|
e.u8(1)
|
|
e.u16(2)
|
|
e.u8(b"b")
|
|
e.op("ClientWrite")
|
|
e.u8(0)
|
|
e.u16(2)
|
|
e.u8(b"c")
|
|
e.op("Exit")
|
|
e.i16(1)
|
|
e.op("Stdout")
|
|
e.u16(4)
|
|
e.u8(b"o")
|
|
e.op("StdoutEof")
|
|
e.op("ClientReadAll")
|
|
e.u8(63)
|
|
add(e.end())
|
|
|
|
return seeds
|
|
|
|
|
|
def main():
|
|
write_seeds("chunk_frame", chunk_frame_seeds())
|
|
write_seeds("negotiation_frame", negotiation_frame_seeds())
|
|
write_seeds("control_json", control_json_seeds())
|
|
write_seeds("session_opseq", session_opseq_seeds())
|
|
print("seeds written")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |