From 321a4e75b5b7267f91507b1c06eedfe402b6462e Mon Sep 17 00:00:00 2001 From: RagavRida Date: Fri, 24 Apr 2026 00:07:24 +0530 Subject: [PATCH] fix(security-classifier): close writer + delete tmp on download error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit downloadFile() opens an fs.WriteStream to '.tmp.' and drives it from a fetch body reader, but if reader.read() or writer.write() throws mid-download the writer is never closed. That leaks an FD per failed attempt and leaves the half-written tmp on disk. A later retry can land in renameSync(tmp, dest) with a truncated TestSavantAI / DeBERTa ONNX file — which then loads but produces garbage classifier verdicts until the user manually nukes the models cache. Wrap the download loop in try/catch. On failure, destroy() the writer and unlink the tmp before rethrowing, so the next attempt starts from a clean slate. --- browse/src/security-classifier.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/browse/src/security-classifier.ts b/browse/src/security-classifier.ts index 68a41ba26..7d8029b3a 100644 --- a/browse/src/security-classifier.ts +++ b/browse/src/security-classifier.ts @@ -144,16 +144,24 @@ async function downloadFile(url: string, dest: string): Promise { const writer = fs.createWriteStream(tmp); // @ts-ignore — Node stream compat const reader = res.body.getReader(); - let done = false; - while (!done) { - const chunk = await reader.read(); - if (chunk.done) { done = true; break; } - writer.write(chunk.value); + try { + let done = false; + while (!done) { + const chunk = await reader.read(); + if (chunk.done) { done = true; break; } + writer.write(chunk.value); + } + await new Promise((resolve, reject) => { + writer.end((err?: Error | null) => (err ? reject(err) : resolve())); + }); + fs.renameSync(tmp, dest); + } catch (err) { + // Close the writer and drop the half-written tmp so we don't leak an FD + // or ship a truncated model file to the renameSync path on retry. + try { writer.destroy(); } catch { /* already destroyed */ } + try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ } + throw err; } - await new Promise((resolve, reject) => { - writer.end((err?: Error | null) => (err ? reject(err) : resolve())); - }); - fs.renameSync(tmp, dest); } async function ensureTestsavantStaged(onProgress?: (msg: string) => void): Promise {