mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(daemon): do not read a stranded login wrapper as a hosted terminal
Raised by a colleague's handoff on the same-day reports. macOS wraps every terminal in /usr/bin/login for TCC attribution, and #13764 shows the wrapper can outlive the shell it wrapped — leaving a session leader that hosts nothing. One affected host had accumulated enough of them to reach swap pressure. That is exactly the evidence this change treats as proof of live work, so a daemon whose sessions had all ended would have been held indefinitely on the strength of the corpses, on precisely the hosts where the problem is worst. Same class as the daemon's own probe PTYs: a session leader is necessary evidence, not sufficient. A wrapper still doing its job has the shell it exec'd beneath it. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -163,6 +163,35 @@ describe('inspectDaemonPtyOwnership on POSIX', () => {
|
||||
).resolves.toBe('owns-live-ptys')
|
||||
})
|
||||
|
||||
it('ignores a login wrapper stranded without its shell (#13764)', async () => {
|
||||
// Why: the macOS TCC wrapper can outlive the shell it wrapped, leaving a session leader
|
||||
// hosting nothing. On hosts where those accumulate, counting them would hold a daemon
|
||||
// whose sessions have all ended — indefinitely, and for no live work.
|
||||
await expect(
|
||||
inspectDaemonPtyOwnership(DAEMON_PID, {
|
||||
platform: 'darwin',
|
||||
readPosixProcessTable: posixTable([
|
||||
daemonRow,
|
||||
row(101, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' }),
|
||||
row(102, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' })
|
||||
])
|
||||
})
|
||||
).resolves.toBe('no-live-ptys')
|
||||
})
|
||||
|
||||
it('still counts a login wrapper that has its shell', async () => {
|
||||
await expect(
|
||||
inspectDaemonPtyOwnership(DAEMON_PID, {
|
||||
platform: 'darwin',
|
||||
readPosixProcessTable: posixTable([
|
||||
daemonRow,
|
||||
row(101, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' }),
|
||||
row(202, 101, { command: '/opt/homebrew/bin/bash --rcfile …' })
|
||||
])
|
||||
})
|
||||
).resolves.toBe('owns-live-ptys')
|
||||
})
|
||||
|
||||
it('reports unknown when the table never contained the daemon', async () => {
|
||||
// Why: an unobserved root yields the same empty result as a childless one —
|
||||
// reading that as "empty" authorizes killing a daemon full of live agents.
|
||||
|
||||
@@ -49,6 +49,9 @@ export const WINDOWS_OWNERSHIP_PROBE_DEADLINE_MS = 6_000
|
||||
*/
|
||||
export const POSIX_OWNERSHIP_PROBE_DEADLINE_MS = 4_000
|
||||
|
||||
/** macos-tcc-login-shell.ts wraps every darwin terminal in this. */
|
||||
const MACOS_LOGIN_WRAPPER_PREFIX = '/usr/bin/login '
|
||||
|
||||
function withDeadline<T>(work: Promise<T>, deadlineMs: number, onDeadline: T): Promise<T> {
|
||||
return new Promise<T>((resolve) => {
|
||||
const timer = setTimeout(() => resolve(onDeadline), deadlineMs)
|
||||
@@ -111,6 +114,16 @@ function isLivePtySessionLeader(row: ProcessTableRow): boolean {
|
||||
return !row.stat.startsWith('Z') && row.stat.includes('s') && !isDaemonSelfSpawnedPty(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS wraps every terminal in `/usr/bin/login` for TCC attribution, and the wrapper can
|
||||
* outlive the shell it wrapped (#13764) — a session leader hosting nothing. Counting those
|
||||
* would hold a daemon whose sessions have all ended, on hosts where they accumulate by the
|
||||
* hundred. A wrapper still doing its job always has the shell it exec'd beneath it.
|
||||
*/
|
||||
function isStrandedLoginWrapper(row: ProcessTableRow, hasChildren: boolean): boolean {
|
||||
return !hasChildren && row.command.trim().startsWith(MACOS_LOGIN_WRAPPER_PREFIX)
|
||||
}
|
||||
|
||||
function collectLivePtyDescendants(rows: ProcessTableRow[], rootPid: number): ProcessTableRow[] {
|
||||
const childrenByPpid = new Map<number, ProcessTableRow[]>()
|
||||
for (const row of rows) {
|
||||
@@ -135,7 +148,10 @@ function collectLivePtyDescendants(rows: ProcessTableRow[], rootPid: number): Pr
|
||||
}
|
||||
visited.add(child.pid)
|
||||
queue.push(child.pid)
|
||||
if (isLivePtySessionLeader(child)) {
|
||||
if (
|
||||
isLivePtySessionLeader(child) &&
|
||||
!isStrandedLoginWrapper(child, childrenByPpid.has(child.pid))
|
||||
) {
|
||||
live.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user