From 040bdf0bc5f1db60e61b1f8f02397db655a0ba96 Mon Sep 17 00:00:00 2001 From: tao <2471314@gmail.com> Date: Thu, 30 Apr 2026 04:33:22 +0800 Subject: [PATCH] refactor(path): more slash normalization #39426 Problem: 1. `vim_getenv` is followed by `TO_SLASH` when getting path-related variables. 2. cmd exits when launched with forward slash. Solution: 1. try calling `TO_SLASH` in `vim_getenv`. 2. pass fullpath via `lpApplicationName`, only include `cmd.exe` in cmdline. Co-authored-by: Justin M. Keyes --- src/nvim/os/env.c | 13 ++++++++- src/nvim/os/pty_proc_win.c | 34 +++++++++++++++++----- src/nvim/runtime.c | 3 +- test/functional/vimscript/environ_spec.lua | 2 +- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index 37460fe1c5..dbf255700a 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -618,7 +618,6 @@ size_t expand_env_esc(const char *restrict srcp, char *restrict dst, int dstlen, #endif *var = NUL; var = vim_getenv(dst); - TO_SLASH(var); mustfree = true; #ifdef UNIX } @@ -872,6 +871,18 @@ char *vim_getenv(const char *name) #endif char *kos_env_path = os_getenv(name); +#ifdef BACKSLASH_IN_FILENAME + if (striequal(name, "VIMRUNTIME") + || striequal(name, "PATH") + || striequal(name, "CDPATH") + || striequal(name, "TMPDIR") + || striequal(name, "TMP") + || striequal(name, "TEMP") + || striequal(name, "VIM") + || striequal(name, "MYVIMRC")) { + TO_SLASH(kos_env_path); + } +#endif if (kos_env_path != NULL) { return kos_env_path; } diff --git a/src/nvim/os/pty_proc_win.c b/src/nvim/os/pty_proc_win.c index 8cc66bf319..d9a03393f5 100644 --- a/src/nvim/os/pty_proc_win.c +++ b/src/nvim/os/pty_proc_win.c @@ -8,6 +8,7 @@ #include "nvim/log.h" #include "nvim/mbyte.h" #include "nvim/memory.h" +#include "nvim/os/fs.h" #include "nvim/os/os.h" #include "nvim/os/pty_conpty_win.h" #include "nvim/os/pty_proc_win.h" @@ -61,6 +62,7 @@ int pty_proc_spawn(PtyProc *ptyproc) HANDLE proc_handle = NULL; uv_connect_t *in_req = NULL; uv_connect_t *out_req = NULL; + wchar_t *file = NULL; wchar_t *cmd_line = NULL; wchar_t *cwd = NULL; wchar_t *env = NULL; @@ -99,8 +101,28 @@ int pty_proc_spawn(PtyProc *ptyproc) } } - status = build_cmd_line(proc->argv, &cmd_line, - os_shell_is_cmdexe(proc->argv[0])); + // cmd.exe can’t recognize forward slash in argv[0] during + // initialization, so we keep only cmd.exe in the cmd_line + // and pass fullpath via lpApplicationName to avoid hijacking. + // See https://www.microsoft.com/en-us/msrc/blog/2014/04/ms14-019-fixing-a-binary-hijacking-via-cmd-or-bat-file + bool is_cmdexe = os_shell_is_cmdexe(proc->argv[0]); + if (is_cmdexe) { + char *path = NULL; + // TODO(ntdiary): Could put the search logic in one place. + // See https://github.com/neovim/neovim/issues/36818#issuecomment-3977147445 + if (!os_can_exe(proc->argv[0], &path, true)) { + status = UV_ENOENT; + emsg = "executable not found"; + goto cleanup; + } + status = utf8_to_utf16(path, -1, &file); + if (status != 0) { + emsg = "utf8_to_utf16(proc->argv[0]) failed"; + goto cleanup; + } + xfree(path); + } + status = build_cmd_line(proc->argv, &cmd_line, is_cmdexe); if (status != 0) { emsg = "build_cmd_line failed"; goto cleanup; @@ -117,7 +139,7 @@ int pty_proc_spawn(PtyProc *ptyproc) if (!os_conpty_spawn(conpty_object, &proc_handle, - NULL, + file, cmd_line, cwd, env)) { @@ -163,6 +185,7 @@ cleanup: } xfree(in_req); xfree(out_req); + xfree(file); xfree(cmd_line); xfree(env); xfree(cwd); @@ -261,10 +284,7 @@ static int build_cmd_line(char **argv, wchar_t **cmd_line, bool is_cmdexe) ArgNode *arg_node = xmalloc(sizeof(*arg_node)); arg_node->arg = xmalloc(buf_len); if (is_cmdexe) { - xstrlcpy(arg_node->arg, *argv, buf_len); - if (argc == 0) { - TO_BACKSLASH(arg_node->arg); - } + xstrlcpy(arg_node->arg, argc == 0 ? "cmd.exe" : *argv, buf_len); } else { quote_cmd_arg(arg_node->arg, buf_len, *argv); } diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index ec3c8ac9d8..ddf00b5f03 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -1847,8 +1847,7 @@ char *runtimepath_default(bool clean_arg) char *const libdir = get_lib_dir(); char *const data_dirs = stdpaths_get_xdg_var(kXDGDataDirs); char *const config_dirs = stdpaths_get_xdg_var(kXDGConfigDirs); - char *vimruntime = vim_getenv("VIMRUNTIME"); - TO_SLASH(vimruntime); + char *const vimruntime = vim_getenv("VIMRUNTIME"); #define SITE_SIZE (sizeof("site") - 1) #define AFTER_SIZE (sizeof("after") - 1) size_t data_len = 0; diff --git a/test/functional/vimscript/environ_spec.lua b/test/functional/vimscript/environ_spec.lua index 9046d2d696..50756f4c8e 100644 --- a/test/functional/vimscript/environ_spec.lua +++ b/test/functional/vimscript/environ_spec.lua @@ -35,7 +35,7 @@ describe('vim.fn.environ()', function() local env = vim.fn.environ() assert(vim.tbl_count(env) > 10, 'environ() should have some env vars!') for k, v in pairs(env) do - if v ~= '' and vim.fn.getenv(k) ~= v then + if v ~= '' and vim.fn.getenv(k) ~= v and vim.fn.getenv(k) ~= v:gsub('\\', '/') then error(('environ()[%q] = %q, but vim.fn.getenv(%q) = %q'):format(k, v, k, vim.fn.getenv(k))) end end