From 9d7fb1c3c23727655e185f26de2c0b87250f0edb Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 23 Mar 2026 15:27:11 -0700 Subject: [PATCH] fix: browse server lock fails when .gstack/ dir missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit acquireServerLock() tried to create a lock file in .gstack/browse.json.lock but ensureStateDir() was only called inside startServer() — after lock acquisition. When .gstack/ didn't exist, openSync threw ENOENT, the catch returned null, and every invocation thought another process held the lock. Fix: call ensureStateDir() before acquireServerLock() in ensureServer(). Also skip DNS rebinding resolution for localhost/private IPs to eliminate unnecessary latency in concurrent E2E test sessions. Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/cli.ts | 3 +++ browse/src/url-validation.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index d48fab9a..58d0635e 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -262,6 +262,9 @@ async function ensureServer(): Promise { } } + // Ensure state directory exists before lock acquisition (lock file lives there) + ensureStateDir(config); + // Acquire lock to prevent concurrent restart races (TOCTOU) const releaseLock = acquireServerLock(); if (!releaseLock) { diff --git a/browse/src/url-validation.ts b/browse/src/url-validation.ts index 8c23d7c4..4f2c922c 100644 --- a/browse/src/url-validation.ts +++ b/browse/src/url-validation.ts @@ -82,8 +82,12 @@ export async function validateNavigationUrl(url: string): Promise { ); } - // DNS rebinding protection: resolve hostname and check if it points to metadata IPs - if (await resolvesToBlockedIp(hostname)) { + // DNS rebinding protection: resolve hostname and check if it points to metadata IPs. + // Skip for loopback/private IPs — they can't be DNS-rebinded and the async DNS + // resolution adds latency that breaks concurrent E2E tests under load. + const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'; + const isPrivateNet = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/.test(hostname); + if (!isLoopback && !isPrivateNet && await resolvesToBlockedIp(hostname)) { throw new Error( `Blocked: ${parsed.hostname} resolves to a cloud metadata IP. Possible DNS rebinding attack.` );