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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ jobs:
frontend/roslyn/samples/AliasDisposeSample.cs \
frontend/roslyn/samples/CloseReleaseSample.cs \
frontend/roslyn/samples/SemaphoreFieldSample.cs \
frontend/roslyn/samples/VoidSubscribeSample.cs \
-o "$RUNNER_TEMP/facts.json"
cat "$RUNNER_TEMP/facts.json"
- name: Check facts through the core
Expand Down Expand Up @@ -300,6 +301,17 @@ jobs:
|| { echo "FAIL: expected the InboxViewModel ignored-Subscribe leak"; exit 1; }
echo "$out" | grep -q "is ignored" \
|| { echo "FAIL: expected the ignored-Subscribe message"; exit 1; }
# P-004 resolve-aware ignored-Subscribe (mined: StackExchange.Redis): a bare `x.Subscribe(...)`
# whose call returns VOID (the Redis `ISubscriber.Subscribe(channel, handler, flags)` shape) has
# no IDisposable token -> must be SILENT; the IDisposable-returning Subscribe still WARNs.
if echo "$out" | grep -q "leaking 'VoidSubscriber'"; then
echo "FAIL: a void-returning .Subscribe(...) was wrongly flagged as an ignored IDisposable subscription"; exit 1
fi
echo "$out" | grep -q "leaking 'DisposableSubscriber'" \
|| { echo "FAIL: an ignored IDisposable-returning .Subscribe(...) must still warn (resolve-aware stays scoped)"; exit 1; }
# Codex control: a `dynamic` receiver's Subscribe has a dynamic return -> unprovable -> still WARN.
echo "$out" | grep -q "leaking 'DynamicSubscriber'" \
|| { echo "FAIL: an ignored dynamic .Subscribe(...) must still warn (dynamic return is unknown, not silenced)"; exit 1; }
if echo "$out" | grep -q "CleanInboxViewModel"; then
echo "FAIL: captured+disposed subscription wrongly reported"; exit 1
fi
Expand Down
18 changes: 16 additions & 2 deletions frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,17 @@
|| t.AllInterfaces.Any(i => i.Name == "IDisposable"
&& i.ContainingNamespace?.ToString() == "System"));

// The ignored result of a member `.Subscribe(...)` is a leakable IDisposable token (WPF004) only
// when the call RETURNS an IDisposable — the Rx `IObservable<T>.Subscribe()` shape. A RESOLVED void
// / non-IDisposable return has no token to leak: StackExchange.Redis's `ISubscriber.Subscribe(channel,
// handler, flags)` returns void, as do many event-bus `Subscribe(handler)` APIs. Only an UNRESOLVED
// return type keeps the syntactic benefit of the doubt (mirrors IsOwnedDisposableType, #83). Mined:
// StackExchange.Redis ConnectionMultiplexer.Sentinel `sub.Subscribe(channel, handler, FireAndForget)`.
static bool SubscribeResultIsDisposable(ITypeSymbol? rt) =>
rt is null or IErrorTypeSymbol
|| rt.TypeKind == TypeKind.Dynamic // a `dynamic` receiver -> dynamic return; can't prove non-disposable (Codex)
|| ImplementsIDisposable(rt);

// Types that implement IDisposable but whose disposal is conventionally OPTIONAL —
// the .NET guidance / Roslyn CA2000 exempt them: Task/ValueTask only hold a
// lazily-allocated wait handle, and the System.Data containers' Dispose() is a
Expand Down Expand Up @@ -2033,7 +2044,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 2047 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 2047 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 2047 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 2047 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 2047 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 2047 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 @@ -2541,11 +2552,14 @@
// WPF004: a `X.Subscribe(...)` whose IDisposable result is ignored — the
// call stands as a bare statement (not assigned/returned/added), so the
// token is dropped and never disposed. Member-access only (`x.Subscribe`),
// to avoid flagging bare void `Subscribe(...)` helpers.
// and RESOLVE-AWARE: the call must return an IDisposable (Rx). A resolved void /
// non-IDisposable `Subscribe` (StackExchange.Redis's `ISubscriber.Subscribe(channel,
// handler, flags)` is void) has no token to leak; an unresolved return still counts.
foreach (var inv in cls.DescendantNodes().OfType<InvocationExpressionSyntax>())
if (inv.Expression is MemberAccessExpressionSyntax m
&& m.Name.Identifier.Text == "Subscribe"
&& inv.Parent is ExpressionStatementSyntax)
&& inv.Parent is ExpressionStatementSyntax
&& SubscribeResultIsDisposable(model.GetTypeInfo(inv).Type))
subs.Add(new
{
@event = m.ToString(),
Expand Down
50 changes: 50 additions & 0 deletions frontend/roslyn/samples/VoidSubscribeSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;

namespace Own.Samples;

// P-004 resolve-aware ignored-Subscribe (WPF004), mined: StackExchange.Redis ConnectionMultiplexer.Sentinel.
// A bare `x.Subscribe(...)` is a leak ONLY when the call returns an IDisposable token (the Rx
// `IObservable<T>.Subscribe()` shape). StackExchange.Redis's `ISubscriber.Subscribe(channel, handler,
// flags)` returns VOID — there is no token to leak — so it must NOT be flagged. The IDisposable-returning
// case stays flagged (DisposableSubscriber below, and MessengerViewModel.InboxViewModel).

// a void-returning Subscribe (the handler overload) -> no IDisposable token -> must be SILENT.
public sealed class VoidSubscriber
{
public VoidSubscriber(IRedisSubscriber sub)
{
sub.Subscribe("+switch-master", (_, _) => { }); // returns void -> nothing to dispose -> SILENT
}
}

// control: a Subscribe that DOES return an IDisposable token, ignored -> STILL a leak -> WARN.
public sealed class DisposableSubscriber
{
public DisposableSubscriber(IObservableBus bus)
{
bus.Subscribe(_ => { }); // returns IDisposable, ignored -> WARN (resolve-aware still fires)

Check warning

Code scanning / Own.NET

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

the result of 'bus.Subscribe' is ignored — the IDisposable subscription is never disposed, leaking 'DisposableSubscriber' (leak) [resource: subscription token]
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}
}

// Codex control: a `dynamic` receiver gives Subscribe a dynamic return type — we cannot prove it is
// void / non-IDisposable, so it keeps the benefit of the doubt and STILL warns.
public sealed class DynamicSubscriber
{
public DynamicSubscriber(dynamic bus)
{
Action<object> handler = _ => { };
bus.Subscribe(handler); // dynamic return -> unknown -> WARN

Check warning

Code scanning / Own.NET

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

the result of 'bus.Subscribe' is ignored — the IDisposable subscription is never disposed, leaking 'DynamicSubscriber' (leak) [resource: subscription token]
}
}

// StackExchange.Redis-style: the handler overload returns void.
public interface IRedisSubscriber
{
void Subscribe(string channel, Action<string, string> handler);
}

// Rx-style: Subscribe hands back an IDisposable unsubscribe token.
public interface IObservableBus
{
IDisposable Subscribe(Action<object> handler);
}
Loading