From e76ea52ae3471e6c9efa1e5bc7ebb0f1ec89f6db Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:11:12 -0400 Subject: [PATCH] test(security): bind fixture reads and mutations to private descriptors --- tests/hooks/gateguard-fact-force.test.js | 7 ++- tests/lib/claude-plugin-setup.test.js | 51 +++++++++++++--------- tests/lib/install-executor.test.js | 21 +++++++-- tests/lib/install-stale-operations.test.js | 14 ++++-- 4 files changed, 64 insertions(+), 29 deletions(-) diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index d5232ffd5..1968cd5df 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -4666,9 +4666,12 @@ function runTests() { } for (const [rel, parts] of byFile) { const file = path.join(trivialRoot, rel); - if (fs.existsSync(file)) continue; fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(file, `${parts.join('\n')}\n`); + try { + fs.writeFileSync(file, `${parts.join('\n')}\n`, { flag: 'wx' }); + } catch (error) { + if (error.code !== 'EEXIST') throw error; + } } }; const trivialRun = (toolName, tool_input, env = {}) => { diff --git a/tests/lib/claude-plugin-setup.test.js b/tests/lib/claude-plugin-setup.test.js index f3715d3c5..f80014a45 100644 --- a/tests/lib/claude-plugin-setup.test.js +++ b/tests/lib/claude-plugin-setup.test.js @@ -648,29 +648,38 @@ test('dry-run snapshots local-scope inventory into isolated Claude and project r '.claude', 'settings.local.json' ); - assert.strictEqual(fs.lstatSync(shadowSettingsPath).isFile(), true); - const shadowInstalled = JSON.parse(fs.readFileSync( - path.join(runOptions.env.CLAUDE_CONFIG_DIR, 'plugins', 'installed_plugins.json'), - 'utf8' - )); - assert.strictEqual( - shadowInstalled.plugins['ecc@ecc'][0].projectPath, - runOptions.cwd + const shadowSettingsFd = fs.openSync( + shadowSettingsPath, + fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0) ); - assert.ok( - shadowInstalled.plugins['ecc@ecc'][0].installPath - .startsWith(runOptions.env.CLAUDE_CONFIG_DIR) - ); - fs.writeFileSync(shadowSettingsPath, '{"providerRead":true}\n'); - fs.mkdirSync(runOptions.env.XDG_DATA_HOME, { recursive: true }); - fs.writeFileSync( - path.join(runOptions.env.XDG_DATA_HOME, 'claude-provider-read.json'), - '{"providerRead":true}\n' - ); - if (args.join(' ') === 'plugin list --json') { - return { stdout: JSON.stringify([installedPlugin('local')]) }; + try { + assert.strictEqual(fs.fstatSync(shadowSettingsFd).isFile(), true); + const shadowInstalled = JSON.parse(fs.readFileSync( + path.join(runOptions.env.CLAUDE_CONFIG_DIR, 'plugins', 'installed_plugins.json'), + 'utf8' + )); + assert.strictEqual( + shadowInstalled.plugins['ecc@ecc'][0].projectPath, + runOptions.cwd + ); + assert.ok( + shadowInstalled.plugins['ecc@ecc'][0].installPath + .startsWith(runOptions.env.CLAUDE_CONFIG_DIR) + ); + fs.ftruncateSync(shadowSettingsFd, 0); + fs.writeFileSync(shadowSettingsFd, '{"providerRead":true}\n'); + fs.mkdirSync(runOptions.env.XDG_DATA_HOME, { recursive: true }); + fs.writeFileSync( + path.join(runOptions.env.XDG_DATA_HOME, 'claude-provider-read.json'), + '{"providerRead":true}\n' + ); + if (args.join(' ') === 'plugin list --json') { + return { stdout: JSON.stringify([installedPlugin('local')]) }; + } + return { stdout: JSON.stringify([officialMarketplace('local')]) }; + } finally { + fs.closeSync(shadowSettingsFd); } - return { stdout: JSON.stringify([officialMarketplace('local')]) }; }; const result = setupClaudePlugin( diff --git a/tests/lib/install-executor.test.js b/tests/lib/install-executor.test.js index a47517eec..f2faf477a 100644 --- a/tests/lib/install-executor.test.js +++ b/tests/lib/install-executor.test.js @@ -903,15 +903,30 @@ function runTests() { assert.strictEqual(fs.readFileSync(userPath, 'utf8'), 'Keep my settings.\n'); } - const modifiedContent = `${fs.readFileSync(toolPath, 'utf8')}\n// User test customisation.\n`; - fs.writeFileSync(toolPath, modifiedContent, 'utf8'); + const toolFd = fs.openSync(toolPath, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0)); + let modifiedContent; + try { + assert.ok(fs.fstatSync(toolFd).isFile()); + modifiedContent = `${fs.readFileSync(toolFd, 'utf8')}\n// User test customisation.\n`; + const modifiedBytes = Buffer.from(modifiedContent); + assert.strictEqual(fs.writeSync(toolFd, modifiedBytes, 0, modifiedBytes.length, 0), modifiedBytes.length); + fs.ftruncateSync(toolFd, modifiedBytes.length); + } finally { + fs.closeSync(toolFd); + } const uninstalled = uninstallInstalledStates(lifecycleOptions); assert.strictEqual(uninstalled.results.length, 1); assert.strictEqual(uninstalled.results[0].status, 'partial', JSON.stringify(uninstalled)); assert.ok(uninstalled.results[0].removedPaths.includes(barrelPath)); assert.ok(!fs.existsSync(barrelPath), 'both legacy and repaired managed barrels are recognised'); assert.deepStrictEqual(uninstalled.results[0].retainedPaths, [toolPath]); - assert.strictEqual(fs.readFileSync(toolPath, 'utf8'), modifiedContent); + const retainedFd = fs.openSync(toolPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); + try { + assert.ok(fs.fstatSync(retainedFd).isFile()); + assert.strictEqual(fs.readFileSync(retainedFd, 'utf8'), modifiedContent); + } finally { + fs.closeSync(retainedFd); + } assert.strictEqual(fs.readFileSync(userPath, 'utf8'), 'Keep my settings.\n'); assert.ok(fs.existsSync(plan.installStatePath), 'modified managed content retains its ownership record'); } diff --git a/tests/lib/install-stale-operations.test.js b/tests/lib/install-stale-operations.test.js index 9714a03d9..9231751bf 100644 --- a/tests/lib/install-stale-operations.test.js +++ b/tests/lib/install-stale-operations.test.js @@ -185,8 +185,8 @@ for (const phase of ['during hashing', 'after hashing']) { const edited = 'user edit bytes\n'; assert.strictEqual(original.length, edited.length); writeFile(file, original); - // Bind the fixture identity to its opened private file, keeping the later - // pathname read an assertion rather than relying on a prior stat check. + // Bind the fixture identity to its opened private file. Validate the + // retained pathname through a fresh descriptor after the injected race. const identityFd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); let identity; try { identity = fs.fstatSync(identityFd, { bigint: true }); } @@ -224,7 +224,15 @@ for (const phase of ['during hashing', 'after hashing']) { fs.fstatSync = originalFstat; } assert.ok(mutationApplied, 'The equal-length mutation must actually execute'); - assert.strictEqual(fs.readFileSync(file, 'utf8'), edited); + const retainedFd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); + try { + const retainedIdentity = fs.fstatSync(retainedFd, { bigint: true }); + assert.ok(retainedIdentity.isFile()); + assert.ok(matches(retainedIdentity), 'The mutated fixture inode must remain at the retained path'); + assert.strictEqual(fs.readFileSync(retainedFd, 'utf8'), edited); + } finally { + fs.closeSync(retainedFd); + } assert.deepStrictEqual(result.removedPaths, []); assert.strictEqual(result.warnings.length, 1); assert.match(result.warnings[0], /changed while being verified/);