MemberCensusBasis::unexpanded_supertypes is an unbounded Vec<String> in a reply — no LIMIT, no cap, no truncation disclosure #205
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#205
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 attributing the
agent_task_benchcontext-ratchet breach (#199's census was most of it). Filed rather than fixed, because the fix I wrote measured zero and I am not shipping a speculative bound.The fact
crates/daemon/src/refactor.rs:The query that fills it has no
LIMIT, the collect has no.take(), and the field has no truncation sibling. It ships on everysafe_deleteandcheck_renamereply where the census fires, and its length is whatever the container's supertype list happens to be.Every other list in these payloads is bounded and says so —
truncated,lines_truncated,dependencies_capped,candidates_available. This one is not.Why it is filed and not fixed
I wrote the cap (8, plus an
unexpanded_supertypes_totalfor what was cut) and measured it on all three benchmark repos. It changed the numbers by exactly zero — nothing in the pinned corpus has more than 8 unexpanded supertypes, includingcs-dapper'sSqlMapper, a partial class split across ten files, which was my prime suspect.So the cap has no caller, no measured effect, and no test that exercises it. Shipping it would have been a bound whose only evidence is that it compiles — and this project has 15k lines of precedent for what that is worth. The right move is to file the hazard and let whoever closes it bring a fixture that can reach the cap.
Why it is still a hazard
The corpus not reaching 8 is a fact about this corpus, not about the field. A deep C# or Python hierarchy, a framework base chain, or a generated class can produce a long list, and the reply would carry all of it with nothing saying it was long. That is the same "unbounded term in somebody's context window" the payload budgets exist to prevent — it is simply invisible here, in the way the tier-1 corpus was once blind to a 58% recall loss because no ripgrep crate is hyphenated.
What closing it needs
inherited_surface_unknownonly asks whether the list is EMPTY, so truncating is safe for correctness — but a short list must never be readable as a small supertype set.What must NOT be done
..._totalbeside it turns "many" into "few" with no way to tell.Related
#199 (the census this field belongs to), and the
ratchet.json_movesentry for 2026-09-07, which records the zero-effect measurement in full.Fixed on
masterat5ac45d9. You were right to refuse the speculative cap, and you were wrong that no evidence exists — the blocker was corpus selection, not reachability.The measurement you asked whoever closed this to bring
Per container, using the census's own predicate:
Your zero-effect result reproduces exactly, and it is not the whole picture.
cs-dapper'sSqlMapper— your prime suspect — sits at exactly 8, one name from being cut, which is why the cap is 8 rather than a rounder number. And one repo outside the pinned corpus has 34 containers past it:Exprincrates/hir-def/src/hir.rsalone would have shipped 27 strings into asafe_deletereply.So the hazard is live rather than speculative, and the corpus was blind to it by construction — the same shape as ripgrep having no hyphenated crate directory. Refusing to ship the cap without a fixture that reaches it was the right call; it just needed a wider net than three repos.
What shipped
SUPERTYPE_LIST_CAP = 8, withunexpanded_supertypes_totalbeside it, and the three-state discipline enforced rather than described:Some(0)included, so "nothing was cut" is a measurement and not an absence.declared_count_may_understateis unaffected by construction (cap ≥ 1, asserted in aconstblock), soinherited_surface_unknownis unchanged.The fixture is one clause, not seven. A
deepcolumn on the existing 7-languageCENSUS_ROWStable; all seven languages reach 12 through their own declaration syntax — Python base list, C#/PHP/TS interface list, Rust supertrait bound, Rubyincludechain.Payload cost, attributed against master: python-flask +38, cs-dapper +15, rust-ripgrep +16 tokens. Recall identical.
Mutations
.len() < SUPERTYPE_LIST_CAPguarddeepfixture must name 12 types this index cannot expand, or the cut is ungraded"serde(default)turning an old daemon's silence intoSome(0)#[serde(...)]attributeskip_serializing_ifI re-ran M4 myself on the merged tree rather than taking the report —
EXIT=101, and the assertion names the property rather than a value:Both survivors are recorded in the test's own doc comment with their reasons, which is the right handling: serde already treats an
Optionfield as optional so M6 changes no behaviour, and the daemon never constructsNoneso M7's arm is unreachable. What actually keeps the field optional is its type — retyping itu64is a compile error, not a red assertion. That is a weaker gate than a failing assert, and the lane said so in the code rather than leaving it implied.A separate defect found here, filed rather than folded in
The field is populated by "any
TYPE_POSITIONtype ref inside the container span, outside every child span" — which is not what its name and doc promise. A Rustenumdeclares zero child symbols, soExpr's 27 entries are variant payload types, not supertypes; a JavaScriptstatic {}block'snew A2()lands there the same way.Bounding the list does not make that right. Correctly left out of a bounding fix and filed separately — a behaviour change smuggled into a cap would have been unattributable, which is the same reason you refused to ship the cap on its own.
Gates
fmt✓,clippy -D warnings✓,cargo test --workspace✓ (329 suites, 0 failures),COSI_E2E_LEG=daemon✓ (62 suites), rustdoc-D warnings✓,doc_citation_gate✓,corpus_ratchet✓,precision_gate7/7phantom_count == 0✓.tests/corpus/baseline.jsondid not move. Post-merge by me:bounding_site_registry17 passed,code-index-daemon --lib194 passed, both EXIT=0.unexpanded_supertypesdoes not hold supertypes: it holds any TYPE_POSITION ref in the container span, so a Rust enum ships its variant payload types #240evidence_gapswarns on every reply that the answer may be short, and gives no way to find out which file —index_coverage(path)needs the path you are trying to learn #241unexpanded_supertypesdoes not hold supertypes: it holds any TYPE_POSITION ref in the container span, so a Rust enum ships its variant payload types #240Correction to my close, and it invalidates two numbers I quoted as measured
#240 is now fixed (
efe6f52), and fixing it showed that two things I wrote above were wrong.1. "
Expr… would have shipped 27 strings into asafe_deletereply" is false. A Rustenumemits zero child symbols — measured, 0 of 825 enums in rust-analyzer — andmember_census_basisruns only for a symbol whose parent is a member container. So no enum has ever been reachable through the shipped tool. I asserted a consequence as measured when only the predicate had been measured.2. The
max 37 / over-8 34table is the same artefact. Those numbers came from running the predicate over every container, not over the population the census can actually reach. Post-#240, with a declared-supertype role instead of span containment, the largest declared supertype list anywhere on the pinned corpus is 5, and nothing reaches 8.So the honest state of
SUPERTYPE_LIST_CAPis the one you refused to ship it in: a cap with no measured caller. It is kept — the list has no structural bound, and the cut is graded at 12 by fixture — but its doc no longer claims to be live on real code.What survives, and it is the part that mattered. Your refusal to ship a bound whose only evidence is that it compiles was right, and it is right for a reason better than the one I gave when closing this. I answered it with a corpus number that turned out to be an artefact of my own query; the discipline you were applying is what eventually surfaced that, two issues later.
What is genuinely unchanged is the three-state work:
unexpanded_supertypes_totalis always present, counted over the full result set on the same cursor, so "nothing was cut" is still a measurement rather than an absence — and #240 kept every arm of it.Leaving this closed; the correction belongs on the record rather than in a reopen.