Skip to content

Don't use callbacks when performing sync IO with async handles#126845

Open
Copilot wants to merge 13 commits into
mainfrom
copilot/remove-callbacks-sync-io
Open

Don't use callbacks when performing sync IO with async handles#126845
Copilot wants to merge 13 commits into
mainfrom
copilot/remove-callbacks-sync-io

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

Description

When performing synchronous I/O on async file handles (FILE_FLAG_OVERLAPPED), RandomAccess.Windows.cs previously used ThreadPoolBoundHandle.UnsafeAllocateNativeOverlapped with an IOCompletionCallback and a custom CallbackResetEvent class that tracked a reference count to handle the race between the IOCP callback and the caller's GetOverlappedResult.

This callback path is unnecessary. Setting the low-order bit of hEvent in the OVERLAPPED structure prevents the I/O completion from being queued to the completion port, eliminating the callback.

Changes:

  • Allocate NativeOverlapped via NativeMemory.AllocZeroed instead of ThreadPoolBoundHandle
  • Set EventHandle = handle | 1 to suppress IOCP notification
  • Cache a ManualResetEvent in SafeFileHandle using an Interlocked rent/return pattern (following the existing _reusableOverlapped caching pattern in SafeFileHandle.OverlappedValueTaskSource.Windows.cs) to avoid creating/disposing an event on every sync I/O call
  • Wait for I/O completion via ManualResetEvent.WaitOne() only for ERROR_IO_PENDING (not ERROR_SUCCESS) to respect SynchronizationContext and COM message pumping while avoiding unnecessary waits
  • Fix use-after-free when WaitOne() throws arbitrary exceptions (e.g., via SynchronizationContext): catch any exception, cancel the pending I/O with CancelIoEx, then wait for completion via GetOverlappedResult(bWait: true) before freeing the overlapped
  • Free overlapped with NativeMemory.Free and return cached event in finally
  • Remove s_callback, AllocateCallback(), GetNativeOverlappedForAsyncHandle(), and CallbackResetEvent
  • Remove EnsureThreadPoolBindingInitialized() from sync-over-async paths (no longer uses ThreadPoolBoundHandle)
  • Remove unnecessary overlapped->InternalLow = IntPtr.Zero in EOF path (only needed for ThreadPoolBoundHandle.FreeNativeOverlapped assertion, not NativeMemory.Free)
  • Fix sync-over-async read/write error handling to only call GetLastWin32ErrorAndDisposeHandleIfInvalid when ReadFile/WriteFile returns failure (0), and treat synchronous completion as ERROR_SUCCESS (avoids using undefined/stale GetLastError values)
  • Add a ThrowingSynchronizationContext test to demonstrate exceptions from WaitOne() are handled safely without memory corruption
  • Update that test to use SafeFileHandle.CreateAnonymousPipe(..., asyncRead: true, asyncWrite: false) so pending I/O is exercised deterministically, and verify the handle remains usable for subsequent reads with content validation after the failure path

This reduces allocations and complexity, preserves managed wait behavior, fixes a potential use-after-free when WaitOne throws via SynchronizationContext, and fixes incorrect error handling when overlapped operations complete synchronously.

Copilot AI requested review from Copilot and removed request for Copilot April 13, 2026 17:28
Copilot AI linked an issue Apr 13, 2026 that may be closed by this pull request
…vent for sync IO with async handles

Instead of using ThreadPoolBoundHandle callbacks and reference counting when
performing synchronous IO with async file handles, allocate the NativeOverlapped
directly via NativeMemory and set the low-order bit of hEvent to suppress I/O
completion port notifications. This eliminates the CallbackResetEvent class,
the s_callback field, and the AllocateCallback/GetNativeOverlappedForAsyncHandle
methods, reducing allocations and complexity.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d7a61400-c63f-401a-9503-0313459af6f7

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 13, 2026 17:58
Copilot AI changed the title [WIP] Remove callbacks for sync IO operations with async handles Don't use callbacks when performing sync IO with async handles Apr 13, 2026
Copilot AI requested a review from adamsitnik April 13, 2026 18:04
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

@copilot please address my feedback (nits)

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
…appedWithEventHandle

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bcdd8838-718e-4561-abf4-90c9c2deb905

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 14, 2026 05:47
Copilot AI requested a review from adamsitnik April 14, 2026 05:48
@adamsitnik adamsitnik marked this pull request as ready for review April 14, 2026 05:53
Copilot AI review requested due to automatic review settings April 14, 2026 05:53
- Cache ManualResetEvent (not raw nint) in SafeFileHandle using Interlocked rent/return pattern
- Use public ManualResetEvent ctor, Reset() on reuse to ensure non-signaled state
- Use WaitOne() to respect SynchronizationContext
- When WaitOne throws, cancel pending IO with CancelIoEx + GetOverlappedResult(bWait:true) before freeing overlapped
- Add test demonstrating SynchronizationContext.Wait exception scenario

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 8, 2026 11:08
Copilot AI requested a review from adamsitnik June 8, 2026 11:09
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

