Skip to content

fix(extractor): #278 — a -= releases only in a proven, unguarded teardown context - #293

Merged
PhysShell merged 7 commits into
mainfrom
claude/own278-soundness-scope-uqvuiu
Jul 18, 2026
Merged

fix(extractor): #278 — a -= releases only in a proven, unguarded teardown context#293
PhysShell merged 7 commits into
mainfrom
claude/own278-soundness-scope-uqvuiu

Conversation

@PhysShell

@PhysShellPhysShell commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Устраняет soundness false negative OWN001 из #278: любой matching -= где угодно в классе засчитывался как гарантированный release — heap-proven leak SectorTS GTD (ctor +=, -= под if (!UnregOnlyGoodys) в non-teardown методе) молча глотался. Теперь -= кредитует release только в доказанном teardown-контексте (Dispose/DisposeAsync/OnClosed/Unloaded-style, wired-in-code lifecycle handler, либо метод/local function, который teardown-путь доказуемо вызывает — symbol-based fixpoint) и без parameter-guard (каноничный позитивный if (disposing) — исключение). Follow-up 1 закрыл четыре silent-exemption пути: finalizer (недостижим, пока delegate держит subscriber), name-only Window_Closing без кодового wiring, name-keyed overload conflation (Cleanup() vs Cleanup(bool)), lexical inheritance для невызванных local functions/lambda. Follow-up 2 закрыл unresolved lifecycle-handler overload ambiguity: при неразрешённом событии method-group имя кредитует teardown только при ровно одном собственном IMethodSymbol с этим именем (0 или 2+ — release не доказан, warning сохраняется); CandidateSymbols-ambiguity больше не используется как teardown evidence; symbol-resolved path точен без изменений. Изменение только в C# Roslyn extractor; OwnIR schema, Python/Rust core, autofix pipeline, timer .Stop(), OwnAudit не тронуты.

CODE SCOPE — ACCEPTED / CLOSED / FROZEN (head 18000d0): новых code changes не будет, кроме исправления фактической ошибки, найденной CI или review.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — без изменения поведения

Как проверено

  • python tests/run_tests.py
  • ruff check . и mypy
  • селфтесты затронутых скриптов (python scripts/<...>.py --selftest)
  • scripts/benchmark.py: 40/44 (до) → 47/51 caught · 51/51 fixes clean · 0 FP; все прежние строки байт-в-байт
  • exact diff frontend/roslyn/samples (только два intended-флипа в FixCandidatesSample из slice 1, golden перегенерирован по tests/goldens/README.md; follow-up 1 и 2 — byte-identical), check_fix_candidates_facts.py, weak-subscribe checks, S0 Part B, tests/gate_regressions.sh
  • CFG/diag parity-фикстуры регенерированы (аддитивно); Rust parity зелёный (cargo test, включая full_parity_on_the_frozen_corpus), Rust-код не менялся
  • 5-repo sweep (WindowsDesktop ref pack): CsvHelper/Dapper/Newtonsoft.Json/RestSharp — идентично; ScreenToGif +9, все одной классифицированной формы (реальные -= в *_Closing, wired только в XAML — осознанный kept-warning trade-off, кандидаты XAML-aware slice). Детали: docs/notes/own278-corpus-diff.md
  • SectorTS-редукция: GTD/PGC → OWN001, KDT остаётся OWN001, Dispose-сосед silent
  • Red fixtures фактически доказаны против предыдущих heads (каждый before.cs silent до фикса, caught после)

Связанные issue

Closes#278. Refs #238, #240, #270, PhysShell/OwnAudit#13.

Чеклист

  • изменение покрыто тестом/селфтестом (или объяснено, почему нет)
  • README/docs обновлены при необходимости
  • коммиты в conventional-commit стиле (feat:, fix:, docs: …)

Draft — не мержить. Merge заблокирован тремя внешними gates:

  1. G50 Acceptance Run Phase 2: net472 build/tests/runtime;
  2. OwnAudit STS baseline: GTD классифицирован как runtime-only;
  3. реальный прогон PR head 18000d0 по STS_new/SectorTS.

