diff --git a/scripts/lib/clean-linux-hopper-runtime.d.mts b/scripts/lib/clean-linux-hopper-runtime.d.mts index dd1010aa1e..d2177fad47 100644 --- a/scripts/lib/clean-linux-hopper-runtime.d.mts +++ b/scripts/lib/clean-linux-hopper-runtime.d.mts @@ -1,3 +1,4 @@ export function assertCleanLinuxHopperRuntime( launcherPaths?: readonly string[], + procRoot?: string, ): Promise; diff --git a/scripts/lib/clean-linux-hopper-runtime.mjs b/scripts/lib/clean-linux-hopper-runtime.mjs index 27a306fce1..fbe15e0d3a 100644 --- a/scripts/lib/clean-linux-hopper-runtime.mjs +++ b/scripts/lib/clean-linux-hopper-runtime.mjs @@ -16,6 +16,7 @@ export const assertCleanLinuxHopperRuntime = async ( ? [] : [process.env.HOPPER_LAUNCHER_PATH]), ], + procRoot = "/proc", ) => { const { stdout } = await execFileAsync("ps", ["-ax", "-o", "command="]); if (/\/opt\/hopper\/bin\/Hopper|\bXvfb\b|hopper-demo-x11\.py/u.test(stdout)) @@ -32,17 +33,17 @@ export const assertCleanLinuxHopperRuntime = async ( // /proc exposes the executed file even when argv[0] names a removed symlink. // Compare file identity, not a display path or a process name. let inspectionError; - for (const entry of await readdir("/proc", { withFileTypes: true })) { + for (const entry of await readdir(procRoot, { withFileTypes: true })) { if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) continue; let executable; try { - executable = await stat(`/proc/${entry.name}/exe`, { bigint: true }); + executable = await stat(`${procRoot}/${entry.name}/exe`, { bigint: true }); } catch (error) { if (error?.code === "ENOENT" || error?.code === "ESRCH") continue; // Other users' executables may be unreadable. The setup lane launches // providers as the caller; unreadable caller-owned processes fail closed. if (error?.code === "EACCES") { - const owner = await stat(`/proc/${entry.name}`).catch((cause) => { + const owner = await stat(`${procRoot}/${entry.name}`).catch((cause) => { if (cause?.code === "ENOENT") return undefined; throw cause; }); diff --git a/src/domain/android/androidAnalysis.ts b/src/domain/android/androidAnalysis.ts index c1ec76ba83..caa3bf5ee0 100644 --- a/src/domain/android/androidAnalysis.ts +++ b/src/domain/android/androidAnalysis.ts @@ -102,7 +102,7 @@ const textCoverage = z.strictObject({ export const dexDescriptorSchema = z .string() .regex( - /^\((?:\[*(?:[ZBSCIJFD]|L[^;()]+;))*\)(?:V|\[*(?:[ZBSCIJFD]|L[^;()]+;))$/u, + /^\((?:\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))*\)(?:V|\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))$/u, ); const method = z.strictObject({ diff --git a/tests/process-global/cleanLinuxHopperRuntime.test.ts b/tests/process-global/cleanLinuxHopperRuntime.test.ts index f6b9fc677b..8b7be17da6 100644 --- a/tests/process-global/cleanLinuxHopperRuntime.test.ts +++ b/tests/process-global/cleanLinuxHopperRuntime.test.ts @@ -1,6 +1,13 @@ import { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { chmod, copyFile, readFile, symlink, unlink } from "node:fs/promises"; +import { + chmod, + copyFile, + mkdir, + readFile, + symlink, + unlink, +} from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -24,6 +31,8 @@ it const directory = await createTestTempDirectory("rea-clean-runtime-test-"); const launcher = join(directory, "Hopper"); const alternate = join(directory, "Hopper-alternate"); + const procRoot = join(directory, "proc"); + await mkdir(procRoot); const alternateMode = mode === "alternate launcher"; if (alternateMode) { await copyFile("/usr/bin/sleep", launcher); @@ -32,7 +41,10 @@ it } // A rejection must come from the fixture we are about to launch. An // existing Xvfb would otherwise mask a broken executable-identity check. - await assertCleanLinuxHopperRuntime(alternateMode ? [launcher] : []); + await assertCleanLinuxHopperRuntime( + alternateMode ? [launcher] : [], + procRoot, + ); const command = alternateMode ? alternate : process.execPath; const arguments_ = alternateMode ? ["30"] : ["-e", childCode]; const parentCode = ` @@ -59,6 +71,11 @@ it const { stdout } = await parent; const { pid } = JSON.parse(stdout) as { pid: number }; orphanPid = pid; + if (alternateMode) { + const processEntry = join(procRoot, String(pid)); + await mkdir(processEntry); + await symlink(`/proc/${String(pid)}/exe`, join(processEntry, "exe")); + } const { stdout: coordinates } = await execFileAsync("ps", [ "-p", String(pid), @@ -92,7 +109,10 @@ it await unlink(alternate); } await expect( - assertCleanLinuxHopperRuntime(alternateMode ? [launcher] : []), + assertCleanLinuxHopperRuntime( + alternateMode ? [launcher] : [], + procRoot, + ), ).rejects.toThrow("Clean Linux runner retained a Hopper demo process"); // Observation must not terminate the fixture, even though it resembles Xvfb. const { stdout: alive } = await execFileAsync("ps", [