Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -798,5 +798,5 @@ jobs:
# (`ViewOwner`), so reading it after Dispose trips OWN002 (POOL002 — `memorypool-view-after-
# dispose`). Remaining backlog: a FIELD-mediated cross-method use-after-dispose, a view stored in
# a FIELD, and an injected-source region-escape. A drop below the floor is a regression.
run: python scripts/benchmark.py --min-recall 17
run: python scripts/benchmark.py --min-recall 19

14 changes: 14 additions & 0 deletions corpus/real-world/memorypool-using-statement-view-escape/after.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// AFTER (fixed). Ownership is TRANSFERRED to the caller: the method returns the
// `IMemoryOwner<T>` directly (no `using` scope), so nothing is disposed here and the pooled
// buffer stays alive for as long as the caller keeps the owner. No dangling borrow, so the
// checker is silent.
using System;
using System.Buffers;

static class MemoryPoolUsingStatementViewEscape
{
static IMemoryOwner<byte> Borrow(int n)
{
return MemoryPool<byte>.Shared.Rent(n); // transfer ownership — the caller disposes it
}
}
23 changes: 23 additions & 0 deletions corpus/real-world/memorypool-using-statement-view-escape/before.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// BEFORE (buggy). The STATEMENT form of `memorypool-using-view-escape` (POOL004). The
// MemoryPool owner is scoped by a `using (...)` STATEMENT rather than a `using`
// declaration, but the dispose semantics are identical: the owner is `Dispose()`d as the
// block exits, so `return owner.Memory` hands the caller a `Memory<T>` backed by buffer
// already returned to the pool — a dangling borrow. Both `using` syntaxes need the same
// scope-exit desugaring (CodeRabbit review on #74). The fix is to transfer ownership:
// return the `IMemoryOwner` itself (see after.cs).
//
// Wrapped in a class so the extractor's per-class flow pass visits it.
using System;
using System.Buffers;

static class MemoryPoolUsingStatementViewEscape
{
static Memory<byte> Borrow(int n)
{
using (IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(n))
{
return owner.Memory; // <-- BUG: owner is disposed as the using block exits, so the
// caller reads a view of buffer already back in the pool
}
}
}
17 changes: 17 additions & 0 deletions corpus/real-world/memorypool-using-statement-view-escape/case.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// OwnLang model of the MemoryPool `using`-STATEMENT view escape (the `using (...) { … }`
// form of memorypool-using-view-escape). `acquire` == MemoryPool.Rent, `release` == the
// implicit scope-exit Dispose of the `using` statement. The block returns `owner.Memory`
// — a borrow of the owner — but the `using` disposes the owner FIRST, so the caller reads
// a view of memory already returned to the pool: a use-after-release lowered to OWN002.
// (The `.own` model is the same as the declaration form — the OwnLang reduction does not
// distinguish the two C# `using` syntaxes; the extractor desugars both identically.)
module Corpus
resource MemoryOwner {
acquire Rent
release Dispose
}
fn lease(n: int) {
let owner = acquire MemoryOwner(n); // using (... = MemoryPool.Rent)
release owner; // implicit scope-exit Dispose
use owner; // return owner.Memory — read by the caller AFTER Dispose -> OWN002
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OWN002
27 changes: 27 additions & 0 deletions corpus/real-world/memorypool-using-statement-view-escape/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# MemoryPool `using (...)`-statement view escape (POOL004, the statement form)

**Pattern:** identical to `memorypool-using-view-escape`, but the owner is scoped by a
`using (...)` **statement** instead of a `using` **declaration**:
`using (IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(n)) { return owner.Memory; }`.
Both syntaxes dispose the owner at scope exit, so the returned `Memory<T>` dangles either
way — a use-after-free of pooled memory. The fix is the same: transfer ownership (return the
`IMemoryOwner`).

**Why this case exists (CodeRabbit review on #74):** the `using`-declaration desugaring in
#74 only matched `LocalDeclarationStatementSyntax` with a `using` keyword. The statement
form (`UsingStatementSyntax`) was still lowered as a plain body, so its MemoryPool owner was
neither tracked nor release-threaded — the same dangle, silent. This slice extends the
desugaring (and the flow-locals candidate scan) to the `using (...)` statement form, so a
tracked `using (owner = MemoryPool.Rent(…)) { return owner.Memory; }` is lowered to
`acquire; try { body } finally { release }` exactly like the declaration form → **OWN002**.

**What the checker says:** the OwnLang model and the real `before.cs` both trip **OWN002**.
The ownership-transfer fix in `after.cs` (return the owner directly, no `using`) is silent.

**Honesty / scope.** `case.own` is a faithful hand reduction (not C# ingested by the
checker); `before.cs` / `after.cs` are representative of the bug and its fix. (Returning the
owner itself from a `using` scope — `using owner; return owner;` — is a distinct, follow-up
gap: the escape pass untracks directly-returned owners as ownership transfers.)

Reference: [P-007](../../../docs/proposals/P-007-arraypool-span.md); the declaration form is
`memorypool-using-view-escape`.
16 changes: 16 additions & 0 deletions corpus/real-world/memorypool-using-view-escape/after.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// AFTER (fixed). Ownership is TRANSFERRED to the caller: the method returns the
// `IMemoryOwner<T>` itself (no `using`), so nothing is disposed here. The caller owns the
// owner's lifetime (`using var owner = Borrow(n); … owner.Memory …`) and the pooled buffer
// stays alive as long as the returned view is used. No dangling borrow, so the extractor
// untracks the escaped owner and the checker is silent.
using System;
using System.Buffers;

static class MemoryPoolUsingViewEscape
{
static IMemoryOwner<byte> Borrow(int n)
{
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(n);
return owner; // transfer ownership to the caller — the caller disposes it
}
}
23 changes: 23 additions & 0 deletions corpus/real-world/memorypool-using-view-escape/before.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// BEFORE (buggy). POOL004 for MemoryPool, the idiomatic-`using` dangle. An
// `IMemoryOwner<T>` from `MemoryPool<T>` is held with a `using` declaration — so it is
// `Dispose()`d at scope exit — but the method RETURNS `owner.Memory`, a borrow of the
// owner's pooled buffer. The implicit dispose runs as the method returns, so the caller
// receives a `Memory<T>` backed by memory already handed back to the pool: a dangling
// borrow / use-after-free. `using owner = …; return owner.Memory;` is exactly
// `try { return owner.Memory; } finally { owner.Dispose(); }` — the MemoryPool twin of
// the ArrayPool try/finally `Memory` escape. The fix is to TRANSFER ownership: return the
// `IMemoryOwner` itself (no `using`) and let the caller own its lifetime (see after.cs).
//
// Wrapped in a class so the extractor's per-class flow pass visits it.
using System;
using System.Buffers;

static class MemoryPoolUsingViewEscape
{
static Memory<byte> Borrow(int n)
{
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(n);
return owner.Memory; // <-- BUG: the `using` disposes owner as we return, so the
// caller gets a view of buffer already returned to the pool
}
}
17 changes: 17 additions & 0 deletions corpus/real-world/memorypool-using-view-escape/case.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// OwnLang model of the MemoryPool `using`-view escape. `acquire` == MemoryPool.Rent,
// `release` == the implicit `using` scope-exit Dispose. The method returns `owner.Memory`
// — a borrow of the owner — but the `using` disposes the owner FIRST (the extractor
// desugars `using owner = …; return owner.Memory;` to `acquire; release; use`), so the
// caller reads a view of memory already returned to the pool: a use-after-release lowered
// to OWN002. (The using-dispose is threaded before the return's view-use, mirroring the
// ArrayPool try/finally Memory escape.)
module Corpus
resource MemoryOwner {
acquire Rent
release Dispose
}
fn lease(n: int) {
let owner = acquire MemoryOwner(n); // using MemoryPool.Rent
release owner; // implicit `using` Dispose at scope exit
use owner; // return owner.Memory — read by the caller AFTER Dispose -> OWN002
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OWN002
31 changes: 31 additions & 0 deletions corpus/real-world/memorypool-using-view-escape/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# MemoryPool `using`-owned view escape (POOL004, the idiomatic dangle)

**Pattern:** an `IMemoryOwner<T>` from `MemoryPool<T>` is held with a `using`
declaration (so it is `Dispose()`d at scope exit), but the method **returns a view of
it** — `using owner = …; return owner.Memory;`. The implicit dispose runs as the method
returns, so the caller receives a `Memory<T>` backed by a buffer already returned to the
pool: a dangling borrow / use-after-free. It is exactly
`try { return owner.Memory; } finally { owner.Dispose(); }` — the MemoryPool twin of the
ArrayPool try/finally `Memory` escape (`arraypool-memory-view-escape`, #70). The fix is to
**transfer ownership**: return the `IMemoryOwner` itself (no `using`) so the caller owns
its lifetime.

**Why it was missed before (Codex review on #73):** `using` locals are skipped by the
flow-locals candidate pass — they are auto-disposed, so normally not leak candidates — so
the owner never entered `tracked` and the returned view was not mapped to it. This slice
**desugars a tracked `using IMemoryOwner = MemoryPool.Rent(…)` declaration** into
`acquire; try { rest } finally { release }`: the implicit scope-exit dispose is threaded
onto the rest's returns (and throws), so the returned view's caller-use lands *after* the
release and trips OWN002 — reusing the same `onReturn` / `ReturnedViewOwners` machinery
that catches the ArrayPool try/finally form. Methods with no such `using` declaration are
lowered exactly as before.

**What the checker says:** the OwnLang model and the real `before.cs` both trip
**OWN002**. The ownership-transfer fix in `after.cs` returns the owner (no `using`), so the
escaped owner is untracked and the checker is silent.

**Honesty / scope.** `case.own` is a faithful hand reduction (not C# ingested by the
checker); `before.cs` / `after.cs` are representative of the bug and its fix.

Reference: [P-007](../../../docs/proposals/P-007-arraypool-span.md); the ArrayPool twin is
`arraypool-memory-view-escape` (#70).
6 changes: 5 additions & 1 deletion docs/proposals/P-007-arraypool-span.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
so its leak / double-dispose ride the same flow as POOL001/003 (corpus `memorypool-double-dispose`
→ OWN003), and its `owner.Memory` / `owner.Memory.Span` is a borrow lowered to a use of the OWNER
(`ViewOwner`), so reading the view after `Dispose` trips **POOL002 → OWN002** (corpus
`memorypool-view-after-dispose`). A POOL005 view stored in a FIELD is next
`memorypool-view-after-dispose`). The idiomatic `using owner = MemoryPool.Rent(…); return
owner.Memory;` — which dangles after the implicit scope-exit dispose — is caught too:
the flow desugars a tracked `using IMemoryOwner` declaration to `acquire; try { rest }
finally { release }`, so the returned view is read after the release (**POOL004 → OWN002**;
corpus `memorypool-using-view-escape`). A POOL005 view stored in a FIELD is next
- **Depends on:** `spec/OwnCore.md` (OWN001 leak, OWN002 use-after-release,
OWN003 double-release, OWN008 release-while-borrowed), the buffer/borrow model
in `spec/`, [P-001](P-001-csharp-extractor.md). See
Expand Down
Loading
Loading