fix(core): fix PTY descriptor shell leak (#16773)

This commit is contained in:
Gal Zahavi
2026-01-16 09:55:29 -08:00
committed by GitHub
parent be37c26c88
commit 013a4e02ff
4 changed files with 171 additions and 5 deletions

View File

@@ -20,7 +20,9 @@ import {
initializeShellParsers,
stripShellWrapper,
hasRedirection,
resolveExecutable,
} from './shell-utils.js';
import path from 'node:path';
const mockPlatform = vi.hoisted(() => vi.fn());
const mockHomedir = vi.hoisted(() => vi.fn());
@@ -33,6 +35,20 @@ vi.mock('os', () => ({
homedir: mockHomedir,
}));
const mockAccess = vi.hoisted(() => vi.fn());
vi.mock('node:fs', () => ({
default: {
promises: {
access: mockAccess,
},
constants: { X_OK: 1 },
},
promises: {
access: mockAccess,
},
constants: { X_OK: 1 },
}));
const mockSpawnSync = vi.hoisted(() => vi.fn());
vi.mock('node:child_process', () => ({
spawnSync: mockSpawnSync,
@@ -463,3 +479,65 @@ describe('hasRedirection (PowerShell via mock)', () => {
expect(hasRedirection('echo "-> arrow"')).toBe(true);
});
});
describe('resolveExecutable', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
mockAccess.mockReset();
});
afterEach(() => {
process.env = originalEnv;
});
it('should return the absolute path if it exists and is executable', async () => {
const absPath = path.resolve('/usr/bin/git');
mockAccess.mockResolvedValue(undefined); // success
expect(await resolveExecutable(absPath)).toBe(absPath);
expect(mockAccess).toHaveBeenCalledWith(absPath, 1);
});
it('should return undefined for absolute path if it does not exist', async () => {
const absPath = path.resolve('/usr/bin/nonexistent');
mockAccess.mockRejectedValue(new Error('ENOENT'));
expect(await resolveExecutable(absPath)).toBeUndefined();
});
it('should resolve executable in PATH', async () => {
const binDir = path.resolve('/bin');
const usrBinDir = path.resolve('/usr/bin');
process.env['PATH'] = `${binDir}${path.delimiter}${usrBinDir}`;
mockPlatform.mockReturnValue('linux');
const targetPath = path.join(usrBinDir, 'ls');
mockAccess.mockImplementation(async (p: string) => {
if (p === targetPath) return undefined;
throw new Error('ENOENT');
});
expect(await resolveExecutable('ls')).toBe(targetPath);
});
it('should try extensions on Windows', async () => {
const sys32 = path.resolve('C:\\Windows\\System32');
process.env['PATH'] = sys32;
mockPlatform.mockReturnValue('win32');
mockAccess.mockImplementation(async (p: string) => {
// Use includes because on Windows path separators might differ
if (p.includes('cmd.exe')) return undefined;
throw new Error('ENOENT');
});
expect(await resolveExecutable('cmd')).toContain('cmd.exe');
});
it('should return undefined if not found in PATH', async () => {
process.env['PATH'] = path.resolve('/bin');
mockPlatform.mockReturnValue('linux');
mockAccess.mockRejectedValue(new Error('ENOENT'));
expect(await resolveExecutable('unknown')).toBeUndefined();
});
});