You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Soundness: the #293 teardown/guard predicates swallow a leak behind an early-return guard and in the else-branch of if (disposing) — adversarial audit, 2 P1 holes pinned red #305
Change type: soundness fix (false negative), not a precision fix
Priority:P1 — both holes silently swallow a leak class, and both sit one mechanical rewrite away from shapes the corpus already pins
Found by: adversarial code reading of the landed predicates (enumerate implicit axioms → attack each → verify against Program.cs), full record in docs/notes/teardown-predicate-adversarial-audit.md
Hole A — a parameter guard spelled as an early return is invisible
publicvoidDispose(){Cleanup(keepAlive:true);}// the only callerpublicvoidCleanup(boolkeepAlive){if(keepAlive)return;// sibling, not ancestor_properties.PropertyChanged-=OnPropertiesChanged;// never reached — CREDITED}
IsParamGuardedRelease walks lexical ancestors of the -=; an early return is a preceding sibling, so the caller-controlled skip is invisible — while the symbol closure credits Cleanup because Dispose calls it, regardless of argument values. The SectorTS UnregOnlyGoodys guard — the exact leak #293 was built to catch — reopens under a semantics-preserving rewrite from if (!flag) { -= } to if (flag) return; -=. This is the C#-side twin of the bridge's D7 defect, fixed there as INF-S3.
Red fixture: corpus/wpf/subscription-teardown-early-return-guard (.own reduction caught by the core; extractor silent).
Hole B — the canonical if (disposing) exception credits the ELSE branch
IsCanonicalDisposingGuardUse classifies the parameter identifier's use in the condition (positive vs negated) and never asks which branch holds the site. The else-branch runs only via ~Finalizer → Dispose(false) — the path the extractor's own finalizer doctrine (#278 follow-up 1) declares unreachable while the subscription pins the subscriber. The predicate contradicts its own doctrine one branch away from the case it handles.
Red fixture: corpus/wpf/subscription-disposing-else-branch-release.
Also recorded (audit §2, no fixtures yet)
C (P2): parameter laundered through a local (bool s = skip; if (!s) { -= }) — guard check sees only IParameterSymbol;
E (minor): switch-arm when-clauses are outside the inspected condition set;
Eleven attacks the predicate survives are recorded in audit §3 (positive assurance).
Fix direction (bounded, lexical — the #293 style, no call graph)
Demote when a parameter-conditioned return lexically precedes the site within the enclosing callable — EXCEPT the canonical negated-disposing exit (if (!disposing) return;), which guarantees the site runs;
the canonical-disposing exception applies only to sites in the THEN branch of the positive guard;
(with C/E) taint parameter-initialized locals in guard conditions; include when clauses.
Worst case of each rule stays "kept warning". Timer twins (timer-stop-*) land with the fix — same predicate, same holes.
Acceptance
both red fixtures flip to caught; subscription-param-guarded-unregister and the canonical positive-branch shape (WinFormsDisposalSample) stay exactly as today;
no new FP on any after.cs (benchmark: fixes clean, 0 FPs);
oracle sweep shows no regression on the "-= genuinely in Dispose" shapes.
Classification
Program.cs), full record indocs/notes/teardown-predicate-adversarial-audit.md-=in the class as a release — a-=behind a flag, or in a method nobody calls, silently swallows a real leak (heap-proven on SectorTS) #278 / fix(extractor): #278 — a-=releases only in a proven, unguarded teardown context #293 (the predicates under audit), fix(extractor): WPF002 Stop() teardown soundness — one doctrine with-=#302 (Stop() shares the same predicate — every hole applies verbatim to WPF002), Post-cutover: summary-backed lifecycle release reachability (generalize the landed #293/#302 predicates) #304 (the general post-cutover answer), P-036Hole A — a parameter guard spelled as an early return is invisible
IsParamGuardedReleasewalks lexical ancestors of the-=; an earlyreturnis a preceding sibling, so the caller-controlled skip is invisible — while the symbol closure creditsCleanupbecauseDisposecalls it, regardless of argument values. The SectorTSUnregOnlyGoodysguard — the exact leak #293 was built to catch — reopens under a semantics-preserving rewrite fromif (!flag) { -= }toif (flag) return; -=. This is the C#-side twin of the bridge's D7 defect, fixed there as INF-S3.Red fixture:
corpus/wpf/subscription-teardown-early-return-guard(.ownreduction caught by the core; extractor silent).Hole B — the canonical
if (disposing)exception credits the ELSE branchIsCanonicalDisposingGuardUseclassifies the parameter identifier's use in the condition (positive vs negated) and never asks which branch holds the site. The else-branch runs only via~Finalizer → Dispose(false)— the path the extractor's own finalizer doctrine (#278 follow-up 1) declares unreachable while the subscription pins the subscriber. The predicate contradicts its own doctrine one branch away from the case it handles.Red fixture:
corpus/wpf/subscription-disposing-else-branch-release.Also recorded (audit §2, no fixtures yet)
bool s = skip; if (!s) { -= }) — guard check sees onlyIParameterSymbol;when-clauses are outside the inspected condition set;Disposecredited on a type that does not implementIDisposable(effect without enrollment) — cheap bounded improvement available;+=/-=— dataflow, honestly out of S0.Eleven attacks the predicate survives are recorded in audit §3 (positive assurance).
Fix direction (bounded, lexical — the #293 style, no call graph)
returnlexically precedes the site within the enclosing callable — EXCEPT the canonical negated-disposing exit (if (!disposing) return;), which guarantees the site runs;whenclauses.Worst case of each rule stays "kept warning". Timer twins (
timer-stop-*) land with the fix — same predicate, same holes.Acceptance
subscription-param-guarded-unregisterand the canonical positive-branch shape (WinFormsDisposalSample) stay exactly as today;after.cs(benchmark: fixes clean, 0 FPs);-=genuinely in Dispose" shapes.