Repository navigation
Research: stdexec-inspired operation lifetime facts to recover closure-capture OWN001 false negatives #406
Description
Classification
- Type: bounded research / precision-recall spike (not a new async framework)
- Priority: P2 until a real-bug gate establishes incremental value
- Area: Roslyn extraction → OwnIR → ownership/lifetime core
- Related: Own.Concurrent: retrofit structured-concurrency contracts onto existing C# async code #282 (outstanding child-task obligations), Owen.Async: cancellation atomicity and abandoned-operation safety #311 (abandoned/cancelled operations), Own.Concurrent: detect ownership-handoff violations across concurrent publication boundaries #366 (concurrent object handoff)
Motivation: a confirmed Own.NET recall gap
The repository explicitly documents a conservative escape rule in closure-capture-escape-precision.md: a disposable local referenced lexically inside a lambda/local function is currently considered escaped and removed from method-local OWN001 tracking, even if that closure never escapes.
That rule fixed an actual false positive involving ShareX's ForEachAsync and a captured SemaphoreSlim, but the note also acknowledges a false-negative tradeoff. For example:
void Leak()
{
var stream = new MemoryStream();
Action use = () => stream.WriteByte(1);
use(); // local, synchronous, non-escaping
} // undisposed stream: should remain OWN001-eligibleToday, lexical capture can suppress the finding. This is a concrete analysis gap, not a speculative need for a general std::execution port.
Inspiration, without confusing C++ and C#
NVIDIA/stdexec / P2300 makes the distinction between describing work, connecting it to a receiver, starting an operation, and completing it explicit. Its operation state has a lifetime obligation until completion. That separation suggests a useful analysis vocabulary, not a drop-in C# runtime feature.
C# execution models must not be conflated:
- invoking a local
Actioncan be fully synchronous; Task-returning methods commonly initiate work at invocation time;Func<Task>merely stores a factory until called;- LINQ
IEnumerable<T>andIAsyncEnumerable<T>can defer work; await,WhenAll, return/transfer, and approved supervisors provide distinct completion/ownership evidence.
A lexical capture does not, by itself, prove that its closure escapes; conversely, an invocation does not by itself prove that asynchronous work has completed.
Hypothesis
For a narrowly provable subset of local callbacks and deferred operations, distinguishing capture, escape, invocation/start, completion/join, and ownership transfer can recover real disposal/lifetime bugs currently hidden by blanket capture suppression, without reintroducing the ShareX false positive.
Bounded scope / first slice
- Inventory the current Roslyn closure-capture/escape handling and the Rust/Python ownership-core consumption of those facts. Record the precise OWN001 suppression path and baseline fixtures.
- Model only same-method, locally assigned delegates initially. Detect the provably non-escaping, synchronously invoked shape (including conditional branches/early exits). Do not implement a general delegate points-to analysis.
- Emit evidence facts, not Roslyn-side verdicts, with a small vocabulary such as:
The exact schema must reuse existing identities/flow and may be smaller. Core decides whether OWN001 tracking is reinstated.
capture(resource, closure) closure_local(closure) invoke_sync(closure, site) escape(closure, target) deferred_or_unknown(closure, reason) join_or_completion(operation, site) - Preserve conservative behavior for unknown callees, stored/returned delegates, non-local aliases, deferred LINQ, async callbacks,
Task.Run, and tasks crossing method boundaries. Do not infer their completion from syntax. - Reuse Own.Concurrent: retrofit structured-concurrency contracts onto existing C# async code #282's task obligations if/when present. This issue does not implement general task cancellation, scheduler modeling, async state-machine reconstruction, or concurrency handoff.
Required fixtures (red → green)
- Positive / recovered FN: local captured disposable, delegate invoked synchronously, no disposal → OWN001.
- Positive: same shape with a path that bypasses
Dispose→ conditional/path-sensitive finding. - Negative: local captured disposable, synchronously invoked, then disposed on all paths → silent.
- Negative / preserve existing FP fix: ShareX-style
ForEachAsynccapturing aSemaphoreSlimin asynchronous lambdas and returningTask.WhenAll→ no method-local OWN001 merely because the captured semaphore lacks immediate disposal; any later ownership/lifetime obligation is separate. - Unknown / no invented clean proof: closure stored, passed to unmodelled method, or used by deferred
Select→ retain conservative behavior and expose unsupported evidence if the reporting infrastructure supports it. - Negative:
nameof(resource)inside the closure does not count as a runtime capture. - Negative: an
asynclambda invoked but not awaited must not be classified as synchronous completion.
Evidence gate before expanding scope
- Reproduce the documented false negative on current
mainand prove the narrow fix with source-level C# and OwnIR/core fixtures. - Mine/adjudicate a small real C# bug-fix sample involving delegate captures and disposal. Keep synthetic tests separate from the real-bug count.
- Compare current Own.NET versus prototype and available IDisposable/Roslyn analyzer baselines. Report recovered true positives, regressions/false positives, unsupported cases, model cost, and analysis time.
- If no incremental real findings appear, document a no-go and keep the existing conservative suppression.
Acceptance criteria
- Current behavior and the documented ShareX exception are reproduced with tests.
- Non-escaping synchronous invocation no longer automatically implies resource escape.
- Recovered OWN001 has a source-mapped path witness (allocation → capture → local invocation(s) → scope exit without disposal).
- Async/deferred/escaping/unknown shapes remain conservative; no fake completion inference.
- All listed positive/negative fixtures pass, including
nameof. - A reproducible real-bug/baseline report leads to a documented go/no-go for a larger operation-lifetime fact family.
Non-goals
No C++ stdexec binding, C# sender/receiver library, wholesale async rewrite, generic scheduler/race proof, or new independent Roslyn verdict engine. This is a targeted Own.NET recall experiment inspired by explicit operation lifetimes, not a promise to implement P2300 for .NET.
Activity
Merge pull request #412 from PhysShell/research/next-bug-class
PhysShell commentedon Oct 20, 2026
NO-GO within current evidence budget (next-bug-class research, #412).
The closure-capture false negative reproduces (
repro/b_406). The bounded search for real bug-fix evidence found no real bug in this mechanism, so it was not chosen as the next bug class. The issue stays open.Details:
docs/evidence/next-bug-class-2026-10/REPORT.md.