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
17 changes: 9 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -625,12 +625,13 @@ jobs:
- name: Score the corpus on real C#
# Precision is gated absolutely (every fix silent, zero false positives);
# recall is pinned at the measured floor and ratchets up as the extractor
# improves. Now 7/10 — pooled buffers ride the path-sensitive flow engine
# (Rent = acquire, Return = release: OWN003/OWN002), and pool recognition is
# now resolved through the Roslyn SemanticModel, so a double-return reached via
# an ALIASED pool receiver (`var p = ArrayPool<int>.Shared; p.Return(buf)`) — a
# miss for the old text heuristic — is caught. Remaining backlog: interprocedural
# handoff, cross-method use-after-dispose, a region-escape shape. A drop below
# the floor is a regression.
run: python scripts/benchmark.py --min-recall 7
# improves. Now 8/10 — pooled buffers ride the path-sensitive flow engine
# (Rent = acquire, Return = release: OWN003/OWN002, pool resolved via the Roslyn
# SemanticModel so an ALIASED receiver is caught), and ownership-transferring
# factory acquires (System.IO.File.Open*/Create*) are now recognised alongside
# `new`, so the leak arm of the interprocedural-handoff case fires OWN001.
# Remaining backlog: the use-after-handoff (OWN002) arm of that case, a
# cross-method use-after-dispose, and an injected-source region-escape. A drop
# below the floor is a regression.
run: python scripts/benchmark.py --min-recall 8

16 changes: 13 additions & 3 deletions corpus/real-world/ownership-handoff-consume/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ points-to is involved: the signature `consume Stream` is the cut point, exactly
as Rust's borrow checker is modular against function signatures.

As with every corpus case, `case.own` is a faithful hand reduction of the C#
pattern in `before.cs` / `after.cs`, **not** C# the checker ingested — OwnLang
has no C# front-end. The corpus shows the ownership *logic* maps onto real bugs,
not that the tool scanned real C#.
pattern in `before.cs` / `after.cs`, **not** C# the `.own` checker ingested —
OwnLang has no C# front-end. The corpus shows the ownership *logic* maps onto
real bugs, not that the `.own` tool scanned real C#.

**Extractor status (real C#, the `corpus-benchmark` path).** The benchmark now
catches the **leak arm** (`Leak` → `OWN001`) end-to-end: `IsOwningFactory`
recognises `File.OpenRead` as an *owned acquire* (a factory, not `new`), so the
un-disposed stream is flagged exactly as a `new`'d one would be, and the
`using var` fix stays silent. The **use-after-handoff arm** (`Run` → `OWN002`) is
still extractor-future — it needs the inter-procedural *consume* contract:
recognise that `Archive` disposes its by-value parameter, then model `Archive(s)`
as a *release* of `s` so the later `s.Length` is a use-after-release. The `.own`
reduction proves both arms today; the C# front-end catches the leak.
29 changes: 23 additions & 6 deletions docs/notes/corpus-benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ correct code), and **recall was 3/9** — the three caught are exactly the
subscription/region class the extractor is strongest at (`zombie-viewmodel` →
OWN001, two static-event escapes → OWN014).

## Ratchet → 7/10 (three ratchets)
## Ratchet → 8/10 (four ratchets)

### → 4/9: a fixture was understating us

Expand Down Expand Up @@ -75,11 +75,28 @@ fixture — `arraypool-aliased-receiver`, a double-return reached through `var p
heuristic's failure mode was recall-leaning (a missed alias is a missed catch, never a false alarm),
so the upgrade only adds — precision stays absolute. **Recall is now 7/10.**

The remaining three misses are genuine **frontend extraction gaps** — the interprocedural
ownership-handoff (`OWN001`+`OWN002`), a field/cross-method use-after-dispose, and a
region-escape shape — the `.own` reductions all catch them, the C# extractor does not yet.
That is the itemized recall backlog; each is a real capability the floor will ratchet up to
as it lands.
### → 8/10: factory acquires, not just `new`

The extractor only ever treated `new X()` as *acquiring* an owned disposable — so a stream
opened by a **factory**, `var s = File.OpenRead(path)`, was invisible, and the leak arm of
`ownership-handoff-consume` (a stream neither disposed nor handed off → a real `OWN001`)
scored a miss. `File.Open*` / `Create*` hand back a fresh `FileStream` the caller owns
exactly as if it had `new`'d one, so a local bound to one is an acquire. `IsOwningFactory`
recognises them off the resolved **symbol** against a curated `System.IO.File` set (precision
over recall — the set grows only where ownership is certain, so a borrowed/cached disposable
handed back by some other API is never mistaken for an acquire). With it the leak arm fires
`OWN001` (the fix's `using var` stays silent), so the case flips to caught — **recall is now
8/10**. The blast radius is exactly one file: nothing else in the corpus or the samples opens
a `File.*` stream, so no `after.cs` and no dog-food scan can newly cry wolf.

The remaining gaps are genuine **frontend extraction** islands. The *use-after-handoff*
(`OWN002`) arm of `ownership-handoff-consume` — caught only as the leak today — needs the
inter-procedural **consume** contract (a method that disposes a by-value parameter, checked at
call sites like Rust's move; the cut is the *signature*, no whole-program points-to). A
field/cross-method use-after-dispose needs cross-method field-state. And the injected-source
region-escape (`viewmodel-escapes-to-app`) needs the source's lifetime *proven* — its DI
registration — which the fixture does not even carry. The `.own` reductions catch all three;
the C# extractor does not yet. Each is a real capability the floor will ratchet up to as it lands.

## Why catch/clean, not exact-code match

Expand Down
15 changes: 9 additions & 6 deletions docs/proposals/P-012-bug-corpus-mining.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
(the bug is caught) and specificity (the fix is silent), gated in the
`corpus-benchmark` CI job. This is the measurement spine — the defensible number,
and the verifiable reward for any future learning loop. First measurement **3/9
caught · 9/9 clean · 0 FP**, ratcheted to **7/10** over three steps: (1) a *fixture*
caught · 9/9 clean · 0 FP**, ratcheted to **8/10** over four steps: (1) a *fixture*
was understating us — `screentogif-loaded-subscription` referenced an undeclared VM
type → `OWN050`, fixed by making it self-contained; (2) a real *capability* —
pooled buffers are now routed through the path-sensitive flow engine (Rent =
Expand All @@ -15,11 +15,14 @@
recognition moved off the receiver-text heuristic onto the **Roslyn SemanticModel**
(binding `System.Buffers.ArrayPool<T>`), so a double-return through an *aliased* pool
receiver (`var p = ArrayPool<int>.Shared; p.Return(buf)`) — a miss for the text
heuristic — is caught (`arraypool-aliased-receiver`, +1 row). Perfect precision
throughout. The remaining 3 misses are genuine frontend extraction gaps
(interprocedural handoff, a cross-method use-after-dispose, a region-escape shape)
— the tracked recall backlog the floor ratchets up to. Still ahead: those, GitHub
mining at scale (stage 1) and the 50–100-repo prevalence scan (stage 2). See
heuristic — is caught (`arraypool-aliased-receiver`, +1 row); (4) ownership-transferring
**factory acquires** (`System.IO.File.Open*`/`Create*`) are recognised alongside `new`,
so the leak arm of the interprocedural-handoff case fires `OWN001`. Perfect precision
throughout. The remaining gaps: the use-after-handoff (`OWN002`) arm of that case (needs
the inter-procedural *consume* contract), a cross-method use-after-dispose, and an
injected-source region-escape — the tracked recall backlog the floor ratchets up to.
Still ahead: those, GitHub mining at scale (stage 1) and the 50–100-repo prevalence
scan (stage 2). See
[docs/notes/corpus-benchmark.md](../notes/corpus-benchmark.md).
- **Depends on:** P-001 (C# → OwnIR extractor — the scanner that does stage 2);
the existing `corpus/` layout (`before.cs`, `after.cs`,
Expand Down
29 changes: 28 additions & 1 deletion frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,8 @@
if (tracked.Contains(v.Identifier.Text)
&& (v.Initializer?.Value is ObjectCreationExpressionSyntax
or ImplicitObjectCreationExpressionSyntax
|| IsPoolRent(v.Initializer?.Value, model))) // ArrayPool<T> Rent
|| IsPoolRent(v.Initializer?.Value, model) // ArrayPool<T> Rent
|| IsOwningFactory(v.Initializer?.Value, model))) // File.Open*/Create* factory
nodes.Add(new { op = "acquire", var = v.Identifier.Text, line = LineOf(v) });
return true;
case ExpressionStatementSyntax es:
Expand Down Expand Up @@ -758,6 +759,30 @@
&& i.ArgumentList.Arguments[0].Expression is IdentifierNameSyntax buf
? buf.Identifier.Text : null;

// A factory call that CREATES and hands back a fresh owned IDisposable the caller must
// release — recognised via the resolved symbol (curated, the same spirit as
// IsDisposableType is for `new`). System.IO.File.Open*/Create*/*Text return a NEW
// FileStream / StreamReader / StreamWriter that the caller owns exactly as if it had
// `new`'d one, so a local bound to one is an acquire. Curated + symbol-resolved, so a
// borrowed/cached disposable handed back by some other API is never mistaken for an
// owned acquire (precision over recall — the set grows only as ownership is certain).
static bool IsOwningFactory(ExpressionSyntax? e, SemanticModel model)
{
if (e is not InvocationExpressionSyntax i
|| model.GetSymbolInfo(i).Symbol is not IMethodSymbol sym
|| sym.Name is not ("OpenRead" or "OpenWrite" or "Open" or "Create"
or "OpenText" or "CreateText" or "AppendText"))
return false;
INamedTypeSymbol? ct = sym.ContainingType;
if (ct is null || ct.Name != "File")
return false;
INamespaceSymbol? ns = ct.ContainingNamespace; // System.IO.File -> IO
if (ns is null || ns.Name != "IO")
return false;
ns = ns.ContainingNamespace; // IO -> System
return ns is { Name: "System" } && ns.ContainingNamespace is { IsGlobalNamespace: true };
}

// A field/local type treated as owned-disposable (syntax-only heuristic — no
// semantic model): a curated set plus a few suffixes. Gated on the class `new`ing
// the value, so injected/borrowed disposables are not flagged. Timer types are
Expand Down Expand Up @@ -949,7 +974,7 @@
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Where(p => p.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
.ToList();
var refNames = new HashSet<string>(tpa.Select(Path.GetFileName), StringComparer.OrdinalIgnoreCase);

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 977 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.
var references = tpa.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToList();
// P-004 WPF profile: widen the reference set with assemblies named by the
// OWN_EXTRA_REF_DIRS env var (colon-separated dirs) — e.g. the WindowsDesktop ref
Expand Down Expand Up @@ -1301,6 +1326,8 @@
candidates.Add(v.Identifier.Text);
poolBuffers.Add(v.Identifier.Text);
}
else if (IsOwningFactory(v.Initializer?.Value, model)) // File.Open*/Create* factory
candidates.Add(v.Identifier.Text);
}
if (candidates.Count == 0)
continue;
Expand Down
Loading