From 759caf101adf584bbedc5fb7c95b0d4f620d6bb9 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 May 2026 07:17:50 +0800 Subject: [PATCH 1/4] Use owner-only permissions for IPC files --- src/session.ts | 21 ++++++++++++++++----- src/test/suite/session.test.ts | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/session.ts b/src/session.ts index a796609d..361728e0 100644 --- a/src/session.ts +++ b/src/session.ts @@ -299,6 +299,14 @@ async function updateActiveTerminalFiles(pipePath: string) { } } +async function setOwnerOnlyPermissions(filePath: string): Promise { + if (process.platform === 'win32') { + return; + } + + await fs.chmod(filePath, 0o600); +} + function makePipePath(): string { const suffix = crypto.randomBytes(8).toString('hex'); if (process.platform === 'win32') { @@ -381,10 +389,12 @@ export async function getGlobalPipePath(): Promise { }); server.listen(pipePath, () => { - globalPipePath = pipePath; - globalSessionServer = server; - console.info(`[SessionServer] Listening on ${pipePath}`); - resolve(pipePath); + void setOwnerOnlyPermissions(pipePath).then(() => { + globalPipePath = pipePath; + globalSessionServer = server; + console.info(`[SessionServer] Listening on ${pipePath}`); + resolve(pipePath); + }).catch(reject); }); }); } @@ -429,7 +439,8 @@ export async function getAttachSessionCommand(): Promise { const sessPath = extensionContext.asAbsolutePath('sess').replace(/\\/g, '/'); const installSessScriptPath = extensionContext.asAbsolutePath(path.join('R', 'install_sess.R')).replace(/\\/g, '/'); const scriptPath = getAttachSessionScriptPath(pipePath); - await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath, installSessScriptPath), { encoding: 'utf-8' }); + await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath, installSessScriptPath), { encoding: 'utf-8', mode: 0o600 }); + await setOwnerOnlyPermissions(scriptPath); attachSessionScriptPath = scriptPath; return `source(${asRStringLiteral(scriptPath)})`; diff --git a/src/test/suite/session.test.ts b/src/test/suite/session.test.ts index a66b706f..87291e6f 100644 --- a/src/test/suite/session.test.ts +++ b/src/test/suite/session.test.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as sinon from 'sinon'; import * as assert from 'assert'; import * as path from 'path'; +import * as fs from 'fs-extra'; import { mockExtensionContext } from '../common/mockvscode'; import * as rTerminal from '../../rTerminal'; @@ -219,4 +220,24 @@ suite('Session Communication', () => { assert.ok(createWebviewPanelSpy.calledWith('webview'), 'webview should be triggered for html file'); }).timeout(45000); + + test('attach session artifacts are owner-only', async () => { + const command = await session.getAttachSessionCommand(); + const commandMatch = command.match(/^source\((.*)\)$/); + assert.ok(commandMatch, 'attach command should be a source(...) call'); + + const scriptPath = JSON.parse(commandMatch![1]); + const scriptStat = await fs.stat(scriptPath); + assert.strictEqual(scriptStat.mode & 0o777, 0o600, 'attach script should be owner-only'); + + const pipePath = session.globalPipePath; + assert.ok(pipePath, 'global pipe path should be set'); + + if (pipePath && process.platform !== 'win32') { + const pipeStat = await fs.stat(pipePath); + assert.strictEqual(pipeStat.mode & 0o777, 0o600, 'socket file should be owner-only'); + } + + await session.shutdownSessionWatcher(); + }).timeout(15000); }); From 661624786caa9718497dcf02462f3eadfa248ecb Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 May 2026 07:21:17 +0800 Subject: [PATCH 2/4] Apply to session files --- src/session.ts | 1 + src/test/suite/session.test.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/session.ts b/src/session.ts index 361728e0..470f3254 100644 --- a/src/session.ts +++ b/src/session.ts @@ -285,6 +285,7 @@ export async function writeSessionFile(pid: string, pipePath: string) { await fs.ensureDir(sessionsDir); const filePath = path.join(sessionsDir, `${pid}.json`); await fs.writeJson(filePath, { pipe: pipePath }); + await setOwnerOnlyPermissions(filePath); } async function updateActiveTerminalFiles(pipePath: string) { diff --git a/src/test/suite/session.test.ts b/src/test/suite/session.test.ts index 87291e6f..4ff8d26c 100644 --- a/src/test/suite/session.test.ts +++ b/src/test/suite/session.test.ts @@ -238,6 +238,13 @@ suite('Session Communication', () => { assert.strictEqual(pipeStat.mode & 0o777, 0o600, 'socket file should be owner-only'); } + const sessionFilePid = `perm-test-${process.pid}`; + await session.writeSessionFile(sessionFilePid, pipePath ?? ''); + const sessionFilePath = path.join(process.env.HOME ?? '', '.vscode-R', 'sessions', `${sessionFilePid}.json`); + const sessionFileStat = await fs.stat(sessionFilePath); + assert.strictEqual(sessionFileStat.mode & 0o777, 0o600, 'session handoff file should be owner-only'); + await fs.remove(sessionFilePath); + await session.shutdownSessionWatcher(); }).timeout(15000); }); From 0ccd38368c3ddf4a7b16fe0e641ed35bc5a2e30b Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 May 2026 07:25:23 +0800 Subject: [PATCH 3/4] Fix linting errors --- src/test/suite/session.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/suite/session.test.ts b/src/test/suite/session.test.ts index 4ff8d26c..7b0e8547 100644 --- a/src/test/suite/session.test.ts +++ b/src/test/suite/session.test.ts @@ -224,9 +224,11 @@ suite('Session Communication', () => { test('attach session artifacts are owner-only', async () => { const command = await session.getAttachSessionCommand(); const commandMatch = command.match(/^source\((.*)\)$/); - assert.ok(commandMatch, 'attach command should be a source(...) call'); + if (!commandMatch) { + throw new Error('attach command should be a source(...) call'); + } - const scriptPath = JSON.parse(commandMatch![1]); + const scriptPath = commandMatch[1].slice(1, -1); const scriptStat = await fs.stat(scriptPath); assert.strictEqual(scriptStat.mode & 0o777, 0o600, 'attach script should be owner-only'); From a27d274d74cfc4b04e81d13af7243fea63956677 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 May 2026 07:30:13 +0800 Subject: [PATCH 4/4] Update test --- src/test/suite/session.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/test/suite/session.test.ts b/src/test/suite/session.test.ts index 7b0e8547..61971588 100644 --- a/src/test/suite/session.test.ts +++ b/src/test/suite/session.test.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as sinon from 'sinon'; import * as assert from 'assert'; import * as path from 'path'; +import * as os from 'os'; import * as fs from 'fs-extra'; import { mockExtensionContext } from '../common/mockvscode'; @@ -228,9 +229,11 @@ suite('Session Communication', () => { throw new Error('attach command should be a source(...) call'); } - const scriptPath = commandMatch[1].slice(1, -1); + const scriptPath = JSON.parse(commandMatch[1]) as string; const scriptStat = await fs.stat(scriptPath); - assert.strictEqual(scriptStat.mode & 0o777, 0o600, 'attach script should be owner-only'); + if (process.platform !== 'win32') { + assert.strictEqual(scriptStat.mode & 0o777, 0o600, 'attach script should be owner-only'); + } const pipePath = session.globalPipePath; assert.ok(pipePath, 'global pipe path should be set'); @@ -242,9 +245,11 @@ suite('Session Communication', () => { const sessionFilePid = `perm-test-${process.pid}`; await session.writeSessionFile(sessionFilePid, pipePath ?? ''); - const sessionFilePath = path.join(process.env.HOME ?? '', '.vscode-R', 'sessions', `${sessionFilePid}.json`); + const sessionFilePath = path.join(os.homedir(), '.vscode-R', 'sessions', `${sessionFilePid}.json`); const sessionFileStat = await fs.stat(sessionFilePath); - assert.strictEqual(sessionFileStat.mode & 0o777, 0o600, 'session handoff file should be owner-only'); + if (process.platform !== 'win32') { + assert.strictEqual(sessionFileStat.mode & 0o777, 0o600, 'session handoff file should be owner-only'); + } await fs.remove(sessionFilePath); await session.shutdownSessionWatcher();