Skip to content

Commit 134912a

Browse files
otaconixbrunoborgesCopilot
authored
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 <brborges@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 831c32f2-a275-45bd-a92b-c387372a1554
1 parent 0781fc6 commit 134912a

10 files changed

Lines changed: 226 additions & 8 deletions

‎__tests__/entrypoints.test.ts‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {afterAll, beforeAll, describe, expect, it} from '@jest/globals';
2+
import {spawnSync} from 'child_process';
3+
import fs from 'fs';
4+
import os from 'os';
5+
import path from 'path';
6+
import {fileURLToPath, pathToFileURL} from 'url';
7+
8+
const dist = fileURLToPath(new URL('../dist/', import.meta.url));
9+
let tempDir: string;
10+
let linkedDist: string;
11+
12+
beforeAll(() => {
13+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-java-entrypoints-'));
14+
linkedDist = path.join(tempDir, 'linked # dist');
15+
fs.symlinkSync(dist, linkedDist, 'junction');
16+
});
17+
18+
afterAll(() => {
19+
fs.rmSync(tempDir, {recursive: true, force: true});
20+
});
21+
22+
function execute(args: string[], input?: string) {
23+
return spawnSync(process.execPath, args, {
24+
encoding: 'utf8',
25+
input,
26+
timeout: 10000,
27+
env: {
28+
PATH: process.env.PATH,
29+
SystemRoot: process.env.SystemRoot
30+
}
31+
});
32+
}
33+
34+
describe.each([
35+
['setup', 1, 'java-version or java-version-file input expected'],
36+
['cleanup', 0, '']
37+
] as const)('%s entrypoint', (name, exitCode, output) => {
38+
it.each(['direct', 'symlink', 'preserved symlink'])(
39+
'executes through a %s path',
40+
mode => {
41+
const entry = path.join(
42+
mode === 'direct' ? dist : linkedDist,
43+
name,
44+
'index.js'
45+
);
46+
const args =
47+
mode === 'preserved symlink'
48+
? ['--preserve-symlinks-main', entry]
49+
: [entry];
50+
const result = execute(args);
51+
52+
expect(result.error).toBeUndefined();
53+
expect(result.status).toBe(exitCode);
54+
expect(result.stderr).toBe('');
55+
expect(result.stdout).not.toContain('skipping the execution');
56+
if (output) {
57+
expect(result.stdout).toContain(output);
58+
} else {
59+
expect(result.stdout).toBe('');
60+
}
61+
}
62+
);
63+
64+
it.each(['eval', 'stdin', 'file'])(
65+
'does not execute when imported from %s',
66+
mode => {
67+
const moduleUrl = pathToFileURL(path.join(dist, name, 'index.js')).href;
68+
const source = `const {run} = await import(${JSON.stringify(moduleUrl)}); console.log(typeof run);`;
69+
const importer = path.join(tempDir, `${name}-importer.mjs`);
70+
fs.writeFileSync(importer, source);
71+
const args =
72+
mode === 'file'
73+
? [importer]
74+
: mode === 'eval'
75+
? ['--input-type=module', '-e', source]
76+
: ['--input-type=module', '-'];
77+
const result = execute(args, mode === 'stdin' ? source : undefined);
78+
79+
expect(result.error).toBeUndefined();
80+
expect(result.status).toBe(0);
81+
expect(result.stderr).toBe('');
82+
expect(result.stdout).toContain('skipping the execution');
83+
expect(result.stdout).toContain('function');
84+
expect(result.stdout).not.toContain('::error::');
85+
}
86+
);
87+
});

‎__tests__/is-main-module.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
2+
import fs from 'fs';
3+
import {isMainModule} from '../src/is-main-module.js';
4+
5+
describe('main module detection', () => {
6+
const originalArgv = process.argv;
7+
8+
beforeEach(() => {
9+
process.argv = [process.execPath, 'entrypoint.js'];
10+
});
11+
12+
afterEach(() => {
13+
process.argv = originalArgv;
14+
jest.restoreAllMocks();
15+
});
16+
17+
it.each([undefined, '-'])(
18+
'skips filesystem access when argv[1] is %s',
19+
entrypoint => {
20+
process.argv =
21+
entrypoint === undefined
22+
? [process.execPath]
23+
: [process.execPath, entrypoint];
24+
const realpath = jest.spyOn(fs, 'realpathSync');
25+
26+
expect(isMainModule(import.meta.url)).toBe(false);
27+
expect(realpath).not.toHaveBeenCalled();
28+
}
29+
);
30+
31+
it.each(['ENOENT', 'ENOTDIR'])(
32+
'treats a non-file entrypoint returning %s as an import',
33+
code => {
34+
jest.spyOn(fs, 'realpathSync').mockImplementation(() => {
35+
throw Object.assign(new Error('No file-based entrypoint'), {code});
36+
});
37+
38+
expect(isMainModule(import.meta.url)).toBe(false);
39+
}
40+
);
41+
42+
it('propagates unexpected filesystem errors', () => {
43+
const error = Object.assign(new Error('Permission denied'), {
44+
code: 'EACCES'
45+
});
46+
jest.spyOn(fs, 'realpathSync').mockImplementation(() => {
47+
throw error;
48+
});
49+
50+
expect(() => isMainModule(import.meta.url)).toThrow(error);
51+
});
52+
});

