Compare commits

..

2 Commits

Author SHA1 Message Date
Alban Auzeill bc1610626e Fix from review 2026-07-13 17:14:12 +02:00
Alban Auzeill 2dfea3d9ad SQSCANGHA-156 GPG signature verification fails when temporary directory path is too long 2026-07-13 16:35:33 +02:00
5 changed files with 115 additions and 16 deletions
+19 -4
View File
@@ -14,6 +14,7 @@ import * as fs$2 from 'node:fs/promises';
import * as os$1 from 'node:os';
import * as path$1 from 'node:path';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import * as fs$1 from 'node:fs';
import fs__default from 'node:fs';
import 'http';
@@ -3900,6 +3901,10 @@ function toSemVer(version) {
const SONARSOURCE_KEY_FINGERPRINT = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
const DEFAULT_KEYSERVER = "hkps://keyserver.ubuntu.com";
const FALLBACK_KEYSERVER = "hkps://keys.openpgp.org";
// Linux/macOS sockaddr_un.sun_path limit is 108 bytes including the NUL terminator.
// S.gpg-agent.browser is the longest socket GPG creates directly under the home directory.
const MAX_GPG_SOCKET_PATH = 107;
const LONGEST_GPG_SOCKET = "/S.gpg-agent.browser";
/**
* Verifies the GPG signature of a downloaded file
@@ -3990,12 +3995,22 @@ function convertToUnixPath(windowsPath) {
* @returns {string} Path to the temporary GPG home directory
*/
function setupGpgHome() {
const tempDir = process.env.RUNNER_TEMP || os$1.tmpdir();
const gpgHome = path$1.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
const dirName = `gpg-${randomBytes(4).toString("hex")}`;
fs$1.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
const runnertemp = process.env.RUNNER_TEMP;
for (const base of [runnertemp, os$1.tmpdir()].filter(Boolean)) {
const gpgHome = path$1.join(base, dirName);
if (process.platform === "win32" || (gpgHome + LONGEST_GPG_SOCKET).length <= MAX_GPG_SOCKET_PATH) {
fs$1.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
return gpgHome;
}
}
return gpgHome;
throw new Error(
`Cannot create a GPG home directory with a short enough path for GPG sockets. ` +
`The longest socket path (gpgHome + "${LONGEST_GPG_SOCKET}") must not exceed ${MAX_GPG_SOCKET_PATH} characters. ` +
`Consider setting RUNNER_TEMP to a shorter path, was "${runnertemp || '<empty>'}".`
);
}
/**
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -20,7 +20,9 @@
import assert from "node:assert/strict";
import * as fs from "node:fs";
import { afterEach, describe, it, mock } from "node:test";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, it, mock } from "node:test";
import { getProxyFromEnv, setupGpgHome, } from "../gpg-verification.js";
/**
@@ -492,6 +494,76 @@ describe("gpg-verification with mocked exec", () => {
});
});
describe("setupGpgHome", () => {
let originalRunnerTemp;
beforeEach(() => {
originalRunnerTemp = process.env.RUNNER_TEMP;
});
afterEach(() => {
if (originalRunnerTemp === undefined) {
delete process.env.RUNNER_TEMP;
} else {
process.env.RUNNER_TEMP = originalRunnerTemp;
}
});
it("should create directory under RUNNER_TEMP when path is short enough", () => {
const runnerTemp = fs.mkdtempSync(path.join(os.tmpdir(), "runner-temp-"));
tempDirs.push(runnerTemp);
process.env.RUNNER_TEMP = runnerTemp;
const gpgHome = setupGpgHome();
tempDirs.push(gpgHome);
assert.equal(path.dirname(gpgHome), runnerTemp);
assert.ok(fs.existsSync(gpgHome));
});
it("should fall back to os.tmpdir() when RUNNER_TEMP is not set", () => {
delete process.env.RUNNER_TEMP;
const gpgHome = setupGpgHome();
tempDirs.push(gpgHome);
assert.equal(path.dirname(gpgHome), os.tmpdir());
assert.ok(fs.existsSync(gpgHome));
});
it("should fall back to os.tmpdir() when RUNNER_TEMP path is too long for GPG sockets", () => {
// Customer's actual RUNNER_TEMP (93 chars) — pushes the dirmngr socket path over the 107-char Linux limit
process.env.RUNNER_TEMP = "/codebuild/output/src2280/src/2bc4e189_61b5_4b91_abc6_9c5c06637b96/actions-runner/_work/_temp";
const gpgHome = setupGpgHome();
tempDirs.push(gpgHome);
assert.equal(path.dirname(gpgHome), os.tmpdir());
assert.ok(fs.existsSync(gpgHome));
});
it("should throw a clear error when all candidate paths are too long", async (t) => {
const longPath = "/codebuild/output/src2280/src/2bc4e189_61b5_4b91_abc6_9c5c06637b96/actions-runner/_work/_temp";
t.mock.module("node:os", {
namedExports: {
tmpdir: () => longPath,
},
});
process.env.RUNNER_TEMP = longPath;
const { setupGpgHome: freshSetupGpgHome } = await import("../gpg-verification.js?test=both-paths-too-long");
assert.throws(
() => freshSetupGpgHome(),
{ message: `Cannot create a GPG home directory with a short enough path for GPG sockets. ` +
`The longest socket path (gpgHome + "/S.gpg-agent.browser") must not exceed 107 characters. ` +
`Consider setting RUNNER_TEMP to a shorter path, was "${longPath}".` }
);
});
});
describe("getProxyFromEnv", () => {
afterEach(() => {
clearProxyEnv();
+3 -6
View File
@@ -101,9 +101,8 @@ async function withMockedEnv(envVars, testFn) {
* @returns {string} The path to the created temporary directory
*/
function createTempDir() {
const tempDir = path.join(os.tmpdir(), `test-runner-temp-${Date.now()}`);
fs.mkdirSync(tempDir, { recursive: true });
return tempDir;
// Use a short prefix so the gpg socket path stays within the 107-char Linux limit
return fs.mkdtempSync(path.join(os.tmpdir(), "r"));
}
describe("gpg-verification", () => {
@@ -177,10 +176,8 @@ describe("gpg-verification", () => {
}
});
it("should create unique directories on multiple calls", async () => {
it("should create unique directories on multiple calls", () => {
const gpgHome1 = createTrackedGpgHome(tempDirs);
// Small delay to ensure different timestamps
await new Promise((resolve) => setTimeout(resolve, 10));
const gpgHome2 = createTrackedGpgHome(tempDirs);
assert.notEqual(gpgHome1, gpgHome2);
+19 -4
View File
@@ -20,6 +20,7 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { randomBytes } from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
@@ -27,6 +28,10 @@ import * as path from "node:path";
const SONARSOURCE_KEY_FINGERPRINT = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
const DEFAULT_KEYSERVER = "hkps://keyserver.ubuntu.com";
const FALLBACK_KEYSERVER = "hkps://keys.openpgp.org";
// Linux/macOS sockaddr_un.sun_path limit is 108 bytes including the NUL terminator.
// S.gpg-agent.browser is the longest socket GPG creates directly under the home directory.
const MAX_GPG_SOCKET_PATH = 107;
const LONGEST_GPG_SOCKET = "/S.gpg-agent.browser";
/**
* Verifies the GPG signature of a downloaded file
@@ -117,12 +122,22 @@ export function convertToUnixPath(windowsPath) {
* @returns {string} Path to the temporary GPG home directory
*/
export function setupGpgHome() {
const tempDir = process.env.RUNNER_TEMP || os.tmpdir();
const gpgHome = path.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
const dirName = `gpg-${randomBytes(4).toString("hex")}`;
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
const runnertemp = process.env.RUNNER_TEMP;
for (const base of [runnertemp, os.tmpdir()].filter(Boolean)) {
const gpgHome = path.join(base, dirName);
if (process.platform === "win32" || (gpgHome + LONGEST_GPG_SOCKET).length <= MAX_GPG_SOCKET_PATH) {
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
return gpgHome;
}
}
return gpgHome;
throw new Error(
`Cannot create a GPG home directory with a short enough path for GPG sockets. ` +
`The longest socket path (gpgHome + "${LONGEST_GPG_SOCKET}") must not exceed ${MAX_GPG_SOCKET_PATH} characters. ` +
`Consider setting RUNNER_TEMP to a shorter path, was "${runnertemp || '<empty>'}".`
);
}
/**