diff --git a/appcontainer/sandbox-cp-hooks.js b/appcontainer/sandbox-cp-hooks.js index 6a098f4..ad33cd0 100644 --- a/appcontainer/sandbox-cp-hooks.js +++ b/appcontainer/sandbox-cp-hooks.js @@ -132,9 +132,29 @@ function isCanonicalPathWithin(rootPath, candidatePath, runtime) { return relative !== "" && !relative.startsWith("..") && !pathMod.isAbsolute(relative); } +function isOpenClawSqliteStagingRoot(value, runtime) { + if (typeof value !== "string" || !pathMod.isAbsolute(value)) return false; + var localAppData = + runtime && runtime.localAppData ? runtime.localAppData : TRUSTED_WORKER_ENV.LOCALAPPDATA; + if (!localAppData) return false; + var root = canonicalExistingPath(pathMod.join(localAppData, "openclaw"), runtime); + var candidate = canonicalExistingPath(value, runtime); + if (!root || !candidate) return false; + var relative = pathMod.relative(root, candidate); + var pid = runtime && runtime.pid !== undefined ? runtime.pid : process.pid; + var expectedName = new RegExp( + "^openclaw-sqlite-readonly-" + + pid + + "-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "i", + ); + return relative === pathMod.basename(value).toLowerCase() && expectedName.test(relative); +} + function isOpenClawInternalSqliteWorkerCommand(cmd, args, stack, runtime, options) { if (options && options.shell) return false; - if (!Array.isArray(args) || args.length !== 4) return false; + if (!Array.isArray(args) || (args.length !== 4 && args.length !== 5)) return false; + if (args.length === 5 && !isOpenClawSqliteStagingRoot(args[4], runtime)) return false; var execPath = normalizeComparablePath( runtime && runtime.execPath ? runtime.execPath : TRUSTED_NODE_EXEC_PATH, ); @@ -164,11 +184,11 @@ function isOpenClawInternalSqliteWorkerCommand(cmd, args, stack, runtime, option var normalizedStack = String(stack || "") .replace(/\//g, pathMod.sep) .toLowerCase(); - var trustedCallerPrefix = - normalizeComparablePath(pathMod.join(packageRoot, "dist")) + - pathMod.sep + - "sqlite-readonly-location-"; - return normalizedStack.indexOf(trustedCallerPrefix) >= 0; + var trustedDist = normalizeComparablePath(pathMod.join(packageRoot, "dist")) + pathMod.sep; + return ( + normalizedStack.indexOf(trustedDist + "sqlite-readonly-location-") >= 0 || + normalizedStack.indexOf(trustedDist + "sqlite-readonly-worker-") >= 0 + ); } function buildInternalSqliteWorkerInvocation(args, options) { @@ -181,6 +201,7 @@ function buildInternalSqliteWorkerInvocation(args, options) { if (options && options.encoding !== undefined) nextOptions.encoding = options.encoding; if (options && options.maxBuffer !== undefined) nextOptions.maxBuffer = options.maxBuffer; if (options && options.timeout !== undefined) nextOptions.timeout = options.timeout; + if (options && options.killSignal !== undefined) nextOptions.killSignal = options.killSignal; return { args: ["--require", TRUSTED_PRELOAD_PATH].concat(args), options: nextOptions, diff --git a/desktop/src/sandbox-logic.test.ts b/desktop/src/sandbox-logic.test.ts index 852b062..098dbd4 100644 --- a/desktop/src/sandbox-logic.test.ts +++ b/desktop/src/sandbox-logic.test.ts @@ -2480,6 +2480,12 @@ describe("OpenClaw internal SQLite worker", () => { const execPath = "C:\\Program Files\\nodejs\\node.exe"; const entryPath = path.join(packageRoot, "openclaw.mjs"); const stateDir = "C:\\Users\\test\\.openclaw"; + const localAppData = "C:\\Users\\test\\AppData\\Local"; + const stagingRoot = path.join( + localAppData, + "openclaw", + "openclaw-sqlite-readonly-2056-139531e6-3179-429e-90e2-f16d5a267869", + ); const workerPath = path.join(packageRoot, "dist", "infra", "sqlite-readonly-location.worker.js"); const args = [ workerPath, @@ -2492,11 +2498,15 @@ describe("OpenClaw internal SQLite worker", () => { const canonical = new Map([ [stateDir.toLowerCase(), stateDir], [args[3].toLowerCase(), args[3]], + [path.join(localAppData, "openclaw").toLowerCase(), path.join(localAppData, "openclaw")], + [stagingRoot.toLowerCase(), stagingRoot], ]); const runtime = { entryPath, execPath, stateDir, + localAppData, + pid: 2056, realpath: (value: string) => { const resolved = canonical.get(value.toLowerCase()); if (!resolved) throw new Error("ENOENT"); @@ -2510,6 +2520,55 @@ describe("OpenClaw internal SQLite worker", () => { ); }); + it.each(["async", "sync"])("accepts the 9.3 %s worker with its owned staging root", (mode) => { + expect( + cpHooks.isOpenClawInternalSqliteWorkerCommand( + execPath, + [workerPath, args[1], mode, args[3], stagingRoot], + stack.replace( + "sqlite-readonly-location-AbCd1234.js", + "sqlite-readonly-worker-AbCd1234.mjs", + ), + runtime, + ), + ).toBe(true); + }); + + it.each([ + "relative", + path.join(stateDir, "output"), + path.join(localAppData, "openclaw", "credentials"), + stagingRoot.replace("-2056-", "-9999-"), + path.join(stagingRoot, "nested"), + ])("rejects an unowned 9.3 staging root: %s", (staging) => { + expect( + cpHooks.isOpenClawInternalSqliteWorkerCommand(execPath, [...args, staging], stack, { + ...runtime, + realpath: (value: string) => value, + }), + ).toBe(false); + }); + + it("rejects redirected staging directories and surplus worker arguments", () => { + expect( + cpHooks.isOpenClawInternalSqliteWorkerCommand(execPath, [...args, stagingRoot], stack, { + ...runtime, + realpath: (value: string) => + value.toLowerCase() === stagingRoot.toLowerCase() + ? path.join(stateDir, path.basename(stagingRoot)) + : runtime.realpath(value), + }), + ).toBe(false); + expect( + cpHooks.isOpenClawInternalSqliteWorkerCommand( + execPath, + [...args, stagingRoot, "--extra"], + stack, + runtime, + ), + ).toBe(false); + }); + it.each([ ["executable", "C:\\Windows\\System32\\cmd.exe", args, stack], [ @@ -2561,6 +2620,8 @@ describe("OpenClaw internal SQLite worker", () => { it("launches the worker with a fixed preload and without caller-controlled Node loading", () => { const invocation = cpHooks.buildInternalSqliteWorkerInvocation(args, { encoding: "utf8", + timeout: 30_000, + killSignal: "SIGKILL", env: { NODE_OPTIONS: '--require "C:\\Users\\test\\.openclaw\\payload.js"', NODE_PATH: "C:\\Users\\test\\.openclaw\\modules", @@ -2573,6 +2634,8 @@ describe("OpenClaw internal SQLite worker", () => { ]); expect(invocation.args.slice(2)).toEqual(args); expect(invocation.options.encoding).toBe("utf8"); + expect(invocation.options.timeout).toBe(30_000); + expect(invocation.options.killSignal).toBe("SIGKILL"); expect(invocation.options.env.NODE_OPTIONS).toBeUndefined(); expect(invocation.options.env.NODE_PATH).toBeUndefined(); expect(invocation.options.env.MICROCLAW_OPENCLAW_INTERNAL_SQLITE_WORKER).toBe("1");