fix(wsl): harden shell identity and PTY teardown

This commit is contained in:
Merge Sim
2026-08-31 16:59:50 -07:00
parent 2d5f0a47f9
commit cccd4c36cf
15 changed files with 256 additions and 21 deletions
@@ -2,7 +2,34 @@
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
@@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES
builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}"
builtin unset ORCA_HISTFILE
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
__orca_osc133_precmd() {
local exit_code=$?
if [[ -n "${__orca_in_command:-}" ]]; then
@@ -2,7 +2,34 @@
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
@@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES
builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}"
builtin unset ORCA_HISTFILE
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
__orca_osc133_precmd() {
local exit_code=$?
if [[ -n "${__orca_in_command:-}" ]]; then
@@ -2,7 +2,34 @@
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
@@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES
builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}"
builtin unset ORCA_HISTFILE
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
if __orca_has_feature identity; then
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=${_orca_stat##*) }
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac
case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac
case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac
case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac
printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty"
}
__orca_emit_shell_identity
unset -f __orca_emit_shell_identity
else
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
fi
__orca_deferred_init() {
# Why first: this body runs after the user's own config, so it would otherwise
# inherit whatever options that config left set. Under NO_UNSET an unset
@@ -57,6 +57,7 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti
const distros = new Set(
result.sessions
.filter((session) => session.isAlive && session.wslDistro)
.filter((session) => session.wslShellAnchor)
.map((session) => session.wslDistro as string)
)
await Promise.all(
@@ -1,5 +1,6 @@
import {
createShellStartupIdentityScanState,
drainShellStartupIdentityHeldBytes,
scanForShellStartupIdentity,
type ShellStartupIdentityScanState
} from '../shell-startup-identity-scanner'
@@ -17,6 +18,22 @@ export class SessionWslShellAnchorTracker {
return this._anchor
}
drainHeldBytes(): string {
if (!this.scanState) {
return ''
}
const held = drainShellStartupIdentityHeldBytes(this.scanState)
this.scanState = null
return held
}
drainInto(accept: (data: string) => void): void {
const held = this.drainHeldBytes()
if (held.length > 0) {
accept(held)
}
}
scan(data: string): string {
if (!this.scanState) {
return data
+12
View File
@@ -184,6 +184,18 @@ describe('Session', () => {
})
})
it('drains a partial WSL identity marker when the subprocess exits', () => {
createSession({ wslDistro: 'Ubuntu' })
const received: string[] = []
session.attachClient({ onData: (data) => received.push(data), onExit: () => {} })
const partial = '\x1b]777;orca-shell-start:v2:Ubuntu:01234567'
subprocess.simulateData(partial)
subprocess.simulateExit(0)
expect(received.join('')).toContain(partial)
})
it('does not confirm shell ownership from historical replay bytes', () => {
createSession({
historySeedChunks: ['\x1b[?1049hOLD-TUI\x1b]133;D;137\x07old-shell-marker']
+2
View File
@@ -291,6 +291,7 @@ export class Session {
// Why: `wasTerminating` below must be read BEFORE the `_state = 'exited'` flip — it guards the
// "dispose while kill() in flight" case and the invariant needs the pre-flip `_state`; do NOT move it down.
this.shellReady.releaseDeviceAttributes()
this.wslShellAnchorTracker.drainInto((data) => this.shellReady.ingestSubprocessData(data))
this.shellReady.releaseHeldBytes()
this.startupIngress.drainAndClose()
// Why after drainAndClose (and before clearClients below): a dispose
@@ -362,6 +363,7 @@ export class Session {
this.shellReady.releaseDeviceAttributes()
this.shellReady.disposePromptReadinessProbe()
this.wslShellAnchorTracker.drainInto((data) => this.shellReady.ingestSubprocessData(data))
this.shellReady.releaseHeldBytes()
this.startupIngress.drainAndClose()
// Why after drainAndClose: drained ingress bytes re-enter the barrier and can
@@ -36,6 +36,7 @@ import { destroyPtyProcess, createPtyPhysicalExit } from './local-pty-terminatio
import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready-startup-command'
import {
createShellStartupIdentityScanState,
drainShellStartupIdentityHeldBytes,
scanForShellStartupIdentity,
type ShellStartupIdentityScanState
} from '../shell-startup-identity-scanner'
@@ -165,6 +166,15 @@ export function activateLocalPtySession(args: {
if (process.platform !== 'win32') {
;(proc as unknown as { kill: (sig?: string) => void }).kill = () => {}
}
// If the PTY dies mid-marker, return the withheld OSC bytes to the normal
// ingress path so teardown cannot silently eat user-visible output.
if (wslIdentityScanState) {
const heldIdentityBytes = drainShellStartupIdentityHeldBytes(wslIdentityScanState)
wslIdentityScanState = null
if (heldIdentityBytes.length > 0) {
readiness.acceptData(heldIdentityBytes)
}
}
readiness.prepareForExit()
clearPtyState(id)
startupIngress.drainAndClose()
@@ -128,7 +128,9 @@ export async function listLocalPtyProcesses(): Promise<PtyProcessInfo[]> {
const wslByDistro = new Map<string, string[]>()
for (const [id] of entries) {
const distro = ptyWslDistroById.get(id)
if (distro) {
// Without a shell anchor the inventory cannot resolve a foreground process;
// avoid paying for a guest scan whose verdict is necessarily unverifiable.
if (distro && ptyWslShellAnchors.has(id)) {
const ids = wslByDistro.get(distro) ?? []
ids.push(id)
wslByDistro.set(distro, ids)
@@ -141,7 +143,12 @@ export async function listLocalPtyProcesses(): Promise<PtyProcessInfo[]> {
})
)
return entries.map(([id, proc]) => {
return entries.flatMap(([id, proc]) => {
// Inventory reads are asynchronous; a PTY may have exited while they ran.
// Do not publish a row for an incarnation that is no longer authoritative.
if (ptyProcesses.get(id) !== proc) {
return []
}
const distro = ptyWslDistroById.get(id)
let title = proc.process || ptyShellName.get(id) || 'shell'
let foregroundProcessEvidence: ForegroundProcessEvidence | undefined
@@ -178,16 +185,18 @@ export async function listLocalPtyProcesses(): Promise<PtyProcessInfo[]> {
}
}
}
return {
id,
...(ptyIncarnations.get(id) ? { incarnationId: ptyIncarnations.get(id) } : {}),
cwd: ptyInitialCwd.get(id) ?? '',
title,
...(ptyWorktreeId.get(id) ? { worktreeId: ptyWorktreeId.get(id) } : {}),
...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {}),
...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}),
...(foregroundProcessEvidence ? { foregroundProcessEvidence } : {})
}
return [
{
id,
...(ptyIncarnations.get(id) ? { incarnationId: ptyIncarnations.get(id) } : {}),
cwd: ptyInitialCwd.get(id) ?? '',
title,
...(ptyWorktreeId.get(id) ? { worktreeId: ptyWorktreeId.get(id) } : {}),
...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {}),
...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}),
...(foregroundProcessEvidence ? { foregroundProcessEvidence } : {})
}
]
})
}
+10
View File
@@ -16,6 +16,16 @@ describe('addOrcaWslInteropEnv', () => {
expect(env.WSLENV).toBe('ORCA_TERMINAL_HANDLE/u:ORCA_SHELL_READY_ROOT/p:ORCA_SHELL_FEATURES/u')
})
it('drops unknown inherited shell features before adding canonical WSL features', () => {
const env: Record<string, string> = {
ORCA_SHELL_FEATURES: 'overlay,evil, markers,overlay, identity '
}
addOrcaWslInteropEnv(env)
expect(env.ORCA_SHELL_FEATURES).toBe('overlay,markers,identity')
})
// Why this is published at all: the wrapper tree is content-addressed, so the
// in-guest login script cannot rebuild its path from ORCA_USER_DATA_PATH -- it
// cannot derive the hash segment. Without this the guest finds no wrapper and
+8 -1
View File
@@ -9,6 +9,7 @@ import {
SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV
} from '../../shared/setup-agent-sequencing'
import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root'
import { SHELL_STARTUP_FEATURES, type ShellStartupFeature } from '../shell-startup-features'
const WSLENV_ENTRY_SEPARATOR = ':'
@@ -61,7 +62,13 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
// WSL's host-side process is always wsl.exe. Ask the guest wrapper to emit
// its shell identity marker so process evidence can anchor to the guest PID.
// The feature selection crosses through WSLENV, never the wsl.exe argv.
const existingFeatures = env.ORCA_SHELL_FEATURES?.split(',').filter(Boolean) ?? []
// A WSL pane can inherit this channel from an Orca shell launched earlier;
// retain only canonical feature names before adding this launch's overlay.
const existingFeatures = (env.ORCA_SHELL_FEATURES?.split(',') ?? [])
.map((feature) => feature.trim())
.filter((feature): feature is ShellStartupFeature =>
(SHELL_STARTUP_FEATURES as readonly string[]).includes(feature)
)
const overlayFeatures = [
'ORCA_OPENCODE_CONFIG_DIR',
'ORCA_MIMOCODE_HOME',
+7 -2
View File
@@ -45,11 +45,16 @@ export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `if __orca_has_feature identi
if [ -n "\${WSL_DISTRO_NAME:-}" ]; then
__orca_emit_shell_identity() {
local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro
local IFS=' '
_orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0
_orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0
_orca_tail=\${_orca_stat##*) }
set -- $_orca_tail
_orca_start=\${20:-}
# zsh does not perform implicit word splitting for unquoted parameters;
# read the proc-stat fields explicitly so field 22 (starttime) is parsed
# correctly in both zsh and bash.
builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest <<EOF
$_orca_tail
EOF
_orca_tty=$(tty 2>/dev/null) || return 0
_orca_distro=\${WSL_DISTRO_NAME:-}
case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac