fix(extractor): treat closure-captured locals as escaped (ShareX throttler FP)#59
Conversation
…ttler FP)
Triaging the ShareX re-mine left one false positive among the surviving
local-disposable findings: Helpers.ForEachAsync's `SemaphoreSlim throttler`. It is
captured by the async lambdas of `items.Select(async i => { await throttler.WaitAsync();
... })` and those lambdas escape via the returned `Task.WhenAll(tasks)`, so the semaphore
must outlive the method frame — it cannot be disposed at method scope and is not a
method-local leak. The flow detector flagged it OWN001.
The escape filter already untracks a local that escapes by return / out / argument (an
ambiguous ownership transfer). A capture into a closure is the same kind of escape, so the
filter now also untracks a candidate whose reference is lexically inside a lambda /
anonymous method / local function (a syntactic ancestor walk to the method body — no
data-flow analysis, crash-proof). It does not require proving the closure escapes: a
captured local MAY outlive the method, and the precision-first stance is to not flag what
cannot be proven to leak (a sound, bounded recall gap, like the argument-passing case).
Not a blanket SemaphoreSlim exemption: SemaphoreSlim is not dispose-optional (accessing
AvailableWaitHandle allocates a handle Dispose must release), so a method-bounded one must
still be flagged. The bug is the capture/escape, not the type.
Pinned by two FlowLocalsSample cases in the wpf-extractor --flow-locals step:
ThrottlerCaptured (a SemaphoreSlim captured by a returned async lambda -> silent) and
SemaphoreLeaks (a non-captured SemaphoreSlim never disposed -> OWN001, the control proving
the exemption is closure-capture). The existing UnitOfWorkFlowSample OWN001 is unaffected:
its `uow` is the receiver of `uow.Member` (outside the `.Where(p => ...)` lambda bodies)
and the `join ... in uow.TempProducts` is query syntax, not an AnonymousFunctionExpression
-- verified the ancestor walk never marks `uow` captured. Writeup in
docs/notes/closure-capture-escape-precision.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe ChangesClosure-Capture Escape Precision for --flow-locals
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac9f99c21f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…pe (Codex #59) Codex review: a `nameof(x)` operand inside a lambda exposes an IdentifierNameSyntax under the closure, but `nameof` is a compile-time string that captures nothing — so the new ancestor-only closure check would mark the local escaped and suppress a real OWN001. The same hole already existed on the argument path (`nameof(s)` makes `s` look like an argument). Skip nameof operands at the top of the escape filter, closing both paths: the local stays tracked and a method-bounded leak is still reported. Pinned by a NameofInLambda sample case (a MemoryStream mentioned only via nameof inside a lambda, never disposed -> OWN001) asserted in the wpf-extractor --flow-locals step. Validated locally: nofLeak -> OWN001 'is never disposed' through the core. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
| // allocates a handle Dispose must release), so it must stay tracked when method-bounded. | ||
| public void SemaphoreLeaks() | ||
| { | ||
| var semLeak = new SemaphoreSlim(1, 1); |
| // never disposed -> a real leak. The nameof operand must not be mistaken for a capture. | ||
| public void NameofInLambda() | ||
| { | ||
| var nofLeak = new MemoryStream(); |
…es for consume inference
Codex P2: ConsumesParam's DescendantNodesAndSelf scan (and the pre-existing DisposesLocal)
descended into nested lambdas / local functions, so a parameter disposed or forwarded ONLY
inside a stored callback — `Register(Stream s) { callbacks.Add(() => Inner(s)); }` — was
wrongly treated as consumed at the call site. That would release the caller's argument at
`Register(s)` and report a phantom use-after-handoff (false OWN002), even though the dispose
runs DEFERRED, not at the call boundary.
Both scans now use DescendantNodesAndSelf(descendIntoChildren: not lambda/local-function) —
the same deferred-body rule the flow lowering already follows (PR #57/#59). A dispose/forward
inside a nested function contributes nothing to the consume inference. Conservative: it can
only DROP a consume (a missed handoff = a leak, never a false use-after-release).
Pinned by a flow-locals silent control: DeferredHandoffNoFalsePositive's `defer` is passed to
a deferred-lambda "consumer", used, then disposed normally — it must stay SILENT (it would
trip a false OWN002 without the fix). The transitive corpus case (Inner forwarded directly,
not in a lambda) is unchanged. corpus 9/9, ownir 116/116.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
The false positive (the one FP from the ShareX triage)
Triaging the ShareX re-mine left six real/defensible local-disposable findings and one false positive —
Helpers.ForEachAsync'sSemaphoreSlim throttler:The flow detector flagged
throttleras an undisposedIDisposable(OWN001). But it's captured by the async lambdas, which escape via the returnedTask.WhenAll— the semaphore must stay alive until every task finishes, so it cannot be disposed at method scope. Not a method-local leak.The fix — capture into a closure is an escape
The escape filter already untracks a local that escapes by return / out / argument (an ambiguous ownership transfer). A capture into a closure is the same kind of escape — the closure can be stored, returned, or run async, so the local outlives the method frame. The filter now also untracks a candidate whose reference is lexically inside a lambda / anonymous method / local function:
Purely syntactic (an ancestor walk to the method body) — crash-proof, no data-flow analysis. It doesn't require proving the closure escapes: a captured local may outlive the method, and the precision-first stance is to not flag what we can't prove leaks (a sound, bounded recall gap, like the argument-passing case).
Not a blanket
SemaphoreSlimexemption.SemaphoreSlimis not dispose-optional — accessingAvailableWaitHandleallocates a handleDispose()must release — so a method-bounded one must still be flagged. The bug is the capture/escape, not the type.Pinned in CI
Two
FlowLocalsSample.cscases in thewpf-extractor--flow-localsstep:ThrottlerCapturedSemaphoreSlimcaptured by a returned async lambdaSemaphoreLeaksSemaphoreSlimnot captured, never disposedThe existing
UnitOfWorkFlowSampleOWN001 is unaffected: itsuowis always the receiver ofuow.Member(outside the.Where(p => …)lambda bodies, which referencep), and thejoin … in uow.TempProductsis query syntax — not anAnonymousFunctionExpressionSyntax— so the ancestor walk never marksuowcaptured (verified).Local validation: the
semLeakcontrol → OWN001 "is never disposed" through the core; the captured throttler emits no facts (escaped → skipped). The extractor's source→facts step is validated in CI. Writeup:docs/notes/closure-capture-escape-precision.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Generated by Claude Code
Summary by CodeRabbit
Improvements
SemaphoreSlim.nameof(...)expressions inside lambdas affect escape/capture detection.Documentation
Tests
nameofinside lambdas to validate the improved detection.