From 1f2f2c3fa1300be4b2450f0f958f88c9a60f9edd Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 9 May 2026 06:00:15 -0700 Subject: [PATCH 1/4] fix(chromium): close file descriptor on protocol stream save error saveProtocolStream opens a write fd but does not close it when client.send('IO.read') throws, leaking one file descriptor per failed stream save. Wrap the read loop in try/finally to ensure the fd is always closed. Signed-off-by: Sebastien Tardif --- .../src/server/chromium/crProtocolHelper.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index cd45d5d13e76f..07589b9b48353 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -47,13 +47,16 @@ export async function saveProtocolStream(client: CRSession, handle: string, path let eof = false; await mkdirIfNeeded(path); const fd = await fs.promises.open(path, 'w'); - while (!eof) { - const response = await client.send('IO.read', { handle }); - eof = response.eof; - const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); - await fd.write(buf); + try { + while (!eof) { + const response = await client.send('IO.read', { handle }); + eof = response.eof; + const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); + await fd.write(buf); + } + } finally { + await fd.close(); } - await fd.close(); await client.send('IO.close', { handle }); } From 144593fd7c7e40ab8d712bb512fe9c0df0fffada Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 9 May 2026 06:07:46 -0700 Subject: [PATCH 2/4] fix: also close protocol IO handle in finally block Move IO.close into the finally block alongside fd.close so the server-side stream handle is released even when IO.read throws. Signed-off-by: Sebastien Tardif --- .../playwright-core/src/server/chromium/crProtocolHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 07589b9b48353..ede27fe7b0ec8 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -56,8 +56,8 @@ export async function saveProtocolStream(client: CRSession, handle: string, path } } finally { await fd.close(); + await client.send('IO.close', { handle }).catch(() => {}); } - await client.send('IO.close', { handle }); } export async function readProtocolStream(client: CRSession, handle: string): Promise { From 26aa2206d3a94e7d3c65ef1336e35df23a398b4b Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 9 May 2026 06:16:44 -0700 Subject: [PATCH 3/4] fix: suppress fd.close error in finally, apply same fix to readProtocolStream - fd.close() can throw in the finally block and mask the original IO.read error. Add .catch(() => {}) for consistency with IO.close. - readProtocolStream has the same IO.close leak pattern. Wrap its read loop in try/finally too. Signed-off-by: Sebastien Tardif --- .../src/server/chromium/crProtocolHelper.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index ede27fe7b0ec8..2b4b03899eda7 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -55,7 +55,7 @@ export async function saveProtocolStream(client: CRSession, handle: string, path await fd.write(buf); } } finally { - await fd.close(); + await fd.close().catch(() => {}); await client.send('IO.close', { handle }).catch(() => {}); } } @@ -63,13 +63,16 @@ export async function saveProtocolStream(client: CRSession, handle: string, path export async function readProtocolStream(client: CRSession, handle: string): Promise { let eof = false; const chunks = []; - while (!eof) { - const response = await client.send('IO.read', { handle }); - eof = response.eof; - const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); - chunks.push(buf); + try { + while (!eof) { + const response = await client.send('IO.read', { handle }); + eof = response.eof; + const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); + chunks.push(buf); + } + } finally { + await client.send('IO.close', { handle }).catch(() => {}); } - await client.send('IO.close', { handle }); return Buffer.concat(chunks); } From 834dbbaab6f9f4fb3c0a38c54bad05bc7e2eaf3d Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 9 May 2026 09:00:04 -0700 Subject: [PATCH 4/4] fix: move IO.close out of finally to avoid nested throws Keep only fd.close() in the finally block. IO.close is moved back to after the try/finally so it only runs on the success path, avoiding nested throws when both IO.read and IO.close fail. readProtocolStream reverted to original since it has no fd to protect; the try/finally was unnecessary. Signed-off-by: Sebastien Tardif --- .../src/server/chromium/crProtocolHelper.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 2b4b03899eda7..deb0158f5f2fe 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -56,23 +56,20 @@ export async function saveProtocolStream(client: CRSession, handle: string, path } } finally { await fd.close().catch(() => {}); - await client.send('IO.close', { handle }).catch(() => {}); } + await client.send('IO.close', { handle }); } export async function readProtocolStream(client: CRSession, handle: string): Promise { let eof = false; const chunks = []; - try { - while (!eof) { - const response = await client.send('IO.read', { handle }); - eof = response.eof; - const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); - chunks.push(buf); - } - } finally { - await client.send('IO.close', { handle }).catch(() => {}); + while (!eof) { + const response = await client.send('IO.read', { handle }); + eof = response.eof; + const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); + chunks.push(buf); } + await client.send('IO.close', { handle }); return Buffer.concat(chunks); }