os/open: guard against tight loop in stderr streaming - #13480
Closed
chyroc wants to merge 1 commit into
Closed
Conversation
The reader loop in openThread can spin without progress when takeDelimiterExclusive returns zero-length slices without signaling EndOfStream. Observed on macOS with Zig's std.Io.Reader at pipe EOF races: after the child (`/usr/bin/open URL`) exits, the stderr reader sits in a state where each takeDelimiterExclusive call returns an empty slice instead of throwing error.EndOfStream. The loop then emits `open stderr=` warnings at ~9,000/sec/thread and pins CPU indefinitely. Add a consecutive-empty-read counter that treats 16 empty reads in a row as EOF and exits the loop. Also move the child wait() into a `defer` so early breaks still reap the process. Reproduced downstream in cmux (which vendors Ghostty) — 5 leaked openThread instances produced ~45,000 empty warnings/sec, driving the parent process to 300-400% CPU with the log-flood surviving surface teardown. Introduced in bb375a2 ("deal with large outputs from xdg-open/rundll32/open"). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
|
Hi @chyroc, thanks for your interest in contributing! This project requires that pull request authors are vouched, and you are not in the list of vouched users. This PR will be closed automatically. See https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md for more details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a runaway loop in
openThreadthat spams tens of thousands of emptyopen stderr=log warnings per second and pins the parent process at 300%+ CPU indefinitely.Reproducer (macOS)
Observed downstream in cmux (which vendors Ghostty via a fork), against Ghostty
1.3.2-dev:[com.mitchellh.ghostty:os-open] os-open: open stderr=(empty payload)log stream --process cmuxreportsMessages dropped during live streaming— flood exceedsos_logthroughputsample <pid>top-of-stack was dominated byos_log_impl_flatten_and_send,os_log_create,_os_log, andzig_os_log_with_type, confirming the hot path is thelog.warninsideopenThread.Root cause
Introduced in bb375a2 ("deal with large outputs from xdg-open/rundll32/open"), which replaced the batched
collectOutputwith a streaming reader loop.takeDelimiterExclusive('\n')onstd.Io.Readercan return zero-length slices without throwingEndOfStreamwhen the underlying pipe has reached EOF but the reader has not yet fully drained its state. On macOS, once/usr/bin/open <url>exits, its stderr pipe enters this state and every subsequent call returns an empty slice — no error variant of the outerswitchmatches, so the loop neverbreaks andlog.warn("open stderr={s}", .{line})fires every iteration with an empty string.Because
openThreadis.detach()'d, each leaked instance runs forever. The five leaks in the reproducer corresponded to five prioropen()calls over the lifetime of the process.A secondary issue: the pre-patch code calls
_ = exe.wait(io) catch {};after the stderr block, so any of the existingbreakpaths that leave the loop early skip the reap.Fix
open/xdg-open/rundll32but future-proof) while providing a hard ceiling that prevents the tight loop.defer exe.wait(): unconditional reap regardless of which branch exits the loop.Verification
openThreads were pinning 3–4 cores at 100 % and floodingos_logat ~45k lines/sec; after applying this patch and rebuilding, CPU is idle and noos-openmessages are emitted after each child exits.EndOfStream).Notes
takeDelimiterExclusivereturning an empty slice at EOF instead ofEndOfStreammay itself be astd.Io.Readerbug worth reporting to Zig upstream, but a defensive guard here is the right call regardless.🤖 Generated with Claude Code