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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ jobs:
frontend/roslyn/samples/AppDomainShutdownSample.cs \
frontend/roslyn/samples/AliasDisposeSample.cs \
frontend/roslyn/samples/CloseReleaseSample.cs \
frontend/roslyn/samples/SemaphoreFieldSample.cs \
-o "$RUNNER_TEMP/facts.json"
cat "$RUNNER_TEMP/facts.json"
- name: Check facts through the core
Expand Down Expand Up @@ -275,6 +276,24 @@ jobs:
# receiver-stripping name match that a same-class ContainingType check would also miss).
echo "$out" | grep -qE "CloseReleaseSample\.cs:[0-9]+:.*\[OWN001\].*'_xconn'" \
|| { echo "FAIL: other-instance .Close() must not credit this field (receiver-scoped to this/alias)"; exit 1; }
# P-004 SemaphoreSlim FIELD dispose-optional (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore):
# a SemaphoreSlim field used only for Wait/Release (AvailableWaitHandle never read) frees nothing on
# Dispose -> must be SILENT.
if echo "$out" | grep -q "'_optionalSem'"; then
echo "FAIL: a SemaphoreSlim field whose AvailableWaitHandle is never read was wrongly reported (dispose-optional)"; exit 1
fi
# gate control: a SemaphoreSlim field whose AvailableWaitHandle IS read allocates a handle Dispose
# must release -> it must STILL warn (proves the exemption is gated, not blanket — Codex).
echo "$out" | grep -qE "SemaphoreFieldSample\.cs:[0-9]+:.*\[OWN001\].*'_handleSem'" \
|| { echo "FAIL: a SemaphoreSlim field whose AvailableWaitHandle is read must still warn"; exit 1; }
# Codex control: an AvailableWaitHandle read THROUGH A FIELD ALIAS must credit the field -> still warn.
echo "$out" | grep -qE "SemaphoreFieldSample\.cs:[0-9]+:.*\[OWN001\].*'_aliasedSem'" \
|| { echo "FAIL: an aliased AvailableWaitHandle read must keep the field tracked (alias-aware gate)"; exit 1; }
# type-scope control: a non-SemaphoreSlim owned IDisposable (CTS) never disposed must STILL warn.
echo "$out" | grep -qE "SemaphoreFieldSample\.cs:[0-9]+:.*\[OWN001\].*'_ctsControl'" \
|| { echo "FAIL: a non-SemaphoreSlim owned IDisposable field must still warn (exemption stays SemaphoreSlim-scoped)"; exit 1; }
# field-scoped: the existing method-bounded LOCAL SemaphoreSlim leak (FlowLocalsSample.semLeak) must
# be UNAFFECTED — checked in the --flow-locals step below; this exemption never touches IsDisposeOptional.
# WPF004: an ignored `X.Subscribe(...)` result leaks; the captured+
# disposed one stays silent. "ignored" is unique to the WPF004 message.
echo "$out" | grep -q "MessengerViewModel.cs" \
Expand Down
30 changes: 30 additions & 0 deletions frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2033,7 +2033,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 2036 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 2036 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 2036 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 2036 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 2036 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 2036 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 @@ -2304,6 +2304,29 @@
&& ThisFieldName(a.Left) is { } cfn) // THIS instance's field only — `other._c` must not exempt our field (CodeRabbit)
eventSourceCounters.Add(cfn);

// P-004 SemaphoreSlim field exemption (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore).
// SemaphoreSlim.Dispose() only frees a LAZILY-allocated wait handle — allocated solely when
// `.AvailableWaitHandle` is read — so a SemaphoreSlim field used purely for Wait/WaitAsync/Release
// leaks nothing and is dispose-optional. GATE: if `.AvailableWaitHandle` IS read on the field,
// that handle exists and Dispose must release it, so the field STAYS tracked (Codex). Collect the
// field names whose AvailableWaitHandle is read (this/bare receiver) so the loop below keeps them.
// Scoped to FIELDS only — the shared IsDisposeOptional (and the flow-locals detector / the
// deliberate method-bounded `semLeak` control) is intentionally left untouched (CodeRabbit).
var waitHandleSemaphores = new HashSet<string>(StringComparer.Ordinal);
foreach (var ma in cls.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
if (ma.Name.Identifier.Text == "AvailableWaitHandle")
{
// credit the FIELD whose AvailableWaitHandle is read, by SYMBOL: a this/bare access bound
// to a real field symbol (so `other._f` and a shadowing local/param are NOT conflated —
// CodeRabbit), OR a field-ALIAS local (`var s = _sem; s.AvailableWaitHandle` -> `_sem` —
// Codex). Anything else (an unrelated local, another instance's field) is ignored.
var recv = model.GetSymbolInfo(ma.Expression).Symbol;
if (recv is IFieldSymbol && ThisFieldName(ma.Expression) is { } whf)
waitHandleSemaphores.Add(whf);
else if (recv is ILocalSymbol ls && aliasToField.TryGetValue(ls, out var fa))
waitHandleSemaphores.Add(fa);
}

foreach (var fd in cls.Members.OfType<FieldDeclarationSyntax>())
{
// a `static` IDisposable field is a process-lifetime singleton (a shared
Expand Down Expand Up @@ -2331,6 +2354,13 @@
if (eventSourceCounters.Contains(v.Identifier.Text)
&& DerivesFromDiagnosticCounter(model.GetTypeInfo(fd.Declaration.Type).Type))
continue;
// a SemaphoreSlim field whose `.AvailableWaitHandle` is never read leaks nothing —
// Dispose() only frees that lazy handle — so it is dispose-optional and silent; if the
// handle IS read (in waitHandleSemaphores) it stays tracked. FIELD-scoped (Npgsql).
if (!waitHandleSemaphores.Contains(v.Identifier.Text)
&& model.GetTypeInfo(fd.Declaration.Type).Type is { Name: "SemaphoreSlim" } st
&& st.ContainingNamespace?.ToString() == "System.Threading")
continue;
subs.Add(new
{
@event = v.Identifier.Text,
Expand Down
60 changes: 60 additions & 0 deletions frontend/roslyn/samples/SemaphoreFieldSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Own.Samples;

// P-004 SemaphoreSlim FIELD dispose-optional (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore).
// SemaphoreSlim.Dispose() only frees a lazily-allocated wait handle (allocated solely when
// AvailableWaitHandle is read), so a SemaphoreSlim field used purely for Wait/WaitAsync/Release leaks
// nothing and is dispose-optional. GATED on AvailableWaitHandle: if the field's AvailableWaitHandle is
// read, the handle exists and Dispose must release it -> the field STAYS tracked (Codex). Scoped to
// FIELDS only — method-bounded LOCAL SemaphoreSlims remain tracked via FlowLocalsSample.semLeak.

// a SemaphoreSlim field used only for WaitAsync/Release, never disposed -> SILENT (dispose-optional).
public sealed class OptionalSemaphore
{
private readonly SemaphoreSlim _optionalSem = new SemaphoreSlim(1, 1);

public async Task RunAsync()
{
await _optionalSem.WaitAsync();
try { /* critical section */ }
finally { _optionalSem.Release(); }
}
}

// control (the gate): the field's AvailableWaitHandle IS read, so the wait handle is allocated and
// Dispose must release it -> the field must STILL warn OWN001.
public sealed class WaitHandleSemaphore
{
private readonly SemaphoreSlim _handleSem = new SemaphoreSlim(0, 1);

Check warning

Code scanning / Own.NET

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

IDisposable field '_handleSem' (type 'SemaphoreSlim') is never disposed — its owner 'WaitHandleSemaphore' leaks it (leak) [resource: disposable field]

public void Block()
{
_handleSem.AvailableWaitHandle.WaitOne(); // reads AvailableWaitHandle -> stays tracked
}
}

// Codex control: AvailableWaitHandle read through a field ALIAS (`var s = _sem; s.AvailableWaitHandle`)
// must credit the FIELD — the handle is allocated, so the field must STILL warn OWN001 (not be exempted
// because the read went through the local name).
public sealed class AliasedWaitHandleSemaphore
{
private readonly SemaphoreSlim _aliasedSem = new SemaphoreSlim(0, 1);

Check warning

Code scanning / Own.NET

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

IDisposable field '_aliasedSem' (type 'SemaphoreSlim') is never disposed — its owner 'AliasedWaitHandleSemaphore' leaks it (leak) [resource: disposable field]

public void Block()
{
var sem = _aliasedSem;
sem.AvailableWaitHandle.WaitOne(); // alias reads AvailableWaitHandle -> _aliasedSem stays tracked
}
}

// control (type scope): a non-SemaphoreSlim owned IDisposable (CancellationTokenSource) never disposed
// must STILL warn — the exemption is SemaphoreSlim-specific, not a blanket "any field".
public sealed class HoldsCtsField
{
private readonly CancellationTokenSource _ctsControl = new CancellationTokenSource();

Check warning

Code scanning / Own.NET

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

IDisposable field '_ctsControl' (type 'CancellationTokenSource') is never disposed — its owner 'HoldsCtsField' leaks it (leak) [resource: disposable field]

public void Cancel() => _ctsControl.Cancel();
}
Loading