diff --git a/src/main/runtime/login-shell-path.test.ts b/src/main/runtime/login-shell-path.test.ts index b2a1d326..aba64e1d 100644 --- a/src/main/runtime/login-shell-path.test.ts +++ b/src/main/runtime/login-shell-path.test.ts @@ -1,5 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import os from 'node:os'; import { applyLoginShellPath, mergePathValues, readLoginShellPath } from './login-shell-path'; const wrap = (value: string) => @@ -24,6 +25,51 @@ test('readLoginShellPath returns null when markers are missing', async () => { assert.equal(value, null); }); +test('readLoginShellPath rejects relative explicit shells before execution', async () => { + for (const shell of ['zsh', './zsh', '']) { + await assert.rejects( + readLoginShellPath({ + env: { SHELL: '/bin/zsh' }, + shell, + runShell: async () => assert.fail('Invalid shell must not execute'), + }), + /Login shell must be an absolute path/, + ); + } +}); + +test('readLoginShellPath falls back through absolute default shells', async () => { + const originalUserInfo = os.userInfo; + try { + for (const shell of ['/bin/bash', 'bash', '', null]) { + Object.defineProperty(os, 'userInfo', { + value: () => ({ shell, username: 'test', uid: 1000, gid: 1000, homedir: '/Users/test' }), + }); + await readLoginShellPath({ + env: { SHELL: './zsh' }, + runShell: async (selectedShell) => { + assert.equal(selectedShell, shell === '/bin/bash' ? '/bin/bash' : '/bin/zsh'); + return wrap('/usr/bin'); + }, + }); + } + Object.defineProperty(os, 'userInfo', { + value: () => { + throw new Error('User lookup failed'); + }, + }); + await readLoginShellPath({ + env: { SHELL: 'zsh' }, + runShell: async (shell) => { + assert.equal(shell, '/bin/zsh'); + return wrap('/usr/bin'); + }, + }); + } finally { + os.userInfo = originalUserInfo; + } +}); + test('mergePathValues puts login entries first and keeps process-only entries', () => { assert.equal( mergePathValues('/Users/me/.local/bin:/usr/bin:/bin', '/usr/bin:/bin:/usr/sbin:/sbin'), diff --git a/src/main/runtime/login-shell-path.ts b/src/main/runtime/login-shell-path.ts index 46009528..76ade52f 100644 --- a/src/main/runtime/login-shell-path.ts +++ b/src/main/runtime/login-shell-path.ts @@ -1,5 +1,6 @@ import { execFile } from 'node:child_process'; import os from 'node:os'; +import { isAbsolute } from 'node:path'; const MARKER = '__SUBMINER_LOGIN_PATH__'; const DEFAULT_TIMEOUT_MS = 5000; @@ -29,9 +30,10 @@ const runShellDefault: RunShell = (shell, args, timeoutMs) => }); function defaultShell(env: NodeJS.ProcessEnv): string { - if (env.SHELL) return env.SHELL; + if (env.SHELL && isAbsolute(env.SHELL)) return env.SHELL; try { - return os.userInfo().shell || '/bin/zsh'; + const shell = os.userInfo().shell; + return shell && isAbsolute(shell) ? shell : '/bin/zsh'; } catch { return '/bin/zsh'; } @@ -43,6 +45,7 @@ function defaultShell(env: NodeJS.ProcessEnv): string { */ export async function readLoginShellPath(options: LoginShellPathOptions): Promise { const shell = options.shell ?? defaultShell(options.env); + if (!isAbsolute(shell)) throw new Error('Login shell must be an absolute path'); const run = options.runShell ?? runShellDefault; // printenv keeps this shell-agnostic (fish exposes $PATH as a list). const command = `printf '%s' '${MARKER}'; /usr/bin/printenv PATH; printf '%s' '${MARKER}'`;