From 134912a529122c8803d42fbefc4adb2bd177f986 Mon Sep 17 00:00:00 2001 From: otaconix Date: Wed, 9 Sep 2026 08:20:21 +0200 Subject: [PATCH] Fix import-safe checks when scripts are run from a path with symlinks (#1265) * Fix import-safe checks when scripts are run from a path with symlinks In v6, setup-java was made "import-safe" to facilitate testing. This prevents setup-java & cleanup-java from doing anything when their sources get imported. This works fine in the general case, but actually invoking the script (`node setup-java/index.js`) when the path to the script contains symlinks led to the script incorrectly believing it was imported, and refuse to actually run. To fix this, we pass `process.argv[1]` through `fs.realpathSync`, which resolves symlinks in the path. Fixes #1264 * Preserve import safety when resolving symlink entrypoints Share entrypoint detection between setup and cleanup, handle non-file entrypoints safely, and normalize both paths for preserved symlinks. Add real-process regression coverage and rebuild action bundles. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 831c32f2-a275-45bd-a92b-c387372a1554 * Update js-yaml to fix merge-source denial of service Bump the transitive development dependency from 3.15.1 to 3.15.2 to address GHSA-2883-xcg3-v3hh without changing dependency ranges or CI checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 831c32f2-a275-45bd-a92b-c387372a1554 --------- Co-authored-by: Bruno Borges Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 831c32f2-a275-45bd-a92b-c387372a1554 --- __tests__/entrypoints.test.ts | 87 +++++++++++++++++++++ __tests__/is-main-module.test.ts | 52 ++++++++++++ __tests__/setup-java.module-loading.test.ts | 1 + __tests__/setup-java.test.ts | 1 + dist/cleanup/index.js | 27 ++++++- dist/setup/index.js | 27 ++++++- package-lock.json | 6 +- src/cleanup-java.ts | 4 +- src/is-main-module.ts | 26 ++++++ src/setup-java.ts | 3 +- 10 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 __tests__/entrypoints.test.ts create mode 100644 __tests__/is-main-module.test.ts create mode 100644 src/is-main-module.ts diff --git a/__tests__/entrypoints.test.ts b/__tests__/entrypoints.test.ts new file mode 100644 index 00000000..1fc07dd0 --- /dev/null +++ b/__tests__/entrypoints.test.ts @@ -0,0 +1,87 @@ +import {afterAll, beforeAll, describe, expect, it} from '@jest/globals'; +import {spawnSync} from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {fileURLToPath, pathToFileURL} from 'url'; + +const dist = fileURLToPath(new URL('../dist/', import.meta.url)); +let tempDir: string; +let linkedDist: string; + +beforeAll(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-java-entrypoints-')); + linkedDist = path.join(tempDir, 'linked # dist'); + fs.symlinkSync(dist, linkedDist, 'junction'); +}); + +afterAll(() => { + fs.rmSync(tempDir, {recursive: true, force: true}); +}); + +function execute(args: string[], input?: string) { + return spawnSync(process.execPath, args, { + encoding: 'utf8', + input, + timeout: 10000, + env: { + PATH: process.env.PATH, + SystemRoot: process.env.SystemRoot + } + }); +} + +describe.each([ + ['setup', 1, 'java-version or java-version-file input expected'], + ['cleanup', 0, ''] +] as const)('%s entrypoint', (name, exitCode, output) => { + it.each(['direct', 'symlink', 'preserved symlink'])( + 'executes through a %s path', + mode => { + const entry = path.join( + mode === 'direct' ? dist : linkedDist, + name, + 'index.js' + ); + const args = + mode === 'preserved symlink' + ? ['--preserve-symlinks-main', entry] + : [entry]; + const result = execute(args); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(exitCode); + expect(result.stderr).toBe(''); + expect(result.stdout).not.toContain('skipping the execution'); + if (output) { + expect(result.stdout).toContain(output); + } else { + expect(result.stdout).toBe(''); + } + } + ); + + it.each(['eval', 'stdin', 'file'])( + 'does not execute when imported from %s', + mode => { + const moduleUrl = pathToFileURL(path.join(dist, name, 'index.js')).href; + const source = `const {run} = await import(${JSON.stringify(moduleUrl)}); console.log(typeof run);`; + const importer = path.join(tempDir, `${name}-importer.mjs`); + fs.writeFileSync(importer, source); + const args = + mode === 'file' + ? [importer] + : mode === 'eval' + ? ['--input-type=module', '-e', source] + : ['--input-type=module', '-']; + const result = execute(args, mode === 'stdin' ? source : undefined); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + expect(result.stdout).toContain('skipping the execution'); + expect(result.stdout).toContain('function'); + expect(result.stdout).not.toContain('::error::'); + } + ); +}); diff --git a/__tests__/is-main-module.test.ts b/__tests__/is-main-module.test.ts new file mode 100644 index 00000000..42a5a257 --- /dev/null +++ b/__tests__/is-main-module.test.ts @@ -0,0 +1,52 @@ +import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals'; +import fs from 'fs'; +import {isMainModule} from '../src/is-main-module.js'; + +describe('main module detection', () => { + const originalArgv = process.argv; + + beforeEach(() => { + process.argv = [process.execPath, 'entrypoint.js']; + }); + + afterEach(() => { + process.argv = originalArgv; + jest.restoreAllMocks(); + }); + + it.each([undefined, '-'])( + 'skips filesystem access when argv[1] is %s', + entrypoint => { + process.argv = + entrypoint === undefined + ? [process.execPath] + : [process.execPath, entrypoint]; + const realpath = jest.spyOn(fs, 'realpathSync'); + + expect(isMainModule(import.meta.url)).toBe(false); + expect(realpath).not.toHaveBeenCalled(); + } + ); + + it.each(['ENOENT', 'ENOTDIR'])( + 'treats a non-file entrypoint returning %s as an import', + code => { + jest.spyOn(fs, 'realpathSync').mockImplementation(() => { + throw Object.assign(new Error('No file-based entrypoint'), {code}); + }); + + expect(isMainModule(import.meta.url)).toBe(false); + } + ); + + it('propagates unexpected filesystem errors', () => { + const error = Object.assign(new Error('Permission denied'), { + code: 'EACCES' + }); + jest.spyOn(fs, 'realpathSync').mockImplementation(() => { + throw error; + }); + + expect(() => isMainModule(import.meta.url)).toThrow(error); + }); +}); diff --git a/__tests__/setup-java.module-loading.test.ts b/__tests__/setup-java.module-loading.test.ts index 7705abfc..e8c1e7d2 100644 --- a/__tests__/setup-java.module-loading.test.ts +++ b/__tests__/setup-java.module-loading.test.ts @@ -27,6 +27,7 @@ jest.unstable_mockModule('@actions/core', () => ({ jest.unstable_mockModule('fs', () => ({ default: { + ...jest.requireActual('fs'), readFileSync: jest.fn() } })); diff --git a/__tests__/setup-java.test.ts b/__tests__/setup-java.test.ts index b22fae79..c75ead63 100644 --- a/__tests__/setup-java.test.ts +++ b/__tests__/setup-java.test.ts @@ -27,6 +27,7 @@ jest.unstable_mockModule('@actions/core', () => ({ jest.unstable_mockModule('fs', () => ({ default: { + ...jest.requireActual('fs'), readFileSync: jest.fn() } })); diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 2ae9fe8d..934dff64 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -35747,6 +35747,7 @@ __nccwpck_require__.d(__webpack_exports__, { var cleanup_java_core = __nccwpck_require__(3838); // EXTERNAL MODULE: external "fs" var external_fs_ = __nccwpck_require__(9896); +var external_fs_default = /*#__PURE__*/__nccwpck_require__.n(external_fs_); // EXTERNAL MODULE: external "path" var external_path_ = __nccwpck_require__(6928); // EXTERNAL MODULE: external "crypto" @@ -35890,6 +35891,30 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten var constants = __nccwpck_require__(7242); // EXTERNAL MODULE: external "url" var external_url_ = __nccwpck_require__(7016); +;// CONCATENATED MODULE: ./src/is-main-module.ts + + +function isMainModule(moduleUrl) { + const entrypoint = process.argv[1]; + if (!entrypoint || entrypoint === '-') { + return false; + } + let entrypointPath; + try { + entrypointPath = external_fs_default().realpathSync(entrypoint); + } + catch (error) { + if (error instanceof Error && + 'code' in error && + (error.code === 'ENOENT' || error.code === 'ENOTDIR')) { + return false; + } + throw error; + } + // Resolve both paths for runtimes using --preserve-symlinks-main. + return entrypointPath === external_fs_default().realpathSync((0,external_url_.fileURLToPath)(moduleUrl)); +} + ;// CONCATENATED MODULE: ./src/cleanup-java.ts @@ -35957,7 +35982,7 @@ async function run() { await cleanup_java_removeGpgHome(); await ignoreError(saveCaches()); } -if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) { +if (isMainModule(import.meta.url)) { run(); } else { diff --git a/dist/setup/index.js b/dist/setup/index.js index d585a373..8e019a02 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -36354,6 +36354,30 @@ function configureProblemMatcher(matcherPath) { // EXTERNAL MODULE: ./src/toolchain-ids.ts var toolchain_ids = __nccwpck_require__(7083); +;// CONCATENATED MODULE: ./src/is-main-module.ts + + +function isMainModule(moduleUrl) { + const entrypoint = process.argv[1]; + if (!entrypoint || entrypoint === '-') { + return false; + } + let entrypointPath; + try { + entrypointPath = external_fs_default().realpathSync(entrypoint); + } + catch (error) { + if (error instanceof Error && + 'code' in error && + (error.code === 'ENOENT' || error.code === 'ENOTDIR')) { + return false; + } + throw error; + } + // Resolve both paths for runtimes using --preserve-symlinks-main. + return entrypointPath === external_fs_default().realpathSync((0,external_url_.fileURLToPath)(moduleUrl)); +} + ;// CONCATENATED MODULE: ./src/setup-java.ts @@ -36364,6 +36388,7 @@ var toolchain_ids = __nccwpck_require__(7083); + async function run() { const versions = setup_java_core/* getMultilineInput */.q3(constants/* INPUT_JAVA_VERSION */.QM); let distributionName = setup_java_core/* getInput */.V4(constants/* INPUT_DISTRIBUTION */.g_); @@ -36480,7 +36505,7 @@ async function validateCacheInput(cache) { function settle(promise) { return promise.then(value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason })); } -if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) { +if (isMainModule(import.meta.url)) { run(); } else { diff --git a/package-lock.json b/package-lock.json index 9ee474f6..bcc00372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4830,9 +4830,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", - "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/cleanup-java.ts b/src/cleanup-java.ts index cf333d00..4f45e183 100644 --- a/src/cleanup-java.ts +++ b/src/cleanup-java.ts @@ -6,7 +6,7 @@ import { isJdkCacheEnabled, isJobStatusSuccess } from './util.js'; -import {fileURLToPath} from 'url'; +import {isMainModule} from './is-main-module.js'; async function removeGpgHome() { const gpgHome = core.getState(constants.STATE_GPG_HOME); @@ -77,7 +77,7 @@ export async function run() { await ignoreError(saveCaches()); } -if (process.argv[1] === fileURLToPath(import.meta.url)) { +if (isMainModule(import.meta.url)) { run(); } else { // https://nodejs.org/api/modules.html#modules_accessing_the_main_module diff --git a/src/is-main-module.ts b/src/is-main-module.ts new file mode 100644 index 00000000..4fcc90f5 --- /dev/null +++ b/src/is-main-module.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import {fileURLToPath} from 'url'; + +export function isMainModule(moduleUrl: string): boolean { + const entrypoint = process.argv[1]; + if (!entrypoint || entrypoint === '-') { + return false; + } + + let entrypointPath: string; + try { + entrypointPath = fs.realpathSync(entrypoint); + } catch (error) { + if ( + error instanceof Error && + 'code' in error && + (error.code === 'ENOENT' || error.code === 'ENOTDIR') + ) { + return false; + } + throw error; + } + + // Resolve both paths for runtimes using --preserve-symlinks-main. + return entrypointPath === fs.realpathSync(fileURLToPath(moduleUrl)); +} diff --git a/src/setup-java.ts b/src/setup-java.ts index 6e7c80a6..23de1547 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -12,6 +12,7 @@ import {getJavaDistribution} from './distributions/distribution-factory.js'; import {JavaInstallerOptions} from './distributions/base-models.js'; import {configureProblemMatcher} from './problem-matcher.js'; import {validateToolchainIds} from './toolchain-ids.js'; +import {isMainModule} from './is-main-module.js'; export async function run() { const versions = core.getMultilineInput(constants.INPUT_JAVA_VERSION); @@ -172,7 +173,7 @@ function settle(promise: Promise): Promise> { ); } -if (process.argv[1] === fileURLToPath(import.meta.url)) { +if (isMainModule(import.meta.url)) { run(); } else { // https://nodejs.org/api/modules.html#modules_accessing_the_main_module