@copilot address my feedback

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
…usability after failure

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 8, 2026 12:35
Copilot AI requested a review from adamsitnik June 8, 2026 12:37
Copilot AI review requested due to automatic review settings June 8, 2026 13:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

@copilot address my feedback

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs Outdated
Copilot AI and others added 2 commits June 8, 2026 14:59
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 8, 2026 15:01
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 8, 2026

@copilot address my feedback

Done. Addressed in a8e69ef and c4d02a6: sync-over-async read/write now only consume last-error on Win32 failure, and the SynchronizationContext regression test now uses async anonymous pipes to reliably hit the pending wait path and verify post-failure usability.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

The changes LGTM, the perf is much better despite still using ManualResetEvent:

(managed_cached is the PR)

BenchmarkDotNet v0.16.0-nightly.20260302.459, Windows 11 (10.0.26200.8390/25H2/2025Update/HudsonValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores
Memory: 63.86 GB Total, 32.62 GB Available
.NET SDK 11.0.100-preview.5.26255.101
  [Host]     : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3
Method Toolchain fileSize userBufferSize options Mean Ratio Allocated
Read \main\corerun.exe 1024 1024 None 145.7 us 1.00 4360 B
Read \main\corerun.exe 1024 1024 Asynchronous 173.9 us 1.00 4576 B
Read \managed_cached\corerun.exe 1024 1024 Asynchronous 156.9 us 0.90 4464 B
Read \native_cached\corerun.exe 1024 1024 Asynchronous 154.6 us 0.89 4440 B
Write \main\corerun.exe 1024 1024 None 609.2 us 1.00 4360 B
Write \main\corerun.exe 1024 1024 Asynchronous 639.3 us 1.00 4577 B
Write \managed_cached\corerun.exe 1024 1024 Asynchronous 627.9 us 0.98 4465 B
Write \native_cached\corerun.exe 1024 1024 Asynchronous 623.3 us 0.98 4441 B
Read \main\corerun.exe 1048576 512 None 779.2 us 1.00 4361 B
Read \main\corerun.exe 1048576 512 Asynchronous 2,756.3 us 1.00 49460 B
Read \managed_cached\corerun.exe 1048576 512 Asynchronous 1,262.3 us 0.46 4466 B
Read \native_cached\corerun.exe 1048576 512 Asynchronous 1,113.6 us 0.40 4442 B
Write \main\corerun.exe 1048576 512 None 7,438.8 us 1.00 4365 B
Write \main\corerun.exe 1048576 512 Asynchronous 7,788.8 us 1.00 49470 B
Write \managed_cached\corerun.exe 1048576 512 Asynchronous 7,507.1 us 0.96 4473 B
Write \native_cached\corerun.exe 1048576 512 Asynchronous 7,387.7 us 0.95 4449 B
Read \main\corerun.exe 1048576 4096 None 709.8 us 1.00 241 B
Read \main\corerun.exe 1048576 4096 Asynchronous 2,929.2 us 1.00 45340 B
Read \managed_cached\corerun.exe 1048576 4096 Asynchronous 1,235.6 us 0.42 346 B
Read \native_cached\corerun.exe 1048576 4096 Asynchronous 1,049.8 us 0.36 322 B
Write \main\corerun.exe 1048576 4096 None 7,197.4 us 1.00 245 B
Write \main\corerun.exe 1048576 4096 Asynchronous 7,838.3 us 1.00 45350 B
Write \managed_cached\corerun.exe 1048576 4096 Asynchronous 7,351.9 us 0.94 353 B
Write \native_cached\corerun.exe 1048576 4096 Asynchronous 7,477.0 us 0.96 329 B
Read \main\corerun.exe 104857600 4096 None 82,969.7 us 1.00 352 B
Read \main\corerun.exe 104857600 4096 Asynchronous 329,198.7 us 1.00 4506328 B
Read \managed_cached\corerun.exe 104857600 4096 Asynchronous 154,884.8 us 0.47 568 B
Read \native_cached\corerun.exe 104857600 4096 Asynchronous 118,937.8 us 0.36 544 B
Write \main\corerun.exe 104857600 4096 None 172,746.9 us 1.00 464 B
Write \main\corerun.exe 104857600 4096 Asynchronous 458,918.6 us 1.00 4506328 B
Write \managed_cached\corerun.exe 104857600 4096 Asynchronous 208,779.5 us 0.46 792 B
Write \native_cached\corerun.exe 104857600 4096 Asynchronous 170,257.2 us 0.37 768 B

@adamsitnik adamsitnik requested a review from jkotas June 8, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Don't use callbacks when performing sync IO with async handles

4 participants