From 35016a380c009d04cfed5289890cf6af79b5e1ce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 04:55:33 +0100 Subject: [PATCH] fix(sandbox): serialize registry mutations and lock usage --- src/agents/sandbox/registry.test.ts | 22 ++++++++++++++-------- src/agents/sandbox/registry.ts | 2 +- src/agents/session-write-lock.ts | 4 +++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/agents/sandbox/registry.test.ts b/src/agents/sandbox/registry.test.ts index 3ed9ff6f631..f60c8e09e1a 100644 --- a/src/agents/sandbox/registry.test.ts +++ b/src/agents/sandbox/registry.test.ts @@ -1,12 +1,18 @@ -import { mkdtempSync } from "node:fs"; import fs from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const TEST_STATE_DIR = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-")); -const SANDBOX_REGISTRY_PATH = path.join(TEST_STATE_DIR, "containers.json"); -const SANDBOX_BROWSER_REGISTRY_PATH = path.join(TEST_STATE_DIR, "browsers.json"); +const { TEST_STATE_DIR, SANDBOX_REGISTRY_PATH, SANDBOX_BROWSER_REGISTRY_PATH } = vi.hoisted(() => { + const path = require("node:path"); + const { mkdtempSync } = require("node:fs"); + const { tmpdir } = require("node:os"); + const baseDir = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-")); + + return { + TEST_STATE_DIR: baseDir, + SANDBOX_REGISTRY_PATH: path.join(baseDir, "containers.json"), + SANDBOX_BROWSER_REGISTRY_PATH: path.join(baseDir, "browsers.json"), + }; +}); vi.mock("./constants.js", () => ({ SANDBOX_STATE_DIR: TEST_STATE_DIR, @@ -183,8 +189,8 @@ describe("registry race safety", () => { }; await Promise.all([ - removeRegistryEntry("container-x"), updateRegistry(containerEntry({ containerName: "container-x", configHash: "updated" })), + removeRegistryEntry("container-x"), ]); const registry = await readRegistry(); @@ -224,8 +230,8 @@ describe("registry race safety", () => { }; await Promise.all([ - removeBrowserRegistryEntry("browser-x"), updateBrowserRegistry(browserEntry({ containerName: "browser-x", configHash: "updated" })), + removeBrowserRegistryEntry("browser-x"), ]); const registry = await readBrowserRegistry(); diff --git a/src/agents/sandbox/registry.ts b/src/agents/sandbox/registry.ts index 8c3358042ef..94b1167a7b2 100644 --- a/src/agents/sandbox/registry.ts +++ b/src/agents/sandbox/registry.ts @@ -70,7 +70,7 @@ function isRegistryFile(value: unknown): value is Regis } async function withRegistryLock(registryPath: string, fn: () => Promise): Promise { - const lock = await acquireSessionWriteLock({ sessionFile: registryPath }); + const lock = await acquireSessionWriteLock({ sessionFile: registryPath, allowReentrant: false }); try { return await fn(); } finally { diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index 847d5c7429d..83fe459d32b 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -375,6 +375,7 @@ export async function acquireSessionWriteLock(params: { timeoutMs?: number; staleMs?: number; maxHoldMs?: number; + allowReentrant?: boolean; }): Promise<{ release: () => Promise; }> { @@ -394,8 +395,9 @@ export async function acquireSessionWriteLock(params: { const normalizedSessionFile = path.join(normalizedDir, path.basename(sessionFile)); const lockPath = `${normalizedSessionFile}.lock`; + const allowReentrant = params.allowReentrant ?? true; const held = HELD_LOCKS.get(normalizedSessionFile); - if (held) { + if (allowReentrant && held) { held.count += 1; return { release: async () => {