Skip to content

fix(report): label a flow-path ArrayPool rent a "pooled buffer", not "disposable"#97

Merged
PhysShell merged 3 commits into
mainfrom
claude/fix-pool-flow-mislabel
Jun 24, 2026
Merged

fix(report): label a flow-path ArrayPool rent a "pooled buffer", not "disposable"#97
PhysShell merged 3 commits into
mainfrom
claude/fix-pool-flow-mislabel

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Why

The --body-throw-edges Npgsql capstone triage surfaced a bug in our own reporting. An ArrayPool<T>.Shared.Rent(...) buffer leaked/misused on the path-sensitive flow path — e.g. Returned on the normal path but not on a throw edge (CompositeBuilder.cs, BitStringConverters.cs) — was reported as:

IDisposable local 'args' … [resource: disposable]

instead of the correct:

pooled buffer 'args' … [resource: pooled buffer]

The flat pool detector (rkind=="pool") already worded never-returned rents correctly; only the flow path mislabelled them, because the flow acquire fact carried no resource kind, so the bridge defaulted every flow-local to "disposable".

Fix

  • Extractor stamps the flow acquire's kind"pool" for an ArrayPool Rent (IsPoolRent), else "disposable".
  • Bridge (ownir._lower_flow) carries pool onto the handle and words/tags the finding:
    • OWN001 → "rented but never returned to the pool" (never) / "may not be returned to the pool on every path" (partial);
    • OWN002/003/009 → phrased in Return terms (used-after-returned / returned-more-than-once);
    • tag [resource: pooled buffer].
  • Plain disposables are unchanged (kind absent / "disposable" → existing wording).

Tests

layer what
tests/fixtures/ownir/flow_pool_partial.facts.json + test_ownir bridge check (now 118): pool partial → "may not be returned on every path", pool never → "rented but never returned", disposable control unchanged — each with the right [resource: …] tag
FlowLocalsSample.PoolReturnedOnOnePath + CI assertion end-to-end: a real ArrayPool.Rent Returned on one path only → the extractor stamps kind and the bridge labels pooled buffer 'partialBuf' may not be returned to the pool on every path

Validated locally: test_ownir 118/118, run_tests corpus/wpf/lifetimes/loops/spec all green.

Scope: ArrayPool (IsPoolRent) only — MemoryPool IMemoryOwner rents are Dispose-released and keep the disposable wording. Backward-compatible: an acquire fact without kind still reads as a disposable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a new sample demonstrating conditional ArrayPool returns on only some control-flow paths.
  • Bug Fixes

    • Improved flow-local diagnostics to correctly identify pooled buffers vs generic disposables, including updated OWN001 wording and proper [resource: pooled buffer] tagging.
  • Tests

    • Added new flow fixture and regression assertions covering partial, never-returned, and disposable control-flow scenarios.
  • Chores

    • Strengthened CI verification for the pooled-buffer labeling case.

…"disposable"

The --body-throw-edges Npgsql capstone surfaced our own reporting bug: an ArrayPool
Rent leaked/misused on the path-sensitive flow path (e.g. Returned on the normal path
but not the throw edge — CompositeBuilder/BitStringConverters) was reported as
"IDisposable local '…' … [resource: disposable]" instead of "pooled buffer '…' …
[resource: pooled buffer]". The flat pool detector (rkind=="pool") already worded
never-returned rents correctly; only the FLOW path mislabelled them, because the
acquire fact carried no resource kind, so the bridge defaulted every flow-local to
"disposable".

Fix: the extractor stamps the flow acquire's `kind` ("pool" for an ArrayPool Rent,
else "disposable"); the bridge carries it onto the handle and words/tags the finding
accordingly — OWN001 reads "rented but never returned" / "may not be returned to the
pool on every path", OWN002/003/009 read in Return terms, tag [resource: pooled
buffer]. Plain disposables are unchanged.

Pinned by tests/fixtures/ownir/flow_pool_partial.facts.json + a test_ownir bridge
check (118 now) and, end-to-end, by FlowLocalsSample.PoolReturnedOnOnePath + a CI
assertion (extractor stamps kind -> bridge labels pooled buffer).

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 23, 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: 7743f8ab-ab0b-4c7f-846a-3c338d1d384b

📥 Commits

Reviewing files that changed from the base of the PR and between 677aa78 and de3f762.

📒 Files selected for processing (2)
  • ownlang/ownir.py
  • tests/test_ownir.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • ownlang/ownir.py
  • tests/test_ownir.py

📝 Walkthrough

Walkthrough

The flow-local analysis pipeline gains pooled-buffer awareness: the Roslyn extractor now emits a kind field ("pool" vs "disposable") on acquire facts, OwnIR lowering propagates this into a pool flag on synthetic handles, and diagnostic formatting branches on that flag to emit pooled-buffer-specific messages and Finding.kind. A new sample method, test fixture, unit tests, and CI assertion cover the partial-return scenario.

Changes

Pool-aware flow-local labeling

Layer / File(s) Summary
Roslyn extractor: emit pool kind on acquire facts
frontend/roslyn/OwnSharp.Extractor/Program.cs, frontend/roslyn/samples/FlowLocalsSample.cs
The acquire fact emitted during flow-local lowering now includes kind: "pool" for ArrayPool<T>.Rent initializers and kind: "disposable" otherwise. A new sample method PoolReturnedOnOnePath demonstrates the conditional-return pattern.
OwnIR lowering: pool handle tagging and diagnostic formatting
ownlang/ownir.py
Acquire lowering reads kind == "pool" and stores a pool boolean on the synthetic handle. OWN001/OWN002/OWN003/OWN009 message generation branches on pool to emit pooled-buffer-specific text, and Finding.kind is set to "pooled buffer" vs "disposable" accordingly.
Test fixture, unit tests, and CI assertion
tests/fixtures/ownir/flow_pool_partial.facts.json, tests/test_ownir.py, .github/workflows/ci.yml
A new fixture defines three PoolFlow functions with pool/disposable acquire kinds and partial-release flows. Unit tests assert exact kind, message substring, and [resource: ...] render tag for each event. The CI workflow adds a grep assertion for the partialBuf OWN001 pooled-buffer message.

Sequence Diagrams

sequenceDiagram
  participant Roslyn as Roslyn Extractor
  participant OwnIR as OwnIR Lowering
  participant Finding as Finding Mapper
  
  Roslyn->>Roslyn: Detect ArrayPool<T>.Rent initialization
  Roslyn->>OwnIR: Emit acquire {kind: 'pool'}
  OwnIR->>OwnIR: Store pool=True on synthetic handle
  OwnIR->>Finding: Route to flow-local OWN001 formatter
  Finding->>Finding: Check handle.pool flag
  Finding->>Finding: Format: "pooled buffer may not be returned to the pool"
  Finding->>Finding: Set Finding.kind = 'pooled buffer'
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • PhysShell/Own.NET#15: Adds the flow-local CFG/fact handling and baseline kind="disposable" messaging in ownlang/ownir.py that this PR extends with the "pooled buffer" kind and pool-specific text.
  • PhysShell/Own.NET#52: Updates the same --flow-locals pool-lowering path in frontend/roslyn/OwnSharp.Extractor/Program.cs, routing pooled buffers through the flow engine for OWN002/OWN003 detection.
  • PhysShell/Own.NET#53: Also modifies the Roslyn extractor's ArrayPool<T>.Rent/Return detection during flow-lowering via SemanticModel-based recognition, directly related to the IsPoolRent check this PR extends.

Poem

🐇 Hop, hop, the buffer was rented with care,
But one branch returned it — the other, bare!
Now kind: "pool" marks what the rent did bestow,
And OWN001 warns where the returns don't flow.
No buffer left orphaned, the diagnostics proclaim:
"Pooled buffer not returned" — and the pool finds its claim! 🎉

🚥 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 directly and accurately captures the main change: correcting the labeling of ArrayPool rents from 'disposable' to 'pooled buffer' in flow-path reporting, which is the core objective of this 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/fix-pool-flow-mislabel

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

// Npgsql capstone on CompositeBuilder/BitStringConverters ArrayPool rents).
public void PoolReturnedOnOnePath(bool c)
{
var partialBuf = ArrayPool<byte>.Shared.Rent(16);
Pure line-wrapping of the new pooled-buffer flow messages via implicit string
concatenation — the rendered strings are byte-identical (test_ownir 118/118,
the CI flow assertion unchanged). No logic change.

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.

🧹 Nitpick comments (1)
tests/test_ownir.py (1)

454-471: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten this regression check to fail on unexpected extra findings.

The loop validates expected events, but this block still passes if additional unrelated findings appear. Add an exact event-set/size assertion before per-event checks.

♻️ Suggested test hardening
     ppfindings = check_facts(ppfacts)
     by = {x.event: x for x in ppfindings}
     checks += 1
+    expected_events = {"buf", "nbuf", "d"}
+    if set(by.keys()) != expected_events:
+        fails.append(f"pool-label: expected events {sorted(expected_events)}, got "
+                     f"{sorted(by.keys())}")
+
     want = [
         ("buf", "pooled buffer", "may not be returned to the pool on every path", "pooled buffer"),
         ("nbuf", "pooled buffer", "rented but never returned to the pool", "pooled buffer"),
         ("d", "disposable", "may not be disposed on every path", "disposable"),
     ]
🤖 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 `@tests/test_ownir.py` around lines 454 - 471, The test currently validates
that expected events exist with correct properties, but does not fail if
unexpected additional findings are present in the ppfindings. Before the loop
that iterates through the want list and checks each event, add an assertion to
verify the exact event-set or size of findings matches expectations. Compare the
keys from the by dictionary (which maps x.event to x for all ppfindings) against
the expected events extracted from the want list to ensure no extra findings are
present that would otherwise be silently ignored.
🤖 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 `@tests/test_ownir.py`:
- Around line 454-471: The test currently validates that expected events exist
with correct properties, but does not fail if unexpected additional findings are
present in the ppfindings. Before the loop that iterates through the want list
and checks each event, add an assertion to verify the exact event-set or size of
findings matches expectations. Compare the keys from the by dictionary (which
maps x.event to x for all ppfindings) against the expected events extracted from
the want list to ensure no extra findings are present that would otherwise be
silently ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1883d757-fd6d-4175-9a6f-3724bf2a1eb6

📥 Commits

Reviewing files that changed from the base of the PR and between 6f32cd5 and 677aa78.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/FlowLocalsSample.cs
  • ownlang/ownir.py
  • tests/fixtures/ownir/flow_pool_partial.facts.json
  • tests/test_ownir.py

…ngs (CodeRabbit)

Harden the flow-pool-label regression: fail on any extra/duplicate finding (set of
events + count), so a future spurious pool/disposable finding can't slip past the
per-event checks.

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

3 participants