Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Fix HttpListener restart - #41867

Closed
scalablecory wants to merge 3 commits into
dotnet:masterfrom
scalablecory:39552-httplistener
Closed

Fix HttpListener restart#41867
scalablecory wants to merge 3 commits into
dotnet:masterfrom
scalablecory:39552-httplistener

Conversation

@scalablecory

Copy link
Copy Markdown

Resolves #39552 throwing ObjectDisposedException.

The first try in #40466 failed due to the ThreadPoolBoundHandle being set to null before the DisconnectAsyncResult callback was hit. This resolves that by keeping a private copy of the ThreadPoolBoundHandle around inside the DisconnectAsyncResult.

This is technically a use-after-Dispose, relying on our current implementation of ThreadPoolBoundHandle which happens to allow this, so the fix may not work long-term. A more correct fix needs more thought and is blocked on https://github.com/dotnet/coreclr/issues/26783 due to how http.sys implements cancellation, which asks us to close the handle to cancel but then requires us to use the handle to free our OVERLAPPED structure. Socket implementation might be doing something similar so that's a good place to reference.

@scalablecory scalablecory added bug Product bug (most likely) area-System.Net labels Oct 17, 2019
@scalablecory scalablecory added this to the 5.0 milestone Oct 17, 2019
@scalablecory
scalablecory requested review from a team and stephentoub October 17, 2019 17:16
@scalablecory scalablecory self-assigned this Oct 17, 2019
@scalablecory

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 4 pipeline(s).

Comment thread src/System.Net.HttpListener/tests/SimpleHttpTests.cs Outdated
Comment thread src/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs Outdated
Comment thread src/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs Outdated

@stephentoub stephentoub left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Other than the naming/tracing comments, LGTM.

if (_requestQueueBoundHandle != null)
{
_requestQueueBoundHandle.Dispose();
_requestQueueBoundHandle = null;

@stephentoub stephentoub Oct 18, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like setting this to null could, with certain interleavings, cause:

internal ThreadPoolBoundHandle RequestQueueBoundHandle

to return null. Might this cause a null reference exception elsewhere? Callers of that property expect it to always return non-null.

@scalablecory scalablecory Oct 18, 2019

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think you're right; if someone calls Stop() at just the right moment, RequestQueueBoundHandle might return null.

I can fix this -- if I include the return _requestQueueBoundHandle in the lock, then we won't NRE due to this. It creates a new situation where we might allocate a new one that doesn't get disposed, but GC will take care of that and Dispose is a no-op right now so that is fine regardless.

This is related to one of those race conditions I alluded to. With this fixed, those other things will still get an error because they all try to, immediately after getting the ThreadPoolBoundHandle, use the _requestQueueHandle that is being disposed right after the _requestQueueBoundHandle. (╯°□°)╯︵ ┻━┻

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(╯°□°)╯︵ ┻━┻

What is that? :)

@stephentoub stephentoub Oct 19, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks. Obviously I need to take an updated emoticon course to learn these things. :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is related to one of those race conditions I alluded to. With this fixed, those other things will still get an error because they all try to, immediately after getting the ThreadPoolBoundHandle, use the _requestQueueHandle that is being disposed right after the _requestQueueBoundHandle.

So does that mean this PR won't make things worse (and obviously will make one case better), or are there cases where this will turn things that were previously nops into failures?

@scalablecory scalablecory Oct 21, 2019

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So does that mean this PR won't make things worse (and obviously will make one case better), or are there cases where this will turn things that were previously nops into failures?

The basic race when stopping in HttpListener is:

  • An outstanding multi-step op completes its first leg successfully.
  • You call Stop() and close everything.
  • The outstanding op starts its second leg and hopefully the handle was set to NULL or INVALID_HANDLE so the http.sys function can error out.

This is what exists today and this PR won't change that.

In .NET 4.8 there's an additional race, which doesn't effect .NET Core because we don't support restarting:

  • An outstanding multi-step op completes its first leg successfully and right before its second leg, the thread ends its quantum.
  • You call Stop() and close everything.
  • You call Start() again.
  • Thread resumes and the outstanding op starts its second leg and it passes now invalid state to http.sys functions.

This PR will re-enable this 2nd race.

_requestQueueBoundHandle = ThreadPoolBoundHandle.BindHandle(_requestQueueHandle);
if (NetEventSource.IsEnabled) NetEventSource.Info($"ThreadPoolBoundHandle.BindHandle({_requestQueueHandle}) -> {_requestQueueBoundHandle}");
}
_requestQueueBoundHandle = handle = ThreadPoolBoundHandle.BindHandle(_requestQueueHandle);

@halter73 halter73 Oct 21, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can the updated DisconnectAsyncResult ctor that now calls into this getter ever run after CloseRequestQueueHandle completes? I'm not familiar with the HttpListener code, but it looks like RegisterForDisconnectNotification isn't synchronized.

Could this risk BindHandle throwing a new ArgumentException since _requestQueueHandle would be closed at that point?

@maryamariyan

Copy link
Copy Markdown

Thank you for your contribution. As announced in dotnet/coreclr#27549 this repository will be moving to dotnet/runtime on November 13. If you would like to continue working on this PR after this date, the easiest way to move the change to dotnet/runtime is:

  1. In your corefx repository clone, create patch by running git format-patch origin
  2. In your runtime repository clone, apply the patch by running git apply --directory src/corefx <path to the patch created in step 1>

@scalablecory

Copy link
Copy Markdown
Author

We've decided to invest time into more thorough fix of HttpListener, so I'm closing out this PR.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Net bug Product bug (most likely)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[HttpListener] HttpListener Start()/Stop()/Start()/BeginGetContext() causes ObjectDisposedException on Windows

7 participants