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

@PhysShell PhysShell 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

coderabbitai Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants