Skip to content

fix(extractor): pooled buffer passed to an escaping constructor is a transfer, not a leak (mined FP)#80

Merged
PhysShell merged 2 commits into
mainfrom
claude/fp-pool-buffer-ctor-transfer
Jun 22, 2026
Merged

fix(extractor): pooled buffer passed to an escaping constructor is a transfer, not a leak (mined FP)#80
PhysShell merged 2 commits into
mainfrom
claude/fp-pool-buffer-ctor-transfer

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Mined false positive (the second one)

The same mine run on Pipelines.Sockets.Unofficial that surfaced the PipeReader/PipeWriter FP (#79) also flagged ArrayPoolBufferWriter.CreateNewSegment:

internal static RefCountedSegment CreateNewSegment(ArrayPool<T> arrayPool, RefCountedSegment previous, int size)
{
    var array = arrayPool.Rent(size);
    return new ArrayPoolRefCountedSegment(arrayPool, array, previous);   // ownership transferred + returned
}

The --flow-locals escape analysis treats a pooled buffer passed as an argument as a borrow (deliberately — so pool.Return(buf); Work(buf) still trips use-after-return), and so expects a same-method Return. It misses that the buffer's ownership is transferred to the constructed object (ArrayPoolRefCountedSegment returns it in ReleaseImpl) and leaves the method inside that returned object → a false OWN001 "never returned".

Fix

New PassedToEscapingCtor: a pooled buffer handed to a new …(…, buf, …) whose result is the direct return value or a field-assignment RHS escapes (untracked), exactly like a returned local. Purely syntactic, so it holds even when the wrapper type doesn't resolve.

static bool PassedToEscapingCtor(IdentifierNameSyntax idn) =>
    idn.Parent is ArgumentSyntax { Parent: ArgumentListSyntax { Parent: BaseObjectCreationExpressionSyntax oce } }
    && (oce.Parent is ReturnStatementSyntax
        || (oce.Parent is AssignmentExpressionSyntax a && a.Right == oce && FieldName(a.Left) is not null));

Only the --flow-locals path is touched (the syntactic D1 path already escapes all arg-passed locals); a plain borrow Work(buf) still leaks if unreturned.

Precision / scope

  • One level only — a new buried in a local (var w = new X(buf); return w;) or another expression stays a borrow (an honest limitation, noted).
  • No corpus case has a return new X(pooledBuf) shape, so the recall floor is unchanged; no existing sample/corpus behaviour changes.

Regression guard

FlowLocalsSample.PooledIntoReturnedCtor (ctorMoved handed to a returned PooledHolder) is added to the --flow-locals job's must-stay-silent list — it fails on the pre-fix analysis and passes with the escape.

Validation

  • Python suite green; no recall-regression risk (verified no corpus return new X(Rent…) pattern).
  • C# validated by CI (the new silent-list entry); not reproducible without a local SDK.

With #79 (PipeReader/PipeWriter) this closes both false positives mined on Pipelines.Sockets.Unofficial (3 findings → 0).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • Tests

    • Added a new Flow Locals sample demonstrating pooled buffer ownership transfer via constructor-based wrappers.
    • Updated flow-sensitive escape analysis test expectations to treat an additional ownership-transfer scenario as an expected outcome.
  • Chores

    • Refreshed CI allowlist/suppression rules to align with improved pooled-buffer constructor detection logic.
  • Bug Fixes

    • Improved classification for pooled buffers passed into certain escaping constructors to avoid incorrect leak/use-after-return findings.

…an ownership transfer, not a leak (mined FP)

Second false positive from mining Pipelines.Sockets.Unofficial:
ArrayPoolBufferWriter.CreateNewSegment does `var array = pool.Rent(size); return
new ArrayPoolRefCountedSegment(pool, array, prev);`. The flow-locals escape
analysis treats a pooled buffer passed as an argument as a BORROW (so
`pool.Return(buf); Work(buf)` still trips use-after-return) and expects a
same-method Return — missing that the buffer's ownership is TRANSFERRED to the
constructed object (which Returns it on its own teardown) and leaves the method
inside that returned object. Result: a false OWN001 "never disposed/returned".

New PassedToEscapingCtor: a pooled buffer handed to a `new` whose result is the
direct return value or a field-assignment RHS escapes (untracked), like a returned
local. One level only — a `new` buried in a local or another expression stays a
borrow (an honest limitation). Purely syntactic, so it holds for unresolved wrapper
types. Only the --flow-locals path is affected (the syntactic D1 path already
escapes all arg-passed locals); a plain borrow `Work(buf)` still leaks if unreturned.

Regression guard: FlowLocalsSample.PooledIntoReturnedCtor (`ctorMoved` handed to a
returned PooledHolder) added to the --flow-locals job's must-stay-silent list. No
corpus case has this shape, so the recall floor is unchanged.

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: e5f9671c-2fe7-4f1a-a033-b2e8ec576efd

📥 Commits

Reviewing files that changed from the base of the PR and between 735d59f and 56fc7df.

📒 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

Adds a PassedToEscapingCtor syntactic helper to the --flow-locals escape analysis in Program.cs that detects when a pooled buffer identifier is passed to a constructor whose result is directly returned or field-assigned. The escape-branching logic is extended to classify such buffers as escaped. A matching PooledIntoReturnedCtor/PooledHolder sample is added to FlowLocalsSample.cs and the CI allowlist gains a ctorMoved exempt entry.

Changes

Escaping-constructor ownership-transfer pattern

Layer / File(s) Summary
Sample fixture and CI allowlist
frontend/roslyn/samples/FlowLocalsSample.cs, .github/workflows/ci.yml
Adds using System.Buffers, a new PooledIntoReturnedCtor(int n) sample method, and an internal sealed PooledHolder wrapper class with a _buffer field and Release(). Registers ctorMoved as an expected-non-finding in the CI Bash allowlist.
PassedToEscapingCtor classifier and escape-rule extension
frontend/roslyn/OwnSharp.Extractor/Program.cs
Introduces PassedToEscapingCtor(IdentifierNameSyntax idn, SemanticModel model) which returns true when the identifier is an argument of a new expression that is directly returned or field-assigned. Extends the escape-rule branch so pooled buffers matching this shape are added to escapedLocals (removed from tracked) instead of being treated as borrows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PhysShell/Own.NET#15: Introduced the --flow-locals pipeline and the FlowLocalsSample-driven CI assertions that this PR directly extends.
  • PhysShell/Own.NET#52: Added pooled ArrayPool acquire/release and escape handling in the same --flow-locals flow-engine code paths that this PR further refines.
  • PhysShell/Own.NET#53: Modified the same pooled-buffer escape/tracking logic in Program.cs using semantic-model-based Rent/Return detection, overlapping directly with the branching extended here.

Poem

🐇 A buffer was rented, then wrapped up with care,
Into a constructor that floated through air—
Returned to the caller, no leak left behind,
The extractor now sees what the owner designed.
No false alarm rings, no OWN002 fright,
Just ctorMoved hopping away, out of sight! ✨

🚥 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 and specifically describes the main change: fixing a false positive in escape analysis for pooled buffers passed to escaping constructors, with the qualifier indicating this was a mined false positive.
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/fp-pool-buffer-ctor-transfer

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: 735d59fba6

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

idn.Parent is ArgumentSyntax { Parent: ArgumentListSyntax
{ Parent: BaseObjectCreationExpressionSyntax oce } }
&& (oce.Parent is ReturnStatementSyntax
|| (oce.Parent is AssignmentExpressionSyntax a && a.Right == oce && FieldName(a.Left) is not null));

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 escaping-ctor assignments to real fields

When the constructed wrapper is assigned to an already-declared local, e.g. PooledHolder h; h = new PooledHolder(buf);, this condition still returns true because FieldName returns a name for any bare identifier, not just fields. The caller at line 2419 then removes buf from tracked, so a wrapper that never leaves the method and never returns the ArrayPool buffer is reported as silent instead of the expected missing-Return leak; this contradicts the intended one-level-only behavior where a constructor result stored in a local stays a borrow.

Useful? React with 👍 / 👎.

…d assignment (Codex)

Codex: FieldName(a.Left) returns a name for any bare identifier, so a constructor
result assigned to a LOCAL (`PooledHolder h; h = new PooledHolder(buf);`) was
mistaken for a field-store and escaped the buffer — silencing a real missing-Return
leak when that local never leaves the method. Gate the field-assignment case on the
LHS resolving to an IFieldSymbol (symbol-based); the return-value case (the mined FP
and the FlowLocalsSample regression) is unchanged.

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

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

🤖 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 1047-1051: The PassedToEscapingCtor method is too permissive in
determining whether an identifier escapes through a constructor call. The
condition on line 1051 uses FieldName(a.Left) is not null to check if an
assignment is to a field, but FieldName also matches local identifiers, causing
local variable assignments like tmp = new Wrapper(buf) to be incorrectly treated
as field escapes. This leads to buf being untracked at line 2419, hiding real
OWN001 leaks. Modify the condition in PassedToEscapingCtor to properly
distinguish between field assignments and local variable assignments, ensuring
that only actual field escapes are considered escaping constructors, not
temporary local variable assignments.
🪄 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: 248d07ac-e609-4d90-8233-fc84c1a4d6cf

📥 Commits

Reviewing files that changed from the base of the PR and between 70376da and 735d59f.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/FlowLocalsSample.cs

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
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