Skip to content

Research: stdexec-inspired operation lifetime facts to recover closure-capture OWN001 false negatives #406

Open

Description

@PhysShell

Classification

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-eligible

Today, 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 Action can be fully synchronous;
  • Task-returning methods commonly initiate work at invocation time;
  • Func<Task> merely stores a factory until called;
  • LINQ IEnumerable<T> and IAsyncEnumerable<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

  1. 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.
  2. 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.
  3. Emit evidence facts, not Roslyn-side verdicts, with a small vocabulary such as:
    capture(resource, closure)
    closure_local(closure)
    invoke_sync(closure, site)
    escape(closure, target)
    deferred_or_unknown(closure, reason)
    join_or_completion(operation, site)
    
    The exact schema must reuse existing identities/flow and may be smaller. Core decides whether OWN001 tracking is reinstated.
  4. 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.
  5. 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 ForEachAsync capturing a SemaphoreSlim in asynchronous lambdas and returning Task.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 async lambda invoked but not awaited must not be classified as synchronous completion.

Evidence gate before expanding scope

  • Reproduce the documented false negative on current main and 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

  1. added a commit that references this issue on Oct 18, 2026
  2. PhysShell commented on Oct 20, 2026

    @PhysShell
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions