Skip to content

feat(extractor): borrow checker — Memory<T> view that escapes after Return → OWN002 (POOL004)#70

Merged
PhysShell merged 3 commits into
mainfrom
claude/memory-view-escape
Jun 21, 2026
Merged

feat(extractor): borrow checker — Memory<T> view that escapes after Return → OWN002 (POOL004)#70
PhysShell merged 3 commits into
mainfrom
claude/memory-view-escape

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Диверсия в тыл: the Memory<T> view that escapes (P-007 POOL004)

The borrow checker's second bite — and the genuinely dangerous one the compiler won't catch for you.

arraypool-span-view-after-return (#69) caught a Span view used after Return inside the method. But a Span is a ref struct — the C# compiler already keeps it from escaping. A Memory<T> is not a ref struct: it can be returned or stored in a field, so a view of a pooled buffer can leave the method and dangle in the caller's hands after the buffer was recycled to someone else:

static Memory<byte> Render(int n)
{
    byte[] buf = ArrayPool<byte>.Shared.Rent(n);
    Memory<byte> view = buf.AsMemory(0, n);   // view BORROWS buf
    ArrayPool<byte>.Shared.Return(buf);        // buf recycled into the pool ...
    return view;                               // ... but a view ESCAPES → dangling (OWN002)
}

How

ViewOwner (renamed from SpanViewOwner) is extended from AsSpan / new Span<T> to AsMemory / new Memory<T> (and the ReadOnly* forms), resolved via the same BCL symbols (System.MemoryExtensions, System.Memory<T> ctor) — not by name. Because a reference to a view lowers to a use of the owner, and return view is such a reference (routed through the return-expression lowering), the escape of a dangling view after Return(buf) trips OWN002 at the escape site.

Purely extractor-side — facts identical to the in-method case (acquire buf; release buf; use buf). The core needs no borrow/escape concept.

Precision (0 FP): only a view of a tracked owner used/returned after the owner's release fires. The fix copies out (AsSpan(..).ToArray()) and returns the copy, so no view escapes — after.cs stays silent.

Pinned

New corpus case arraypool-memory-view-escape (before → OWN002, after silent), lifting the benchmark recall floor 12 → 13. Validated locally: case.own → OWN002, corpus 11/11, constructed extractor facts → OWN002.

First slice covers the return escape; a view stored in a field (object-level escape) and view reassignment are left for later rounds.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Extended memory safety detection so returned System.Memory<T> / ReadOnlyMemory<T> views are treated as dangling borrows after pooled-buffer Return, aligning with existing Span<T> handling.
  • Documentation
    • Added and documented the new OWN002 “pooled memory view escape” scenario and the recommended copy-out prevention guidance.
    • Updated the ArrayPool/Span proposal text to cover AsMemory() / Memory<T> escape behavior and revised target sequencing.
    • Updated the real-world “Кейсы” table with the new scenario.
  • Tests
    • Added a new real-world corpus case (arraypool-memory-view-escape) and updated expected diagnostics.
  • Chores
    • Increased the CI benchmark quality gate threshold.

…eturn → OWN002 (POOL004)

The borrow checker's second bite, and the genuinely dangerous one. `arraypool-span-view-after-return`
(#69) caught a `Span` view used after `Return` INSIDE the method — but a `Span` is a ref struct, so
the C# compiler already keeps it from escaping. A `Memory<T>` is NOT a ref struct: it can be RETURNED
or stored in a field, so a `Memory` view of a pooled buffer can leave the method and dangle in the
caller's hands after the buffer was recycled (a silent cross-tenant corruption). That is the escape
the borrow checker has to catch, and the compiler will not.

The view recognition (`ViewOwner`, renamed from `SpanViewOwner`) is extended from `AsSpan` /
`new Span<T>` to `AsMemory` / `new Memory<T>` (and the `ReadOnly*` forms), resolved via the same BCL
symbols (`System.MemoryExtensions`, `System.Memory<T>` ctor) — not by name, so a look-alike is not
mistaken for a borrow. Because a reference to a view lowers to a use of the owner, and `return view`
is such a reference (routed through the return-expression lowering), the escape of a dangling view
after `Return(buf)` trips OWN002 at the escape site. Purely extractor-side; the facts are identical
to the in-method case (`acquire buf; release buf; use buf`).

Conservative (0 FP): only a view of a tracked owner used/returned AFTER the owner's release fires; the
fix copies out of the buffer (`AsSpan(..).ToArray()`) and returns the copy, so no view escapes (the
`after.cs` arm stays silent). A view used before the return, or of an untracked buffer, adds nothing.

Pinned by corpus `arraypool-memory-view-escape` (before → OWN002, after silent), lifting the recall
floor 12 → 13. First slice covers the RETURN escape; a view stored in a FIELD and view reassignment
are left for later. Validated locally: case.own → OWN002, corpus 11/11, constructed facts → OWN002.

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 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 50b3f1ae-a844-40cb-a4ad-7221c13996fc

📥 Commits

Reviewing files that changed from the base of the PR and between dfc4634 and a4b7298.

📒 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

The Roslyn extractor's view-owner detection helper is generalized from Span-only to recognize AsMemory, new Memory<T>, and new ReadOnlyMemory<T> constructors. The ReturnStatementSyntax lowering now detects escaping view locals, maps them to owner-buffer locals, and injects use(owner) operations into the onReturn chain to model OWN002 after finally-based owner release. A new real-world corpus case (arraypool-memory-view-escape) is added with before/after C# examples, an OwnLang model, expected diagnostics, and notes. Documentation and the CI benchmark recall gate are updated accordingly.

Changes

Memory/ReadOnlyMemory View Escape Detection

Layer / File(s) Summary
Generalize view-owner detection to Memory/ReadOnlyMemory
frontend/roslyn/OwnSharp.Extractor/Program.cs
ReturnStatementSyntax lowering now detects when the returned expression contains escaping span/memory view locals, maps those locals back to their owner-buffer locals, and injects use(owner) operations into onReturn before method exit. ViewOwner helper replaces span-only logic to recognize AsSpan/AsMemory and Span/ReadOnlySpan/Memory/ReadOnlyMemory constructors via resolved BCL symbols. ViewOwnerOf and inline dangling-borrow comment are updated accordingly.
New arraypool-memory-view-escape corpus case
corpus/real-world/arraypool-memory-view-escape/before.cs, corpus/real-world/arraypool-memory-view-escape/after.cs, corpus/real-world/arraypool-memory-view-escape/case.own, corpus/real-world/arraypool-memory-view-escape/expected-diagnostics.txt, corpus/real-world/arraypool-memory-view-escape/notes.md
before.cs demonstrates a Memory<byte> view escaping after ArrayPool.Return in a try/finally block; after.cs shows the safe fix using ToArray() to copy data before returning the buffer; case.own models a Buffer resource with early release followed by use; expected-diagnostics.txt declares OWN002 expectation; notes.md documents the detection scope (tracked owners with post-release use), the fix strategy (copy via ToArray()), and deferred scope (field storage and view reassignment).
Docs and CI gate update
corpus/real-world/README.md, docs/proposals/P-007-arraypool-span.md, .github/workflows/ci.yml
README gains the arraypool-memory-view-escape table row with OWN002/POOL004 metadata. P-007 status bullets are reworded to reference AsMemory view forms, clarify ViewOwner naming, expand POOL002 description, and explicitly mark POOL004 (view escape) as having a first slice built while deferring POOL003/field-storage/POOL005. CI benchmark min-recall gate is raised from 12 to 13.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#69: Introduced SpanViewOwner/ViewOwnerOf in the extractor for Span/ReadOnlySpan view-owner lowering — the current PR directly generalizes that same helper to cover Memory/ReadOnlyMemory.
  • PhysShell/Own.NET#52: Modified the same --flow-locals ArrayPool OWN002/OWN003 modeling in Program.cs, operating on the same code path that this PR extends for Memory views.
  • PhysShell/Own.NET#34: Implemented the onReturn continuation threading through try/finally and control-flow lowering that the current PR's view-owner use(owner) injection targets.

Poem

🐇 A Memory view once slipped away,
Past the pool's return on that fateful day.
But now the borrow-checker sees the snare —
AsMemory, new Memory, caught mid-air!
The corpus grows, the gate ticks up to 13,
No dangling view shall ever go unseen. 🔍

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes the main change: extending the borrow checker to detect Memory views that escape after Return, triggering OWN002 (POOL004). It is specific, concise, and directly reflects the primary objective of the PR.
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/memory-view-escape

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: 25da937830

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

&& m.Name.Identifier.Text is "AsSpan" or "AsMemory"
&& m.Expression is IdentifierNameSyntax recv
&& model.GetSymbolInfo(inv).Symbol is IMethodSymbol { ContainingType: { Name: "MemoryExtensions" } mct }
&& IsInNamespace(mct, "System"))

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 Detect returns whose finally releases the owner

When the new AsMemory view is returned from inside a try whose finally returns the pooled buffer, the escape is missed: try { return view; } finally { ArrayPool<byte>.Shared.Return(buf); } evaluates the return expression before the finally, but the caller receives the Memory<T> only after the owner has been released. The current lowering treats the mapped owner reference as a normal use before the onReturn releases, so this common cleanup pattern is considered clean even though it is the same dangling POOL004 escape.

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: 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 851-856: The pattern match in the ViewOwner method currently only
checks for ObjectCreationExpressionSyntax, which misses C# 9+ target-typed
constructor expressions. Replace ObjectCreationExpressionSyntax with
BaseObjectCreationExpressionSyntax in the "if (e is
ObjectCreationExpressionSyntax oc" condition so that the variable oc is typed as
BaseObjectCreationExpressionSyntax instead. This broader base type will match
both explicit object creation syntax and implicit target-typed new expressions,
ensuring borrows are properly linked to their owners for diagnostics.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a58394bd-61be-489c-bd03-f8d1a06bac67

📥 Commits

Reviewing files that changed from the base of the PR and between 46a7f30 and 25da937.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • corpus/real-world/README.md
  • corpus/real-world/arraypool-memory-view-escape/after.cs
  • corpus/real-world/arraypool-memory-view-escape/before.cs
  • corpus/real-world/arraypool-memory-view-escape/case.own
  • corpus/real-world/arraypool-memory-view-escape/expected-diagnostics.txt
  • corpus/real-world/arraypool-memory-view-escape/notes.md
  • docs/proposals/P-007-arraypool-span.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
…w (Codex/CodeRabbit)

Two review fixes on the Memory-view-escape slice:

- Codex (P2): the escape was missed for THE idiomatic ArrayPool form,
  `try { return view; } finally { Return(buf); }`. The `return view` is evaluated before the
  finally, but the caller receives the `Memory<T>` only AFTER the finally recycles the buffer — a
  dangling escape. The return lowering placed the view's owner-use BEFORE the finally release, so it
  looked clean. Now a returned view's owner-use is re-emitted AFTER the finally release(s) — inserted
  just before the exit of the `onReturn` chain (`[finally…, exit]`) — so the finally-Return then
  trips OWN002. New helper `ReturnedViewOwners`; outside a try the use was already after any earlier
  release, so nothing changes there. The corpus `before.cs` is now this idiomatic try/finally form.
  Core fact shape validated locally (acquire; use; release; use; exit → OWN002).

- CodeRabbit (Major): `ViewOwner` matched only `ObjectCreationExpressionSyntax`, missing C# 9+
  target-typed `Memory<byte> v = new(buf, 0, n)`. Switched to `BaseObjectCreationExpressionSyntax`
  (covers explicit + implicit `new`; `ArgumentList`/`GetSymbolInfo` resolve on the base), matching
  the codebase idiom.

corpus 11/11, ownir 116/116; case.own → OWN002.

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 897-905: The ReturnedViewOwners method currently scans all
identifiers in the return expression, incorrectly flagging any tracked owner
identifier as being returned, even when only a property or method result is
returned (like return view.Length returns int, not view). Refactor the method to
only consider expressions where the view itself is being returned by checking
the root expression and its direct structure rather than iterating through all
descendant identifiers. Handle only the cases where the actual returned value is
the view: direct identifier returns, casts, parentheses, method calls that
return views (like AsMemory), and conditional expressions where branches return
views. Change the approach from
DescendantNodesAndSelf().OfType<IdentifierNameSyntax>() iteration to analyzing
the expression structure itself.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebfe831d-d0c8-47c5-903a-05bcc2f82d68

📥 Commits

Reviewing files that changed from the base of the PR and between 25da937 and dfc4634.

📒 Files selected for processing (5)
  • corpus/real-world/arraypool-memory-view-escape/after.cs
  • corpus/real-world/arraypool-memory-view-escape/before.cs
  • corpus/real-world/arraypool-memory-view-escape/case.own
  • corpus/real-world/arraypool-memory-view-escape/notes.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
✅ Files skipped from review due to trivial changes (1)
  • corpus/real-world/arraypool-memory-view-escape/notes.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • corpus/real-world/arraypool-memory-view-escape/after.cs
  • corpus/real-world/arraypool-memory-view-escape/before.cs
  • corpus/real-world/arraypool-memory-view-escape/case.own

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
…ot every identifier (CodeRabbit)

CodeRabbit (Major): the helper scanned every descendant identifier of the return expression, so
`try { return view.Length; } finally { Return(buf); }` injected a post-finally `use buf` and tripped
a false OWN002 — even though only an `int` (the length) escapes, not the view. The escape happens
only when the RETURNED VALUE itself is the view.

Replaced the descendant scan with a structural visitor that follows only the returned value: a view
local (`return view`), an inline view expression (`return buf.AsMemory(…)`), and through
casts / parentheses / conditional branches. A member or call result of a view is not visited, so it
no longer escapes. The corpus case (`return view`) still fires; `after.cs` (`return …ToArray()`)
stays silent. corpus 11/11, ownir 116/116.

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