Skip to content

extractor: close two oracle FP tails — using-statement local release + no-op-dispose BCL wrappers#159

Merged
PhysShell merged 2 commits into
mainfrom
claude/mos-ownership-summary-n3q3j4
Jun 28, 2026
Merged

extractor: close two oracle FP tails — using-statement local release + no-op-dispose BCL wrappers#159
PhysShell merged 2 commits into
mainfrom
claude/mos-ownership-summary-n3q3j4

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Что и зачем

Закрываем два оставшихся открытых хвоста из docs/notes/oracle-known-fps.md — обе чинки sound и узкие (помним урок со static-class):

  1. Tail 1 (root-cause Remove unnecessary .txt extensions from example and source files #1): релизим уже-отслеживаемый локал, использованный как ресурс using (existingLocal) { ... }. --flow-locals уже умел using (var x = ...) и MemoryPool-форму, но не expression-форму над ранее захваченным локалом (var timer = new ...; using (timer) { ... }) — локал «висел живым» к выходу из метода → ложный OWN001. Теперь release протягивается на normal-completion и return/throw выходы (без acquire — локал уже захвачен на new). Чинит protobuf-net Page.xaml.cs timer.
  2. Tail 2 (root-cause Add buffer storage policies with mandatory logging #2): расширяем no-op-dispose исключение StringWriter/StringReader на BCL pass-through обёртки. Поле StreamReader/StreamWriter/BinaryReader/BinaryWriter, каждая конструкция которого оборачивает in-memory backing (MemoryStream/StringWriter/StringReader), каскадит Dispose только в managed-память → dispose-optional. Закрытый allowlist из 4 адаптеров (GZipStream/CryptoStream держат native-ресурс → остаются под флагом); путь new StreamReader(path) (реальный handle) поле под флагом оставляет.

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

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

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

  • селфтесты затронутых скриптов: oracle_compare.py --selftest → 31/31, validate_contrib.py --selftest → 16/16
  • python tests/test_corpus.py → 29/29 (два новых фикстура)
  • ruff check → clean
  • C# экстрактор собирается/гоняется в CI (golden C#, leak extractor, corpus benchmark) — локально .NET недоступен.

Связанные issue

Refs docs/notes/oracle-known-fps.md root-causes #1, #2. Самостоятельной issue нет.

Чеклист

Soundness wall (честно)

Tail 2 не закрывает мотивирующую находку — Newtonsoft TraceJsonReader._textWriter (JsonTextWriter над StringWriter). JsonTextWriterthird-party обёртка, доказать что её Dispose pass-through нельзя без анализа тела; молча душить = тот же unsound over-reach, что и отклонённое static-class исключение. Поэтому она остаётся в baseline, с записанным обоснованием. Аналогично NLog _xmlSource/_reusable*Stream (тоже third-party обёртки) остаются видимыми. Запись protobuf Page.xaml.cs помечена fix-landed и ждёт live-oracle прогона на confirm-and-delete.

🤖 Generated with Claude Code

https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm


Generated by Claude Code

…+ no-op-dispose BCL wrappers

Two precision fixes for the remaining open root-causes in oracle-known-fps.md.

Tail 1 (root-cause #1, custom dispose-sink): release a tracked local used as
the resource of a `using (existingLocal) { ... }` statement. The --flow-locals
lowering already handled the `using (var x = ...)` declaration form and the
MemoryPool owner statement form, but not the expression form over an
already-acquired local (`var timer = new ...; using (timer) { ... }`) — it left
the local looking live at method exit, a spurious OWN001. Now threads a
scope-exit release onto the body's normal completion and its return/throw exits
(no acquire — the local was acquired at its `new`). Clears protobuf-net's
assorted/ Silverlight Page.xaml.cs `timer` (baseline entry marked fix-landed,
pending a live-oracle re-run to confirm-and-delete).

Tail 2 (root-cause #2, no-op Dispose): extend the StringWriter/StringReader
field exemption to BCL pass-through wrappers. A StreamReader/StreamWriter/
BinaryReader/BinaryWriter field whose every construction wraps an in-memory
backing (MemoryStream/StringWriter/StringReader) cascades disposal only to
managed memory, so it is dispose-optional. Closed allowlist of those four
adapters (GZipStream/CryptoStream own native/extra resource — stay flagged);
a path building `new StreamReader(path)` (a real handle) keeps the field
flagged. Does NOT clear Newtonsoft's _textWriter (a third-party JsonTextWriter
wrapper whose Dispose we can't prove is pass-through without body analysis —
the soundness wall); that stays baselined, with the reasoning recorded so it
isn't unsoundly suppressed.

Corpus fixtures: local-dispose-via-using-statement, field-noop-dispose-wrapper
(each: before.cs caught / after.cs clean, proving the exemption stays narrow).
Docs: new no-op-dispose-wrapper.md; oracle-known-fps.md root-causes #1/#2 and
baseline annotations updated. Selftests: oracle 31/31, corpus 29/29,
validate_contrib 16/16, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

More reviews will be available in 3 minutes and 43 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

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 credits.

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e250b0c-3dd6-4e6e-a69e-980d89833639

📥 Commits

Reviewing files that changed from the base of the PR and between 69c79b0 and 017c67a.

📒 Files selected for processing (14)
  • corpus/oracle-fp-baseline.txt
  • corpus/real-world/field-noop-dispose-wrapper/after.cs
  • corpus/real-world/field-noop-dispose-wrapper/before.cs
  • corpus/real-world/field-noop-dispose-wrapper/case.own
  • corpus/real-world/field-noop-dispose-wrapper/expected-diagnostics.txt
  • corpus/real-world/field-noop-dispose-wrapper/notes.md
  • corpus/real-world/local-dispose-via-using-statement/after.cs
  • corpus/real-world/local-dispose-via-using-statement/before.cs
  • corpus/real-world/local-dispose-via-using-statement/case.own
  • corpus/real-world/local-dispose-via-using-statement/expected-diagnostics.txt
  • corpus/real-world/local-dispose-via-using-statement/notes.md
  • docs/notes/no-op-dispose-wrapper.md
  • docs/notes/oracle-known-fps.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/mos-ownership-summary-n3q3j4

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

@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: e8cdaafc4d

ℹ️ 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".

{
if (model.GetTypeInfo(oce).Type is not { } wt
|| wt.ContainingNamespace?.ToString() != "System.IO"
|| wt.Name is not ("StreamReader" or "StreamWriter" or "BinaryReader" or "BinaryWriter"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude buffered writers from no-op disposal

Including StreamWriter in this allowlist suppresses OWN001 for fields such as new StreamWriter(new MemoryStream()), but disposing a StreamWriter is not a no-op: Microsoft documents Dispose(bool) as flushing buffered data to the underlying stream (https://learn.microsoft.com/en-us/dotnet/api/system.io.streamwriter.dispose). If the owner never disposes or flushes the field, the MemoryStream can be missing buffered characters/encoder state, so this exemption can hide real correctness bugs rather than just managed-memory-only leaks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in 017c67a. Writers do flush on Dispose, so a never-disposed StreamWriter/BinaryWriter over an in-memory backing can silently drop buffered data — a real bug worth keeping the OWN001 for. The allowlist is now restricted to the read-only adapters StreamReader/BinaryReader (their Dispose only discards a read-ahead buffer and closes the in-memory backing). Docs/fixture notes updated to readers-only.


Generated by Claude Code

bodyOnThrow.AddRange(onThrow ?? exit);
}
if (us.Statement is not null
&& !LowerFlowStmt(us.Statement, tracked, model, nodes, canEscape, bodyOnThrow, bodyOnReturn))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Model explicit throws inside using bodies

When a using (existingLocal) body contains an explicit throw, this call passes a non-null bodyOnThrow into LowerFlowStmt, but the ThrowStatementSyntax case returns false whenever onThrow is non-null. That causes the entire method's flow facts to be skipped; for example, var leaked = new Res(); using (r) { if (bad) throw ...; } will no longer report the unrelated leaked local. The using lowering should route explicit throws through the release continuation instead of bailing the method.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 017c67a. You're right that the non-null bodyOnThrow makes the ThrowStatementSyntax case bail the whole method, losing unrelated leaks. The fully sound fix (route using-throws through the release continuation) needs an "onThrow is unconditional" signal threaded through the lowering to distinguish a using (no catch) from a try (a throw may be caught) — invasive, so deferred. For now the using-local release is gated on the body having no explicit escaping throw; when it does, we fall through to the plain lowering so the method stays analysed (status quo — r itself may still show undisposed on the throw path, no worse than before this fix). Documented in the fixture notes.


Generated by Claude Code

…ow bail)

Two P2 review findings from Codex:

1. Exclude buffered writers from the no-op-dispose allowlist. StreamWriter /
   BinaryWriter FLUSH buffered output on Dispose, so a never-disposed writer
   field can leave even an in-memory backing missing buffered data — a real
   correctness bug, not a managed-memory-only no-op. IsNoOpDisposeWrapper now
   allows only the read-only adapters StreamReader / BinaryReader, whose Dispose
   just discards a read-ahead buffer and closes the (in-memory) backing.

2. Don't bail the whole method on an explicit throw inside a `using (existingLocal)`
   body. Threading a non-null bodyOnThrow makes the ThrowStatementSyntax case
   return false (it refuses a non-null onThrow — a throw inside a try might be
   caught), which skipped the method's entire flow analysis and lost UNRELATED
   leaks. The using-local release is now gated on the body having no explicit
   escaping throw; otherwise it falls through to the plain lowering (method stays
   analysed). Routing using-throws through the release would need invasive
   onThrow-is-unconditional threading — deferred and documented.

Docs/fixtures updated to readers-only + the explicit-throw caveat. Selftests:
oracle 31/31, corpus 29/29, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
@PhysShell

Copy link
Copy Markdown
Owner Author

@CodeRabbit reviw

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

@PhysShell I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PhysShell PhysShell merged commit 673a736 into main Jun 28, 2026
15 checks passed
PhysShell pushed a commit that referenced this pull request Jun 28, 2026
…hrowDefinite)

Follow-up to PR #159's deferred item. An explicit throw inside a
using (existingLocal) body previously had to bail the whole method (the
ThrowStatementSyntax case refuses a non-null onThrow, since a throw inside a
try might be caught), losing detection of unrelated leaks; #159 worked around
it by gating the using-release out when the body threw.

This threads an `onThrowDefinite` flag alongside `onThrow` through the lowering
(LowerFlowStmt / LowerFlowStatements / LowerSwitchSection). It is true when a
throw that runs `onThrow` definitely leaves the method uncaught — a `using` or
a finally-only `try` (neither has a catch) — and false for a `try` with any
`catch` (the throw may be caught, a match this lowering does not model). The
throw case then routes the throw through the release continuation when definite,
and still bails otherwise. Throws inside a `finally` keep bailing (IsInsideFinally
guard, unchanged).

Net: the using-local release no longer needs the explicit-throw gate (removed);
finally-only `try` bodies with an explicit throw are now analysed instead of
bailed (sound bonus). try-with-catch is unchanged (still conservative). No
existing golden sample has an explicit throw in a using/finally-only-try body,
so no expected-output changes.

Fixture: using-statement-throw-releases (before.cs: an unrelated leak is still
caught despite a throw in the using body — a direct no-bail regression test;
after.cs clean). Selftests: corpus 30/30, oracle 31/31, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
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