Skip to content

feat(pool): pooled-buffer view-in-a-field dangle → OWN002 (POOL005)#78

Merged
PhysShell merged 2 commits into
mainfrom
claude/pooled-view-in-field
Jun 22, 2026
Merged

feat(pool): pooled-buffer view-in-a-field dangle → OWN002 (POOL005)#78
PhysShell merged 2 commits into
mainfrom
claude/pooled-view-in-field

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Generalises the field-mediated use-after-dispose pass (#75/#76) to pooled owners and their Memory views — the sound, zero-FP form of "view-in-a-field" (POOL005).

What

An IMemoryOwner<T> field released in Dispose() is a released owner, and a Memory/ReadOnlyMemory field assigned _owner.Memory is recorded as a view of that owner. A read of the view field — or of the owner directly — in a live subscription-target handler (directly or one hop through a private helper), with no if (_disposed) return; guard before it, is lowered to a synthetic acquire/release/use flow on the owner → OWN002. The view field aliases the owner, so reading it after the owner's release is a use of the owner after release.

ViewOwner(_owner.Memory) already returns the owner name for a bare-field receiver (the receiver is an IdentifierNameSyntax; the IMemoryOwner<T>.Memory symbol resolves regardless), so no new view-recognition was needed. A new ReleasedOwner(field) unifies the cases: the field itself when it is a released owner, or the owner it aliases when it is a view field. Both FirstUnguardedDisposedRead and the handler trigger loop route through it, so the existing disposed-field reads (#75/#76) are unchanged and view-field reads are purely additive.

Why this scope (the FP finding)

The naive reading — flag a public getter that exposes a pooled-backed Memory — is FP-dangerous: IMemoryOwner<T>.Memory is itself a legitimate "valid-until-Dispose" pattern, so exposing such a view is sound; the bug is the caller reading it post-Dispose, which is cross-object and not type-locally provable. The only type-local, zero-FP proof of a post-release read is the handler reachability (a live handler can fire after Dispose()) — the same argument that makes #75/#76 sound. So this reuses that, and exposure is deliberately not flagged.

Precision

  • Owner must be released in the dispose lifecycle; view→owner alias is symbol-resolved (IMemoryOwner<T>.Memory); a guarded or unsubscribed handler never fires.
  • FP audit: every IMemoryOwner in the existing corpus is a local (not a field) and there are no view-field assignments anywhere, so dispoFieldLine (now incl. IMemoryOwner) and viewFieldOwner stay empty on all existing cases — zero new fires.

Validation

  • case.own[OWN002] ... [resource: pooled buffer] (test_wpf 6/6, test_corpus green).
  • Full Python suite green (lifetimes, loops, spec, ownir 117/117); metamorphic 8/8 (44 .own swept).
  • Benchmark recall floor 22 → 23; the corpus-benchmark job validates the real C# under the SDK in CI.

Honest scope / follow-ups

IMemoryOwner owner + member-access view read (_view.Span). Follow-ups: an ArrayPool byte[] buffer field returned in Dispose() (interacts with the existing per-member pool-leak pass), a view passed as a bare argument (Consume(_view)), and element-access reads (_buf[i]).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Improved detection of use-after-dispose when pooled buffers are accessed after disposal via stored memory views that alias the original owner.
  • Tests

    • Added a new corpus scenario and expected diagnostic coverage for the pooled-memory-after-dispose case.
  • Documentation

    • Documented the pooled-buffer use-after-release pattern and expected diagnostic behavior.
  • Chores

    • Tightened the CI corpus benchmark recall threshold.

…IMemoryOwner field read after Dispose → OWN002

Generalises the field-mediated use-after-dispose pass (#75/#76) to POOLED owners
and their Memory views. An `IMemoryOwner<T>` field released in `Dispose()` is a
released owner, and a `Memory`/`ReadOnlyMemory` field assigned `_owner.Memory`
(recognised by ViewOwner, which returns the owner name for a bare-field receiver)
is recorded as a VIEW of that owner. A read of the view field — or of the owner
directly — in a live subscription-target handler (directly or one hop through a
private helper), with no `if (_disposed) return;` guard before it, is lowered to a
synthetic acquire/release/use flow on the owner → OWN002. The view-field aliases
the owner, so reading it after the owner's release is a use of the owner after
release.

New `ReleasedOwner(field)` unifies the two: the field itself when it is a released
owner, or the owner it aliases when it is a view field. `FirstUnguardedDisposedRead`
and the handler trigger loop both route through it, so direct disposed-field reads
(#75/#76) are unchanged and view-field reads are additive.

Precision: same handler reachability that makes the disposed-field UAF sound (a
live handler can fire after Dispose). The owner must be released in the dispose
lifecycle; the view→owner alias is symbol-resolved (`IMemoryOwner<T>.Memory`); a
guarded/unsubscribed handler never fires. Exposing such a view via a public getter
is deliberately NOT flagged — `IMemoryOwner.Memory` is itself a valid
"valid-until-Dispose" pattern, so only a provably-post-release read is a bug.

New corpus case corpus/wpf/pooled-view-after-dispose (before → OWN002, guarded
after → silent). Benchmark recall floor 22 -> 23. Scope: IMemoryOwner owner +
member-access view read; ArrayPool byte[]-buffer-field views, bare-arg reads, and
element-access reads are honest follow-ups. Python suite + metamorphic green; C#
validated by CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f6599d7a-582d-4859-8e18-1df516ae3989

📥 Commits

Reviewing files that changed from the base of the PR and between 243c439 and fa09b1c.

📒 Files selected for processing (1)
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

📝 Walkthrough

Walkthrough

Extends the OwnSharp extractor's --flow-locals pooled-buffer analysis to recognize Memory<T>/ReadOnlyMemory<T> view fields that alias an IMemoryOwner<T> owner field, resolving post-dispose reads of those view fields to the underlying owner and emitting OWN002. A new corpus test case (pooled-view-after-dispose) exercises this detection, and the CI recall gate is incremented from 22 to 23.

Changes

IMemoryOwner View-Field Alias Detection and Corpus Coverage

Layer / File(s) Summary
Extractor: IMemoryOwner view-field alias resolution
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds ReleasedOwner to resolve a field read (direct owner or aliased Memory/ReadOnlyMemory view) back to the released owner field. Extends dispoFieldLine construction to include IMemoryOwner*-typed fields via IsMemoryOwnerType. Builds a viewFieldOwner map from assignments where the RHS is an IMemoryOwner<T>.Memory view via FieldViewOwner. Updates FirstUnguardedDisposedRead to thread the viewFieldOwner map and uses ReleasedOwner resolution in both helper-chase and handler-scan call sites.
Corpus: pooled-buffer use-after-dispose test case
corpus/wpf/pooled-view-after-dispose/before.cs, corpus/wpf/pooled-view-after-dispose/after.cs, corpus/wpf/pooled-view-after-dispose/case.own, corpus/wpf/pooled-view-after-dispose/expected-diagnostics.txt, corpus/wpf/pooled-view-after-dispose/notes.md
before.cs shows a FrameDecoder that stores _owner.Memory in _view and reads _view.Span in a post-dispose handler. after.cs shows the fixed version with a _disposed guard. case.own models the acquire/release/late-use flow for OWN002. expected-diagnostics.txt registers OWN002. notes.md documents the view→owner alias analysis, precision assumptions, and deferred follow-ups.
CI: raise corpus-benchmark recall gate
.github/workflows/ci.yml
--min-recall incremented from 22 to 23 in the corpus-benchmark job; adjacent comment updated to match.

Possibly related PRs

  • PhysShell/Own.NET#52: Adds core pooled-buffer Rent/Return acquire/release tracking in Program.cs; this PR extends that same --flow-locals path with Memory/AsMemory view-field alias resolution.
  • PhysShell/Own.NET#69: Resolves Span/ReadOnlySpan view identifiers back to their pooled buffer owner via AsSpan/Span<T> constructors; this PR applies the analogous resolution for Memory<T>/ReadOnlyMemory<T> view fields via IMemoryOwner<T>.Memory/AsMemory.
  • PhysShell/Own.NET#75: Improves guard/subscription precision for direct _field member reads in live handlers; this PR further extends that handler-scan path to resolve view fields aliasing released IMemoryOwner<T> owners.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A memory view, it hides the owner inside,
Disposed too early — a dangling byte ride!
I sniff out the alias, the view to its source,
OWN002 fires and sets us back on course.
The recall ticks up, one more bug in the net,
No pooled buffer read goes unnoticed — not yet! 🎉

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature being added—detection of pooled-buffer view-in-a-field use-after-dispose patterns mapped to OWN002 diagnostic (POOL005)—and accurately reflects the core changes across files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/pooled-view-in-field

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

@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: 243c4398a0

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

continue;
if (!IsDisposableType(fd.Declaration.Type.ToString()))
var ftype = fd.Declaration.Type.ToString();
if (!IsDisposableType(ftype) && !ftype.StartsWith("IMemoryOwner", StringComparison.Ordinal))

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 Recognize qualified IMemoryOwner field types

When the owner field is written as System.Buffers.IMemoryOwner<byte> or through a type alias, fd.Declaration.Type.ToString() no longer starts with IMemoryOwner, so the field is never added to dispoFieldLine; releasedAt stays empty and the new pooled-view-after-dispose path is completely missed even though ViewOwner resolves _owner.Memory semantically. This makes the new checker depend on a using System.Buffers; spelling rather than the actual BCL type.

Useful? React with 👍 / 👎.

var viewFieldOwner = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var a in assigns)
if (a.IsKind(SyntaxKind.SimpleAssignmentExpression)
&& FieldName(a.Left) is { } vf

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 Restrict recorded view fields to this instance

For assignments like other._view = _owner.Memory, FieldName(a.Left) records _view as if this object's field aliases _owner. A later subscribed handler reading this instance's _view.Span will then emit OWN002 even though only another instance's field was populated from the owner. The view-field map should use the same bare/this. restriction as the release/read paths to avoid this false positive.

Useful? React with 👍 / 👎.

foreach (var a in assigns)
if (a.IsKind(SyntaxKind.SimpleAssignmentExpression)
&& FieldName(a.Left) is { } vf
&& ViewOwner(a.Right, model) is { } vo)

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 Accept this-qualified owner.Memory views

If the constructor uses the common spelling _view = this._owner.Memory (or this._view = this._owner.Memory), this call to ViewOwner returns null because ViewOwner only accepts an IdentifierNameSyntax receiver for owner.Memory. The owner still gets disposed and the handler still reads _view.Span, but the view-owner alias is never recorded, so the advertised pooled view-in-a-field dangle is missed for this-qualified field access.

Useful? React with 👍 / 👎.

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

Actionable comments posted: 2

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

Inline comments:
In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 2025-2029: The current code in the foreach loop iterates through
assignments and records field-to-owner mappings using viewFieldOwner[vf] = vo
without verifying that the field being assigned belongs to the this instance.
This can incorrectly capture aliases from other object instances like
other._view. Add a check to ensure that a.Left refers to a field access on the
this instance before recording the alias in the viewFieldOwner dictionary,
filtering out assignments to fields on other objects.
- Around line 1994-1995: Replace the string-based check
`ftype.StartsWith("IMemoryOwner", StringComparison.Ordinal)` with semantic type
resolution. Instead of relying on the string representation of the field type
(which may include fully qualified names or aliases), use Roslyn's semantic
analysis to check if the actual type symbol represents IMemoryOwner<T>. This
will properly identify all valid IMemoryOwner declarations regardless of how
they are written in the source code, ensuring OWN002 cases are not skipped due
to naming variations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b2daae83-7c58-448b-a6b5-165270fb0ff5

📥 Commits

Reviewing files that changed from the base of the PR and between 5b31c36 and 243c439.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • corpus/wpf/pooled-view-after-dispose/after.cs
  • corpus/wpf/pooled-view-after-dispose/before.cs
  • corpus/wpf/pooled-view-after-dispose/case.own
  • corpus/wpf/pooled-view-after-dispose/expected-diagnostics.txt
  • corpus/wpf/pooled-view-after-dispose/notes.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
…c IMemoryOwner, this-qualified owner (CodeRabbit + Codex)

Three precision/robustness fixes to the pooled view-in-a-field pass, each flagged
by review (the first two by BOTH CodeRabbit and Codex):

1. FP: the view-field alias map keyed the LHS with FieldName, so `other._view =
   _owner.Memory` recorded THIS instance's `_view` as aliasing `_owner` — a later
   handler read of this object's `_view.Span` would then falsely fire. Use
   ThisFieldName for the LHS, matching the release/read paths (this/bare only).

2. Recall: an owner field written `System.Buffers.IMemoryOwner<byte>` (or an alias,
   or a concrete type implementing IMemoryOwner) was missed by the
   `StartsWith("IMemoryOwner")` string check, so releasedAt stayed empty and the
   case was skipped. New `IsMemoryOwnerType` resolves the field's type SYMBOL
   (interface or via AllInterfaces); the cheap StartsWith still short-circuits the
   common unqualified spelling. IDisposable stays syntactic (unresolved customs).

3. Recall: `this._owner.Memory` was not recognised as a view (ViewOwner needs an
   IdentifierNameSyntax receiver). New `FieldViewOwner` accepts a this/bare-field
   receiver (ThisFieldName) for the `IMemoryOwner<T>.Memory` borrow, so both
   `_owner.Memory` and `this._owner.Memory` alias, while `other._owner.Memory`
   (another instance) does not.

Behaviour on the corpus is unchanged (the `_owner.Memory` view still aliases and
fires OWN002, the guarded after stays silent); the FP fix only narrows the map.
C# validated by CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell PhysShell merged commit 70376da into main Jun 22, 2026
22 checks passed
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