Three round-2 Opus audits ran on the vc=35+vc=36 surface. CVE
returned no new CRITs (round-1 fixes hold) but found 5 new HIGH.
Code-health found 2 CRIT — both my own vc=35 regressions. Function-
correctness found 5 BROKEN that the round-1 sweep missed.
CRIT (from code-health round 2)
R1 Subs feed avatar-backfill self-cancel loop.
Subscriptions.updateAvatar emits a new _subs reference;
SubsPane's LaunchedEffect(subs) reacts → refreshIfStale →
refresh() → inFlight.cancel(). With N channels needing
backfill the parallel-12 batch degenerated into N sequential
single-channel fetches that kept aborting each other. Gated
refreshIfStale on inFlight.isActive != true.
R2 HistoryStore.recordAllWatches O(N²) input.
The vc=35 bulk-import path collapsed N SP writes into 1
(good) but used ArrayList.add(0, item) inside a loop walking
up to 50k input rows before take(50). ~1.25B shifts worst
case. Rewritten: walk newest-first, filter blanks + seen
IDs, stop at MAX_WATCHES. O(N) bounded by output cap.
HIGH (from CVE round 2)
CVE-1 PlayerScreen + VideoDetailScreen rendered raw
error.message into the UI — Media3 HttpDataSource
exceptions include the full request URI with sig=/pot=.
User screenshots a playback error to a chat → full
session credentials in the picture. Both surfaces now
scrub via LogDump.scrubLine before rendering.
CVE-2 SubscriptionsStore.addAll counter race —
updateAndGet's lambda re-runs on CAS retry; var-outside-
lambda increment double-counted. Now derives `added`
from next.size - cur.size delta.
CVE-3 sweepStale ran deleteRecursively() on cacheDir (up to
~256MB) on the main thread inside Application.onCreate.
Moved to appScope.launch(Dispatchers.IO).
CVE-MED-2 Expanded LogDump.SIGNED_PARAM_RE alternation to
include n / lsig / ei / key / sparams.
CVE-MED-3 PlayerScreen + VideoDetailScreen error handlers now
also NowPlaying.clear() so the minibar doesn't keep
claiming a dead session is loaded.
CVE-MED-4 SettingsImport validates imported subscription /
playlist / history URLs against IMPORT_ALLOWED_HOSTS
at import time. Hostile NewPipe export can no
longer smuggle attacker-controlled URLs.
HIGH (from code-health round 2)
R3 Store constructors hit SP + JSON-decode on main thread at
Application.onCreate. Small stores (Settings, History,
Subscriptions, Playlists) stay eager — sub-millisecond
cost. Heavy stores (FeedCache ~225 KB, SearchCache ~150
KB) now lazy-init: their `init()` just stashes
applicationContext; the actual Store + disk decode is
built on first `get()`, which happens from VM IO-dispatched
coroutines.
R4 SearchViewModel.pool race with init coroutine. Switched
pool to a plain @Volatile var (no observers anyway — LOW-
R14) and exposed rebuildPool() so the cache-toggle handler
and a future explicit hook can refresh it.
R5 SubsPane first-paint empty flash. Seeded
SubscriptionFeedUiState(loading = true) in the VM's
initial state — the init coroutine always runs.
R6 Dropped dead uploaderAvatar field on StreamItem. Written
three places, read zero. Saved bytes in every cache entry.
R7 Split mergeFromCache into pruneCacheToSubs + mergeFromCache
(no side effect in the reader). Callers do prune then
merge.
R8 Settings cache-disable wipe now runs on Dispatchers.IO
(3 SP-edit calls were on the UI thread).
HIGH (from function-correctness round 2)
B1 refresh() empty-channels also wipes disk cache (was
in-memory only — disk orphans accumulated).
B2 Settings cache OFF→ON now triggers feedVm.refresh() +
searchVm.rebuildPool() so the user doesn't have to
navigate away and back to repopulate.
B3 SearchViewModel.submit() cache lookup was still doing
SearchCache.get().load() on main (CRIT-C1 was only
partial). Now uses entries.value (StateFlow snapshot).
B5 SearchCacheStore.record now atomic via MutableStateFlow
+ updateAndGet (was load()→write() with no atomicity, so
concurrent records lost entries).
Q9 History.recordWatch wrapped in withContext(Dispatchers.IO).
Q11 Minibar onPlayerError also stops the controller + clears
media items (was leaking dead controller state).
MED
R10 Added comments at the 4 pre-flight NowPlaying checks
noting they're optimizations, claim() is the safety guard.
Prevents a future refactor recreating the round-1 race
after deleting "the guard."
R11 Minibar Toast continues but now layered with the
controller.stop() + clearMediaItems().
CVE-MED-1 NowPlaying.claim updates metadata fields when the
same URL is re-claimed (was returning false unconditionally,
pinning truncated search titles over fresh-from-detail titles).
Q3 onQueryChange clears state.error so a failed-submit's
banner doesn't haunt the next reactive preview.
Deferred to vc=38 (intentional cost/benefit):
CVE HIGH-2 (Rust strawcore::search query= info-log) — needs
a separate strawcore-core edit + rebuild. Logged as a
follow-up.
CVE HIGH-3 (DownloadManager setVisibleInDownloadsUi
deprecated on API 29+) — only the direct-streaming
download replacement is a full fix; that's a multi-day
refactor.
Q5/Q8 (SettingsImport hostile zip silent abort UX) —
cosmetic dialog-title fix.
Q12 (loadedUrl assignment ordering) — pre-existing,
deferred again.