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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@ jobs:
# previously-missed leak in ShareX's DeriveCryptoData.
echo "$out" | grep -qE "OWN001.*'rngLeak' is never disposed" \
|| { echo "FAIL: expected OWN001 on the undisposed crypto-factory acquire"; exit 1; }
# TcpListener precision (Codex review on #61): Stop() IS the cleanup (Dispose()
# delegates to Stop()), so a Stop()'d listener is modelled as released -> silent
# ('stopped'); a listener NEVER Stop()'d still holds the socket -> OWN001 ('tcpLeak'),
# proving the release is Stop()-specific, not a blanket TcpListener exemption.
echo "$out" | grep -qE "OWN001.*'tcpLeak' is never disposed" \
|| { echo "FAIL: expected OWN001 on the never-stopped TcpListener"; exit 1; }
# dispose-optional (Task), disposed/escaping locals, a `for` loop whose
# disposable is disposed after it (`looped`, balanced), a balanced
# acquire+dispose in a loop (`whileClean`), a try/finally dispose (`tfClean`,
Expand All @@ -420,7 +426,7 @@ jobs:
# case disposes (no default) -> last case is the tail, no phantom no-match leak.
# `ncf`: `ncf?.Dispose()` (null-conditional) in a threaded finally IS a release
# (member-binding form), so it is disposed on the return path -> silent (Codex review).
for ok in clean looped esc exemptTask whileClean asyncDisposed asyncDisposedCfg tfClean tfCatch tfRet tfNull cda daci cif ctorLater lamPrior other doClean swAll ncf captured shaClean; do
for ok in clean looped esc exemptTask whileClean asyncDisposed asyncDisposedCfg tfClean tfCatch tfRet tfNull cda daci cif ctorLater lamPrior other doClean swAll ncf captured shaClean stopped; do
if echo "$out" | grep -q "'$ok'"; then echo "FAIL: silent/exempt case '$ok' was reported"; exit 1; fi
done
echo "OK: flow-sensitive OWN001/002/003 on real C# (path-sensitive, loops via while/foreach/for, try/finally sequential, never-vs-every-path wording, dispose-optional exempt, beyond flat)"
Expand Down
75 changes: 75 additions & 0 deletions docs/notes/tcplistener-stop-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# `TcpListener.Stop()` is a release — Stop() is the cleanup, not a half-measure

A precision fix from a **Codex review** (PR #61). While triaging the ShareX re-mine I
nearly locked an undisposed-`TcpListener` finding as a corpus regression — Codex caught
that it is a **false positive**, and it was right.

## The false positive

```csharp
public static int GetRandomUnusedPort() // ShareX WebHelpers.cs:191
{
TcpListener listener = new TcpListener(IPAddress.Loopback, 0);
try
{
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
finally
{
listener.Stop();
}
}
```

The flow detector flagged `listener` OWN001: it is an `IDisposable` (`TcpListener`
implements `IDisposable` since .NET Core 3.0), and `Stop()` is not in the release set
(`Dispose`/`Close`/`DisposeAsync`). But **`TcpListener.Dispose()` just delegates to
`Stop()`** — `Stop()` disposes the listen socket and clears it. So after `Stop()` there is
no held resource: an undisposed-but-stopped listener is **not a leak**, and flagging it
dents the 0-FP precision claim (CA2000 / CodeQL flag it conventionally, but it is not a
real resource leak).

## The fix — Stop() is a release, for `TcpListener`

`EmitFlowExpr` now models `tcpListener.Stop()` as a `release` of the local, alongside the
`Dispose()`/`Close()`/pool-`Return`/`Show()` releases:

```csharp
if (expr is InvocationExpressionSyntax tlinv
&& tlinv.Expression is MemberAccessExpressionSyntax
{ Name.Identifier.Text: "Stop", Expression: IdentifierNameSyntax tlid }
&& tracked.Contains(tlid.Identifier.Text)
&& model.GetSymbolInfo(tlinv).Symbol is IMethodSymbol { ContainingType: { Name: "TcpListener" } tct }
&& IsInNamespace(tct, "System", "Net", "Sockets"))
{
nodes.Add(new { op = "release", var = tlid.Identifier.Text, line = LineOf(tlinv) });
return;
}
```

It is **`TcpListener`-specific**, resolved on the method's containing type via the
SemanticModel — `Stop()` on a `Timer` / `Process` / `Stopwatch` / etc. does **not** dispose
and is untouched (stays a tracked use). And it is path-sensitive (a release at the call
site), like the WinForms `Show()` model: a listener never `Stop()`'d (nor disposed) still
leaks on the path that misses it.

## Pinned in CI

Two `FlowLocalsSample.cs` cases in the `wpf-extractor` `--flow-locals` step:

- `TcpListenerStopped` — `new TcpListener(...); Start(); Stop();` → **silent** (the FP this
removes);
- `TcpListenerNeverStopped` — `new TcpListener(...); Start();` (no `Stop`) → **OWN001**, the
control proving the release is `Stop()`-specific, not a blanket `TcpListener` exemption.

## The sibling that is *not* fixed (and why)

The same triage had a `StreamReader` finding (a reader over a `using`-scoped `MemoryStream`,
the reader not disposed). That is also a non-leak *in that specific shape* — the stream is
disposed separately and the reader holds no unmanaged resource of its own. But unlike
`TcpListener`, the extractor's general behaviour there is **correct**: a `StreamReader` over
a stream that is *not* otherwise disposed genuinely leaks the stream (the reader owns it).
Suppressing it would need stream-ownership reasoning and would cost real recall, so it is
left as-is — the narrow `using`-scoped-stream variant is an accepted, rare residual, not a
systematic FP class like `TcpListener.Stop()`.
16 changes: 16 additions & 0 deletions frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,22 @@
nodes.Add(new { op = "release", var = sid.Identifier.Text, line = LineOf(sinv) });
return;
}
// x.Stop() on a tracked System.Net.Sockets.TcpListener -> release: TcpListener.Dispose()
// just delegates to Stop() (which disposes the listen socket and clears it), so a
// Stop()'d listener holds no resource and is not a leak (Codex review on PR #61). It is
// TcpListener-specific, resolved via the method symbol — Stop() on a Timer / Process / etc.
// does NOT dispose, so it stays a tracked use; a listener never Stop()'d (nor Disposed)
// still leaks.
if (expr is InvocationExpressionSyntax tlinv
&& tlinv.Expression is MemberAccessExpressionSyntax
{ Name.Identifier.Text: "Stop", Expression: IdentifierNameSyntax tlid }
&& tracked.Contains(tlid.Identifier.Text)
&& model.GetSymbolInfo(tlinv).Symbol is IMethodSymbol { ContainingType: { Name: "TcpListener" } tct }
&& IsInNamespace(tct, "System", "Net", "Sockets"))
Comment on lines +721 to +726

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat TcpListener.Stop as release-shaped in try blocks

Because this release predicate only lives in EmitFlowExpr, StatementMayThrow still treats TcpListener.Stop() as an arbitrary throwing call and injects an exceptional exit before the cleanup when it appears inside a try. That creates a synthetic path that skips the release, so shapes like try { listener.Stop(); } finally { } can still produce OWN001 even though this change is explicitly modeling Stop as cleanup; the Stop predicate needs to be shared with the dispose-shaped/throw-edge logic too.

Useful? React with 👍 / 👎.

{
nodes.Add(new { op = "release", var = tlid.Identifier.Text, line = LineOf(tlinv) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Model Start after Stop as reacquire

When callers reuse a listener (listener.Stop(); listener.Start(); ...), this release leaves the local in the released state forever. TcpListener.Stop closes the current socket and the next Start creates a new one on the same object (see Microsoft docs: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener.stop), so a valid restart followed by a final Stop/Dispose will be reported as OWN002/OWN003, while a restart without final cleanup can be reported as use-after-release instead of the real OWN001 leak. Please either model Start as a reacquire after Stop or avoid treating Stop as a terminal release when the object is reused.

Useful? React with 👍 / 👎.

return;
}
// x?.Dispose()/x?.Close()/x?.DisposeAsync() (null-conditional) is the release too — the
// call is a member BINDING under a conditional access, not a member access. Mirrors
// IsDisposeShaped so a `?.` dispose (e.g. in a finally) is not mistaken for a bare use,
Expand Down Expand Up @@ -1101,7 +1117,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 1120 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 1120 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 1120 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 1120 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 1120 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 1120 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.
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
22 changes: 22 additions & 0 deletions frontend/roslyn/samples/FlowLocalsSample.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -12,7 +14,7 @@
// OWN002: used after Dispose()
public void UseAfterDispose()
{
var uad = new MemoryStream();

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN002

[OWN002] IDisposable local 'uad' is used after it is disposed [resource: disposable]

Check warning on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'uad' is used after it is disposed

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'uad' is used after it is disposed

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN002

[OWN002] IDisposable local 'uad' is used after it is disposed [resource: disposable]

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN002

[OWN002] IDisposable local 'uad' is used after it is disposed [resource: disposable]

Check warning on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'uad' is used after it is disposed

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'uad' is used after it is disposed

Check failure on line 17 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN002

[OWN002] IDisposable local 'uad' is used after it is disposed [resource: disposable]
uad.WriteByte(1);
uad.Dispose();
uad.WriteByte(2);
Expand All @@ -21,7 +23,7 @@
// OWN001: disposed only on the `then` path -> leaks on the else path
public void LeakOnElse(bool c)
{
var leak = new MemoryStream();

Check warning on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'leak' may not be disposed on every path (leak)

Check failure on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'leak' may not be disposed on every path (leak)

Check failure on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'leak' may not be disposed on every path (leak) [resource: disposable]

Check warning on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'leak' may not be disposed on every path (leak)

Check failure on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'leak' may not be disposed on every path (leak)

Check failure on line 26 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'leak' may not be disposed on every path (leak) [resource: disposable]
if (c)
{
leak.Dispose();
Expand All @@ -31,7 +33,7 @@
// OWN003: disposed twice
public void DoubleDispose()
{
var dbl = new MemoryStream();

Check failure on line 36 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'dbl' is disposed more than once

Check failure on line 36 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN003

[OWN003] IDisposable local 'dbl' is disposed more than once [resource: disposable]

Check failure on line 36 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'dbl' is disposed more than once

Check failure on line 36 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN003

[OWN003] IDisposable local 'dbl' is disposed more than once [resource: disposable]
dbl.Dispose();
dbl.Dispose();
}
Expand Down Expand Up @@ -61,7 +63,7 @@
{
while (n > 0)
{
var whileLeak = new MemoryStream();

Check failure on line 66 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'whileLeak' is never disposed (leak)

Check failure on line 66 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'whileLeak' is never disposed (leak) [resource: disposable]

Check failure on line 66 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'whileLeak' is never disposed (leak)

Check failure on line 66 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'whileLeak' is never disposed (leak) [resource: disposable]
whileLeak.WriteByte(1);
n = n - 1;
}
Expand All @@ -72,7 +74,7 @@
{
foreach (var it in items)
{
var foreachLeak = new MemoryStream();

Check failure on line 77 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'foreachLeak' is never disposed (leak)

Check failure on line 77 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'foreachLeak' is never disposed (leak) [resource: disposable]

Check failure on line 77 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'foreachLeak' is never disposed (leak)

Check failure on line 77 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'foreachLeak' is never disposed (leak) [resource: disposable]
foreachLeak.WriteByte((byte)it);
}
}
Expand All @@ -84,7 +86,7 @@
{
for (int i = 0; i < n; i++)
{
var forLeak = new MemoryStream();

Check failure on line 89 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'forLeak' is never disposed (leak)

Check failure on line 89 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'forLeak' is never disposed (leak) [resource: disposable]

Check failure on line 89 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

IDisposable local 'forLeak' is never disposed (leak)

Check failure on line 89 in frontend/roslyn/samples/FlowLocalsSample.cs

View workflow job for this annotation

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

OWN001

[OWN001] IDisposable local 'forLeak' is never disposed (leak) [resource: disposable]
forLeak.WriteByte((byte)i);
}
}
Expand Down Expand Up @@ -462,6 +464,26 @@
shaClean.ComputeHash(new byte[1]);
shaClean.Dispose();
}

// NOT a leak (precision): TcpListener.Stop() IS the cleanup — TcpListener.Dispose() just
// delegates to Stop(), which disposes the listen socket. So a Stop()'d listener holds no
// resource; the extractor models Stop() on a TcpListener as a release. Reduced from a
// ShareX false positive (WebHelpers.GetRandomUnusedPort, Codex review on #61). Silent.
public void TcpListenerStopped()
{
var stopped = new TcpListener(IPAddress.Loopback, 0);
stopped.Start();
stopped.Stop();
}

// control (must still leak): a TcpListener never Stop()'d (nor Disposed) holds the listen
// socket -> OWN001. Proves the release is Stop()-specific, not a blanket TcpListener
// exemption (a Timer/Process Stop() would NOT release).
public void TcpListenerNeverStopped()
{
var tcpLeak = new TcpListener(IPAddress.Loopback, 0);

Check warning

Code scanning / Own.NET

owned resource not released on all paths (possible leak) Warning

IDisposable local 'tcpLeak' is never disposed (leak) [resource: disposable]
tcpLeak.Start();
}
}

// A domain exception type literally named `Exception`, in a non-System namespace — the
Expand Down
Loading