Files
alktty/fuzz/gen_fuzz_seeds.py
T
glm-5.3-flash 49f70d9efb feat(fuzz): cargo-fuzz workspace, 3 wire-surface targets; bump alkcall 0.8.1
- Bump alkcall 0.8.0 -> 0.8.1 (duplicate adopt/open channel-state
  destruction fix from alkcall's fuzz campaign; AGENTS.md stale-pin fix).
- Adopt fuzzing per docs/plans/fuzzing.md (mirrors alkcall's
  docs/research/fuzzing.md as-built layout): fuzz/ workspace with own
  [workspace] table, nightly pinned for the subtree only, invariant
  logic in a stable-toolchain shared crate.
- Targets: chunk_frame (5-byte chunk codec), negotiation_frame
  (negotiation framing + NegotiateRequest + error_response_bytes +
  the cross-codec peek-disambiguation seam), control_json
  (ControlMessage JSON + signal_from_name). src/local/ out of scope
  (not wire-attacker-shaped).
- 328 committed seeds (deterministic generator); grown corpora
  gitignored. Corpus replay on stable is the standing fuzz gate
  (cargo test --manifest-path fuzz/shared/Cargo.toml).
- Detached-runner rule (fuzz/run-detached.sh): campaigns never run in
  the foreground of an agent session - OOM in a target must cost the
  fuzzer, never the session host.
- Root Cargo.toml gained [workspace] members/exclude (MSRV vs fuzz
  nightly dev-deps footgun) and fuzz/ in the publish exclude.

Verification: cargo test (113), cargo test --all-features (156),
clippy stable + wasm32-unknown-unknown, fmt, doc, publish dry-run,
corpus replay 328 seeds - all green. 10-min detached campaigns on all
three targets: 0 crashes/hangs/OOMs/leaks (chunk_frame cov-saturated
at 710 edges; negotiation_frame 3.69M execs cov 3157; control_json
8.6M execs cov 2035). Corpus replay caught three harness-model
mismatches pre-campaign (payload-slice shape; the negotiation framing
layer is length-prefix-only - Json unreachable from read_frame;
zero-length frames admitted by the reader, rejected by the adapter's
parse). Campaigns recorded in docs/plans/fuzzing.md section 8.
2026-09-28 08:07:30 +00:00

252 lines
8.1 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
def main():
write_seeds("chunk_frame", chunk_frame_seeds())
write_seeds("negotiation_frame", negotiation_frame_seeds())
write_seeds("control_json", control_json_seeds())
print("seeds written")
if __name__ == "__main__":
main()