SECURITY DOC: the shipped README claims the plugin worker has "no filesystem and no network"; both are false, and our own threat model says so #114
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#114
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while auditing #79's containment criteria. Verified first-hand in source before filing, and the README half is already fixed in the working tree (uncommitted) — filing it because it shipped, in v0.26.1, released today.
The claim
README.md:299-301, as shipped:Both halves are false, and we already knew
Filesystem — false on every platform
There is no filesystem confinement whatsoever. The decisive artefact is a passing test, named for the property it proves:
Its own doc is explicit that this is deliberate and that the opposite assertion would be a lie:
_prdoc/guides/80-threat-model.md§6 says the same in plain terms:Network — false everywhere except Linux x86_64/aarch64
crates/plugin-supervisor/src/contain.rs:1279-1286gates the entire seccomp module:Everything else returns
NetworkDenial::Unsupported. The threat model states it: "On any platform that is not linux, and on a linux this build has no syscall table for, there is no filter at all." So the claim is false on Windows and macOS, both of which we ship.The part that is true, and should be said instead
The guest is wasm with zero imports — it cannot name a syscall at all, on any platform. That is a genuinely strong, structural property and it is what the README should be claiming. The distinction the shipped text loses is between the guest as loaded (which truly has no filesystem and no network) and the worker process it runs in (which is not a sandbox).
A second, smaller finding from the same check
The threat model says the unfiltered case is disclosed — "and the worker says so, in the payload." That is true only of the worker's self-report to its host.
NetworkDenialhas 33 references, every one insidecrates/plugin-supervisorandcrates/plugin-host(verified withfind_references, not grep). It reaches no MCP payload, noplugin status, noindex_coverage.crates/plugin-host/src/main.rs:88-94confirms the intent and its limit — theeprintln!is explicitly "for an operator running a worker by hand", and the parent reads none of it because stdio is null.So on an unfiltered platform an operator has to know, rather than be told. That is a real disclosure gap and it is the more interesting half of this issue: the product's own rule is that absent must never read as negative.
I nearly shipped this same error while fixing it — my first draft of the README correction said the worker "says so in its payload", which is exactly the overclaim above. It survived only because I checked
NetworkDenial's references before leaving the edit.What was done, and what is left
enforced/not_requested/unsupported/unavailable:<errno>, on the three-state contract — absent means this build did not report, never "fine".NetworkDenial's own doc already has the right design and says so: "EVERY VARIANT IS A STATEMENT. There is no variant meaning 'fine' that a worker could reach by doing nothing." It just stops at the crate boundary.Why this is worth a tracked record rather than a silent doc fix
A security overclaim in a README is the one piece of documentation a user reads before deciding to trust a package. We had the honest analysis written down in
80-threat-model.mdthe whole time — the defect was that the summary aimed at users contradicted the document aimed at reviewers. Worth a checkable rule that the README's security claims cannot exceed the threat model's, rather than trusting the next edit to remember.Related
Found auditing #79. Same family as #109 and #108 (a gate green while the thing it checked was false). The disclosure half belongs with the honesty work in #101/#99/#107.
#![cfg(unix)]file-wide, so its properties are ungraded on the Windows we ship #117#[tool]method gets an EARNED zero from search_symbols, while the tool's own note names that exact case as unmeasurable #118archive_refusedreaches nocoverage_reasonscode, so an agent's answer is qualified by nothing #124#![cfg(unix)]file-wide, so its properties are ungraded on the Windows we ship #117Triage 2026-09-06: CLOSING. Both halves are in, and the repair is a gate over every shipped README rather than three edits — which is what makes it durable.
Verified against master; landed in
8e6bcac.Half (a) — the claim
search_text("no filesystem and no network")returns 2 hits, and both are in code that forbids the sentence:crates/daemon/tests/readme_security_claims.rs:6andcrates/daemon/src/activation.rs:472. The claim itself is gone from every shipped README.The important part is why it took a gate.
8e6bcac's own message records it: fixing the two narrative sentences by hand left the same claims shipping from two places nobody had looked at — the components table thirty lines below, and the archive README thatrelease.ymlwrites into every release artifact, in both packaging arms. A reader who downloads a release never sees the repo README at all, so the copy that mattered most was the copy neither correction touched.So the unit of
crates/daemon/tests/readme_security_claims.rsis every shipped README —README.mdplus eachcat > README.md << EOFheredoc inrelease.yml. And the thresholds are derived, not transcribed: fromFILESYSTEM_CONFINEMENT,NetworkDenial::Enforced.encode(), andNETWORK_FILTER_SUPPORTED's owncfg!(crates/plugin-supervisor/src/contain.rs:690). A copied spelling would have drifted; a derivation cannot.It carries its own control —
the_checker_rejects_the_sentence_that_shipped— which caught a vacuous first draft.Half (b) — the disclosure this issue left open
Closed.
ActivationDisclosure::worker_containment: Option<WorkerContainment>—crates/daemon/src/activation.rs:511;WorkerContainmentat:539;NETWORK_NOT_REQUESTED:555,NETWORK_UNSUPPORTED:558,NETWORK_REQUESTED_UNVERIFIED:561.And it is honest in the way this issue demanded. The parent structurally cannot learn a worker's real
NetworkDenial— it rides only inSelfReportbehind a policy false at every production site, stderr is null,--abi-reportis a different process, and a failed filter is non-fatal so exit codes cannot discriminate. Soenforcedis unpublishable, and what ships isrequested_unverified | unsupported | not_requested, read from the live host's policy and graded by spawning a real worker rather than by restating acfg. Printingenforcedoff acfgwould have been this issue's own overclaim one level down.Runs (exit 0)
incl.
no_shipped_readme_calls_the_worker_a_sandbox,no_shipped_readme_claims_the_product_has_no_network,the_platform_scope_in_every_readme_is_the_filters_own_cfg,the_checker_rejects_the_sentence_that_shipped,no_platform_lets_this_daemon_claim_the_filter_is_enforced,a_waived_filter_is_reported_as_waived_on_every_platform.Residual
None. Worth carrying forward as a lesson rather than as work: a correction is a new claim, and the reason this closes cleanly is that the correction was made checkable instead of merely made.
🤖 Triage lane, 2026-09-06, master
45cf6e4packages.rs:4478-4480says the wire has no bit foris_extension, contradictingrecord.rs:18and a comment fifteen lines above it in the same expression #185evidence_gaps.partial_sources_in_index: 1fires on EVERY reply whilepartial_sourcesis never populated — a three-state disclosure that only ever renders its unfalsifiable state #200