Skip to content

Conversation

@adamsitnik
Copy link
Member

@adamsitnik adamsitnik commented Nov 18, 2022

This week I've spent few hours debugging a resource leak that was causing full Microbenchmarks run to fail on my Linux machine: dotnet/performance#2738.

To my surprise, despite the fact that AnonymousPipeServerStream was wrapped with a using block, the resource leak was still happening.

using AnonymousPipeServerStream inputFromBenchmark = new (PipeDirection.In, HandleInheritability.Inheritable);
using AnonymousPipeServerStream acknowledgments = new (PipeDirection.Out, HandleInheritability.Inheritable);

This is how I've learned about the existence of DisposeLocalCopyOfClientHandle method (dotnet/BenchmarkDotNet#2200).

I've taken a quick look to see if we could call it right after the client handle gets exposed, but it's impossible because the call to this method needs to happen after the creation of child process (so an opened handle can be inherited).
As of now, it's impossible. We could in theory call it after first successful read/write but tracking it would be error prone too.

My current best idea for now is to dispose the client handle (stored by the server) when the server instance gets disposed. Only in scenarios, where it was created with HandleInheritability.Inheritable. I can't see any valid scenarios, where users would like to use client pipe handle owned by the server after the server got disposed.

…nheritable, its Dispose method should dispose the client no matter if it was exposed or not.

This is going to allow users who did not call DisposeLocalCopyOfClientHandle to avoid resource leaks.
@adamsitnik adamsitnik added this to the 8.0.0 milestone Nov 18, 2022
@ghost
Copy link

ghost commented Nov 18, 2022

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

This week I've spent few hours debugging a resource leak that was causing full Microbenchmarks run to fail on my Linux machine: dotnet/performance#2738.

To my surprise, despite the fact that AnonymousPipeServerStream was wrapped with a using block, the resource leak was still happening.

using AnonymousPipeServerStream inputFromBenchmark = new (PipeDirection.In, HandleInheritability.Inheritable);
using AnonymousPipeServerStream acknowledgments = new (PipeDirection.Out, HandleInheritability.Inheritable);

This is how I've learned about the existence of DisposeLocalCopyOfClientHandle method (dotnet/BenchmarkDotNet#2200).

I've taken a quick look to see if we could call it right after the client handle gets exposed, but it's impossible because the call to this method needs to happen after the creation of child process (so an opened handle can be inherited).
As of now, it's impossible.

My current best idea for now is to dispose the client handle (stored by the server) when the server instance gets disposed. Only in scenarios, where it was created with HandleInheritability.Inheritable. I can't see any valid scenarios, where users would like to use client pipe handle owned by the server after the server got disposed.

Author: adamsitnik
Assignees: -
Labels:

area-System.IO

Milestone: 8.0.0

@ghost ghost assigned adamsitnik Nov 18, 2022
@stephentoub
Copy link
Member

Won't this be a breaking change if anonymous pipes are being used intraprocess? Let's say one thread is writing to the server stream, finishes writing, and disposes of the stream. Another thread is reading from the client stream. Today won't the client simply get EOF and their read will return 0, whereas now they could get an ObjectDisposedException? Or if they do get an exception today in this or related scenarios, won't it be a different type of exception?

@stephentoub
Copy link
Member

Oh, wait, you're still checking !_clientHandleExposed. Nevermind.

@stephentoub
Copy link
Member

Oh, no, you're not, if it was created as inheritable. My original question still stands :)

@adamsitnik
Copy link
Member Author

Won't this be a breaking change if anonymous pipes are being used intraprocess?

It will, but only in case someone created anonymous pipe as inheritable (the intent of this is to perform outproc communication), but then uses it for inproc. I know the fix is not perfect, but IMO it's a lesser evil (I expect that way more people are going to avoid resource leak).

@adamsitnik
Copy link
Member Author

@stephentoub Could we include this breaking change in .NET 8 Preview 1 and eventually revert it if it causes a lot of trouble (I really doubt that)?

@stephentoub
Copy link
Member

I know the fix is not perfect, but IMO it's a lesser evil (I expect that way more people are going to avoid resource leak).

Outside of a benchmarking suite that's specifically testing creating tons of these, how impactful is that really? We're talking about one SafeHandle being left for finalization if someone doesn't follow the right pattern. Are these apps you're concerned about creating massive numbers of anonymous server/client pipe streams?

@adamsitnik
Copy link
Member Author

Outside of a benchmarking suite that's specifically testing creating tons of these

In this case it's not about benchmarking the pipes, BDN uses them internally for the host-benchmark process communication. So we create two pipes per every benchmark.

We're talking about one SafeHandle being left for finalization if someone doesn't follow the right pattern.

The problem is that it's never left for finalization:

public string GetClientHandleAsString()
{
_clientHandleExposed = true;
GC.SuppressFinalize(_clientHandle);

So basically, if the user creates an inheritable handle, uses it in anyway, but does not call DisposeLocalCopyOfClientHandle the handle is not closed until the app exits.

@stephentoub
Copy link
Member

The problem is that it's never left for finalization

If GetClientHandleAsString is used. I was talking about a scenario where it's not used.

Maybe a further mitigation is to split _clientHandleExposed. Have an additional _clientHandleExposedAsString, where GetClientHandleAsString sets both, and only do your proposed change if _clientHandleExposedAsString has been set.

@jozkee
Copy link
Member

jozkee commented Nov 22, 2022

Is it possible to call GC.ReRegisterForFinalize(_clientHandle), rather than _clientHandle.Dispose() to avoid the described breaking change?

…created with HandleInheritability.Inheritable and not exposed via SafePipeHandle property
@adamsitnik
Copy link
Member Author

Maybe a further mitigation is to split _clientHandleExposed. Have an additional _clientHandleExposedAsString, where GetClientHandleAsString sets both, and only do your proposed change if _clientHandleExposedAsString has been set.

Done, PTAL.

Is it possible to call GC.ReRegisterForFinalize(_clientHandle), rather than _clientHandle.Dispose() to avoid the described breaking change?

This would be also a breaking change and it would be non-deterministic:

AnonymousPipeServerStream server = new (PipeDirection.Out, HandleInheritability.Inheritable);
SafePipeHandle clientPipeHandle = new (nint.Parse(server.GetClientHandleAsString()), ownsHandle: true);
server.Dispose();
server = null;
// Even if we re-register client handle owned by the server for finalization,
// it's not rooted by any object and going to be closed when the finalizer is executed.
// It might "work for a while" and then stop when the GC cleanups the memory.
Use(clientPipeHandle);

@jozkee jozkee added breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet labels Nov 22, 2022
@ghost
Copy link

ghost commented Nov 22, 2022

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

Tagging @dotnet/compat for awareness of the breaking change.

@adamsitnik adamsitnik merged commit 0fffb89 into dotnet:main Nov 23, 2022
@adamsitnik
Copy link
Member Author

Breaking change created: dotnet/docs#32713

@adamsitnik adamsitnik removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Nov 23, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Dec 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO breaking-change Issue or PR that represents a breaking API or functional change over a prerelease.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants