From f61e7f8bfe2ca576723364080dec8861436dbec9 Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:50:25 +0000 Subject: [PATCH] fix(browser): stop malformed localhost pages from freezing the new tab page probe (#34427) The title and icon-link regexes rescanned the rest of the body from every unclosed opener, so a 192 KB page of repeated '' blocked the main or runtime event loop for seconds. Scan tags forward only. --- .../ports/workspace-port-page-probe.test.ts | 16 ++++++++++ src/main/ports/workspace-port-page-probe.ts | 30 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/main/ports/workspace-port-page-probe.test.ts b/src/main/ports/workspace-port-page-probe.test.ts index 2e635e32f63..cd7e61771e8 100644 --- a/src/main/ports/workspace-port-page-probe.test.ts +++ b/src/main/ports/workspace-port-page-probe.test.ts @@ -59,6 +59,22 @@ describe('probeWorkspacePortPage', () => { ).resolves.toMatchObject({ kind: 'page', title: 'Sign in', faviconDataUrl: null }) }) + it.each([ + ['unclosed links', `ok${''.repeat(27_000), null] + ])('parses a page of %s without stalling the event loop', async (_name, page, title) => { + const port = await serve((_request, response) => { + response.writeHead(200, { 'content-type': 'text/html' }).end(page) + }) + + const startedAt = performance.now() + await expect( + probeWorkspacePortPage({ host: 'localhost', port, protocol: 'http' }) + ).resolves.toEqual({ kind: 'page', title, faviconDataUrl: null }) + // Rescanning the body from each unclosed opener took seconds on this ~190 KB page. + expect(performance.now() - startedAt).toBeLessThan(500) + }) + it('leaves out APIs and error pages', async () => { const api = await serve((_request, response) => { response.writeHead(200, { 'content-type': 'application/json' }).end('{"ok":true}') diff --git a/src/main/ports/workspace-port-page-probe.ts b/src/main/ports/workspace-port-page-probe.ts index 0540f54acaa..1c260ced832 100644 --- a/src/main/ports/workspace-port-page-probe.ts +++ b/src/main/ports/workspace-port-page-probe.ts @@ -110,9 +110,26 @@ function fetchLoopback(url: URL, maxBytes: number): Promise]*>([\s\S]*?)<\/title>/i.exec(html) - const title = match ? decodeHtmlEntities(match[1]).replace(/\s+/g, ' ').trim() : '' + const open = /', open.lastIndex) + if (tagEnd === -1) { + return null + } + const close = /<\/title>/gi + close.lastIndex = tagEnd + 1 + const end = close.exec(html) + if (!end) { + return null + } + const title = decodeHtmlEntities(html.slice(tagEnd + 1, end.index)) + .replace(/\s+/g, ' ') + .trim() return title ? title.slice(0, 120) : null } @@ -133,7 +150,14 @@ function decodeHtmlEntities(text: string): string { } function findIconHref(html: string): string | null { - for (const tag of html.match(/]*>/gi) ?? []) { + const open = /', open.lastIndex) + if (end === -1) { + return null + } + const tag = html.slice(open.lastIndex, end) + open.lastIndex = end + 1 const attributes = new Map() for (const [, name, double, single, bare] of tag.matchAll( /\b(rel|href)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi