Three Opus round-6 audits found important regressions on the vc=39/40
fixes plus a fresh hostile-zip attack surface.
HIGH
R6-1 SearchViewModel.submit fence-by-query swallowed valid
results. `onQueryChange` mutates _ui.value.query without
cancelling inFlight (for reactive cache filtering as the
user types). The vc=40 string-equality fence treated a
still-valid result as stale just because the user kept
typing after submitting. Re-fenced by Job identity via
ensureActive() — only a fresh submit (which calls
inFlight?.cancel()) invalidates us, mere typing doesn't.
R6-2 SettingsImport.run wrapped `runInner` (suspend) in plain
runCatching, swallowing CancellationException and
surfacing it as a Result.failure that produced a misleading
"import failed" banner on a user-back abort. Migrated to
runCatchingCancellable — exactly what round 5 added the
util for; this call site was missed.
R6-3 VideoDetail/ChannelViewModel.load early-return on bad URL
didn't cancel inFlight. An in-flight prior load could
resolve past the suspension point, see its fence pass
(loadedUrl unchanged because we didn't update it on the
rejected call), and clobber the "Unsupported URL" error
banner the user is looking at. Now: inFlight?.cancel() +
inFlight = null before the early return.
R6-4 Rust ensure_initialized mutex contention pathology. The
vc=40 mutex-first ordering correctly serialized init but
blocked N concurrent tokio workers for the full duration
of a slow ReqwestDownloader::new() (e.g. ~7s on a 6s DNS
timeout). On a 4-core phone that froze the app for 7s.
New: backoff check FIRST (lock-free), then try_lock — if
someone else is initializing, return immediately. Caller
gets one DownloaderMissing they can retry past, instead
of multi-second UI lockup.
MED
R6-5 Hostile zip with duplicate `newpipe.db` entries could
masquerade a benign first DB past any pre-validation and
ship the second malicious one (ZipInputStream walks in
order; second write overwrites). Now: reject the archive
when either `newpipe.db` or `preferences.json` appears
twice.
R6-6 importPlaylists outer + inner queries had no LIMIT — a
crafted DB with 10M playlist rows or 10M items could walk
unbounded cursors into memory even under the 256 MB DB
size cap. LIMIT 256 / LIMIT 5000 now match the discipline
on the other import paths.
R6-7 SponsorBlock pickActiveSegment had a `posSec < endSec -
0.05` exclusion that combined with the 150ms polling
cadence missed short (<200ms) filler/reminder segments
entirely. Dropped — the rate-limit work made it
unnecessary.
R6-8 SettingsImport.importSettings `applied++` was outside the
`want != have` branch — inflated the import-summary count
to "12 settings applied" when only 2 changed.
Deferred:
- PlaylistsStore URL canonicalization (still deferred — needs
shared YT-id-extract util, not blocking)
- SQLite header magic validation (low-value defense-in-depth)
- SP write reorder serialization (bigger refactor)
- updateAvatar coalesced persists (12 parallel = wasted CPU
but not visible)
- Proguard rules staging (paired with R8 enable, both deferred)
- DASH/HLS maxResolution cap via TrackSelectionParameters
(real bug but needs careful Media3 wiring)