Skip to content

Read subprocess stderr with a blocking loop instead of readabilityHandler - #73

Merged
rintaro merged 1 commit into
swiftlang:mainfrom
rintaro:clangd-stderr-blocking-read
Jul 15, 2026
Merged

Read subprocess stderr with a blocking loop instead of readabilityHandler#73
rintaro merged 1 commit into
swiftlang:mainfrom
rintaro:clangd-stderr-blocking-read

Conversation

@rintaro

@rintaro rintaro commented Jul 9, 2026

Copy link
Copy Markdown
Member

JSONRPCConnection's subprocess launcher reads the child's stderr (e.g. clangd's) via FileHandle.readabilityHandler, which installs a libdispatch read source on the pipe's file descriptor. Tearing that source down can race the descriptor being closed, which has intermittently crashed the process in _dispatch_event_loop_drain on Linux CI while a subprocess connection is torn down — a dangling epoll muxnote left behind by the source cancellation.

This drains the subprocess's stderr with a blocking read loop on a dedicated queue instead. A blocking read never registers the file descriptor with libdispatch's event loop, so no read source (and no epoll muxnote) is created for the pipe. This mirrors the approach already used for the receiveFD read loop in the same file, where the JSON-RPC input is read with blocking reads rather than a dispatch source. End of stream ends the loop; there is no asynchronous source cancellation to race the descriptor close.

This is a speculative workaround for swiftlang/sourcekit-lsp#2717: the underlying defect is a teardown race in the runtime (Foundation FileHandle / libdispatch), tracked separately in swiftlang/swift-corelibs-foundation#5507. This PR removes the only libdispatch read source in the subprocess connection path — Process monitoring on Linux uses CFRunLoop and the JSON-RPC pipes are already read with blocking reads — so it sidesteps the crash without changing observable behavior (stderr is still forwarded to the log).

let stderrQueue = DispatchQueue(label: "\(name)-stderr-read-queue")
stderrQueue.async {
while true {
let data = orLog("Reading stderr from \(name)") { try stderrReadFileHandle.read(upToCount: 8 * 1024) }

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.

Could we significantly reduce the amount of data we read here (maybe even 1 byte). Currently, if the server emits 60 bytes of stderr, it will not get reported until the pipe closes. Having the stderr logging close to the point when they were emitted can significantly help diagnosability of issues.

And since I don’t expect too much data to be sent on stderr, I don’t think that a small buffer size would be a big problem here. WDYT?

@rintaro
rintaro marked this pull request as draft July 10, 2026 13:02
@rintaro

rintaro commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

swiftlang/swift-corelibs-foundation#5507 The stress test passed.
So I don't think this PR fixes the issue. keep this on hold.

@ahoppen

ahoppen commented Jul 10, 2026

Copy link
Copy Markdown
Member

I think moving away from DispatchIO is a good move regardless, given how much trouble it has caused us in the past for stdin reading.

@rintaro
rintaro force-pushed the clangd-stderr-blocking-read branch 2 times, most recently from d226499 to faaf9cf Compare July 10, 2026 17:18
@rintaro

rintaro commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Reopening.
I reproduced the issue in my local docker setup. It's rare (took 264 iterations first) but reproducible.
With this change, I didn't see the crash after 600 iterations.

@rintaro
rintaro marked this pull request as ready for review July 10, 2026 22:03

@ahoppen ahoppen 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.

Great find. Just a few nitpicks inline

Comment on lines +268 to +269
// Reached end-of-file (0) or hit an unrecoverable error; stop reading.
break

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.

Should we log an error with errno if an unrecoverable error was reached?

/// A blocking read is used rather than a `readabilityHandler`. `readabilityHandler` installs a libdispatch
/// read source on the file descriptor; tearing that source down races epoll event delivery on Linux and
/// can crash the process in `_dispatch_event_loop_drain`. A blocking read never registers the descriptor
/// with libdispatch.

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.

Worth referencing the corelibs-libdispatch issue here?

@hamishknight hamishknight left a comment

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.

LGTM modulo Alex's comments

@rintaro
rintaro force-pushed the clangd-stderr-blocking-read branch from faaf9cf to 114bcbc Compare July 13, 2026 14:05
`readabilityHandler` installs a libdispatch read source on the stderr
pipe's file descriptor. Tearing that source down races epoll event
delivery on Linux: `_dispatch_event_loop_drain` can dereference a
`dispatch_muxnote` that a concurrent source teardown has already freed,
crashing the process. This was observed intermittently in Linux CI while
tearing down clangd connections and confirmed by core-dump analysis.

Forward stderr with a blocking read loop on a dedicated queue instead. A
blocking read never registers the file descriptor with libdispatch's
event loop, so no read source is created for the pipe.

Read the raw file descriptor rather than `FileHandle.read(upToCount:)`:
on non-Darwin platforms the latter blocks until it accumulates the full
requested count or reaches EOF, which would withhold small,
intermittently emitted messages until the subprocess exits. A raw
`read` returns as soon as any bytes are available.

Extract the loop into `readInBackground` and add tests covering
forwarding through end-of-file and prompt delivery of data before EOF.
@rintaro
rintaro force-pushed the clangd-stderr-blocking-read branch from 114bcbc to 88c1a25 Compare July 13, 2026 14:51
@rintaro
rintaro merged commit 22a4c9c into swiftlang:main Jul 15, 2026
55 of 57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants