diff --git a/src/nvim/channel.c b/src/nvim/channel.c index b090f47f0a..31f6c80bd6 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -406,7 +406,10 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s proc->cwd = cwd; proc->env = env; proc->overlapped = overlapped; +#ifdef MSWIN + // Windows: spawn channel jobs with noinherit (so writes don't leak to TUI #40074). proc->stdio_noinherit = true; +#endif char *cmd = xstrdup(proc_get_exepath(proc)); bool has_out, has_err; diff --git a/src/nvim/event/defs.h b/src/nvim/event/defs.h index f57a038803..95a7303006 100644 --- a/src/nvim/event/defs.h +++ b/src/nvim/event/defs.h @@ -170,6 +170,7 @@ struct proc { proc_state_cb state_cb; internal_proc_cb internal_exit_cb, internal_close_cb; bool closed, detach, overlapped, fwd_err; - bool stdio_noinherit; ///< MSWIN: channel jobs don't inherit stdio, to prevent CON leaking to TUI + bool stdio_noinherit; ///< MSWIN: don't share parent console with child (prevent leaking to TUI). + ///< No equivalent on Unix, where inherited stdio is a pipe, not terminal. MultiQueue *events; }; diff --git a/src/nvim/event/libuv_proc.c b/src/nvim/event/libuv_proc.c index c77f11d9a1..50ab55e86f 100644 --- a/src/nvim/event/libuv_proc.c +++ b/src/nvim/event/libuv_proc.c @@ -16,6 +16,62 @@ #include "event/libuv_proc.c.generated.h" +/// Configures a stdio slot (`idx`) before spawning a process: connects the parent end to +/// `parent_pipe` and sets up the fd/pipe the child inherits. `child_readable` is true for the +/// child's stdin (the child reads), false for stdout/stderr (the child writes). +/// +/// On Windows, `win_create_pipe` requests a non-inherited pipe (UV_CREATE_PIPE). +/// - stdout always needs this (IOCP); +/// - channel jobs also set it for stdin/stderr. +/// - NOTE(!): libuv sets CREATE_NO_WINDOW only when *all* stdio slots are non-UV_INHERIT_FD! +/// A single UV_INHERIT_FD slot re-attaches child => leaks CON writes to TUI #40074. +/// https://github.com/libuv/libuv/blob/601a1537bb5628398c2389efbc7eecd062e8aac2/src/win/process.c#L1032-L1041 +/// +/// Records any fd the parent must close after spawn in `to_close[idx]`. +static void libuv_proc_stdio(LibuvProc *uvproc, int idx, uv_pipe_t *parent_pipe, + bool child_readable, bool overlapped, bool win_create_pipe, + int *to_close) +{ +#ifdef MSWIN + if (win_create_pipe) { + uvproc->uvstdio[idx].flags = UV_CREATE_PIPE + | (child_readable ? UV_READABLE_PIPE : UV_WRITABLE_PIPE); + if (overlapped) { + // Pipe must also be readable for IOCP to work on Windows. + uvproc->uvstdio[idx].flags |= UV_OVERLAPPED_PIPE | UV_READABLE_PIPE; + } + uvproc->uvstdio[idx].data.stream = (uv_stream_t *)parent_pipe; + return; + } +#endif + + // Inherited-fd pipe: create a uv_pipe() pair, hand one end to child via UV_INHERIT_FD and keep + // other for parent. + // + // On non-Windows uv_pipe() is preferred over UV_CREATE_PIPE: as of libuv 1.51, UV_CREATE_PIPE + // uses socketpair() (behaves confusingly on Linux: breaks /proc//fd/0, which the Linux + // socket maintainer disowned). + int child_flags = 0; +#ifdef MSWIN + // Overlapped child stdin must be non-blocking. + child_flags = (child_readable && overlapped) ? UV_NONBLOCK_PIPE : 0; +#endif + // pipe_pair[0] is the read end, pipe_pair[1] the write end; the parent end is non-blocking. + uv_file pipe_pair[2]; + uv_pipe(pipe_pair, + child_readable ? child_flags : UV_NONBLOCK_PIPE, + child_readable ? UV_NONBLOCK_PIPE : child_flags); + + // child_readable: child reads pipe_pair[0], parent writes pipe_pair[1]. + // else: child writes pipe_pair[1], parent reads pipe_pair[0]. + int child_fd = child_readable ? pipe_pair[0] : pipe_pair[1]; + int parent_fd = child_readable ? pipe_pair[1] : pipe_pair[0]; + uvproc->uvstdio[idx].flags = UV_INHERIT_FD; + uvproc->uvstdio[idx].data.fd = child_fd; + to_close[idx] = child_fd; + uv_pipe_open(parent_pipe, parent_fd); +} + /// @returns zero on success, or negative error code int libuv_proc_spawn(LibuvProc *uvproc) FUNC_ATTR_NONNULL_ALL @@ -63,90 +119,18 @@ int libuv_proc_spawn(LibuvProc *uvproc) int to_close[3] = { -1, -1, -1 }; if (!proc->in.closed) { -#ifdef MSWIN - if (proc->stdio_noinherit) { - // Let channel jobs use CREATE_NO_WINDOW so CON writes do not leak. - uvproc->uvstdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; - uvproc->uvstdio[0].flags |= proc->overlapped ? UV_OVERLAPPED_PIPE : 0; - uvproc->uvstdio[0].data.stream = (uv_stream_t *)(&proc->in.uv.pipe); - } else { - uv_file pipe_pair[2]; - int client_flags = proc->overlapped ? UV_NONBLOCK_PIPE : 0; - - uv_pipe(pipe_pair, client_flags, UV_NONBLOCK_PIPE); - - uvproc->uvstdio[0].flags = UV_INHERIT_FD; - uvproc->uvstdio[0].data.fd = pipe_pair[0]; - to_close[0] = pipe_pair[0]; - - uv_pipe_open(&proc->in.uv.pipe, pipe_pair[1]); - } -#else - uv_file pipe_pair[2]; - - // As of libuv 1.51, UV_CREATE_PIPE can only create pipes - // using socketpair(), not pipe(). We want the latter on linux - // as socket pairs behave different in some confusing ways, like - // breaking /proc/0/fd/0 which is disowned by the linux socket maintainer. - uv_pipe(pipe_pair, 0, UV_NONBLOCK_PIPE); - - uvproc->uvstdio[0].flags = UV_INHERIT_FD; - uvproc->uvstdio[0].data.fd = pipe_pair[0]; - to_close[0] = pipe_pair[0]; - - uv_pipe_open(&proc->in.uv.pipe, pipe_pair[1]); -#endif + libuv_proc_stdio(uvproc, 0, &proc->in.uv.pipe, true, proc->overlapped, proc->stdio_noinherit, + to_close); } if (!proc->out.s.closed) { -#ifdef MSWIN - // TODO(bfredl): in theory it would have been nice if the uv_pipe() branch - // also worked for windows but IOCP happens because of reasons. - uvproc->uvstdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - // pipe must be readable for IOCP to work on Windows. - uvproc->uvstdio[1].flags |= proc->overlapped - ? (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0; - uvproc->uvstdio[1].data.stream = (uv_stream_t *)(&proc->out.s.uv.pipe); -#else - uv_file pipe_pair[2]; - uv_pipe(pipe_pair, UV_NONBLOCK_PIPE, 0); - - uvproc->uvstdio[1].flags = UV_INHERIT_FD; - uvproc->uvstdio[1].data.fd = pipe_pair[1]; - to_close[1] = pipe_pair[1]; - - uv_pipe_open(&proc->out.s.uv.pipe, pipe_pair[0]); -#endif + // Windows: stdout always uses a non-inherited pipe (IOCP). + libuv_proc_stdio(uvproc, 1, &proc->out.s.uv.pipe, false, proc->overlapped, true, to_close); } if (!proc->err.s.closed) { -#ifdef MSWIN - if (proc->stdio_noinherit) { - uvproc->uvstdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - // pipe must be readable for IOCP to work on Windows. - uvproc->uvstdio[2].flags |= proc->overlapped - ? (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0; - uvproc->uvstdio[2].data.stream = (uv_stream_t *)(&proc->err.s.uv.pipe); - } else { - uv_file pipe_pair[2]; - uv_pipe(pipe_pair, UV_NONBLOCK_PIPE, 0); - - uvproc->uvstdio[2].flags = UV_INHERIT_FD; - uvproc->uvstdio[2].data.fd = pipe_pair[1]; - to_close[2] = pipe_pair[1]; - - uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]); - } -#else - uv_file pipe_pair[2]; - uv_pipe(pipe_pair, UV_NONBLOCK_PIPE, 0); - - uvproc->uvstdio[2].flags = UV_INHERIT_FD; - uvproc->uvstdio[2].data.fd = pipe_pair[1]; - to_close[2] = pipe_pair[1]; - - uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]); -#endif + libuv_proc_stdio(uvproc, 2, &proc->err.s.uv.pipe, false, proc->overlapped, + proc->stdio_noinherit, to_close); } else if (proc->fwd_err) { uvproc->uvstdio[2].flags = UV_INHERIT_FD; uvproc->uvstdio[2].data.fd = STDERR_FILENO;