Files
orca/src/main/ssh/ssh-relay-reset.test.ts
T
e7f785f0ac fix(ssh): AND lsof selectors so relay reset can't kill unrelated processes (#8849)
* fix(ssh): AND lsof selectors so relay reset can't kill unrelated processes

`forceStopRelayForTarget` resolved PIDs with `lsof -t -U "$sock"`.
lsof ORs its selectors by default, so this selects every process holding
ANY unix socket in addition to the socket's owner. On hosts where lsof
cannot match AF_UNIX sockets by path, the path term matches nothing and
the sweep degrades to all unix-socket holders — including systemd --user
— which the TERM+KILL loop then takes down (#8762).

Add `-a` to AND the unix-socket and path selectors: where lsof supports
path matching the result is exactly the socket's holders (verified: 1
PID with -a vs 97 without on macOS); where it doesn't, the result is
empty and the existing pgrep fallback — scoped to command lines that
reference the relay's per-instance socket name — takes over.

Fixes #8762

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: add relay reset process-safety coverage

Exercise the generated POSIX reset script with controlled lsof and pgrep commands plus a mocked kill, and keep the selector rationale concise.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-15 13:55:48 -07:00

159 lines
4.8 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createServer, type Server } from 'node:net'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('./ssh-relay-deploy-helpers', () => ({
execCommand: vi.fn().mockResolvedValue('')
}))
import { forceStopRelayForTarget } from './ssh-relay-reset'
import { execCommand } from './ssh-relay-deploy-helpers'
import { relaySocketNameForInstanceId } from './ssh-relay-instance-id'
import type { SshConnection } from './ssh-connection'
const TARGET_PID = '11111'
const UNRELATED_PID = '22222'
type LsofMode = 'match' | 'empty'
function writeExecutable(filePath: string, body: string): void {
writeFileSync(filePath, `#!/bin/sh\n${body}\n`, { mode: 0o755 })
}
async function listenOnSocket(server: Server, socketPath: string): Promise<void> {
await new Promise<void>((resolve, reject) => {
const onError = (error: Error): void => reject(error)
server.once('error', onError)
server.listen(socketPath, () => {
server.off('error', onError)
resolve()
})
})
}
async function closeServer(server: Server): Promise<void> {
if (!server.listening) {
return
}
await new Promise<void>((resolve) => server.close(() => resolve()))
}
async function capturedResetScript(): Promise<string> {
const conn = {} as SshConnection
await forceStopRelayForTarget(conn, 'ssh-1')
return vi.mocked(execCommand).mock.lastCall?.[1] ?? ''
}
async function runResetScript(lsofMode: LsofMode): Promise<{
killCalls: string[]
pgrepCalls: string[]
socketExists: boolean
}> {
const script = await capturedResetScript()
const home = mkdtempSync(join(tmpdir(), 'orca-'))
const binDir = join(home, 'bin')
const socketDir = join(home, '.orca-remote')
const socketPath = join(socketDir, relaySocketNameForInstanceId('ssh-1'))
const killLog = join(home, 'kill.log')
const pgrepLog = join(home, 'pgrep.log')
mkdirSync(binDir)
mkdirSync(socketDir, { recursive: true })
writeFileSync(killLog, '')
writeFileSync(pgrepLog, '')
const lsofBody =
lsofMode === 'empty'
? 'exit 1'
: `case " $* " in
*" -a "*) printf '%s\\n' "$TARGET_PID" ;;
*) printf '%s\\n' "$TARGET_PID" "$UNRELATED_PID" ;;
esac`
writeExecutable(join(binDir, 'lsof'), lsofBody)
writeExecutable(
join(binDir, 'pgrep'),
`printf 'called\\n' >> "$PGREP_LOG"
printf '%s\\n' "$TARGET_PID"`
)
writeExecutable(join(binDir, 'sleep'), 'exit 0')
const server = createServer()
try {
await listenOnSocket(server, socketPath)
execFileSync(
'/bin/sh',
[
'-c',
`kill() {
printf '%s\\n' "$*" >> "$KILL_LOG"
}
eval "$RESET_SCRIPT"`
],
{
env: {
...process.env,
HOME: home,
KILL_LOG: killLog,
PATH: `${binDir}${delimiter}${process.env.PATH ?? ''}`,
PGREP_LOG: pgrepLog,
RESET_SCRIPT: script,
TARGET_PID,
UNRELATED_PID
}
}
)
return {
killCalls: readFileSync(killLog, 'utf8').split('\n').filter(Boolean),
pgrepCalls: readFileSync(pgrepLog, 'utf8').split('\n').filter(Boolean),
socketExists: existsSync(socketPath)
}
} finally {
await closeServer(server)
rmSync(home, { recursive: true, force: true })
}
}
describe('forceStopRelayForTarget', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('targets only the relay socket for the requested SSH target', async () => {
const conn = {} as SshConnection
await forceStopRelayForTarget(conn, 'ssh-1')
const command = vi.mocked(execCommand).mock.calls[0]?.[1] ?? ''
expect(execCommand).toHaveBeenCalledWith(conn, expect.any(String))
expect(command).toContain(`sock_name='${relaySocketNameForInstanceId('ssh-1')}'`)
expect(command).toContain('lsof -t -a -U "$sock"')
expect(command).toContain('pgrep -f "$sock_name"')
expect(command).toContain('rm -f "$sock"')
})
it.skipIf(process.platform === 'win32')(
'never passes unrelated unix-socket holders to kill',
async () => {
const result = await runResetScript('match')
expect(result.killCalls).toEqual([`-TERM ${TARGET_PID}`, `-KILL ${TARGET_PID}`])
expect(result.killCalls.join(' ')).not.toContain(UNRELATED_PID)
expect(result.pgrepCalls).toEqual([])
expect(result.socketExists).toBe(false)
}
)
it.skipIf(process.platform === 'win32')(
'uses the command-line fallback when lsof cannot match the socket',
async () => {
const result = await runResetScript('empty')
expect(result.killCalls).toEqual([`-TERM ${TARGET_PID}`, `-KILL ${TARGET_PID}`])
expect(result.pgrepCalls).toEqual(['called'])
expect(result.socketExists).toBe(false)
}
)
})