diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 56c31135ed..b090f47f0a 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -406,6 +406,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s proc->cwd = cwd; proc->env = env; proc->overlapped = overlapped; + proc->stdio_noinherit = true; 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 e3d9d15684..f57a038803 100644 --- a/src/nvim/event/defs.h +++ b/src/nvim/event/defs.h @@ -170,5 +170,6 @@ 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 MultiQueue *events; }; diff --git a/src/nvim/event/libuv_proc.c b/src/nvim/event/libuv_proc.c index 7405892af7..c77f11d9a1 100644 --- a/src/nvim/event/libuv_proc.c +++ b/src/nvim/event/libuv_proc.c @@ -63,23 +63,39 @@ int libuv_proc_spawn(LibuvProc *uvproc) int to_close[3] = { -1, -1, -1 }; if (!proc->in.closed) { - uv_file pipe_pair[2]; - int client_flags = 0; #ifdef MSWIN - client_flags |= proc->overlapped ? UV_NONBLOCK_PIPE : 0; -#endif + 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, client_flags, UV_NONBLOCK_PIPE); + 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 } if (!proc->out.s.closed) { @@ -104,6 +120,24 @@ int libuv_proc_spawn(LibuvProc *uvproc) } 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); @@ -112,6 +146,7 @@ int libuv_proc_spawn(LibuvProc *uvproc) to_close[2] = pipe_pair[1]; uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]); +#endif } else if (proc->fwd_err) { uvproc->uvstdio[2].flags = UV_INHERIT_FD; uvproc->uvstdio[2].data.fd = STDERR_FILENO; diff --git a/src/nvim/event/proc.h b/src/nvim/event/proc.h index 94efaf3a4c..bbd76adb18 100644 --- a/src/nvim/event/proc.h +++ b/src/nvim/event/proc.h @@ -31,6 +31,7 @@ static inline Proc proc_init(Loop *loop, ProcType type, void *data) .internal_exit_cb = NULL, .detach = false, .fwd_err = false, + .stdio_noinherit = false, }; } diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 70da97a63d..27d6374626 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -111,6 +111,41 @@ describe('TUI', function() feed_data(':') screen:expect(s1) end) + + it('jobstart child writing to CON does not leak to screen', function() + -- A child job that writes to CON must not leak onto the TUI screen. The + -- embedded server AttachConsole()'s the parent terminal (so server io.stdout + -- e.g. SIXEL works), but jobs must get their own (windowless) console so + -- writes to CON go nowhere visible. + t.skip(not is_os('win'), 'N/A Windows only') + n.clear() + finally(function() + n.check_close() + end) + + local screen = tt.setup_child_nvim({ + '--clean', + '--cmd', + 'colorscheme vim', + '--cmd', + nvim_set .. ' laststatus=2 background=dark', + }, { env = env_notermguicolors }) + tt.override_screen_expect_for_conpty(screen) + + feed_data('iZZZSENTINEL') + + -- Leave insert mode and run a job that writes directly to CON, blocking + -- until it finishes. The CON output must not appear anywhere on screen. + feed_data("\027\027:call jobwait([jobstart(['cmd', '/c', 'echo LEAKEDXYZ > CON'])])\013") + + screen:expect([[ + ZZZSENTINE^L | + {100:~ }|*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | + ]]) + end) end) describe('TUI :detach', function()