Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Audit report (milestone M8, release 0.9.0)

This is the report of the project audit of RFC 020: what was examined, what was found, what was done about each finding, and what remains. It is written so that a reader can check each fix against the test that pins it.

What the audit was

  • Scope. The whole project as built through 0.8.0. That covers:
    • the data models and the external contract;
    • the networking: clients, resolver, timeouts, pools, proxies, redirects and every bound;
    • test adequacy, requirement by requirement;
    • the measurements of REQ-PERF;
    • the dependency tree;
    • dead code and drift from the RFCs;
    • the CI and release pipeline.
  • Method (RFC 020, D1):
    • Reports. The development team investigated in five audit reports: network, data and contract, tests and code, measurements, and dependencies and CI. Each finding came with its evidence and how it was obtained (run, read or searched), and each report ended with what was checked and what was not.
    • Re-performance. The architect reviewed each report and re-ran at least a fifth of its findings, including every High and Medium resting on a run. The architect also set the final severities and the fix list.
    • Decisions. The owner decided 24 questions (RFC 020, D-1 to D-24 below).
    • Fixes. Each fix was a handoff of its own (020g to 020x), with tests first, shown failing before the fix, and its own review.
  • Dates. 2026-10-29 (RFC 020 accepted) to 2026-11-08.
  • Limits of the audit, stated plainly. The readers were the project’s own roles, not an outside auditor: the audit’s independence is the reviewer’s re-performance, not a second organization. Windows could not be compiled on the audit machine; Windows is covered by CI only. Live providers were contacted only through the opt-in live checks.

Summary

86 findings came from the five reports, plus 7 that arose while fixing them.

AreaHighMediumLowNote
Network (NET)1692
Data and contract (DAT)08132
Tests and code (TST)0862
Measurements (PRF)0243
Dependencies and CI (DEP)0587
Found during the fixes0132

Where a finding had two severities (NET-4, TST-14), it is counted at the higher one. The one High finding and every Medium one are fixed. The open items are listed under What remains, each with whose decision it is.

Contract changes that follow from the fixes are announced in the 0.9.0 section of the changelog, as REQ-MAIN-006 requires. They are why 0.9.0 is a minor release.

Findings and their fixes

Each row gives:

  • the final severity;
  • what was true before the fix;
  • what was done: a fix in a handoff, or a decision D-n (of RFC 020) and then a fix;
  • the requirement the fix is held to;
  • one or more of the tests that pin the fix, by function name.

A test whose name starts with req_ lives under crates/.

Network

IdSev.BeforeDoneRequirementPinned by
NET-1HighA TCP connection that sent no byte was never closed, so 64 of them blocked every later requestRFC 020, D-4; 020g, and a read-and-replay of the first bytes under one deadlineREQ-SEC-022req_sec_022_a_silent_connection_is_closed_after_the_idle_time, req_sec_022_silent_connections_cannot_hold_every_permit, req_sec_022_one_ambiguous_byte_then_silence_is_closed_after_the_idle_time
NET-2MediumThe accept loop retried at once on a descriptor-limit error (a full core, a flood of log lines)020gREQ-SEC-018req_sec_018_accept_errors_back_off_and_are_logged_once_per_interval
NET-3MediumA same-host redirect target was not checked against robots.txtRFC 020, D-1; 020hREQ-CMP-005req_cmp_005_every_redirect_target_is_checked_against_robots_txt
NET-4MediumThe refusal table admitted six non-global IPv6 blocksRFC 020, D-2; 020h, with the table now checked against the IANA registryREQ-SEC-006req_sec_006_every_special_purpose_block_is_refused
NET-5MediumParse time grew quadratically with nesting, so two deep pages made a third host waitRFC 020, D-3; 020iREQ-PAGE-006, REQ-SEC-012req_sec_012_a_page_nested_100000_deep_parses_quickly, req_sec_012_deep_pages_on_two_hosts_do_not_starve_a_third
NET-6MediumA fetch ended by its deadline was given the search’s message020kREQ-ERR-004req_sec_011_slow_server_hits_the_deadline
NET-7MediumNo behavior test for the connection cap, the idle time or the stream bound020gREQ-SEC-018, REQ-SEC-022req_sec_018_the_connection_cap_refuses_the_third_and_frees_on_close, req_sec_022_an_http2_stream_beyond_max_streams_is_refused
NET-8LowThe threat register said “unauthenticated” where only a local process qualifies020g (register reworded; the warning’s size bounded by LIM-H7)—documents
NET-11LowThe Bearer scheme was matched case-sensitively020gREQ-SEC-018req_sec_018_the_bearer_scheme_is_case_insensitive
NET-12LowHeader sizes had no limit of ours (a 400,000-byte header was accepted)020g: LIM-H7, 16 KiBREQ-SEC-018req_sec_018_an_oversized_header_block_is_431, req_sec_018_the_http2_header_list_is_bounded
NET-13Low29 lock sites would panic on a poisoned lock020jREQ-SEC-014req_sec_014_lock_recovers_a_poisoned_mutex, req_sec_014_no_expect_on_a_standard_library_lock
NET-16LowTwo rows of the limits table were worded wrongly020g—tools/check-limits

Data and contract

IdSev.BeforeDoneRequirementPinned by
DAT-1Mediumcheck passed a configuration that serve --transport http refused020lREQ-OPS-001req_ops_001_check_and_serve_refuse_the_same_callers_alike
DAT-2Mediumcheck printed four items and warned about nothingRFC 020, D-6; 020lREQ-OPS-001req_ops_001_check_prints_every_section, req_ops_001_each_risky_setting_has_its_warning
DAT-3MediumA non-http scheme was told it had been “retried over https”020kREQ-ERR-004req_err_004_a_scheme_that_is_not_http_has_its_own_message
DAT-4Medium404, 403 and 410 were unavailable, and the pages said to retryRFC 020, D-7; 020kREQ-FETCH-011req_fetch_011_the_message_says_whether_retrying_helps
DAT-5MediumThe stability page promised more #[non_exhaustive] types than the code had020mREQ-ARCH-007req_arch_007_stability_page_lists_match_the_source
DAT-6MediumNo fixture pinned the input schemas and the tool descriptions020mREQ-MCP-011req_mcp_011_tools_list_matches_the_fixture
DAT-7Medium“Nothing on standard output” had no test in HTTP mode020mREQ-MCP-002req_mcp_002_http_serve_writes_nothing_to_stdout
DAT-8MediumEvery returned URL had its query re-serialized020nREQ-RANK-003req_rank_003_returned_url_table
DAT-9LowThe comparison key could merge different resources and showed http over httpsRFC 020, D-8; 020nREQ-RANK-002, REQ-RANK-004req_rank_002_the_twelve_pairs_of_the_audit, req_rank_004_the_https_url_is_shown_when_the_best_is_http
DAT-10LowSearchResult could be deserialized past the boundary020mREQ-SEC-001compile_fail doc tests in boundary.rs
DAT-11, 12, 14LowConfiguration messages missed the key, named Rust types, or named an unset key020lREQ-CFG-005req_cfg_005_the_invalid_configurations_of_the_audit, req_cfg_005_a_wrong_kind_of_value_names_its_key
DAT-13LowTen “operator” limits had no keyRFC 020, D-9; 020i, 020s (keys for LIM-S12 and LIM-O1; the rest “fixed”)REQ-CFG-005req_cfg_005_the_connect_timeout_and_the_shutdown_time_are_keys
DAT-15LowThe secret check refused ordinary text and passed many credential shapes020nREQ-PRIV-002req_priv_002_the_corpus_of_ordinary_text_passes, req_priv_002_the_corpus_of_credentials_is_refused
DAT-16LowThe boundary removed joiners that carry orthographyRFC 020, D-10; 020nREQ-SEC-015req_sec_015_joiners_that_carry_orthography_are_kept
DAT-17, 19, 20LowThree wrong error texts; a tool description and a notice documented two ways020kREQ-ERR-004, REQ-MCP-008req_search_003_an_unserved_category_lists_the_served_ones, req_mcp_008_the_web_fetch_description_is_the_designs
DAT-18LowA disabled engine did not say why020k (an optional reason)REQ-STATUS-001req_status_001_a_disabled_engine_says_why

Tests and code

IdSev.BeforeDoneRequirementPinned by
TST-1MediumThe coverage gate could not tell a test that names a requirement from one that verifies itRFC 020, D-18, D-19 (as amended by RFC 020, D-23): the verification ledger, mutation testing in the release checksREQ-TEST-019req_test_019_the_release_checks_mutate_the_six_security_modules
TST-2MediumNo log line carried a request id or the caller020rREQ-OBS-005req_obs_005_every_event_of_a_call_has_its_request_id_and_caller
TST-3, TST-4MediumA range test took its expectations from the table it tested; the URL table missed the case that failed020h, 020nREQ-SEC-006, REQ-RANK-003req_sec_006_every_special_purpose_block_is_refused, req_rank_003_returned_url_table
TST-5MediumNo test saw an http → https upgrade succeed020qREQ-FETCH-010req_fetch_010_an_http_url_is_fetched_over_https_when_the_site_speaks_it
TST-6MediumNo test of what a fetch sends, or of proxy variables in the environment020qREQ-FETCH-007, REQ-SEC-009req_fetch_007_a_fetch_sends_get_and_only_the_headers_rypsok_sets, req_sec_009_a_proxy_in_the_environment_is_not_used
TST-7MediumMutants survived on security paths020q, 020s: 54 missed mutants to 0, six equivalent ones listed with reasons in .cargo/mutants.tomlREQ-TEST-019the release checks’ mutation jobs
TST-8Low35 tests depended on wall-clock time020s (paused time, exact windows)REQ-AGG-003req_agg_003_overall_deadline_holds
TST-9LowThe crate did not compile without its default features020s; CI checks each feature aloneREQ-TEST-012req_test_012_ci_checks_every_feature_alone_and_none
TST-10LowInert public items and an unused limit020s (removals announced)REQ-MCP-007req_mcp_007_a_cancelled_call_drops_its_engine_task_within_lim_o2
TST-11, 12LowError messages were bounded only in debug builds; some tests checked source text, not behavior020o, 020q, 020sREQ-ERR-004req_err_004_every_error_constructor_has_a_bounded_message_without_external_text
TST-13NoteFour example answers could not be regenerated020s (the page, written back from the answers, and a regeneration tool)—req_doc_002_the_page_behind_the_fetched_answers_is_kept_and_described
TST-14MediumParsers of untrusted input had no fuzz target020u (five targets); seeds for all 14 (020w)REQ-SEC-023req_test_019_every_fuzz_target_has_committed_seeds
TST-15Low16 claims of the RFCs had drifted from the code020s (RFC amendments; the agent client’s keepalive set)REQ-SEC-007req_sec_007_the_agent_client_sets_tcp_keepalive_and_the_idle_timeout

Measurements

IdSev.BeforeDoneRequirementPinned by
PRF-1LowFour default fetches peaked above the published memory derivation020l, 020x: the estimate is built from measured costs, one set per Linux buildREQ-PERF-003req_perf_003_the_estimate_is_above_every_measured_peak_without_sessions
PRF-2MediumOne 20 MiB page cost about 120 MiB; four of them were killed inside the documented unitRFC 020, D-20; 020l: fetch.max_bytes ceiling 10 MiB, and check prints the derived worst caseREQ-PERF-003req_perf_003_the_new_ceiling_fits_the_unit_and_the_old_one_did_not
PRF-3MediumAbout 58.5 KiB per session; the documented maximum did not fit the unitRFC 020, D-21; 020l: ceiling 2048, cost in the estimateREQ-PERF-003, REQ-SEC-024req_sec_024_max_sessions_has_its_range
PRF-4LowWith swap, the memory limit only slowed the host020t: the unit and the container line forbid swapREQ-PERF-003req_perf_003_the_deployment_page_forbids_swap
PRF-5LowThe post-processing reserve LIM-S6 cited no measurementKept at 200 ms; its basis cites the measurement (this report’s change)REQ-PERF-004documents
PRF-6, 7, 9Note, LowThe burst behavior of the Wikipedia admission was unstated; the memory tool did not measure the delivered configuration; the LIM-S4 basis said “to be measured”020t—documents; tools/measure-memory --defaults
PRF-8NoteRelease binaries were not strippedRFC 020, D-22; 020pREQ-SEC-014req_sec_014_the_release_profile_strips_symbols_and_keeps_unwinding

Dependencies, CI and release

IdSev.BeforeDoneRequirementPinned by
DEP-1, 15MediumThe container image was not a release file, and was compiled apart from the release binariesRFC 020, D-12; 020p: the image is the release binary on a pinned base, listed in SHA256SUMSREQ-DEP-005req_dep_002_the_image_is_made_from_the_release_binary
DEP-2MediumLinux aarch64 was not builtRFC 020, D-13; 020pREQ-RUST-002req_dep_005_the_release_and_its_checks_build_the_same_four_targets
DEP-3MediumReleases were built with a floating compiler020o, 020w, 020p: one named compiler, in one placeREQ-TEST-019req_test_019_the_release_compiler_is_named_in_one_place
DEP-4NoteThe Windows binary differed between two builds of one commit020o (/Brepro); the release checks compare two builds of every targetREQ-TEST-019req_test_019_the_reproducibility_check_builds_twice_and_compares_the_binaries
DEP-5, 6Medium, LowWorkflows held more permissions than they used020oREQ-SEC-020req_sec_020_every_workflow_declares_least_permissions
DEP-7, 8Medium, LowScheduled checks could fail without failing; fuzzing ran two minutes a week020o, then 020w: the release checks, with no timer (RFC 020, D-23)REQ-TEST-019req_test_019_no_workflow_runs_on_a_timer, req_test_019_the_release_checks_fuzz_every_target_for_300_s
DEP-9, 10, 14LowCI did not use --locked; an unused dependency; floating tools and base images020oREQ-TEST-012, REQ-SEC-020req_test_012_cargo_commands_of_the_workflows_are_locked, req_sec_020_tools_and_the_release_compiler_are_pinned
DEP-17NoteNo proof of where a release file was builtRFC 020, D-16; 020p: provenance attestations, made by a job that runs no build codeREQ-SEC-020req_sec_020_only_the_checksums_job_signs_and_runs_no_build_code
DEP-13LowSeveral pinned actions were a major version behind, and nobody was told of a new releaseRFC 020, D-14 as amended by D-23: the dependency step at the start of each cycle; first done for 0.10.0 (022b), every action at its latest releaseREQ-SEC-020the release procedure, step 1
DEP-18LowThe book was published on every push and described unreleased codeRFC 020, D-17; 020t: published from the release tag, and the first page names the releaseREQ-DOC-002req_doc_002_the_book_is_published_from_the_release_tag

Found while fixing

IdSev.BeforeDonePinned by
F-1LowA panic wrote plain text, with its message, to standard error020s: one JSON line without the messagereq_obs_005_a_panic_is_one_json_line_without_its_message
F-2LowA fuzz target asserted more than the driver promises (the product was correct)020wreq_eng_001_oddly_written_links_are_made_canonical_by_the_pipeline
F-3LowA client that could not be built did not say why, and said so twice020xreq_ops_001_a_missing_trust_store_is_named_once_at_start
F-4Mediumcheck’s memory estimate was exceeded by a glibc build020x: one measured set per Linux build, and the build namedreq_perf_003_check_names_the_build_of_the_estimate
N1NoteNo page said which certificates outbound fetches trust020tdocuments
N2NoteA result whose host had an empty label (such as a leading dot) was shown, although web_fetch refuses itRFC 020, D-24; 020y: dropped and counted as url_invalidreq_rank_003_a_host_web_fetch_would_refuse_is_invalid
——A property test could generate an address as a host name020q (tests only)req_sec_006_an_address_deny_rule_is_a_network_rule_not_a_host_rule

Found after the release

Found after 0.9.0 was released, and fixed in 0.10.0. The audit did not find them, and the report keeps them here so that it stays true.

IdSev.BeforeDoneRequirementPinned by
A-1HighIn HTTP mode, rmcp 3.5.0 kept every tools/call tool name in a map with no bound (RUSTSEC-2026-0461, published after the release); 0.4.0 to 0.9.0 were affected, and stdio was notRFC 022; 022a: rmcp 3.9.0REQ-SEC-018, REQ-SEC-024the advisory check (cargo deny), on every push and in the release checks
A-2LowSIGTERM before the client’s first message was not heard until that message arrived or the input closed; the ledger did not list REQ-OPS-002 as partly verified022aREQ-OPS-002req_ops_002_sigterm_before_any_message_shuts_down_within_limit
A-3LowThe fuzz workspace has its own lock file, and it was never updated with the main one: 29 crates shared by both, toml and encoding_rs among them, were fuzzed at older versions than those released022b: the two lock files agree, and a gate holds them to itREQ-TEST-019tools/check-fuzz-lock

The decisions (RFC 020)

  • Networking and egress.
    • RFC 020, D-1: every redirect target is checked against robots.txt.
    • RFC 020, D-2: the refusal table follows the IANA special-purpose registries.
    • RFC 020, D-3: parse time is linear in nesting depth.
    • RFC 020, D-4: LIM-H3 bounds a connection that never speaks.
  • Interface and contract.
    • RFC 020, D-6: what check prints and warns about.
    • RFC 020, D-7: a fetch says whether retrying can help.
    • RFC 020, D-8: which URLs are the same resource.
    • RFC 020, D-9: a limit without a key is “fixed”.
    • RFC 020, D-10: joiners that carry orthography are kept.
    • RFC 020, D-11: the frozen public surface is a 1.0.0 decision.
  • Release path.
    • RFC 020, D-12: the image is built from the release binary.
    • RFC 020, D-13: Linux aarch64 is built.
    • RFC 020, D-14, amended by D-23: dependencies are updated once per release cycle.
    • RFC 020, D-15: a ruleset protects main and the tags; this is an owner setting.
    • RFC 020, D-16: build provenance.
    • RFC 020, D-17: the book is published from the release tag.
  • Tests.
    • RFC 020, D-18: the verification debt on security paths is closed, and the rest is a public ledger.
    • RFC 020, D-19, amended by D-23: a committed exclusion list for mutation testing.
  • Limits and build.
    • RFC 020, D-20: a 10 MiB page ceiling, and check prints the worst case.
    • RFC 020, D-21: the session ceiling is 2048.
    • RFC 020, D-22: stripped release binaries.
  • RFC 020, D-23: when checks run (When checks run). CI on every push, the deep checks in each change and before every release, and no timer.
  • RFC 020, D-24: a search result is shown only with a host that web_fetch would accept; one with an empty label is invalid.
  • RFC 020, D-5: one deliberate DuckDuckGo no-results request; it is under “What remains”.

What remains

Recorded, with no change (each was judged as needing none, or not yet):

  • NET-9 (Low). A refused request writes one log line, two for a foreign Host, with no rate limit of its own. The refusal counters are written once a minute.
  • NET-10 (Low). An event stream of the legacy session revision is bounded by neither the request time (LIM-H5) nor the idle time (LIM-H3). Only an authenticated caller with a session can open one.
  • NET-14 (Low). A resolver that never answers leaves one blocking thread per lookup until it gives up. The caller is released at the connect timeout, and the web_fetch rate limit bounds the count.
  • NET-15 (Note). The engines’ clients use the system resolver, not the judging one. An engine’s address is the operator’s setting and the operator’s trust (REQ-SEC-017).
  • DAT-21 (Note). The output schema allows any notice reason with any notice code. Per-code schemas would let a host validate this.
  • DAT-23 (Note). A library user can hold a Config that is not yet validated. The service validates before it uses one.
  • DEP-11, 12, 19, 20 (Note).
    • The declared minimum Rust version, 1.88, is true and tested.
    • Four crates appear in two versions each.
    • The Actions caches were above GitHub’s 10 GB.
    • The repository’s history holds no secret.
  • Residuals inside fixed rows:
    • From accept to a complete header block can take up to twice LIM-H3 (NET-1).
    • The SDK’s warning for a foreign Host still carries header text, now at most 16 KiB (NET-8).
    • With fewer engine features the tests compile, but not all of them pass (TST-9).

Open, and whose decision it is:

ItemWhatWhose
The verification ledger40 requirements whose tests verify part of what they say (41 at the release of 0.9.0; REQ-AGG-003 closed since, RFC 021) (ledger)closed before 1.0.0 (RFC 020, D-18)
DAT-22, RFC 020, D-11Which public items are frozenthe 1.0.0 preparation
NET-18, RFC 020, D-5Confirmed on 2026-11-08: DuckDuckGo’s no-results page marks itself with span.no-results, and the driver read div.no-results, so a search with no results counted as a parse failure toward the circuitfixed before the release in handoff 020z
DEP-16, RFC 020, D-15main and the release tags have no protection rulethe owner (a repository setting)
First runsThe provenance attestations, the attach of the images, and the book from a tag run for the first time on the 0.9.0 tagread in that run, as the release procedure says

Repeating the audit

Every fix above is pinned by a test or a gate in the tree. cargo test --workspace --locked runs them, and the release checks (When checks run) run the fuzzing, the mutation testing and the reproducibility check. The audit reports’ raw evidence was kept in the project’s private review folder. A finding is named here only once its fix is in the tree.