straw/strawApp
Kayos 567423336c vc=37: round-2 audit-fix sprint — 2 CRIT + 11 HIGH + 4 MED
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.
2026-05-25 14:05:58 -07:00
..
src/main vc=37: round-2 audit-fix sprint — 2 CRIT + 11 HIGH + 4 MED 2026-05-25 14:05:58 -07:00
build.gradle.kts vc=26: look + feel pass — sulkta.com palette + Material Icons 2026-05-25 17:46:23 +00:00