os/open: consume the newline when draining opener stderr

takeDelimiterExclusive never consumes the delimiter: it tosses only the
exclusive length, so the '\n' stays buffered. Once the spawned opener
writes a single line to stderr, every subsequent call returns an empty
slice without advancing the stream, and openThread's loop spins forever
- one pinned core per affected open(), logging empty
"open stderr=" warnings at tens of thousands of messages per second for
the lifetime of the process. The thread also never reaches exe.wait(),
so the child is never reaped.

Read inclusively instead (which does consume the delimiter) and trim
the '\n' for logging.

Repro: open a link whose handler writes to stderr, e.g. an OSC 8 link
with an unknown scheme; watch a core disappear and the unified log
flood with "os-open: open stderr=".

See discussion #14100.
This commit is contained in:
Johannes Zillmann
2026-09-02 10:45:08 -06:00
parent b0481f5aa7
commit 3b8141fbd8

View File

@@ -86,7 +86,11 @@ fn openThread(io: std.Io, exe_: std.process.Child) void {
var stream = stderr.readerStreaming(io, &buffer);
const reader = &stream.interface;
while (true) {
const line = reader.takeDelimiterExclusive('\n') catch |outer| switch (outer) {
// Read inclusively so the delimiter is consumed:
// takeDelimiterExclusive leaves the '\n' buffered, so once the
// child writes a line this loop would receive an empty slice
// forever, pinning a core and spamming empty warnings.
const line = reader.takeDelimiterInclusive('\n') catch |outer| switch (outer) {
error.EndOfStream => break,
error.ReadFailed => break,
error.StreamTooLong => reader.take(buffer.len) catch |inner| switch (inner) {
@@ -94,7 +98,7 @@ fn openThread(io: std.Io, exe_: std.process.Child) void {
error.EndOfStream => break,
},
};
log.warn("open stderr={s}", .{line});
log.warn("open stderr={s}", .{std.mem.trimEnd(u8, line, "\n")});
}
}
_ = exe.wait(io) catch {};