Skip to content

test(fuzz): add service_control target for control loop dispatch (#120) - #137

Open
fogydes wants to merge 2 commits into
anyplaynet:mainfrom
fogydes:feat/fuzz-service-control
Open

fogydes wants to merge 2 commits into
anyplaynet:mainfrom
fogydes:feat/fuzz-service-control

Conversation

@fogydes

@fogydes fogydes commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #120.

Overview

As agreed in #120 and noted in docs/gaps.md under Fuzzing beyond the parsers, this adds the service_control fuzz target and companion hardening test to drive the net service's control dispatcher against in-memory duplex streams.

Invariants Tested & Documented (fuzz/README.md)

  1. Crash & Panic Freedom: Arbitrary chunked byte sequences (valid/invalid JSON, corrupt framing, unexpected commands, missing arguments) never crash or panic the service.
  2. Bounded Line Buffering: Lines exceeding control::MAX_LINE (1 MiB) are discarded without unbounded memory growth.
  3. Clean Shutdown on EOF: When the launcher's stream ends, the service loop terminates cleanly within bounded time.
  4. Protocol Output Validity: Every output emitted by the service fits within MAX_LINE, ends with \n, is valid UTF-8 and valid JSON, and parses as a control Output (Answer or Event).
  5. Request-Reply Correspondence: For unique command IDs, at most one answer is returned.

Verification

  • cargo fmt --manifest-path fuzz/Cargo.toml -- --check
  • cargo fmt --manifest-path Cargo.toml --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo clippy --manifest-path fuzz/Cargo.toml --all-targets -- -D warnings
  • cargo test -p anyplay-net (all 154 unit tests and 48 integration tests pass)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@AlxcBTW AlxcBTW left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this, @fogydes. Fuzzing what the service does with the messages (not just the parsers) is exactly the gap in docs/gaps.md, and the structure is good: the service.rs change is test-only (no runtime behaviour touched), the workflow diff is fine, and the target uses the real run over duplex streams. Three things need to change before it can land; I have not waited for the fuzz job to prove them, they follow from reading Service::command.

1. The "one answer per id" assertion will fire on valid input (false positive). The reader panics on any repeated answer.id. But the fuzzer controls the ids, and the service legitimately answers each line: for example {"id":1,"do":"nope"}\n sent twice gives two unknown_command answers for id 1 (Input::BadCommand(Some(id))), and two hellos give a bad_state answer each. The invariant only holds for unique ids. Please assert it relative to the input: track which ids the wire contained (or parse the wire lines with parse_command/the id field) and check that the number of answers for an id never exceeds the number of lines that carried it. Alternatively only assert on ids that occur once.

2. The target is not hermetic once hello is accepted. hello takes any http(s) URL and builds a real cloud client; after it, commands such as browser_sign_in or the e-mail code request make real HTTP requests to whatever host the fuzzer wrote (and browser::start may try to open a browser). Fuzz inputs would therefore reach the network and become non-deterministic. Please keep the fuzzer away from that: simplest is to drop (or rewrite to a fixed http://127.0.0.1:9-style address) hello commands in the harness, or to run only the pre-hello surface plus a fixed hello you control and filter the other lines that need a cloud. A short comment on which surface is covered would help too. (bind: 127.0.0.1:0, the bridge listener and Traffic::start bind loopback sockets per iteration; that is acceptable, but please mention it in the doc comment and keep an eye on exec/s.)

3. The hardening test is weaker than its name and the PR body. join_code_revoke is sent before hello, so the single answer is only the "Say hello first" bad_state failure. Please assert that explicitly (ans.id == 10 and that it is a failure with Failure::BAD_STATE), and also add a line that checks the oversized line really is skipped and the following line is still read (the id 10 answer already shows that, so just say so in a comment). Bonus, optional: send one {"id":1,"do":"hello",...}-free unknown command like {"id":5,"do":"nope"} to cover UNKNOWN_COMMAND.

Small things: the doc comment of the target lists "output ends with \n, valid UTF-8" as checked, but the reader works on lines(), so that is not actually asserted; either drop the claim or check it. fuzz/README.md says "lines over 1 MiB are discarded without unbounded memory growth", which the target cannot observe either; I would phrase it as "an oversized line does not stop the loop".

Follow-up from our side after merge: update the "Fuzzing beyond the parsers" entry in docs/gaps.md, no need for you to do it. Happy to re-run CI as soon as you push.

@fogydes

fogydes commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @AlxcBTW for the thorough and helpful review! Addressed all points in dcc5893:

  1. Answer assertion: Switched from unique set tracking to tracking sent_id_counts on input lines matching command IDs and asserted count <= sent for each answered ID. Repeated commands with the same ID now pass without false positives.
  2. Hermeticity: Filtered out hello commands in the fuzzer harness so the fuzzer strictly exercises the pre-hello dispatch surface, error reporting, bad commands, and line handling without making cloud requests or invoking the browser. Documented the loopback socket bindings in the target doc comment.
  3. Hardening test: Extended the test to assert answers[0].id == 5 with UNKNOWN_COMMAND and answers[1].id == 10 with BAD_STATE, noting that receiving answer 10 proves the preceding oversized line (> 1 MiB) was cleanly skipped without stalling subsequent command reads.
  4. Docs & README: Rephrased the invariant in fuzz/README.md to "an oversized line does not stop the loop" and removed the unasserted raw newline/UTF-8 claims in the target's doc comment.

@fogydes
fogydes requested a review from AlxcBTW October 28, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Fuzz targets that drive service message handling against fakes (docs/gaps.md)

2 participants