Skip to content

fix(stt): verify cached model checksum - #239

Open
arhxam wants to merge 1 commit into
getopenscreen:mainfrom
arhxam:codex/verify-cached-stt-model
Open

fix(stt): verify cached model checksum#239
arhxam wants to merge 1 commit into
getopenscreen:mainfrom
arhxam:codex/verify-cached-stt-model

Conversation

@arhxam

@arhxam arhxam commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • verify the SHA-256 of a non-empty cached STT model before accepting it
  • quarantine corrupt cache entries and re-download a verified replacement
  • verify downloads before the atomic final rename, so an invalid file never occupies the live model path

Related issue

No linked issue; found by corrupting a non-empty cached model and exercising startup.

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Not applicable; model-cache integrity behavior.

Testing

  • npx vitest --run electron/stt/modelManager.test.ts electron/stt/index.test.ts (14 passed)
  • npx vitest --run --exclude electron/recording/webm-seek-index.test.ts (1,531 passed)
  • npx tsc --noEmit
  • npx tsc -p tsconfig.test.json --noEmit
  • npm run lint (passes with 11 existing warnings)
  • npm run docs:check
  • npm run i18n:check
  • npx vite build

@arhxam
arhxam requested a review from EtienneLescot as a code owner August 3, 2026 23:18
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@arhxam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55db617d-e426-4ccd-ba47-d0f149a5c74f

📥 Commits

Reviewing files that changed from the base of the PR and between 545043d and ac1bce7.

📒 Files selected for processing (2)
  • electron/stt/modelManager.test.ts
  • electron/stt/modelManager.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot force-pushed the main branch 2 times, most recently from 89e07a8 to 545043d Compare August 4, 2026 00:00
@arhxam
arhxam force-pushed the codex/verify-cached-stt-model branch from 594c29f to 597f664 Compare August 4, 2026 00:04
@arhxam
arhxam force-pushed the codex/verify-cached-stt-model branch from 597f664 to ac1bce7 Compare August 4, 2026 00:05

@EtienneLescot EtienneLescot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both holes are real. The second is the better catch: if (await areModelsPresent(opts.baseDir)) continue; gated the entire loop on the whisper file existing, so a second entry in STT_MODELS would silently never download. And verifying before the final rename rather than after is plainly right — main's version parks a corrupt file at the live model path on its way to throwing.

What I can't take is the quarantine. rename(tmp, filePath) is already atomic, so .bad closes no window; it only destroys the user's only copy before we know a replacement is obtainable. Offline, HF 5xx, disk full — main left a model behind, this leaves none, plus a stranded 264 MB file nothing in this tree ever deletes.

The failure mode I'd want covered before merge is upstream digest drift; details inline. Short version: the model URL goes through resolve/main, so the pin is a bet, and this PR converts "breaks new installs" into "invalidates every existing cache at once."

Rebase note: main has moved 30 commits since your base, including an STT status rework, but it only added completedSec/totalSec to SttStatusEvent and modelManager.ts never touches that type — you're clean there. One UX consequence: prepare() emits phase: "model" before ensureModels, which the renderer maps to mediaStage.downloadingModel, so the new cache hash renders as "Downloading speech model" for a second. Worth a separate beat or a reworded string.

Comment on lines +171 to +173
const badPath = `${filePath}.bad`;
await rm(badPath, { force: true });
await rename(filePath, badPath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part that needs to go. rename(tmp, filePath) on line 203 is already atomic, so quarantining first closes no window — it just removes the only copy on disk before we know a replacement is obtainable. fetchWithRetry throws when the machine is offline and on a non-retryable HTTP status; pipeline throws on ENOSPC. In each of those, main still had a model at the live path and this leaves the user with none, plus a 264 MB .bad that nothing in this repo ever cleans up. It also doubles peak disk (.bad + .partial) in exactly the path meant to recover from trouble. Drop 171-173: leave the stale file where it is, write .partial, verify that, and let the final rename replace it.

Comment on lines +168 to +169
if (!expectedSha256) return;
const actual = await sha256OfFile(filePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes who a stale pin hurts, and I think that deserves an answer in the PR. STT_MODELS.whisper.files[0].url resolves through resolve/main — a mutable branch pointer — so expectedSha256 describes whatever HuggingFace served the day it was recorded, not an immutable revision. Today a re-upload breaks first-time downloads only; everyone with a cache skips the check and keeps working. After this, every cached install is invalidated on the same launch, and since the fresh download mismatches too it throws, so they land on no model rather than a stale one. Pinning the URL to a commit SHA in this same PR would make the digest an actual invariant instead of a bet on main.

Comment on lines +195 to +197
const badPath = `${filePath}.bad`;
await rm(badPath, { force: true });
await rename(tmp, badPath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main had .catch(() => undefined) on this rename and it was load-bearing. On Windows, renaming a file we just finished writing hits EPERM/EBUSY often enough to matter — AV scanners hold a handle for a beat after the stream closes. Unguarded, that errno propagates out of ensureFile and the user sees a bare EPERM: operation not permitted, rename instead of the SHA-256 mismatch message that tells them what actually went wrong. Restore the .catch so the informative error is the one that escapes. Same applies to the rename on line 173 if any form of it survives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants