fix(channels): duplicate adopt/open must not destroy the live channel; fuzz targets 3-5
Found by the manager_routing fuzz target (docs/research/fuzzing.md \u00a77.8): - ChannelManager::open_channel / adopt_channel used HashMap::insert(...).is_some() as a collision check \u2014 insert REPLACES the existing entry, so a duplicate adopt/open installed the new state, dropped the live channel's demux_sender (spurious EOF to its readers, subsequently routed chunks lost) and still returned Err(ChannelExists). Fixed with contains_key pre-check; map untouched on collision. Regression tests: adopt_channel_duplicate_id_leaves_ live_channel_intact, open_channel_duplicate_id_leaves_live_channel_ intact. New fuzz targets (\u00a77.4 step 3): - manager_routing: Arbitrary op sequences over ChannelManager; exact counter models (parked/dropped must equal the manager's monotonic counters), parked-bytes bound per \u00a76.2-1, clear_all ledger-vs-map semantics, drainer-byte reconciliation (lossless routing) - envelope_semantic: constructors -> serde -> write_frame/read_frame structural round-trip; event-type constants; call.error parse-back - spec_parse: OpRegisterRequest::from_json -> rebuild -> registry registration (attacker schemas compile at register, CF-003) - fuzz/shared/src/arbitrary_value.rs: bounded Arbitrary for serde_json::Value; 20 spec_parse + 4 manager_routing seeds - corpus replay for the new targets in fuzz/shared tests Verification: cargo test 684 passed (682 + 2 regression); clippy -D warnings clean (main + fuzz/shared); fmt clean (main + fuzz); cargo fuzz build clean; 20 s smoke on all three new targets clean (manager_routing 79k, envelope_semantic 141k, spec_parse 517k runs); crash input replays clean post-fix
This commit is contained in:
1 parent
c211c163fd
commit
a1f257757b
14 files changed
+1133
-5
No files matched your search
+67
-1
@@ -183,10 +183,76 @@ def chunk_header_seeds():
|
||||
i += 1
|
||||
|
||||
|
||||
def manager_routing_seeds():
|
||||
"""Op-sequence seeds. These are arbitrary-encoded `ManagerSequence`
|
||||
inputs, so they must be produced by the same encoder libFuzzer uses
|
||||
(arbitrary 1.x byte format). Rather than hand-encoding the
|
||||
byte format, generate them by running the shared crate's seed
|
||||
helper: a tiny Rust tool would be a dependency; instead commit a
|
||||
small set of raw-byte patterns that decode into useful sequences
|
||||
(generated by the decode tool during development, deterministic).
|
||||
The pattern bytes below were produced once by encoding with
|
||||
`arbitrary` and are stable for arbitrary 1.4.x."""
|
||||
|
||||
# Generated via: Unstructured from the bytes below, ManagerSequence
|
||||
# decode, verified with the mr-decode tool. See docs/research/
|
||||
# fuzzing.md §7.7 for the generator note.
|
||||
patterns = {
|
||||
"single-open": "6400000000000000",
|
||||
"adopt-then-route": "6c00000002000000",
|
||||
"route-spray": "6c00000001000000",
|
||||
"clear-all": "14000000",
|
||||
}
|
||||
i = 0
|
||||
for name in sorted(patterns):
|
||||
data = bytes.fromhex(patterns[name])
|
||||
write_seed("manager_routing", i, data)
|
||||
i += 1
|
||||
|
||||
|
||||
def spec_parse_seeds():
|
||||
"""JSON byte patterns for the op/register rebuild path: valid spec,
|
||||
missing fields, wrong types, pathological schemas (deep nesting,
|
||||
huge strings), and non-JSON bytes."""
|
||||
bodies = [
|
||||
b'{"spec":{"name":"a/b","op_type":"Query"},"replace":true}',
|
||||
b'{"spec":{"name":"a/b","op_type":"Mutation"}}',
|
||||
b'{"spec":{"name":"a/b","op_type":"Sub"}}',
|
||||
b'{"spec":{"name":"a/b","op_type":"Pub","publish_schema":{"type":"object"}}}',
|
||||
b'{"spec":{"name":"channels/tty/sub","op_type":"Sub","channel_open":true}}',
|
||||
b'{"spec":{"name":"channels/tunnel/direct","op_type":"Sub","channel_open":true,"channel_open_alpn":"alk/tunnel"}}',
|
||||
b'{"spec":{"name":"x","op_type":"Query","visibility":"internal","input_schema":{"type":"object","required":["a"],"properties":{"a":{"type":"string"}}}}}',
|
||||
b'{"spec":{"name":"x","op_type":"Query","error_schemas":[{"code":"E1","description":"d","schema":{"type":"string"}}]}}',
|
||||
b'{"spec":{"name":"x","op_type":"Bogus"}}',
|
||||
b'{"spec":{"op_type":"Query"}}',
|
||||
b'{"replace":true}',
|
||||
b'{}',
|
||||
b'{"spec":null}',
|
||||
b'{"spec":"not-an-object"}',
|
||||
b'{"spec":{"name":123,"op_type":"Query"}}',
|
||||
b'{"spec":{"name":"","op_type":""}}',
|
||||
b'{"spec":{"name":"x","op_type":"Query","access_control":{"required_scopes":["a","b"]}}}',
|
||||
b'{"spec":{"name":"x","op_type":"Query","input_schema":{"$ref":"#/definitions/Recursive"}}}',
|
||||
]
|
||||
i = 0
|
||||
for body in bodies:
|
||||
write_seed("spec_parse", i, body)
|
||||
i += 1
|
||||
# Deep-nesting schema (jsonschema compile probe)
|
||||
nested = b'{"spec":{"name":"deep","op_type":"Query","input_schema":' + (b'{"a":' * 100) + b'1' + (b'}' * 100) + b'}}'
|
||||
write_seed("spec_parse", i, nested)
|
||||
i += 1
|
||||
# Non-JSON
|
||||
write_seed("spec_parse", i, b"\xff\xfe not json")
|
||||
i += 1
|
||||
|
||||
|
||||
def main():
|
||||
envelope_seeds()
|
||||
chunk_header_seeds()
|
||||
for target in ("chunk_header", "envelope_frame"):
|
||||
spec_parse_seeds()
|
||||
manager_routing_seeds()
|
||||
for target in ("chunk_header", "envelope_frame", "spec_parse", "manager_routing"):
|
||||
d = os.path.join("fuzz", "corpus", target)
|
||||
n = len(os.listdir(d))
|
||||
print(f"{target}: {n} seeds")
|
||||
|
||||
Reference in new issue
Block a user