java-topology/whitepaper/outreach/strawberry.md
russell@unturf.com 6784cdf1cf feat: add 39 outreach docs (batches 6-8)
Batch 6 (9): dolibarr, jitsi-videobridge, zed, tryton, suricata,
  strawberry, zulip, zesarux, zephyr
Batch 7 (15): xonotic (4), xash3d (3), xenia, xtuple, zabbix (2),
  zathura, zebra, yabause, zephyr-0001
Batch 8 (15): woodpecker (2), wine (4), widelands (3), wesnoth (3),
  wekan (3)

Mix of CWE-407 and CWE-312.
2026-04-14 17:06:28 -04:00

4.8 KiB

Strawberry Music Player — CWE-407 Disclosure Brief

2026-04-13 · Patches available — awaiting upstream merge

Finding

Two O(n^2) defects in Strawberry Music Player across the collection watcher and scrobbler cache. Both patched. Patches ready for upstream review. The collection watcher defect fires on every library rescan; the scrobbler defect fires on every batch flush to Last.fm/Libre.fm.

The Defects

strawberry-0001 (PATCHED — HIGH): src/collection/collectionwatcher.cpp:577

// QStringList files_on_disk — O(S*F + F^2) across two patterns
QStringList files_on_disk;
// ...
// Pattern 1: deleted-songs detection — O(S*F)
for (const Song &song : songs_in_db) {
    if (files_on_disk.contains(song.file)) { ... }  // O(F) per song
}
// Pattern 2: removeAll in scan loop — O(F^2) aggregate
files_on_disk.removeAll(file);  // O(F) called up to F times

files_on_disk is a QStringList (linear container). Two O(n^2) patterns compound:

  1. The deleted-songs detection loop iterates S songs in the database and calls files_on_disk.contains() for each, costing O(S*F) where F = files on disk.
  2. Multiple files_on_disk.removeAll() calls inside the main scan loop cost O(F) each, called up to F times total, giving O(F^2) aggregate.

Additionally, files_changed_path_ (also QStringList) uses .contains() in both the inner scan loop and the deleted-songs loop.

strawberry-0002 (PATCHED — MEDIUM): src/scrobbler/scrobblercache.cpp:295

// QList contains + removeAll — O(F*C) per flush
for (ScrobblerCacheItemPtr cache_item : cache_items) {
    if (scrobbler_cache_.contains(cache_item)) {     // O(C) per item
        scrobbler_cache_.removeAll(cache_item);       // O(C) per item
    }
}

scrobbler_cache_ is a QList. Each item in the flush batch triggers a linear scan via contains() plus a linear scan via removeAll(). With F items to flush and C total cache entries, total cost: O(F*C).

Complexity Proof

strawberry-0001: At F=5,000 files on disk, S=5,000 songs in database:

  • Defective: 5,000 * 5,000 + 5,000^2 = 50,000,000 string comparisons
  • Fixed: 5,000 + 5,000 = 10,000 set operations
  • ~250x speedup at F=5,000.

strawberry-0002: At F=C=1,000 scrobbles:

  • Defective: 1,000 * 1,000 * 2 = 2,000,000 pointer comparisons
  • Fixed: 1,000 set builds + 1,000 single-pass filter = 2,000 operations
  • ~250x speedup at F=C=1,000.

Impact

Strawberry Music Player serves as a popular open-source music player for Linux, macOS, and Windows, forked from Clementine. The collection watcher (strawberry-0001) fires on every library rescan, which happens at startup, on directory change notifications, and when users manually trigger a rescan. Users with large music libraries (5,000+ files per directory) experience multi-second hangs during rescans.

The scrobbler cache flush (strawberry-0002) fires after every successful batch submission to Last.fm, Libre.fm, or ListenBrainz. Users who go offline and accumulate hundreds of pending scrobbles hit the quadratic cost on reconnection.

The Fix

strawberry-0001: Convert QStringList to QSet<QString> for O(1) contains/remove:

// Before
QStringList files_on_disk;
files_on_disk.contains(file);   // O(F)
files_on_disk.removeAll(file);  // O(F)

// After
QSet<QString> files_on_disk;
files_on_disk.contains(file);   // O(1)
files_on_disk.remove(file);     // O(1)

Also converts files_changed_path_ from QStringList to QSet<QString> in the header.

strawberry-0002: Build a QSet of items to remove, then single-pass filter:

// Before
for (auto item : cache_items) {
    if (scrobbler_cache_.contains(item)) {
        scrobbler_cache_.removeAll(item);
    }
}

// After
QSet<ScrobblerCacheItemPtr> to_remove(cache_items.begin(), cache_items.end());
scrobbler_cache_.erase(
    std::remove_if(scrobbler_cache_.begin(), scrobbler_cache_.end(),
        [&to_remove](const auto &item) { return to_remove.contains(item); }),
    scrobbler_cache_.end());

Patch

Fixes available:

  • defects/strawberry/patch/strawberry-0001-collectionwatcher-files-on-disk.patch
  • defects/strawberry/patch/strawberry-0002-scrobblercache-flush-contains.patch

Two-file patch across collectionwatcher.cpp, collectionwatcher.h, and scrobblercache.cpp. strawberry-0001: ~250x speedup at F=5,000. strawberry-0002: ~250x speedup at F=C=1,000.

What We Ask

Patches ready for review.

  1. Confirm receipt and assign a GitHub issue reference (strawberrymusicplayer/strawberry).
  2. Assess severity — strawberry-0001 fires on every library rescan and causes visible hangs with large music libraries.
  3. Coordinate a disclosure date — we target 90 days from first contact.
  4. We will credit the Strawberry team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.