‎__tests__/setup-java.module-loading.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jest.unstable_mockModule('@actions/core', () => ({
2727

2828
jest.unstable_mockModule('fs', () => ({
2929
default: {
30+
...jest.requireActual<typeof import('fs')>('fs'),
3031
readFileSync: jest.fn()
3132
}
3233
}));

‎__tests__/setup-java.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jest.unstable_mockModule('@actions/core', () => ({
2727

2828
jest.unstable_mockModule('fs', () => ({
2929
default: {
30+
...jest.requireActual<typeof import('fs')>('fs'),
3031
readFileSync: jest.fn()
3132
}
3233
}));

‎dist/cleanup/index.js‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35747,6 +35747,7 @@ __nccwpck_require__.d(__webpack_exports__, {
3574735747
var cleanup_java_core = __nccwpck_require__(3838);
3574835748
// EXTERNAL MODULE: external "fs"
3574935749
var external_fs_ = __nccwpck_require__(9896);
35750+
var external_fs_default = /*#__PURE__*/__nccwpck_require__.n(external_fs_);
3575035751
// EXTERNAL MODULE: external "path"
3575135752
var external_path_ = __nccwpck_require__(6928);
3575235753
// EXTERNAL MODULE: external "crypto"
@@ -35890,6 +35891,30 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten
3589035891
var constants = __nccwpck_require__(7242);
3589135892
// EXTERNAL MODULE: external "url"
3589235893
var external_url_ = __nccwpck_require__(7016);
35894+
;// CONCATENATED MODULE: ./src/is-main-module.ts
35895+
35896+
35897+
function isMainModule(moduleUrl) {
35898+
const entrypoint = process.argv[1];
35899+
if (!entrypoint || entrypoint === '-') {
35900+
return false;
35901+
}
35902+
let entrypointPath;
35903+
try {
35904+
entrypointPath = external_fs_default().realpathSync(entrypoint);
35905+
}
35906+
catch (error) {
35907+
if (error instanceof Error &&
35908+
'code' in error &&
35909+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
35910+
return false;
35911+
}
35912+
throw error;
35913+
}
35914+
// Resolve both paths for runtimes using --preserve-symlinks-main.
35915+
return entrypointPath === external_fs_default().realpathSync((0,external_url_.fileURLToPath)(moduleUrl));
35916+
}
35917+
3589335918
;// CONCATENATED MODULE: ./src/cleanup-java.ts
3589435919

3589535920

@@ -35957,7 +35982,7 @@ async function run() {
3595735982
await cleanup_java_removeGpgHome();
3595835983
await ignoreError(saveCaches());
3595935984
}
35960-
if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) {
35985+
if (isMainModule(import.meta.url)) {
3596135986
run();
3596235987
}
3596335988
else {

‎dist/setup/index.js‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36354,6 +36354,30 @@ function configureProblemMatcher(matcherPath) {
3635436354

3635536355
// EXTERNAL MODULE: ./src/toolchain-ids.ts
3635636356
var toolchain_ids = __nccwpck_require__(7083);
36357+
;// CONCATENATED MODULE: ./src/is-main-module.ts
36358+
36359+
36360+
function isMainModule(moduleUrl) {
36361+
const entrypoint = process.argv[1];
36362+
if (!entrypoint || entrypoint === '-') {
36363+
return false;
36364+
}
36365+
let entrypointPath;
36366+
try {
36367+
entrypointPath = external_fs_default().realpathSync(entrypoint);
36368+
}
36369+
catch (error) {
36370+
if (error instanceof Error &&
36371+
'code' in error &&
36372+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
36373+
return false;
36374+
}
36375+
throw error;
36376+
}
36377+
// Resolve both paths for runtimes using --preserve-symlinks-main.
36378+
return entrypointPath === external_fs_default().realpathSync((0,external_url_.fileURLToPath)(moduleUrl));
36379+
}
36380+
3635736381
;// CONCATENATED MODULE: ./src/setup-java.ts
3635836382

3635936383

@@ -36364,6 +36388,7 @@ var toolchain_ids = __nccwpck_require__(7083);
3636436388

3636536389

3636636390

36391+
3636736392
async function run() {
3636836393
const versions = setup_java_core/* getMultilineInput */.q3(constants/* INPUT_JAVA_VERSION */.QM);
3636936394
let distributionName = setup_java_core/* getInput */.V4(constants/* INPUT_DISTRIBUTION */.g_);
@@ -36480,7 +36505,7 @@ async function validateCacheInput(cache) {
3648036505
function settle(promise) {
3648136506
return promise.then(value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }));
3648236507
}
36483-
if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) {
36508+
if (isMainModule(import.meta.url)) {
3648436509
run();
3648536510
}
3648636511
else {

‎package-lock.json‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cleanup-java.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
isJdkCacheEnabled,
77
isJobStatusSuccess
88
} from './util.js';
9-
import {fileURLToPath} from 'url';
9+
import {isMainModule} from './is-main-module.js';
1010

1111
async function removeGpgHome() {
1212
const gpgHome = core.getState(constants.STATE_GPG_HOME);
@@ -77,7 +77,7 @@ export async function run() {
7777
await ignoreError(saveCaches());
7878
}
7979

80-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
80+
if (isMainModule(import.meta.url)) {
8181
run();
8282
} else {
8383
// https://nodejs.org/api/modules.html#modules_accessing_the_main_module

‎src/is-main-module.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import fs from 'fs';
2+
import {fileURLToPath} from 'url';
3+
4+
export function isMainModule(moduleUrl: string): boolean {
5+
const entrypoint = process.argv[1];
6+
if (!entrypoint || entrypoint === '-') {
7+
return false;
8+
}
9+
10+
let entrypointPath: string;
11+
try {
12+
entrypointPath = fs.realpathSync(entrypoint);
13+
} catch (error) {
14+
if (
15+
error instanceof Error &&
16+
'code' in error &&
17+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
18+
) {
19+
return false;
20+
}
21+
throw error;
22+
}
23+
24+
// Resolve both paths for runtimes using --preserve-symlinks-main.
25+
return entrypointPath === fs.realpathSync(fileURLToPath(moduleUrl));
26+
}

‎src/setup-java.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {getJavaDistribution} from './distributions/distribution-factory.js';
1212
import {JavaInstallerOptions} from './distributions/base-models.js';
1313
import {configureProblemMatcher} from './problem-matcher.js';
1414
import {validateToolchainIds} from './toolchain-ids.js';
15+
import {isMainModule} from './is-main-module.js';
1516

1617
export async function run() {
1718
const versions = core.getMultilineInput(constants.INPUT_JAVA_VERSION);
@@ -172,7 +173,7 @@ function settle<T>(promise: Promise<T>): Promise<PromiseSettledResult<T>> {
172173
);
173174
}
174175

175-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
176+
if (isMainModule(import.meta.url)) {
176177
run();
177178
} else {
178179
// https://nodejs.org/api/modules.html#modules_accessing_the_main_module

0 commit comments

Comments
 (0)