После gates: приложить evidence к PR → дождаться полного зелёного CI → ready-for-review → review → merge → повторить OwnAudit GTD scenario и доказать переход runtime-only → confirmed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK

claude added 4 commits July 18, 2026 13:33
Two corpus/wpf cases pin the OWN001 false negative from issue #278, both
heap-motivated by the SectorTS GTD leak (66% retained heap, ClrMD-proven):
* subscription-param-guarded-unregister — ctor `+=`, the only `-=` inside
`UnregisterEventHandlers(bool UnregOnlyGoodys)` behind `if (!UnregOnlyGoodys)`;
the leaking callers pass `true`. before.cs must be OWN001; after.cs releases
unconditionally in Dispose and must stay silent.
* subscription-nonteardown-release — ctor `+=`, an unconditional `-=` in an
arbitrary non-teardown method nobody is proven to call. before.cs must be
OWN001; after.cs detaches in a handler wired to the class's own Unloaded
lifecycle event and must stay silent (the recognised-teardown control).
Under the shipped "any matching `-=` in the class = released" model both
before.cs are silent — the red half of this pair. The case.own reductions
already fail honestly (guard modelled as an early return past the release;
non-teardown `-=` not modelled as a release).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
…rdown context
Honour P-001/P-004 as written: a matching `target -= handler` credits the
subscription's release ONLY when it is proven to run at the subscriber's
end-of-life. The `unsub` collector now requires both:
* a recognised TEARDOWN CONTEXT — Dispose/DisposeAsync/OnClosed/OnClosing/
OnUnloaded/OnFormClosed/OnFormClosing by name, a finalizer, a handler wired
(`+=`, bare/`this.` receiver) to the class's OWN Closed/Closing/Unloaded/
FormClosed/FormClosing/Disposed lifecycle event (inline lambda handlers
included), the XAML-wiring `*_Closed`/`*_Closing`/`*_Unloaded`/... naming
convention, or any method such a context calls directly on `this`
(intra-class fixpoint — deliberately NO whole-program call graph);
* no parameter guard — a `-=` under a branch whose condition depends on a
parameter of its enclosing method cannot be proven to run from the
subscription site (SectorTS: `if (!UnregOnlyGoodys)`, callers pass true).
The one canonical exception is a POSITIVE `if (disposing)` in
`Dispose(bool)`; `if (!disposing)` still demotes.
A `-=` in an arbitrary method, a ctor, or behind a caller-controlled flag now
keeps the honest OWN001/OWN014 instead of silently swallowing the leak class —
the #238 doctrine. Self-detaching handlers, old->new rotation and the timer
`.Stop()` release are untouched. OwnIR schema, the Python core, and the S0/S2
fix pipeline are unchanged (the `--fix-candidates` teardown metadata keeps its
own candidate scan by design).
Evidence (docs/notes/own278-corpus-diff.md): corpus benchmark 40/44 -> 42/46
caught, 46/46 fixes clean, 0 FPs, every pre-existing row byte-identical; the
samples diff flips exactly InpcAmbiguousTeardown + HandlerReassignedField
(golden regenerated per tests/goldens/README.md, byte-parity gate passes);
ScreenToGif sweep +4 findings, all one triaged shape (release only in a
custom-named Destroy()); CsvHelper and the oracle push-target fixture
byte-identical. The SectorTS reduction now flags GTD and PGC while KDT stays
flagged and a Dispose-releasing sibling stays silent.
Closes#278 acceptance rules 1-3; the call-graph reachability rule stays out
of this slice by design.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
…n unproven release
Four corpus/wpf cases pin the silent-exemption holes the #278 review found in
the first slice's teardown model. Each before.cs is SILENT under that slice
(verified against its extractor) and must be OWN001:
* subscription-finalizer-release — the only `-=` in the finalizer. Circularly
unreachable: the publisher's delegate keeps the subscriber reachable, so the
finalizer never runs while the subscription is live.
* subscription-xaml-name-only-release — the only `-=` in a method NAMED
`Window_Closing` that nothing in code wires. A name is not wiring; a bare
handler-shaped name may be stale dead code. after.cs pins both wired good
forms (method group + inline lambda on `this.Closing`).
* subscription-overload-conflated-cleanup — Dispose calls `Cleanup()`; the
`-=` lives only in the uncalled `Cleanup(bool)`. A name-keyed closure
conflates the overloads.
* subscription-uncalled-local-function — the `-=` in a local function (and a
lambda) DECLARED inside Dispose but never invoked. Declaration is not
execution. after.cs pins the called-local-function good form.
The previously name-carried control screentogif-loaded-subscription/after.cs
now wires `Closing += Window_Closing` in code — the honest, provable form of
the same fix (the real ScreenToGif attaches it in XAML, which the extractor
never sees); the name-only shape moves to the new bad case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
…se teardown paths
1. A FINALIZER is no longer a teardown context (InTeardownContext:
DestructorDeclarationSyntax => false). For a subscription leak the
publisher's delegate keeps the subscriber reachable, so the finalizer never
runs while the subscription is live — its `-=` can never break the hold.
2. The `*_Closed`/`*_Closing`/`*_Unloaded`/... NAME-SUFFIX exemption is
removed. A XAML attach never reaches the extractor, so a name alone proves
nothing (it may be stale dead code). A `Window_Closing`-style handler
counts only when the class provably wires it in code; XAML-backed release
stays a kept warning until a XAML-aware slice can credit the attach with
evidence.
3. The intra-class teardown closure is SYMBOL-based (IMethodSymbol +
SymbolEqualityComparer): an invocation extends the set only with the
specific own method/local function it RESOLVES to, so `Dispose() =>
Cleanup();` credits exactly `Cleanup()` — never an uncalled `Cleanup(bool)`
overload. Unresolved calls extend nothing. One narrow name fallback stays,
for method-GROUP handlers wired to an UNRESOLVED lifecycle event
(`Closing +=` under an unreferenced WPF Window base): a method group
carries no argument list, so its name denotes the whole overload set — not
the invocation-overload conflation above.
4. Nested callables no longer inherit their lexical teardown context. A local
function counts only when the symbol closure proves a teardown CALLS it; a
lambda only as the handler wired to a lifecycle event. The closure walks
each callable's own body (never descending into nested function bodies), so
an invocation inside an uncalled nested function extends nothing either.
Timer .Stop(), rotation, self-detach, OwnIR schema, the core, and the fix
pipeline are unchanged. Evidence (docs/notes/own278-corpus-diff.md, follow-up
section): corpus benchmark 46/50 caught, 50/50 fixes clean, 0 FPs, all
pre-existing rows unchanged; samples output byte-identical to slice 1; golden
untouched; full suite/ruff/mypy green. 5-repo sweep: CsvHelper, Dapper,
Newtonsoft.Json, RestSharp identical; ScreenToGif +9, all one triaged shape —
real `-=` in `*_Closing` handlers wired ONLY in XAML, the deliberate rule-2
kept-warning trade-off and the first candidates for the XAML-aware slice.
SectorTS reduction unchanged: GTD/PGC/KDT flagged, Dispose sibling silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
@coderabbitai

coderabbitaiBot commented Jul 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PhysShell, you've reached your PR review limit, so we couldn't start this review.

Next review available in:19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98303d21-0792-4703-be4f-59f1a5255d27

📥 Commits

Reviewing files that changed from the base of the PR and between 366bbf9 and 18000d0.

📒 Files selected for processing (43)
  • corpus/real-world/screentogif-loaded-subscription/after.cs
  • corpus/real-world/screentogif-loaded-subscription/notes.md
  • corpus/wpf/subscription-ambiguous-overload-wiring/after.cs
  • corpus/wpf/subscription-ambiguous-overload-wiring/before.cs
  • corpus/wpf/subscription-ambiguous-overload-wiring/case.own
  • corpus/wpf/subscription-ambiguous-overload-wiring/expected-diagnostics.txt
  • corpus/wpf/subscription-ambiguous-overload-wiring/notes.md
  • corpus/wpf/subscription-finalizer-release/after.cs
  • corpus/wpf/subscription-finalizer-release/before.cs
  • corpus/wpf/subscription-finalizer-release/case.own
  • corpus/wpf/subscription-finalizer-release/expected-diagnostics.txt
  • corpus/wpf/subscription-finalizer-release/notes.md
  • corpus/wpf/subscription-nonteardown-release/after.cs
  • corpus/wpf/subscription-nonteardown-release/before.cs
  • corpus/wpf/subscription-nonteardown-release/case.own
  • corpus/wpf/subscription-nonteardown-release/expected-diagnostics.txt
  • corpus/wpf/subscription-nonteardown-release/notes.md
  • corpus/wpf/subscription-overload-conflated-cleanup/after.cs
  • corpus/wpf/subscription-overload-conflated-cleanup/before.cs
  • corpus/wpf/subscription-overload-conflated-cleanup/case.own
  • corpus/wpf/subscription-overload-conflated-cleanup/expected-diagnostics.txt
  • corpus/wpf/subscription-overload-conflated-cleanup/notes.md
  • corpus/wpf/subscription-param-guarded-unregister/after.cs
  • corpus/wpf/subscription-param-guarded-unregister/before.cs
  • corpus/wpf/subscription-param-guarded-unregister/case.own
  • corpus/wpf/subscription-param-guarded-unregister/expected-diagnostics.txt
  • corpus/wpf/subscription-param-guarded-unregister/notes.md
  • corpus/wpf/subscription-uncalled-local-function/after.cs
  • corpus/wpf/subscription-uncalled-local-function/before.cs
  • corpus/wpf/subscription-uncalled-local-function/case.own
  • corpus/wpf/subscription-uncalled-local-function/expected-diagnostics.txt
  • corpus/wpf/subscription-uncalled-local-function/notes.md
  • corpus/wpf/subscription-xaml-name-only-release/after.cs
  • corpus/wpf/subscription-xaml-name-only-release/before.cs
  • corpus/wpf/subscription-xaml-name-only-release/case.own
  • corpus/wpf/subscription-xaml-name-only-release/expected-diagnostics.txt
  • corpus/wpf/subscription-xaml-name-only-release/notes.md
  • docs/notes/own278-corpus-diff.md
  • docs/notes/subscription-leaks-and-profiles.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • tests/fixtures/cfg_parity.json
  • tests/fixtures/diag_parity.json
  • tests/goldens/fix_candidates_off.golden.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/own278-soundness-scope-uqvuiu

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

claude added 3 commits July 18, 2026 14:02
…rpus cases
tests/run_tests.py went red in CI: the P-022 Rust-parity fixtures
(tests/fixtures/cfg_parity.json, diag_parity.json) are generated over the
whole corpus, so the six new corpus/wpf case.own files made the committed
copies stale. Regenerated per the tests' own instruction
(test_cfg_fixtures.py --write / test_diag_fixtures.py --write) — the diff is
purely additive (the new cases' lowerings and (line, code) pairs) — and the
Rust side replays them clean: cargo test passes, including
full_parity_on_the_frozen_corpus, with no Rust changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
… ground a teardown
corpus/wpf/subscription-ambiguous-overload-wiring pins the last silent-
exemption residue: `Closing += Window_Closing` on an UNRESOLVED lifecycle
event with TWO `Window_Closing` overloads, where the delegate-compatible
overload detaches nothing and the `-=` sits in the never-attached sibling.
The runtime delegate attaches exactly one overload — chosen by the event's
delegate signature, the very information the extractor lacks — so the name
fallback that credited every same-named method silently cleared OWN001
(verified silent at the previous head). before.cs must be OWN001; after.cs
keeps a single `Window_Closing` holding the `-=` (unambiguous name) and must
stay silent.
CFG/diag parity fixtures regenerated for the new case.own (additive); the
Rust side replays them clean with no Rust changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
…en unambiguous
The unresolved-lifecycle-event fallback (`Closing += Window_Closing` where the
event binds no definite symbol) now grounds a teardown ONLY when exactly one
method with that name exists in the immediate class. Zero or 2+ same-named
methods credit nothing — the delegate attaches exactly one overload, selected
by the event's delegate signature the extractor is missing, so an ambiguous
name may not let a `-=` in the never-attached overload clear OWN001 (the
unresolved twin of the invocation-overload conflation closed previously). The
former CandidateSymbols crediting is removed along with it: candidates of a
failed method-group binding are the same ambiguous overload set by another
name. The symbol-RESOLVED path is unchanged — a resolved event credits the
delegate's exact target, even among overloads.
Evidence (docs/notes/own278-corpus-diff.md, follow-up 2): benchmark 47/51
caught, 51/51 fixes clean, 0 FPs, prior rows unchanged; samples byte-identical;
golden unchanged; suite/ruff/mypy, fix-candidates, S2 gates, and Rust parity
(full_parity_on_the_frozen_corpus) green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
@PhysShell
PhysShell marked this pull request as ready for review July 18, 2026 15:01
@PhysShell
PhysShell merged commit a7d8499 into mainJul 18, 2026
44 checks passed
PhysShell pushed a commit that referenced this pull request Jul 25, 2026
…, enrollment split
Four blockers plus the smaller fixes from the architecture review:
1. Migration plan no longer describes merged work as future. #258 is closed
(spec/Bridge.md is the merged normative contract); #278 is closed by #293
and extended to WPF002 Stop() by #302 — those landed extractor predicates
are named the current bounded implementation and the regression floor.
Phase 2 is retargeted to the new post-cutover tracker #304 (summary-backed
lifecycle release reachability); #278 stays the historical motivating
incident, not a reusable implementation issue.
2. MVP guarded-effects policy defined: summaries preserve guards over simple
boolean/null parameter predicates, callsite application substitutes
statically-known constants, anything outside that vocabulary degrades to
May/Unknown — never Must, never silence. Without this the summary layer
loses to the landed predicates on the flagship Teardown(bool) case.
input_contract semantics defined in the summary envelope.
3. Lifecycle reasoning split into two theorems: LifecycleEffect (release
happens IF the root runs) vs LifecycleEnrollment (this instance provably
reaches that root). Effect without enrollment is degraded/conditional,
never clean — a perfect Dispose() nobody calls proves nothing.
4. Bridge-boundary authority table added: until parity+cutover #258/#259 own
the boundary (MOS in own-bridge, byte-parity); after cutover a dedicated
extraction slice per this proposal; wire schema, verdicts, and parity
artifacts invariant across both.
Also: OwnCFG claim corrected to intended-MIR-equivalent (today: plain succ
edges, calls as instructions, AST re-export); Call-instruction vs
Invoke-terminator model made explicit; evidence split into a single proof DAG
vs a per-finding displayed witness.
Refs #303 review; tracker: #304.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015de4MezSeUnZBoWq1fFU5M
PhysShell pushed a commit that referenced this pull request Jul 25, 2026
…cates
Adversarial code reading of the landed teardown-context/guard predicates
(enumerate the implicit axioms, attack each, verify against Program.cs).
Full attack matrix — six confirmed hole families, eleven survived attacks,
doctrine assessment, bounded fix directions — in
docs/notes/teardown-predicate-adversarial-audit.md.
Two P1 holes (silently swallowed leaks, the #238 doctrine violation) are
pinned as red corpus fixtures; the CI corpus benchmark is the empirical
arbiter (before.cs MISSED expected — recall floor is an absolute count;
after.cs reuses proven-silent shapes):
- subscription-teardown-early-return-guard: the SectorTS flag guard rewritten
from `if (!flag) { -= }` to `if (flag) return; -=` — semantically identical,
invisible to IsParamGuardedRelease (ancestors-only walk), while the symbol
closure credits the helper regardless of argument values. The C# twin of the
bridge's D7/INF-S3 defect.
- subscription-disposing-else-branch-release: a `-=` in the ELSE of the
canonical `if (disposing)` is credited (the exception classifies the
parameter's use in the condition, never the branch holding the site) — yet
it runs only on the finalizer path the extractor's own doctrine declares
unreachable while the subscription is live.
Both .own reductions are caught by the branch-sensitive core (wpf corpus
26/26) — extractor gaps, not core gaps. cfg/diag parity fixtures regenerated
(additive); Rust parity green on the grown corpus. WPF002 Stop() shares the
predicate, so the holes apply verbatim — timer twins land with the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015de4MezSeUnZBoWq1fFU5M
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants

@PhysShell@claude