Skip to content

[Robustness] Servy.Service Service.cs — _fileSemaphore and _healthCheckSemaphore never disposed, kernel handle leak on teardown #807

@Christophe-Rogiers

Description

@Christophe-Rogiers

Severity: Warning

Summary

Service.cs owns two long-lived SemaphoreSlim instances but never disposes them. Each SemaphoreSlim lazily allocates a kernel AvailableWaitHandle; leaving them undisposed leaks a kernel handle for every service teardown cycle.

File and lines

src/Servy.Service/Service.cs

Fields (lines 52, 59):

private readonly SemaphoreSlim _fileSemaphore = new SemaphoreSlim(1, 1);
...
private readonly SemaphoreSlim _healthCheckSemaphore = new SemaphoreSlim(1, 1);

Usage sites that prove they are in active use: 660, 677, 692, 708, 742, 803, 1548, 1584, 1724, 1759, 1861, 1904.

Teardown paths that dispose everything else but these two:

Cleanup() at lines 2179–…:

// disposes _childProcess output writers, stops _healthCheckTimer, etc.
_healthCheckTimer?.Stop();
_healthCheckTimer?.Dispose();
_healthCheckTimer = null;
// (no dispose for _fileSemaphore or _healthCheckSemaphore anywhere)

ExecuteTeardown() finally block at lines 2156–2169:

finally
{
    if (performedCleanup)
    {
        _disposed = true;
        _cancellationSource?.Dispose();
        _cancellationSource = null;
        _secureData?.Dispose();
    }
}

Grepping the whole file for _fileSemaphore.Dispose or _healthCheckSemaphore.Dispose returns zero hits.

Explanation

SemaphoreSlim only allocates its underlying kernel event the first time AvailableWaitHandle is accessed or when contention forces it, but WaitAsync on a contended semaphore will pay that cost. Once allocated, the handle lives until Dispose is called — the finalizer does not clean it up. For a service that restarts repeatedly (recovery cycles, Restart-Service loops), this is a slow but real kernel-handle leak: every teardown keeps two Semaphore handles alive until the hosting process exits.

This is distinct from:

Suggested fix

Add disposals in the ExecuteTeardown finally block alongside the other owned resources:

finally
{
    if (performedCleanup)
    {
        _disposed = true;

        _cancellationSource?.Dispose();
        _cancellationSource = null;

        _secureData?.Dispose();

        _fileSemaphore?.Dispose();
        _healthCheckSemaphore?.Dispose();
    }
}

Ordering note: dispose the semaphores after _healthCheckTimer has been stopped and any in-flight WaitAsync has either completed or been cancelled via _cancellationSource — otherwise pending waiters will throw ObjectDisposedException (already partially handled at line 1585, but cleaner to avoid triggering it).

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions