mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(sidecar): bound macOS private IPC socket paths (#8002)
Long macOS temporary-directory paths can push private sidecar sockets beyond the 103-byte pathname limit and crash startup. Fall back to /tmp only for oversized macOS paths, preserving the full digest and UID isolation. Add byte-boundary, identity, platform-compatibility, and native macOS IPC regression coverage.
This commit is contained in:
@@ -164,7 +164,10 @@ export function resolvePrivateIpcPath(stamp: SidecarStamp, platform: NodeJS.Plat
|
||||
})()
|
||||
: String(process.getuid?.() ?? process.env.USER ?? "unknown");
|
||||
const digest = createHash("sha256").update(`${principal}\n${sidecarStampKey(stamp)}`).digest("hex").slice(0, 32);
|
||||
return platform === "win32"
|
||||
? `\\\\.\\pipe\\open-design-sidecar-${digest}`
|
||||
: join(tmpdir(), `od-sidecar-${principal}`, `${digest}.sock`);
|
||||
if (platform === "win32") return `\\\\.\\pipe\\open-design-sidecar-${digest}`;
|
||||
const endpoint = join(tmpdir(), `od-sidecar-${principal}`, `${digest}.sock`);
|
||||
// macOS sun_path allows 103 path bytes plus a null terminator.
|
||||
return platform === 'darwin' && Buffer.byteLength(endpoint) > 103
|
||||
? join('/tmp', `od-sidecar-${principal}`, `${digest}.sock`)
|
||||
: endpoint;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { lstat } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { createJsonIpcServer, requestJsonIpc } from '../src/json-ipc.js';
|
||||
import { resolvePrivateIpcPath, SIDECAR_STAMP_FIELDS, type SidecarStamp } from '../src/stamp.js';
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const os = await importOriginal<typeof import('node:os')>();
|
||||
return { ...os, tmpdir: vi.fn(os.tmpdir) };
|
||||
});
|
||||
|
||||
const macTmpdir = '/var/folders/44/fmg2qx4n1wl13f0nrcjctpm4hjtjw_/T/';
|
||||
const stamp: SidecarStamp = {
|
||||
channel: 'stable',
|
||||
namespace: 'release-stable',
|
||||
source: 'packaged',
|
||||
mode: 'runtime',
|
||||
app: 'daemon',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(tmpdir).mockReturnValue(macTmpdir);
|
||||
vi.stubGlobal('process', { ...process, getuid: () => 555566986 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('private sidecar IPC paths', () => {
|
||||
it('fits the macOS socket limit with the reported long temp directory and UID', () => {
|
||||
const endpoint = resolvePrivateIpcPath(stamp, 'darwin');
|
||||
|
||||
expect(Buffer.byteLength(endpoint)).toBeLessThanOrEqual(103);
|
||||
expect(endpoint).toMatch(/[\\/]od-sidecar-555566986[\\/][a-f0-9]{32}\.sock$/);
|
||||
expect(resolvePrivateIpcPath(stamp, 'darwin')).toBe(endpoint);
|
||||
});
|
||||
|
||||
it('measures UTF-8 bytes rather than characters', () => {
|
||||
vi.mocked(tmpdir).mockReturnValue(`/tmp/${'\u00e9'.repeat(20)}`);
|
||||
|
||||
expect(Buffer.byteLength(resolvePrivateIpcPath(stamp, 'darwin'))).toBeLessThanOrEqual(103);
|
||||
});
|
||||
|
||||
it('preserves macOS endpoints that already fit', () => {
|
||||
vi.mocked(tmpdir).mockReturnValue('/tmp');
|
||||
const shortEndpoint = resolvePrivateIpcPath(stamp, 'darwin');
|
||||
vi.mocked(tmpdir).mockReturnValue('/var/tmp');
|
||||
|
||||
expect(resolvePrivateIpcPath(stamp, 'darwin')).toBe(join('/var', shortEndpoint));
|
||||
});
|
||||
|
||||
it('preserves a macOS endpoint at the 103-byte boundary', () => {
|
||||
const tempRoot = `/${'a'.repeat(43)}`;
|
||||
vi.mocked(tmpdir).mockReturnValue(tempRoot);
|
||||
const endpoint = resolvePrivateIpcPath(stamp, 'darwin');
|
||||
|
||||
expect(Buffer.byteLength(endpoint)).toBe(103);
|
||||
expect(dirname(endpoint)).toBe(join(tempRoot, 'od-sidecar-555566986'));
|
||||
});
|
||||
|
||||
it('keeps every stamp field and the OS user in the fallback identity', () => {
|
||||
const endpoint = resolvePrivateIpcPath(stamp, 'darwin');
|
||||
for (const field of SIDECAR_STAMP_FIELDS) {
|
||||
expect(resolvePrivateIpcPath({ ...stamp, [field]: 'other' }, 'darwin')).not.toBe(endpoint);
|
||||
}
|
||||
vi.stubGlobal('process', { ...process, getuid: () => 555566987 });
|
||||
expect(resolvePrivateIpcPath(stamp, 'darwin')).not.toBe(endpoint);
|
||||
});
|
||||
|
||||
it('preserves Linux socket paths', () => {
|
||||
vi.mocked(tmpdir).mockReturnValue('/var/tmp');
|
||||
|
||||
expect(dirname(resolvePrivateIpcPath(stamp, 'linux'))).toBe(join('/var/tmp', 'od-sidecar-555566986'));
|
||||
});
|
||||
|
||||
it('keeps Windows named pipes independent of the temporary directory', () => {
|
||||
const endpoint = resolvePrivateIpcPath(stamp, 'win32');
|
||||
vi.mocked(tmpdir).mockReturnValue('/tmp');
|
||||
|
||||
expect(endpoint).toMatch(/^\\\\\.\\pipe\\open-design-sidecar-[a-f0-9]{32}$/);
|
||||
expect(resolvePrivateIpcPath(stamp, 'win32')).toBe(endpoint);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== 'darwin')('binds private sockets for two macOS namespaces with a long TMPDIR', async () => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.mocked(tmpdir).mockReturnValue(`${macTmpdir}${'long'.repeat(30)}`);
|
||||
const endpoints = [0, 1].map(() => resolvePrivateIpcPath({ ...stamp, namespace: randomUUID() }));
|
||||
const servers = [];
|
||||
try {
|
||||
for (const [index, socketPath] of endpoints.entries()) {
|
||||
servers.push(await createJsonIpcServer({ socketPath, handler: () => index }));
|
||||
expect((await lstat(dirname(socketPath))).mode & 0o777).toBe(0o700);
|
||||
}
|
||||
for (const [index, endpoint] of endpoints.entries()) {
|
||||
expect(await requestJsonIpc(endpoint, { type: 'status' })).toBe(index);
|
||||
}
|
||||
} finally {
|
||||
await Promise.all(servers.map((server) => server.close()));
|
||||
}
|
||||
for (const endpoint of endpoints) {
|
||||
await expect(lstat(endpoint)).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user