diff --git a/src/setup.ts b/src/setup.ts index bf1689794..440cf7ca0 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -94,10 +94,20 @@ export async function setup( // (SessionStart in particular) can spawn and snapshot process.env. if (feature('UDS_INBOX')) { const m = await import('./utils/udsMessaging.js') - await m.startUdsMessaging( - messagingSocketPath ?? m.getDefaultUdsSocketPath(), - { isExplicit: messagingSocketPath !== undefined }, - ) + try { + await m.startUdsMessaging( + messagingSocketPath ?? m.getDefaultUdsSocketPath(), + { isExplicit: messagingSocketPath !== undefined }, + ) + } catch (error) { + logError(error) + console.error( + chalk.red( + `Error: Failed to start messaging socket (UDS_INBOX): ${errorMessage(error)}`, + ), + ) + process.exit(1) + } } } diff --git a/src/utils/__tests__/udsMessaging.test.ts b/src/utils/__tests__/udsMessaging.test.ts index 2113a4400..8f66edecf 100644 --- a/src/utils/__tests__/udsMessaging.test.ts +++ b/src/utils/__tests__/udsMessaging.test.ts @@ -21,6 +21,8 @@ import { MAX_UDS_INBOX_BYTES, MAX_UDS_FRAME_BYTES, MAX_UDS_CLIENTS, + MAX_UNIX_SOCKET_PATH_LENGTH, + assertValidUnixSocketPath, formatUdsAddress, parseUdsTarget, sendUdsMessage, @@ -34,11 +36,23 @@ let previousConfigDir: string | undefined let tempConfigDir = '' function socketPath(label: string): string { - const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}-${label}` + const suffix = `${process.pid}-${Math.random().toString(16).slice(2)}-${label}` if (process.platform === 'win32') { return `\\\\.\\pipe\\claude-code-test-${suffix}` } - return join(tmpdir(), 'claude-code-test', `${suffix}.sock`) + const base = + process.platform === 'darwin' + ? '/tmp/claude-uds-test' + : join(tmpdir(), 'cc-uds-test') + return join(base, `${suffix}.sock`) +} + +function shortTestDir(prefix: string): string { + const id = `${process.pid}-${Math.random().toString(16).slice(2)}` + if (process.platform === 'darwin') { + return join('/tmp', `${prefix}-${id}`) + } + return join(tmpdir(), `${prefix}-${id}`) } function sleep(ms: number): Promise { @@ -499,6 +513,27 @@ describe('UDS inbox retention', () => { expect(getDefaultUdsSocketPath()).not.toBe(firstPath) }) + test('default socket path stays within AF_UNIX length limit', () => { + const path = getDefaultUdsSocketPath() + if (process.platform === 'win32') return + expect(Buffer.byteLength(path, 'utf8')).toBeLessThanOrEqual( + MAX_UNIX_SOCKET_PATH_LENGTH, + ) + expect(() => assertValidUnixSocketPath(path)).not.toThrow() + }) + + test('rejects socket paths longer than AF_UNIX limit', () => { + if (process.platform === 'win32') return + const longPath = `/tmp/${'x'.repeat(MAX_UNIX_SOCKET_PATH_LENGTH)}.sock` + expect(() => assertValidUnixSocketPath(longPath)).toThrow(/max 104/) + }) + + test('default socket path can bind on Node.js', async () => { + const path = getDefaultUdsSocketPath() + await startUdsMessaging(path, { isExplicit: true }) + await stopUdsMessaging() + }) + test('rejects oversized receiver responses before retaining them', async () => { const path = socketPath('oversized-response') if (process.platform !== 'win32') { @@ -688,10 +723,7 @@ describe('UDS inbox retention', () => { }) test('fails closed when an explicit socket parent is not private', async () => { - const parent = join( - tmpdir(), - `uds-socket-parent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`, - ) + const parent = shortTestDir('uds-sp') await mkdir(parent, { recursive: true, mode: 0o755 }) await chmod(parent, 0o755) @@ -707,10 +739,7 @@ describe('UDS inbox retention', () => { }) test('fails closed when an explicit socket parent is a file', async () => { - const parentFile = join( - tmpdir(), - `uds-socket-parent-file-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`, - ) + const parentFile = shortTestDir('uds-spf') await writeFile(parentFile, 'not a directory', 'utf-8') try { diff --git a/src/utils/udsMessaging.ts b/src/utils/udsMessaging.ts index 28de0ae13..049588fb8 100644 --- a/src/utils/udsMessaging.ts +++ b/src/utils/udsMessaging.ts @@ -85,13 +85,26 @@ export const MAX_UDS_CLIENTS = 128 export const UDS_AUTH_TIMEOUT_MS = 2_000 export const UDS_IDLE_TIMEOUT_MS = 30_000 +/** macOS/BSD AF_UNIX `sun_path` limit (bytes, excluding NUL). */ +export const MAX_UNIX_SOCKET_PATH_LENGTH = 104 + // --------------------------------------------------------------------------- // Public API — socket path helpers // --------------------------------------------------------------------------- +export function assertValidUnixSocketPath(path: string): void { + if (process.platform === 'win32') return + const byteLength = Buffer.byteLength(path, 'utf8') + if (byteLength > MAX_UNIX_SOCKET_PATH_LENGTH) { + throw new Error( + `[udsMessaging] socket path is ${byteLength} bytes (max ${MAX_UNIX_SOCKET_PATH_LENGTH}): ${path}`, + ) + } +} + /** - * Default socket path based on PID, placed in a tmpdir subdirectory so it - * survives across config-home changes and avoids polluting ~/.claude. + * Default socket path based on PID. Uses a flat file under a short temp + * directory so the path stays within the AF_UNIX limit on macOS. * * On Windows, Node.js requires named pipe paths in the `\\.\pipe\` namespace; * file-system paths like `C:\...\Temp\x.sock` cause EACCES. Bun handles both @@ -99,17 +112,19 @@ export const UDS_IDLE_TIMEOUT_MS = 30_000 */ export function getDefaultUdsSocketPath(): string { if (defaultSocketPath) return defaultSocketPath - const nonce = randomBytes(16).toString('hex') + const nonce = randomBytes(8).toString('hex') if (process.platform === 'win32') { defaultSocketPath = `\\\\.\\pipe\\claude-code-${process.pid}-${nonce}` return defaultSocketPath } + defaultSocketPath = join( tmpdir(), - 'claude-code-socks', + 'cc-socks', `${process.pid}-${nonce}`, 'messaging.sock', ) + assertValidUnixSocketPath(defaultSocketPath) return defaultSocketPath } @@ -416,6 +431,8 @@ export async function startUdsMessaging( return } + assertValidUnixSocketPath(path) + // Ensure parent directory exists (skip on Windows — pipe paths aren't files) if (process.platform !== 'win32') { await ensureSocketParent(path)