Skip to content

fix(extractor): WPF002 Stop() teardown soundness — one doctrine with -= - #302

Merged
PhysShell merged 3 commits into
mainfrom
claude/wpf002-stop-teardown
Jul 19, 2026
Merged

fix(extractor): WPF002 Stop() teardown soundness — one doctrine with -=#302
PhysShell merged 3 commits into
mainfrom
claude/wpf002-stop-teardown

Conversation

@PhysShell

@PhysShell PhysShell commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Закрытие WPF002 Stop()-дыры — того же класса false negative, который #278 закрыл для -=: существование cleanup не является доказательством его исполнения. До этого PR stopped-сет кредитовал ЛЮБОЙ <recv>.Stop() где угодно в классе; таймеру достаточно было Stop() в произвольном методе, финализаторе, невайренном Window_Closing, невызванном helper'е/lambda или за caller-parameter guard'ом, чтобы замолчать.

Новый контракт: Stop existed AND targets the matching timer receiver AND executes in a proven teardown context AND is not caller-parameter-guarded → released. Реализация — не вторая копия модели, а буквально общий предикат: InTeardownContext и IsParamGuardedRelease обобщены с AssignmentExpressionSyntax до SyntaxNode (оба и так только ходили вверх по предкам), и сбор stopped гейтится ими же — со всей доктриной #278: точные teardown-roots (Dispose/DisposeAsync/OnClosed/…), code-wired lifecycle handlers, symbol-resolved транзитивные helper'ы, исключения finalizer/unwired-name/uncalled local-fn/lambda/param-guard (с каноничным if (disposing)). Receiver identity не менялся: чужой Stop() по-прежнему не освобождает проверяемый таймер.

История (три ступени): red 8f936f5 (7 corpus-фикстур + перегенерация P-022 parity) → green 07ac2cf (общий предикат + gating + комментарии Program.cs) → docs c1d4fe6 (subscription-leaks-and-profiles.md).

Фикстуры на DispatcherTimer stand-in (in-file, как в samples/SampleTypes.cs): он не IDisposable — Stop() и есть release (сам паттерн WPF002), и WPF003 (disposable-field) не контаминирует specificity-половину benchmark.

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

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

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

  • RED воспроизведён на реальном C# (extractor + core): 6 негативных before.cs ложно молчали — timer fact получал released: true от постороннего Stop:
    timer-stop-nonteardown-release (произвольный метод) · timer-stop-finalizer-release · timer-stop-unwired-lifecycle (Window_Closing без wiring) · timer-stop-uncalled-helper · timer-stop-uncalled-lambda · timer-stop-param-guarded; седьмой — timer-stop-wrong-receiver — пин уже честного receiver-matching
  • GREEN: 7/7 before → OWN001 [resource: timer] c released: false; 7/7 after (positive twins: Dispose / OnClosed / wired handler / транзитивный helper из Dispose / wired lambda / field-guard) — silent, released: true
  • Полный corpus benchmark на реальном C#: 57/58 caught · 58/58 fixes clean · 0 false positives (floor 25 не менялся); единственный miss — viewmodel-escapes-to-app, документированный pre-existing backlog (injected-source region-escape, в файле нет ни одного Stop, диффом не затрагивается); других corpus-дельт вне добавленных timer-stop кейсов нет
  • -=-матрица Soundness: OWN001 treats any -= 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 без изменения поведения: все subscription-* кейсы caught/clean как раньше; extractor-S2 samples идентичны (TimerViewModel flagged / CleanTimerViewModel credited)
  • wpf-suite 24/24 (было 17/17); P-022 parity fixtures перегенерированы (cfg 83 / diag 84), frozen-corpus Rust parity зелёный; полный python suite + ruff; build 0 warnings
  • Непризнанный Stop сохраняет timer fact с released: false (проверено по JSON-фактам)

Связанные issue

Продолжение линии #278 (та же teardown-доктрина, перенесённая на второй release-механизм WPF002). Refs #238 (доктрина «худший случай exemption — честное предупреждение»).

Чеклист

  • изменение покрыто тестом/селфтестом
  • README/docs обновлены при необходимости (header Program.cs + subscription-leaks-and-profiles.md — формулировки «any Stop()» устранены)
  • коммиты в conventional-commit стиле

Draft — не мержить, не self-accept. Гейт: независимый review — подтверждение, что (1) предикат действительно один (не копия), (2) все 6 негативных контекстов закрыты и позитивные twins не потеряны, (3) receiver identity не переопределён, (4) -=-матрица не сдвинута, (5) вне scope ничего не протащено (XAML-wiring, call graph, CFG-teardown, схема OwnIR, Python core, Rust crates).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK

Summary by CodeRabbit

  • Bug Fixes

    • Improved detection of timer resources that are not reliably stopped during teardown.
    • Prevents false release credit for finalizers, uncalled helpers or lambdas, unwired lifecycle methods, caller-controlled guards, and the wrong timer instance.
    • Correctly recognizes deterministic cleanup patterns, including disposal and lifecycle-triggered shutdown.
  • Documentation

    • Clarified timer release and teardown-soundness rules.
  • Tests

    • Added comprehensive coverage for timer cleanup and diagnostic scenarios.

The stopped set credits ANY <recv>.Stop() anywhere in the class — existence
as proof of execution, the exact false-negative class #278 closed for -=.
Reproduced on real C# through the extractor + core (DispatcherTimer
stand-in — not IDisposable, so Stop() IS the release and WPF003 stays out
of the way): six before.cs are falsely SILENT today (timer fact stamped
released: true by a Stop() that nothing proves runs):

* timer-stop-nonteardown-release — Stop() only in an arbitrary method
* timer-stop-finalizer-release   — Stop() only in the finalizer
* timer-stop-unwired-lifecycle   — Stop() in an unwired Window_Closing
* timer-stop-uncalled-helper     — Stop() in a helper no teardown calls
* timer-stop-uncalled-lambda     — Stop() in a stored, never-invoked lambda
* timer-stop-param-guarded       — Stop() behind a caller-parameter guard

Each after.cs is the positive twin (Dispose / platform teardown / wired
handler / transitively-called helper / wired lambda / field guard) and is
silent both before and after the fix. The seventh case pins what already
holds: timer-stop-wrong-receiver — a sibling's Stop() must not release the
checked timer (receiver identity; flagged today, stays flagged).

case.own reductions extend the wpf suite to 24/24; the P-022 parity
fixtures (cfg 83 / diag 84 cases) are regenerated for the new corpus and
the frozen-corpus Rust parity test stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The extractor now credits timer Stop() calls only when teardown execution is proven and parameter guards do not block release. Seven WPF timer corpus cases, documentation notes, and CFG/diagnostic parity fixtures were added.

Changes

WPF timer Stop ownership

Layer / File(s) Summary
Generalize timer teardown detection
frontend/roslyn/OwnSharp.Extractor/Program.cs
Timer .Stop() discovery now reuses teardown-context and parameter-guard checks before recording a released timer receiver.
Add timer ownership pattern corpus
corpus/wpf/timer-stop-*/before.cs, corpus/wpf/timer-stop-*/after.cs, corpus/wpf/timer-stop-*/case.own, corpus/wpf/timer-stop-*/expected-diagnostics.txt, corpus/wpf/timer-stop-*/notes.md
Adds seven WPF timer scenarios covering finalizers, non-teardown methods, guarded stops, uncalled helpers and lambdas, unwired lifecycle handlers, and wrong receivers, with corresponding corrected examples.
Register parity fixtures and rule documentation
docs/notes/subscription-leaks-and-profiles.md, tests/fixtures/cfg_parity.json, tests/fixtures/diag_parity.json
Documents the Stop release criteria and records CFG and OWN001 diagnostic expectations for the new corpus cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RoslynSyntax
  participant TeardownPredicates
  participant TimerAnalysis
  RoslynSyntax->>TeardownPredicates: inspect Stop invocation
  TeardownPredicates->>TeardownPredicates: verify teardown context and guard status
  TeardownPredicates->>TimerAnalysis: provide eligible receiver
  TimerAnalysis->>TimerAnalysis: record stopped timer or retain OWN001
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement the directly linked #39 SARIF output requirement; it instead focuses on WPF timer soundness. Add SARIF output for oracle-related actions and align the code and docs with issue #39's required format.
Out of Scope Changes check ⚠️ Warning Most changes, including WPF timer fixtures, extractor logic, docs, and parity data, are unrelated to #39's SARIF requirement. Remove or split out the unrelated WPF timer corpus, extractor, and docs changes, and keep this PR scoped to SARIF output.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main change: tightening WPF002 Stop() teardown soundness by reusing the -= doctrine.
Description check ✅ Passed The description follows the template sections and is mostly complete, with clear scope, change type, verification, related issues, and checklist items.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wpf002-stop-teardown

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 2 commits July 19, 2026 03:01
…ctrine

One predicate, one context model: InTeardownContext and
IsParamGuardedRelease generalize from AssignmentExpressionSyntax to
SyntaxNode (they only ever walked ancestors), and the stopped set now
admits a receiver only when its .Stop() invocation passes BOTH — the exact
gate the -= release has had since #278. A Stop() in an arbitrary method,
a finalizer, an unwired lifecycle-looking handler, an uncalled helper or
lambda, or behind a caller-parameter guard credits nothing; the timer
fact keeps released: false and the honest OWN001 [resource: timer]
stands. Receiver identity is unchanged: a sibling's Stop() never
releases the checked timer.

Verified on real C# through the extractor + core: all 7 red before.cs now
flag OWN001 (released: false), all 7 after.cs twins stay silent
(released: true); the extractor-S2 samples behave identically
(TimerViewModel flagged / CleanTimerViewModel credited); full corpus
benchmark 57/58 caught, 58/58 fixes clean, 0 false positives (the one
miss is the documented pre-existing injected-source region-escape
backlog case, which contains no Stop at all); build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
subscription-leaks-and-profiles.md records the extended doctrine: the
WPF002 row's Stop() release uses the SAME teardown/guard context
predicate as -=, with the seven timer-stop corpus cases listed beside
the #278 subscription set. The extractor-header wording fix rode the
green commit (it sits in Program.cs); tcplistener-stop-release.md is
deliberately untouched — the flow-local TcpListener.Stop() release is a
different mechanism, outside this slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdYFKUaygHz1T9H1qJ7BqK
@PhysShell
PhysShell force-pushed the claude/wpf002-stop-teardown branch from 6309312 to c1d4fe6 Compare July 19, 2026 03:01

Copy link
Copy Markdown
Owner Author

Независимый review — финальный verdict на head c1d4fe6f49e076cff94ff41dd461021513b10729:

APPROVED, no blocking findings. Verified on exact head c1d4fe6: three-stage RED→GREEN→DOCS history, one shared teardown/guard predicate for -= and WPF002 Stop(), all six false-negative contexts closed, positive twins preserved, receiver identity unchanged, additive parity update, no out-of-scope changes, and green CI.

Merge авторизован при post-draft гейтах (CodeRabbit/Codex review завершены; head неизменен; CI зелёный; mergeable clean; нет unresolved blocking threads). Косметический nit («Does this -= sit…» перед `InTeardownContext») — по решению reviewer'а head из-за него не двигается; уходит в residual-очередь на следующую правку этого файла.


Generated by Claude Code

@PhysShell
PhysShell marked this pull request as ready for review July 19, 2026 03:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1d4fe6f49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/roslyn/OwnSharp.Extractor/Program.cs (1)

5196-5208: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Handle conditional access for .Stop() invocations.

The current check strictly matches MemberAccessExpressionSyntax, which misses idiomatic conditional-access teardowns like _timer?.Stop(). In a conditional access tree, the invocation's expression is a MemberBindingExpressionSyntax (.Stop), and the receiver is the Expression of the enclosing ConditionalAccessExpressionSyntax.

Consider expanding this logic to capture both patterns, as _timer?.Stop() is a common, safe cleanup practice that will currently be ignored, resulting in a false positive (unreleased timer).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/roslyn/OwnSharp.Extractor/Program.cs` around lines 5196 - 5208,
Update the stopped-timer detection loop around stopped and InTeardownContext to
recognize both direct MemberAccessExpressionSyntax calls and conditional-access
invocations whose MemberBindingExpressionSyntax is Stop. For conditional access,
use the enclosing ConditionalAccessExpressionSyntax.Expression as the receiver
when recording the stopped timer, while preserving the existing teardown and
parameter-guard checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 5196-5208: Update the stopped-timer detection loop around stopped
and InTeardownContext to recognize both direct MemberAccessExpressionSyntax
calls and conditional-access invocations whose MemberBindingExpressionSyntax is
Stop. For conditional access, use the enclosing
ConditionalAccessExpressionSyntax.Expression as the receiver when recording the
stopped timer, while preserving the existing teardown and parameter-guard
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d4c638e8-31be-4f0f-a03b-1a206f84b3a7

📥 Commits

Reviewing files that changed from the base of the PR and between 70b33a9 and c1d4fe6.

📒 Files selected for processing (39)
  • corpus/wpf/timer-stop-finalizer-release/after.cs
  • corpus/wpf/timer-stop-finalizer-release/before.cs
  • corpus/wpf/timer-stop-finalizer-release/case.own
  • corpus/wpf/timer-stop-finalizer-release/expected-diagnostics.txt
  • corpus/wpf/timer-stop-finalizer-release/notes.md
  • corpus/wpf/timer-stop-nonteardown-release/after.cs
  • corpus/wpf/timer-stop-nonteardown-release/before.cs
  • corpus/wpf/timer-stop-nonteardown-release/case.own
  • corpus/wpf/timer-stop-nonteardown-release/expected-diagnostics.txt
  • corpus/wpf/timer-stop-nonteardown-release/notes.md
  • corpus/wpf/timer-stop-param-guarded/after.cs
  • corpus/wpf/timer-stop-param-guarded/before.cs
  • corpus/wpf/timer-stop-param-guarded/case.own
  • corpus/wpf/timer-stop-param-guarded/expected-diagnostics.txt
  • corpus/wpf/timer-stop-param-guarded/notes.md
  • corpus/wpf/timer-stop-uncalled-helper/after.cs
  • corpus/wpf/timer-stop-uncalled-helper/before.cs
  • corpus/wpf/timer-stop-uncalled-helper/case.own
  • corpus/wpf/timer-stop-uncalled-helper/expected-diagnostics.txt
  • corpus/wpf/timer-stop-uncalled-helper/notes.md
  • corpus/wpf/timer-stop-uncalled-lambda/after.cs
  • corpus/wpf/timer-stop-uncalled-lambda/before.cs
  • corpus/wpf/timer-stop-uncalled-lambda/case.own
  • corpus/wpf/timer-stop-uncalled-lambda/expected-diagnostics.txt
  • corpus/wpf/timer-stop-uncalled-lambda/notes.md
  • corpus/wpf/timer-stop-unwired-lifecycle/after.cs
  • corpus/wpf/timer-stop-unwired-lifecycle/before.cs
  • corpus/wpf/timer-stop-unwired-lifecycle/case.own
  • corpus/wpf/timer-stop-unwired-lifecycle/expected-diagnostics.txt
  • corpus/wpf/timer-stop-unwired-lifecycle/notes.md
  • corpus/wpf/timer-stop-wrong-receiver/after.cs
  • corpus/wpf/timer-stop-wrong-receiver/before.cs
  • corpus/wpf/timer-stop-wrong-receiver/case.own
  • corpus/wpf/timer-stop-wrong-receiver/expected-diagnostics.txt
  • corpus/wpf/timer-stop-wrong-receiver/notes.md
  • docs/notes/subscription-leaks-and-profiles.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • tests/fixtures/cfg_parity.json
  • tests/fixtures/diag_parity.json

@PhysShell
PhysShell merged commit 2c30c56 into main Jul 19, 2026
44 of 45 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

Development

Successfully merging this pull request may close these issues.

2 participants