Skip to content

feat(pool): POOL005 .Length spelling + MemoryPool tracking (POOL001/002/003)#72

Merged
PhysShell merged 2 commits into
mainfrom
claude/pool005-clear-pool003
Jun 22, 2026
Merged

feat(pool): POOL005 .Length spelling + MemoryPool tracking (POOL001/002/003)#72
PhysShell merged 2 commits into
mainfrom
claude/pool005-clear-pool003

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

Two pooled-buffer additions, both riding machinery that already exists in the core — so there are no core changes, only the Roslyn extractor + corpus + spec.

POOL005 — the .Length spelling (sibling of #71's unbounded buf.AsSpan())

A pooled array is oversized (Rent(n)Length >= n), so passing the buffer's own buf.Length as the operative length reaches the whole array — past the rented n, into the stale [n, Length) tail:

Emit(buf.AsSpan(0, buf.Length));   // ❌ OWN025: length is buf.Length, not n → stale tail
Array.Clear(buf, 0, buf.Length);   // ❌ OWN025: over-clear past n into the pooled tail
// fix: buf.AsSpan(0, n), Array.Clear(buf, 0, n)
  • FullViewOwner now also matches buf.AsSpan(0, buf.Length) / AsMemory(0, buf.Length) and new Span<T>(buf, 0, buf.Length) (the length argument is the receiver's own .Length).
  • New ArrayClearOverOwner matches Array.Clear(buf, 0, buf.Length).
  • A real bound (buf.AsSpan(0, n)) is not matched; the single-arg Array.Clear(buf) whole-buffer wipe is deliberately left alone (low-FP). All emit the existing overspan fact → OWN025.
  • Corpus: arraypool-clear-past-length.

MemoryPool — the other pool was untracked entirely

MemoryPool<T>.Shared.Rent returns an IMemoryOwner<T> released by Dispose (there is no Return). It's now tracked as a regular Dispose-released local (IsMemoryPoolRent, mirroring IsPoolRent but not a poolBuffer — arg-passing is an escape, like any IDisposable). With that one acquire recognised, the existing flow covers the whole family:

MemoryPool
POOL001 never disposed → OWN001
POOL002 used after Dispose → OWN002
POOL003 disposed twice → OWN003
  • Corpus: memorypool-double-dispose (the MemoryPool twin of arraypool-double-return) → OWN003.

On POOL003

ArrayPool double-return was already green before this PR — the benchmark catches it as OWN003 in two variants (arraypool-double-return try/finally and arraypool-aliased-receiver). This PR adds its MemoryPool sibling, which was the real gap.

Validation

  • No core changesoverspan→OWN025 (feat(pool): POOL005 — full-length view of a pooled buffer over-reads its logical length → OWN025 #71) and acquire/release→OWN003 already exist; this is extractor + corpus + spec only.
  • Corpus 14/14 (both new cases), suite EXIT 0, metamorphic 8/8 (38 files), ruff + mypy clean.
  • Benchmark recall floor 14 → 16 (both new cases caught on real C#). The extractor (Program.cs) is CI-validated.
  • Spec: BufferPolicies B9 (the .Length spelling), P-007 (status, scope, open Q#4 — the one-model-two-spellings question — resolved).

Scope

Catches the .Length over-read/over-clear and the MemoryPool owner lifecycle. The IMemoryOwner.Memory.Span view borrow and a POOL005 view stored in a FIELD are noted follow-ups.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added detection for MemoryPool double-dispose patterns and buffer over-read scenarios.
    • Extended view shape recognition to include additional bounding patterns.
  • Documentation

    • Updated buffer safety rules to clarify over-read vs. write operations.
    • Added notes documenting new detection patterns and fixes.
  • Chores

    • Increased benchmark recall threshold for regression detection.

…02/003)

Two pooled-buffer extractor additions, both riding machinery that already exists in
the core — so there are NO core changes, only the Roslyn extractor + corpus + spec.

POOL005 — the `.Length` spelling of the over-read/over-clear (sibling of #71's
unbounded `buf.AsSpan()`). A pooled array is oversized (Rent(n) -> Length >= n), so
passing the buffer's OWN `buf.Length` as the operative length reaches the whole array,
past the rented n, into the stale [n, Length) tail:
  - FullViewOwner now also matches buf.AsSpan(0, buf.Length) / buf.AsMemory(0, buf.Length)
    and new Span<T>(buf, 0, buf.Length) (length arg is the receiver's own .Length);
  - a new ArrayClearOverOwner matches Array.Clear(buf, 0, buf.Length) (the over-clear).
A real bound (buf.AsSpan(0, n), Array.Clear(buf, 0, n)) is not matched; the single-arg
Array.Clear(buf) wipe is deliberately left alone. All emit the existing `overspan` fact
-> OWN025. Corpus: arraypool-clear-past-length.

MemoryPool — the OTHER pool was untracked entirely. MemoryPool<T>.Shared.Rent returns
an IMemoryOwner<T> released by Dispose (no Return), so it is tracked as a regular
Dispose-released local (IsMemoryPoolRent, mirroring IsPoolRent but NOT a poolBuffer:
arg-passing is an escape, like any IDisposable). With that one acquire recognised, the
existing flow covers POOL001 (never disposed -> OWN001), POOL002 (use after Dispose ->
OWN002) and POOL003 (double Dispose -> OWN003). Corpus: memorypool-double-dispose (the
MemoryPool twin of arraypool-double-return) -> OWN003.

POOL003 for ArrayPool was already green (try/finally + aliased-receiver, both in the
benchmark) — this adds its MemoryPool sibling. Benchmark recall floor 14 -> 16.
Spec: BufferPolicies B9 (the .Length spelling), P-007 (status, scope, open Q#4 resolved).
metamorphic.py already knows `overspan`; the new .own models reuse acquire/release.

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: df5f02fa-7c2f-4c4b-8184-9963b8c53bc8

📥 Commits

Reviewing files that changed from the base of the PR and between b3bf5b1 and 5a9f22f.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • corpus/real-world/arraypool-length-overread/after.cs
  • corpus/real-world/arraypool-length-overread/before.cs
  • corpus/real-world/arraypool-length-overread/case.own
  • corpus/real-world/arraypool-length-overread/expected-diagnostics.txt
  • corpus/real-world/arraypool-length-overread/notes.md
  • docs/proposals/P-007-arraypool-span.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • spec/BufferPolicies.md
💤 Files with no reviewable changes (1)
  • corpus/real-world/arraypool-length-overread/expected-diagnostics.txt
✅ Files skipped from review due to trivial changes (3)
  • corpus/real-world/arraypool-length-overread/after.cs
  • corpus/real-world/arraypool-length-overread/notes.md
  • corpus/real-world/arraypool-length-overread/case.own
🚧 Files skipped from review as they are similar to previous changes (2)
  • spec/BufferPolicies.md
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

Adds two new real-world corpus test cases — memorypool-double-dispose (POOL003/OWN003) and arraypool-length-overread (POOL005/OWN025). The extractor gains IsMemoryPoolType/IsMemoryPoolRent helpers and an expanded FullViewOwner that recognizes .Length-bounded and no-arg AsSpan/AsMemory forms. Spec rule B9 and proposal P-007 are updated accordingly, and the CI benchmark minimum recall is raised from 14 to 16.

Changes

POOL003 / POOL005 Extractor and Corpus Extension

Layer / File(s) Summary
Spec and proposal updates (B9 / P-007)
spec/BufferPolicies.md, docs/proposals/P-007-arraypool-span.md
Rule B9 is expanded to include .Length-bounded overspan view forms and explicitly exclude Array.Clear tail-wipes. P-007 status, scope table, and open question #4 are updated to reflect POOL003/POOL005 completion and the MemoryPool unified release model.
Extractor: MemoryPool recognition and FullViewOwner expansion
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds IsMemoryPoolType/IsMemoryPoolRent symbol-resolved helpers and reworks FullViewOwner to match no-arg AsSpan/AsMemory and .Length-bounded forms via new IsLengthOf/IsZeroInt helpers. Wires IsMemoryPoolRent into LowerFlowStmt acquire tracking and the flow-locals candidate selection. Updates EmitOverspans comment to exclude Array.Clear.
ArrayPool length-overread corpus case (POOL005 / OWN025)
corpus/real-world/arraypool-length-overread/*
before.cs uses buf.Length as the span bound causing over-read of stale pooled tail; after.cs bounds by n. case.own models the overspan fact, expected-diagnostics.txt expects OWN025, and notes.md documents the POOL005 pattern including the Array.Clear exclusion.
MemoryPool double-dispose corpus case (POOL003 / OWN003)
corpus/real-world/memorypool-double-dispose/*
before.cs calls owner.Dispose() twice via try/finally; after.cs uses a using declaration. case.own models the double-release as OWN003, expected-diagnostics.txt expects OWN003, and notes.md documents the POOL003 pattern.
CI benchmark recall threshold bump
.github/workflows/ci.yml
Raises --min-recall in the corpus benchmark step from 14 to 16 to reflect improved checker recall from the two new corpus cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#52: Adds ArrayPool rent/return release modeling to --flow-locals tracking for OWN002/OWN003; this PR extends the same acquire/release machinery to MemoryPool<T>.Rent/IMemoryOwner.Dispose.
  • PhysShell/Own.NET#71: Directly precedes this PR's overspan work — both touch EmitOverspans/FullViewOwner in Program.cs for POOL005/OWN025 detection.
  • PhysShell/Own.NET#53: Upgrades ArrayPool pool Rent/Return flow-lowering in LowerFlowStmt to semantic symbol detection, which this PR mirrors for MemoryPool<T>.

Poem

🐇 Hop, hop — I rent a pool,
but I dispose it twice, like a fool!
Now using keeps it clean and neat,
and .Length bounds keep overreads beat.
OWN003 and OWN025 are found —
the checker's recall is homeward bound! 🎉

🚥 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 and concisely summarizes the main changes: adding POOL005 (.Length spelling detection) and MemoryPool tracking for POOL001/002/003 diagnostics.
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/pool005-clear-pool003

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: b3bf5b1095

ℹ️ 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 +995 to +998
return args.Count == 3
&& args[0].Expression is IdentifierNameSyntax buf
&& IsLengthOf(args[2].Expression, buf.Identifier.Text)
? buf.Identifier.Text : 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 Do not flag whole-buffer clears as overspans

In cleanup code that intentionally wipes a rented buffer before returning it, Array.Clear(buf, 0, buf.Length) is the same whole-buffer wipe as Array.Clear(buf) or returning with clearing enabled; it only overwrites the pooled tail and does not read or expose stale bytes. This condition turns that safe idiom into OWN025 whenever the array came from ArrayPool, so correct fixes that clear the full buffer before Return will be reported as bugs.

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

🧹 Nitpick comments (1)
spec/BufferPolicies.md (1)

42-53: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Rule B9 revision is accurate and aligns with POOL005 overspan detection.

The expansion correctly identifies both unsafe patterns (full-length views and .Length-based forms), distinguishes them from the bounded-by-n safe case, and clearly separates B9 (touching too much) from B6 (clearing too little). The spec precisely matches the extractor's pattern-matching logic.

Consider a brief note that the oversizing principle applies to all pooled buffer types: ArrayPool<T>.Rent(n) returns an oversized array directly, while MemoryPool<T>.Rent(n) returns an IMemoryOwner<T> wrapping an oversized backing array (accessed via .Memory.Length). Both exhibit the same over-reach risk when using the backing .Length instead of the requested n.

🤖 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 `@spec/BufferPolicies.md` around lines 42 - 53, The spec section for B9
currently focuses on ArrayPool<T>.Rent(n) patterns but should clarify that the
oversizing principle applies to all pooled buffer types. Add a brief explanatory
note in the B9 section clarifying that while ArrayPool<T>.Rent(n) returns an
oversized array directly, MemoryPool<T>.Rent(n) returns an IMemoryOwner<T>
wrapping an oversized backing array (accessible via .Memory.Length), and both
exhibit the same over-reach risk when operations use the backing .Length instead
of the requested n parameter. This ensures the spec accurately represents the
threat model across all pooled buffer abstractions.
🤖 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 @.github/workflows/ci.yml:
- Around line 791-799: The benchmark script command in the CI workflow is
configured with --min-recall 16, but the test corpus only contains 14 cases (7
arraypool cases and 1 memorypool case), creating an impossible threshold that
will always fail. Either add the two missing test cases referenced in the PR
objectives to the corpus to bring it to 16 cases, or reduce the --min-recall
threshold value from 16 to 14 to match the current actual corpus size. Verify
the corpus size is accurate before adjusting the threshold.

In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 956-957: The conditions checking IsLengthOf at lines 957, 970, and
995 match any .Length expression without verifying that the start or index
parameter is set to 0, causing false positives beyond the intended POOL005
pattern. For each of these three locations, add an additional validation check
to the condition to ensure the start/index argument is explicitly 0. This
requires examining the second argument in the args collection and confirming its
value is a literal 0 before marking it as a valid POOL005 match.

---

Nitpick comments:
In `@spec/BufferPolicies.md`:
- Around line 42-53: The spec section for B9 currently focuses on
ArrayPool<T>.Rent(n) patterns but should clarify that the oversizing principle
applies to all pooled buffer types. Add a brief explanatory note in the B9
section clarifying that while ArrayPool<T>.Rent(n) returns an oversized array
directly, MemoryPool<T>.Rent(n) returns an IMemoryOwner<T> wrapping an oversized
backing array (accessible via .Memory.Length), and both exhibit the same
over-reach risk when operations use the backing .Length instead of the requested
n parameter. This ensures the spec accurately represents the threat model across
all pooled buffer abstractions.
🪄 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: 3e410335-6435-4e61-8767-6ab51a9841cb

📥 Commits

Reviewing files that changed from the base of the PR and between f206dbf and b3bf5b1.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • corpus/real-world/arraypool-clear-past-length/after.cs
  • corpus/real-world/arraypool-clear-past-length/before.cs
  • corpus/real-world/arraypool-clear-past-length/case.own
  • corpus/real-world/arraypool-clear-past-length/expected-diagnostics.txt
  • corpus/real-world/arraypool-clear-past-length/notes.md
  • corpus/real-world/memorypool-double-dispose/after.cs
  • corpus/real-world/memorypool-double-dispose/before.cs
  • corpus/real-world/memorypool-double-dispose/case.own
  • corpus/real-world/memorypool-double-dispose/expected-diagnostics.txt
  • corpus/real-world/memorypool-double-dispose/notes.md
  • docs/proposals/P-007-arraypool-span.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • spec/BufferPolicies.md

Comment thread .github/workflows/ci.yml
Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
…re zero start

Review feedback on #72 (Codex + CodeRabbit), all precision tightening on an
already-green PR:

1. Codex: do not flag Array.Clear(buf, 0, buf.Length). It is a WRITE/wipe — it only
   zeros the pooled tail (a safe clear-before-Return idiom) and exposes nothing; the
   over-read bug is READING/COPYING the tail through a view. Removed ArrayClearOverOwner
   entirely; EmitOverspans now flags only read-capable VIEWS (FullViewOwner). Renamed
   the corpus case arraypool-clear-past-length -> arraypool-length-overread and made it
   the `.Length` VIEW over-read (buf.AsSpan(0, buf.Length)); after.cs bounds by n.

2. CodeRabbit: the `.Length` view match now also requires the START/index to be the
   constant 0 (IsZeroInt via the SemanticModel) — buf.AsSpan(k, buf.Length) for k != 0
   is out of range, not this pattern. Applies to AsSpan/AsMemory(0, buf.Length) and
   new Span<T>(buf, 0, buf.Length).

3. CodeRabbit (spec nitpick): B9 now notes the oversize principle covers MemoryPool too
   (IMemoryOwner.Memory wraps an oversized backing via .Memory.Length), and that a
   write/wipe is not flagged.

NOT changed: --min-recall stays 16. CodeRabbit flagged it as "impossible (corpus is 14)"
but that miscounts — the benchmark scores corpus/real-world (14) AND corpus/wpf (4) = 18
total, and --min-recall is the CAUGHT floor, not the corpus size. #72's benchmark is
green at 16 (16 caught of 18); the `.Length` VIEW form still catches arraypool-length-
overread after dropping Array.Clear. spec/P-007/ci.yml comments updated to match.

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