Skip to content

feat(extractor): borrow checker — pooled-buffer Span view used after Return → OWN002 (POOL002)#69

Merged
PhysShell merged 2 commits into
mainfrom
claude/borrow-span-view
Jun 21, 2026
Merged

feat(extractor): borrow checker — pooled-buffer Span view used after Return → OWN002 (POOL002)#69
PhysShell merged 2 commits into
mainfrom
claude/borrow-span-view

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Тяжёлая артиллерия: the borrow checker's first bite on real C#

The B4 / P-007 POOL002 frontier — the differentiating lever (lifetime/aliasing analysis, Rust's home turf, where the flat "disposed anywhere?" scanners don't go).

The flat pass and the existing pool flow only caught a use-after-return when the buffer local itself was referenced after Return. When the read goes through a stored Span view, the buffer name never appears at the use site — so it looked released-and-untouched. A miss, and a nasty silent aliasing bug (the array is recycled to another caller, so the write corrupts someone else's data):

byte[] buf = ArrayPool<byte>.Shared.Rent(n);
Span<byte> view = buf.AsSpan(0, n);     // view BORROWS buf
ArrayPool<byte>.Shared.Return(buf);     // buf recycled into the pool ...
view[0] = 42;                           // ... written through here → OWN002  (MISSED before)

How

A Span/ReadOnlySpan view of a tracked buffer is modelled as a borrow: a reference to the view local lowers to a use of the owner. ViewOwnerOf resolves the view through its own declaration's initializer (buf.AsSpan(..) / new Span<T>(buf)), with the Span-returning symbol confirmed via the SemanticModel. A use after Return(buf) then trips OWN002.

Purely extractor-side — the core needs no borrow concept; it sees a plain use-after-release (emitted facts are identical to arraypool-use-after-return: acquire buf; release buf; use buf). A ref-struct Span cannot escape the method, which is exactly what makes "use of the view = use of the owner, here" sound.

Precision (0 FP): a view of an untracked/escaped buffer, or one used before the Return (after.cs), adds no finding — the borrow only lowers to a use of an owner that is still tracked, so it can never invent a release.

Pinned

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

First slice: Span/ReadOnlySpan via AsSpan() / new Span<T>. Deliberately left for later rounds: Memory<T> (which can escape the method — needs escape modelling) and view reassignment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Improved analysis to catch use-after-return patterns involving Span/ReadOnlySpan views created from pooled buffers, when the buffer is returned before the view is used.
  • Documentation

    • Updated the proposal’s progress status for pooled-buffer detection.
    • Added a new real-world corpus case (including reproduction code, expected diagnostics, and detailed notes), and updated the “Кейсы” table.
  • Chores

    • Increased the CI corpus-benchmark minimum recall threshold.

…fter Return → OWN002 (POOL002)

The first bite of the borrow checker on real C#, and the heavy-artillery frontier (B4 / P-007
POOL002). The flat pass and the existing pool flow only caught a use-after-return when the BUFFER
LOCAL ITSELF was referenced after Return. When the read goes through a stored Span view —
`Span<byte> view = buf.AsSpan(0, n); pool.Return(buf); view[0] = 42;` — the buffer name never
appears at the use site, so the buffer looked released-and-untouched: a miss (and a nasty silent
aliasing bug, since the array is recycled to another caller).

A `Span`/`ReadOnlySpan` view of a tracked buffer is now modelled as a BORROW: a reference to the
view local lowers to a USE OF THE OWNER (`ViewOwnerOf` resolves the view through its declaration's
`buf.AsSpan(..)` / `new Span<T>(buf)` initializer, the Span-returning symbol confirmed via the
SemanticModel). A use after `Return(buf)` then trips OWN002. Purely extractor-side — the core needs
no borrow concept; it sees a plain use-after-release (the emitted facts are identical to
arraypool-use-after-return: acquire buf; release buf; use buf).

A ref-struct `Span` cannot escape the method, which is exactly what makes "use of the view = use of
the owner, here" sound. Conservative (0 FP): a view of an untracked/escaped buffer, or one used
BEFORE the Return (after.cs), adds no finding — the borrow only lowers to a use of an owner that is
still tracked, so it can never invent a release.

Pinned by corpus `arraypool-span-view-after-return` (before → OWN002, after silent), lifting the
benchmark recall floor 11 → 12. First slice: `Span`/`ReadOnlySpan` via `AsSpan()` / `new Span<T>`;
`Memory<T>` (escapable) and view reassignment are left for later rounds. Validated locally:
case.own → OWN002, corpus 10/10, constructed extractor 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: 4611df98-e2ca-46c7-a43e-fe8d02e89285

📥 Commits

Reviewing files that changed from the base of the PR and between 98ec925 and ca02820.

📒 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 extractor's EmitFlowExpr flow-lowering is extended to treat Span<T>/ReadOnlySpan<T> view identifiers as borrows of their underlying tracked owner buffer, using two new helpers (SpanViewOwner, ViewOwnerOf). A matching corpus test case (arraypool-span-view-after-return) with before/after C# samples, an OwnLang model, expected diagnostics, and notes is added. The CI recall threshold is raised from 11 to 12.

Changes

Span View-After-Return Detection (OWN002)

Layer / File(s) Summary
Span view borrow resolution in EmitFlowExpr
frontend/roslyn/OwnSharp.Extractor/Program.cs
EmitFlowExpr is updated so untracked Span/ReadOnlySpan identifiers are resolved to their owning tracked local via ViewOwnerOf and SpanViewOwner, which inspect variable initializers for AsSpan(...) calls and new Span<T>(owner, ...) / new ReadOnlySpan<T>(owner) constructors, then record the owner as used to preserve OWN002 detection.
Corpus test case: arraypool-span-view-after-return
corpus/real-world/arraypool-span-view-after-return/before.cs, .../after.cs, .../case.own, .../expected-diagnostics.txt, .../notes.md
Adds the full corpus case: before.cs demonstrates the bug (span written after ArrayPool.Return), after.cs the fix, case.own models the OwnLang use-after-release triggering OWN002, expected-diagnostics.txt declares the expected OWN002 output, and notes.md documents detection scope and deferred cases.
Docs and CI recall threshold
corpus/real-world/README.md, docs/proposals/P-007-arraypool-span.md, .github/workflows/ci.yml
README gains the new OWN002 table row; the P-007 proposal status is refined to separate POOL001/POOL002 milestones with borrow-lowering detail; CI benchmark gate raises --min-recall from 11 to 12 with updated backlog comments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PhysShell/Own.NET#50: Introduced the corpus-benchmark CI job and the scripts/benchmark.py gating logic that this PR raises from --min-recall 11 to --min-recall 12.
  • PhysShell/Own.NET#52: Modified the same flow-lowering path in OwnSharp.Extractor/Program.cs to handle pooled-buffer borrow patterns, which this PR extends with Span/ReadOnlySpan view resolution.
  • PhysShell/Own.NET#34: Both PRs update EmitFlowExpr flow-lowering logic to improve use-after-release detection, with this PR extending the extractor to resolve Span/ReadOnlySpan identifiers back to their tracked owner buffers.

Poem

🐇 A span was borrowed, the buffer returned too soon,
A silent corruption beneath the full moon.
Now ViewOwnerOf traces the borrow back home,
The owner is used, no more unsafe to roam.
OWN002 caught — the pool sleeps safe tonight! 🌙

🚥 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 'feat(extractor): borrow checker — pooled-buffer Span view used after Return → OWN002 (POOL002)' accurately and specifically summarizes the main change: adding borrow checker functionality to detect use-after-return bugs involving pooled buffer Span views. It is clear, 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/borrow-span-view

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: 98ec925255

ℹ️ 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 on lines +842 to +845
&& model.GetSymbolInfo(inv).Symbol is IMethodSymbol { ReturnType.Name: "Span" or "ReadOnlySpan" })
return recv.Identifier.Text;
if (e is ObjectCreationExpressionSyntax oc
&& oc.Type is GenericNameSyntax { Identifier.Text: "Span" or "ReadOnlySpan" }

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 Require BCL Span symbols before lowering views

When a project has its own AsSpan extension on arrays, or a non-System.Span<T> type named Span<T>, this code still treats the value as a borrow of the pooled buffer because it only checks ReturnType.Name / the syntactic type name. For example, a custom byte[].AsSpan() that returns a span over a fresh array will be reported as OWN002 after Return(buf) even though the view does not alias buf; the new precision claim depends on checking the resolved System.MemoryExtensions/System.Span symbols (and the constructor symbol) rather than just names.

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.

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

838-848: Consider hardening Span identity checks to prevent potential false borrow-owner mappings.

SpanViewOwner currently matches by simple names (ReturnType.Name, GenericNameSyntax.Identifier.Text). While no custom Span or ReadOnlySpan types were found in this codebase, this approach is vulnerable to misclassification if such types are introduced. The suggested fix adds semantic type-identity checking to ensure only BCL types are recognized:

Suggested fix
+static bool IsSystemSpanLike(ITypeSymbol? t) =>
+    t is INamedTypeSymbol nt
+    && nt.Name is "Span" or "ReadOnlySpan"
+    && nt.ContainingNamespace?.ToDisplayString() == "System";
+
 static string? SpanViewOwner(ExpressionSyntax? e, SemanticModel model)
 {
     if (e is InvocationExpressionSyntax inv
         && inv.Expression is MemberAccessExpressionSyntax m
         && m.Name.Identifier.Text == "AsSpan"
         && m.Expression is IdentifierNameSyntax recv
-        && model.GetSymbolInfo(inv).Symbol is IMethodSymbol { ReturnType.Name: "Span" or "ReadOnlySpan" })
+        && model.GetSymbolInfo(inv).Symbol is IMethodSymbol ms
+        && IsSystemSpanLike(ms.ReturnType))
         return recv.Identifier.Text;
     if (e is ObjectCreationExpressionSyntax oc
-        && oc.Type is GenericNameSyntax { Identifier.Text: "Span" or "ReadOnlySpan" }
+        && model.GetSymbolInfo(oc).Symbol is IMethodSymbol ctor
+        && IsSystemSpanLike(ctor.ContainingType)
         && oc.ArgumentList is { Arguments.Count: > 0 }
         && oc.ArgumentList.Arguments[0].Expression is IdentifierNameSyntax arg)
         return arg.Identifier.Text;
     return null;
 }
🤖 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 838 - 848, The
Span and ReadOnlySpan type checks in the if conditions are vulnerable to false
matches because they only check the simple name rather than verifying semantic
type identity. For the AsSpan invocation check using ReturnType.Name and the
ObjectCreationExpressionSyntax check using GenericNameSyntax.Identifier.Text,
add semantic type-identity validation to ensure only BCL types from the System
namespace are matched. Use the semantic model (already available through the
model variable used in GetSymbolInfo) to verify that the matched return type and
created type are specifically the BCL Span or ReadOnlySpan types, not custom
types with the same names.
🤖 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 838-848: The Span and ReadOnlySpan type checks in the if
conditions are vulnerable to false matches because they only check the simple
name rather than verifying semantic type identity. For the AsSpan invocation
check using ReturnType.Name and the ObjectCreationExpressionSyntax check using
GenericNameSyntax.Identifier.Text, add semantic type-identity validation to
ensure only BCL types from the System namespace are matched. Use the semantic
model (already available through the model variable used in GetSymbolInfo) to
verify that the matched return type and created type are specifically the BCL
Span or ReadOnlySpan types, not custom types with the same names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f6ac2de3-6fee-4ebf-b429-f8ed29f366a0

📥 Commits

Reviewing files that changed from the base of the PR and between 789255e and 98ec925.

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

…iew borrow

Codex P2: SpanViewOwner matched on the NAME `AsSpan` / `Span<T>` only, so a project's own `AsSpan`
extension, a non-System type named `Span<T>`, or an `AsSpan` that returns a span over a fresh copy
would be treated as a borrow of the pooled buffer — a false OWN002 after `Return(buf)` even though
the view does not alias it.

Now the borrow is recognised by the RESOLVED symbols: `System.MemoryExtensions.AsSpan` (aliases its
receiver array) and the `System.Span<T>` / `System.ReadOnlySpan<T>` constructor (wraps its array
argument), both confirmed via `IsInNamespace(..., "System")`. A look-alike in another namespace is
no longer mistaken for a borrow. The corpus case (`buf.AsSpan(0,n)` — the genuine BCL extension)
still fires; behaviour on real BCL spans is unchanged.

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