From cdf09b6a87038a2563ad05a2a5b84f9c791569fb Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 14:54:18 -0700 Subject: [PATCH 1/6] libfetchers: Use the configured ssh program for git-lfs-authenticate Expose the ssh-program build option from libstore as sshProgram(), mirroring gitProgram() in libfetchers, so the Git LFS ssh handshake no longer hardcodes "ssh" from PATH. --- src/libfetchers/git-lfs-fetch.cc | 5 +++-- src/libstore/include/nix/store/ssh.hh | 7 +++++++ src/libstore/meson.options | 2 +- src/libstore/ssh.cc | 6 ++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/libfetchers/git-lfs-fetch.cc b/src/libfetchers/git-lfs-fetch.cc index 91e9e7f38946..ea23aefa295e 100644 --- a/src/libfetchers/git-lfs-fetch.cc +++ b/src/libfetchers/git-lfs-fetch.cc @@ -73,11 +73,12 @@ LfsApiInfo getLfsApi(ParsedURL url) args.push_back(string_to_os_string(url.renderPath(/*encode=*/false))); args.push_back(OS_STR("download")); - auto [status, output] = runProgram({{.program = "ssh", .args = args}}); + auto [status, output] = runProgram({{.program = sshProgram(), .args = args}}); if (output.empty()) throw Error( - "git-lfs-authenticate: no output (cmd: 'ssh %s')", + "git-lfs-authenticate: no output (cmd: '%s %s')", + sshProgram().string(), concatMapStringsSep( " ", args, [](const OsString & s) { return escapeShellArgAlways(os_string_to_string(s)); })); diff --git a/src/libstore/include/nix/store/ssh.hh b/src/libstore/include/nix/store/ssh.hh index d56d1331b566..235c2c5bfb55 100644 --- a/src/libstore/include/nix/store/ssh.hh +++ b/src/libstore/include/nix/store/ssh.hh @@ -9,6 +9,13 @@ namespace nix { +/** + * The ssh program used for all ssh invocations (build-time configurable + * via the `ssh-program` option; either a name resolved via PATH or an + * absolute path). + */ +const std::filesystem::path & sshProgram(); + OsStrings getNixSshOpts(); class SSHMaster diff --git a/src/libstore/meson.options b/src/libstore/meson.options index fc55dd962732..9b44a85794cc 100644 --- a/src/libstore/meson.options +++ b/src/libstore/meson.options @@ -44,7 +44,7 @@ option( 'ssh-program', type : 'string', value : 'ssh', - description : 'the ssh program used for remote stores, remote builders and store copies (a name resolved via PATH, or an absolute path)', + description : 'the ssh program used for remote stores, remote builders, store copies and Git LFS authentication (a name resolved via PATH, or an absolute path)', ) option( diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index da56433b3445..8ded7d776146 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -59,6 +59,12 @@ static void checkValidAuthority(const ParsedURL::Authority & authority) } } +const std::filesystem::path & sshProgram() +{ + static const std::filesystem::path program = SSH_PROGRAM; + return program; +} + OsStrings getNixSshOpts() { std::string sshOpts = getEnv("NIX_SSHOPTS").value_or(""); From 21afa11333739546d3b61b8c287b684b94537557 Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 15:21:34 -0700 Subject: [PATCH 2/6] libstore: Use sshProgram() instead of SSH_PROGRAM in ssh.cc --- src/libstore/ssh.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index 8ded7d776146..611a8d211124 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -136,7 +136,7 @@ bool SSHMaster::isMasterRunning() auto res = runProgram( RunOptions{ - .spawnOptions = {.program = SSH_PROGRAM, .args = std::move(args)}, + .spawnOptions = {.program = sshProgram(), .args = std::move(args)}, .mergeStderrToStdout = true, }); return res.first == 0; @@ -177,7 +177,7 @@ std::unique_ptr SSHMaster::startCommand(OsStrings && comm std::filesystem::path program; if (!fakeSSH) { - program = SSH_PROGRAM; + program = sshProgram(); args = {string_to_os_string(hostnameAndUser), OS_STR("-x")}; addCommonSSHOpts(args); if (!socketPath.empty()) @@ -268,7 +268,7 @@ std::filesystem::path SSHMaster::startMaster() state->sshMaster = spawnProgram( { - .program = SSH_PROGRAM, + .program = sshProgram(), .lookupPath = true, .args = std::move(args), .environment = createSSHEnv(), From c9640797307405d6e357002f932ee3cf46430b32 Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 14:54:26 -0700 Subject: [PATCH 3/6] nix shell: Use the configured bash when $SHELL is unset The bash-program build option covered nix develop and nix-shell, but nix shell still fell back to a bare "bash" from PATH. --- src/nix/env.cc | 4 +++- src/nix/meson.options | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nix/env.cc b/src/nix/env.cc index 4022c3effa81..3ff760956b57 100644 --- a/src/nix/env.cc +++ b/src/nix/env.cc @@ -3,6 +3,8 @@ #include #include "nix/cmd/command.hh" + +#include "cli-config-private.hh" #include "nix/expr/eval.hh" #include "run.hh" #include "nix/util/strings.hh" @@ -37,7 +39,7 @@ struct CmdShell : InstallablesCommand, MixEnvironment using InstallablesCommand::run; - std::vector command = {getEnv("SHELL").value_or("bash")}; + std::vector command = {getEnv("SHELL").value_or(FALLBACK_BASH)}; CmdShell() { diff --git a/src/nix/meson.options b/src/nix/meson.options index 50843bfa9929..a80ce75d70a5 100644 --- a/src/nix/meson.options +++ b/src/nix/meson.options @@ -26,5 +26,5 @@ option( 'bash-program', type : 'string', value : 'bash', - description : 'the bash used as the fallback interactive shell for nix develop / nix-shell when bashInteractive from nixpkgs is unavailable (a name resolved via PATH, or an absolute path)', + description : 'the bash used as the fallback interactive shell for nix develop / nix-shell when bashInteractive from nixpkgs is unavailable, and for nix shell when $SHELL is unset (a name resolved via PATH, or an absolute path)', ) From 84c15e4f298529c5b9e83e4cebc4cc20fa3164a2 Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 15:05:45 -0700 Subject: [PATCH 4/6] libutil: Add an sh-program build option Nix runs some command strings through a POSIX shell. Examples are the pager and ssh LocalCommand. Instead of using a hardcoded /bin/sh, make that path configurable at build time and expose it as shProgram(), like gitProgram() and sshProgram(). It has to be absolute because neither execl nor ssh searches PATH for it. --- src/libutil/include/nix/util/processes.hh | 6 ++++++ src/libutil/meson.build | 10 ++++++++++ src/libutil/meson.options | 7 +++++++ src/libutil/processes.cc | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/src/libutil/include/nix/util/processes.hh b/src/libutil/include/nix/util/processes.hh index 2854afa08933..0efbd93786d5 100644 --- a/src/libutil/include/nix/util/processes.hh +++ b/src/libutil/include/nix/util/processes.hh @@ -129,6 +129,12 @@ struct ProcessOptions pid_t startProcess(fun processMain, const ProcessOptions & options = ProcessOptions()); #endif +/** + * The POSIX shell used to run command strings with `-c` (build-time + * configurable via the `sh-program` option; always an absolute path). + */ +const std::filesystem::path & shProgram(); + /** * Run a program and return its stdout in a string (i.e., like the * shell backtick operator). diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 7c0af0bd02b1..6f5b1406529b 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -144,6 +144,16 @@ deps_public += nlohmann_json cxx = meson.get_compiler('cpp') +fs = import('fs') + +sh_program = get_option('sh-program') +if host_machine.system() != 'windows' and not fs.is_absolute(sh_program) + # sh_program is exec'd directly and handed to ssh as $SHELL, + # neither of which does a PATH lookup. It's only meaningful on Linux. + error('sh-program must be an absolute path, got \'' + sh_program + '\'') +endif +configdata_priv.set_quoted('SH_PROGRAM', sh_program) + config_priv_h = configure_file( configuration : configdata_priv, output : 'util-config-private.hh', diff --git a/src/libutil/meson.options b/src/libutil/meson.options index a82806162972..480c75e130da 100644 --- a/src/libutil/meson.options +++ b/src/libutil/meson.options @@ -5,3 +5,10 @@ option( type : 'feature', description : 'determine microarchitecture levels with libcpuid (only relevant on x86_64)', ) + +option( + 'sh-program', + type : 'string', + value : '/bin/sh', + description : 'the POSIX shell used to run command strings such as $PAGER and ssh LocalCommand (must be an absolute path)', +) diff --git a/src/libutil/processes.cc b/src/libutil/processes.cc index 68c54f9c0744..37863474bee0 100644 --- a/src/libutil/processes.cc +++ b/src/libutil/processes.cc @@ -2,10 +2,18 @@ #include "nix/util/serialise.hh" #include "nix/util/signals.hh" +#include "util-config-private.hh" + namespace nix { void ExecError::anchor() {} +const std::filesystem::path & shProgram() +{ + static const std::filesystem::path program = SH_PROGRAM; + return program; +} + Pid & Pid::operator=(Pid && other) noexcept { swap(*this, other); From e4e6e861ae3989304037b1be0ac4a6af3ca68cef Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 16:21:02 -0700 Subject: [PATCH 5/6] libmain: Use the configured sh for the pager Run $NIX_PAGER/$PAGER through shProgram() instead of a hardcoded /bin/sh. --- src/libmain/shared.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index e5afbcd6fbb9..1fe79ddea007 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -391,6 +391,8 @@ RunPager::RunPager() Pipe toPager; toPager.create(); + const auto & sh = shProgram(); + pid = startProcess([&]() { if (dup2(toPager.readSide.get(), STDIN_FILENO) == -1) throw SysError("dupping stdin"); @@ -398,7 +400,7 @@ RunPager::RunPager() setEnv("LESS", "FRSXMK"); restoreProcessContext(); if (pager) - execl("/bin/sh", "sh", "-c", pager, nullptr); + execl(sh.c_str(), "sh", "-c", pager, nullptr); execlp("pager", "pager", nullptr); execlp("less", "less", nullptr); execlp("more", "more", nullptr); From e00e4721a0595f3a20821dbafec622d3f1014fe4 Mon Sep 17 00:00:00 2001 From: Philip Taron Date: Mon, 5 Oct 2026 16:22:22 -0700 Subject: [PATCH 6/6] libstore: Use the configured sh as SHELL for ssh LocalCommand The local ssh client runs -oLocalCommand through $SHELL, so point it at shProgram() instead of a hardcoded /bin/sh. --- src/libstore/ssh.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index 611a8d211124..2fb244fddf95 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -144,16 +144,16 @@ bool SSHMaster::isMasterRunning() static OsStringMap createSSHEnv() { - // Copy the environment and set SHELL=/bin/sh + // Copy the environment and set SHELL to the configured sh OsStringMap env = getEnvOs(); // SSH will invoke the "user" shell for -oLocalCommand, but that means // $SHELL. To keep things simple and avoid potential issues with other - // shells, we set it to /bin/sh. + // shells, we set it to the configured POSIX shell. // Technically, we don't need that, and we could reinvoke ourselves to print // "started". Self-reinvocation is tricky with library consumers, but mostly // solved; refer to the development history of nixExePath in libstore/globals.cc. - env.insert_or_assign(OS_STR("SHELL"), OS_STR("/bin/sh")); + env.insert_or_assign(OS_STR("SHELL"), shProgram().native()); return env; }