From 0b5409ff95049e4df69f1062afe157bb3c4303d5 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 10:39:32 -0400 Subject: [PATCH] =?UTF-8?q?libtorrent-0001/qbittorrent:=20CWE-407=20scan?= =?UTF-8?q?=20=E2=80=94=201=20defect,=201=20CLEAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libtorrent-0001: file_storage::get_or_add_path std::find on m_paths vector O(F*P) MEDIUM 250x qBittorrent: CLEAN (QSet/QHash throughout for membership tests) --- ...storage-get-or-add-path-linear-dedup.patch | 53 ++++++++ .../LibtorrentTest$FileStorageDefective.class | Bin 0 -> 829 bytes .../LibtorrentTest$FileStorageFixed.class | Bin 0 -> 1124 bytes defects/libtorrent/unit/LibtorrentTest.class | Bin 0 -> 2976 bytes defects/libtorrent/unit/LibtorrentTest.java | 114 ++++++++++++++++++ defects/qbittorrent/patch/CLEAN.md | 28 +++++ 6 files changed, 195 insertions(+) create mode 100644 defects/libtorrent/patch/libtorrent-0001-file-storage-get-or-add-path-linear-dedup.patch create mode 100644 defects/libtorrent/unit/LibtorrentTest$FileStorageDefective.class create mode 100644 defects/libtorrent/unit/LibtorrentTest$FileStorageFixed.class create mode 100644 defects/libtorrent/unit/LibtorrentTest.class create mode 100644 defects/libtorrent/unit/LibtorrentTest.java create mode 100644 defects/qbittorrent/patch/CLEAN.md diff --git a/defects/libtorrent/patch/libtorrent-0001-file-storage-get-or-add-path-linear-dedup.patch b/defects/libtorrent/patch/libtorrent-0001-file-storage-get-or-add-path-linear-dedup.patch new file mode 100644 index 000000000..5c9a3e029 --- /dev/null +++ b/defects/libtorrent/patch/libtorrent-0001-file-storage-get-or-add-path-linear-dedup.patch @@ -0,0 +1,53 @@ +# UNDF: UNDF-2026-000000777 +# UNDF: (leave blank) +# CWE-407: file_storage::get_or_add_path uses std::find on m_paths vector O(F*P) +# +# file_storage::get_or_add_path() is called once per file added to a torrent. +# For each call it does a linear scan of m_paths (vector) to check for +# duplicate directory paths. For a torrent with F files and P unique paths, the +# total cost of building the file_storage is O(F*P). +# +# Large torrents with 50,000+ files across hundreds of directories hit this on +# every .torrent parse and metadata exchange. At P=500, F=50000, that's 25M +# string comparisons. +# +# Fix: maintain an unordered_map alongside m_paths for +# O(1) lookup. The vector is retained for index-based access. +# +# Severity: MEDIUM (torrent load/parse path, not per-packet) +# Speedup: ~250x at P=500 + +--- a/src/file_storage.cpp ++++ b/src/file_storage.cpp +@@ -210,7 +210,7 @@ + aux::path_index_t file_storage::get_or_add_path(string_view const path) + { +- // do we already have this path in the path list? +- auto const p = std::find(m_paths.rbegin(), m_paths.rend(), path); ++ // O(1) lookup via hash map instead of O(P) linear scan ++ auto const it = m_path_index.find(std::string(path)); + +- if (p == m_paths.rend()) ++ if (it == m_path_index.end()) + { + // no, we don't. add it + auto const ret = m_paths.end_index(); + TORRENT_ASSERT(path.size() == 0 || path[0] != '/'); + m_paths.emplace_back(path.data(), path.size()); ++ m_path_index.emplace(std::string(path), ret); + return ret; + } + else + { +- // yes we do. use it +- return aux::path_index_t{aux::numeric_cast( +- p.base() - m_paths.begin() - 1)}; ++ return it->second; + } + } + +--- a/include/libtorrent/file_storage.hpp ++++ b/include/libtorrent/file_storage.hpp +@@ (add to private members, near m_paths declaration) ++ // O(1) path deduplication index (path string -> path_index_t) ++ std::unordered_map m_path_index; diff --git a/defects/libtorrent/unit/LibtorrentTest$FileStorageDefective.class b/defects/libtorrent/unit/LibtorrentTest$FileStorageDefective.class new file mode 100644 index 0000000000000000000000000000000000000000..bccf58a61fb262c3c8713b8ffe85834e81f720f5 GIT binary patch literal 829 zcmZuw%Wl&^6g^`nb>g~hXq)uqKA-{epbc!;q^%SMAtHqqDHRBIlXwVIj8p7MCD_7G zAU=RiH%LGgkPvHrgTFwCawmft-wfsrIQ^k}mEDY7yP@xRkHs-PDUZFIS34fDC+ac~+udhP5$^J) zPl!X(cfyM2-J_PAkudPj*BHhcinpHL<@*CSyV2=}Euq^>!x?V+y6zf7Vw(nVrxPg) z>(t6&4kfMyfe5QUk0L>}PycUHSfS|}v^5NqdcO2uCCfmzkJ9=Oa+#28FzE!O3P)gt zFR&gSA^8#Z`@ZiL`s$^ld&H=lz*Xc1p05Zu38@x7!0sVMt@1YAw^-J;+e5~{H_R75 z!F_|cd-&Qoes}!iFLzU)iao3tc;FsV7y~-l5^W}sp>#7Ob(XLUNnF59xby%Pk*8Z5 e<}WyG?kBwh6NSESkytv$8cKZzqw?2q9f?0A$+u4c literal 0 HcmV?d00001 diff --git a/defects/libtorrent/unit/LibtorrentTest$FileStorageFixed.class b/defects/libtorrent/unit/LibtorrentTest$FileStorageFixed.class new file mode 100644 index 0000000000000000000000000000000000000000..aa4347969cb4d27dde9775aadf37548bba83b882 GIT binary patch literal 1124 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 paths = new ArrayList<>(); + + /** Simulates get_or_add_path with linear scan */ + int getOrAddPath(String path) { + // std::find(m_paths.rbegin(), m_paths.rend(), path) + for (int i = paths.size() - 1; i >= 0; i--) { + if (paths.get(i).equals(path)) { + return i; + } + } + int ret = paths.size(); + paths.add(path); + return ret; + } + } + + // === FIXED: hash map dedup === + static class FileStorageFixed { + private final List paths = new ArrayList<>(); + private final Map pathIndex = new HashMap<>(); + + int getOrAddPath(String path) { + Integer idx = pathIndex.get(path); + if (idx != null) return idx; + int ret = paths.size(); + paths.add(path); + pathIndex.put(path, ret); + return ret; + } + } + + /** + * Simulate adding F files across P unique directory paths. + * Each file's directory is looked up via get_or_add_path. + */ + static long benchmarkDefective(int numFiles, int numPaths) { + FileStorageDefective fs = new FileStorageDefective(); + String[] dirs = new String[numPaths]; + for (int i = 0; i < numPaths; i++) { + dirs[i] = "dir" + i + "/subdir" + (i % 10); + } + long ops = 0; + for (int f = 0; f < numFiles; f++) { + String dir = dirs[f % numPaths]; + // linear scan: worst case scans all P paths + for (int i = fs.paths.size() - 1; i >= 0; i--) { + ops++; + if (fs.paths.get(i).equals(dir)) break; + } + fs.getOrAddPath(dir); + } + return ops; + } + + static long benchmarkFixed(int numFiles, int numPaths) { + FileStorageFixed fs = new FileStorageFixed(); + String[] dirs = new String[numPaths]; + for (int i = 0; i < numPaths; i++) { + dirs[i] = "dir" + i + "/subdir" + (i % 10); + } + long ops = 0; + for (int f = 0; f < numFiles; f++) { + String dir = dirs[f % numPaths]; + ops++; // O(1) hash lookup + fs.getOrAddPath(dir); + } + return ops; + } + + public static void main(String[] args) { + System.out.println("=== LibtorrentTest: CWE-407 file_storage::get_or_add_path ===\n"); + + // libtorrent-0001: get_or_add_path linear dedup + int numFiles = 50000; + int numPaths = 500; + long defectOps = benchmarkDefective(numFiles, numPaths); + long fixedOps = benchmarkFixed(numFiles, numPaths); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("libtorrent-0001: get_or_add_path linear path dedup%n"); + System.out.printf(" F=%d files, P=%d unique paths%n", numFiles, numPaths); + System.out.printf(" defect ops: %,d%n", defectOps); + System.out.printf(" fixed ops: %,d%n", fixedOps); + System.out.printf(" ratio: %.1fx%n", ratio); + + boolean pass = ratio > 10.0; + System.out.printf(" result: %s%n%n", pass ? "PASS" : "FAIL"); + + // Summary + System.out.println("=== SUMMARY ==="); + System.out.printf("libtorrent-0001 get_or_add_path O(F*P) -> O(F): %s (%.1fx)%n", + pass ? "PASS" : "FAIL", ratio); + + if (!pass) { + System.exit(1); + } + } +} diff --git a/defects/qbittorrent/patch/CLEAN.md b/defects/qbittorrent/patch/CLEAN.md new file mode 100644 index 000000000..01d65e8f9 --- /dev/null +++ b/defects/qbittorrent/patch/CLEAN.md @@ -0,0 +1,28 @@ +# qBittorrent — CWE-407 Scan Result: CLEAN + +Scanned: 2026-03-30 +Target: qBittorrent (C++/Qt) +Source: https://github.com/qbittorrent/qBittorrent + +## Scope + +- `src/base/bittorrent/` — session, torrent impl, tracker, peer management +- `src/base/rss/` — RSS auto-downloader, parser, feed management +- `src/base/search/` — search plugin manager +- `src/gui/` — transfer list, tracker list, tag/category filtering +- `src/webui/` — API controllers, sync controller + +## Finding + +CLEAN. qBittorrent uses appropriate data structures throughout: + +- **Tags**: `OrderedSet` (std::set) — O(log N) membership +- **Categories**: `QHash` — O(1) membership +- **Torrents**: `QHash` — O(1) lookup +- **Banned IPs**: sorted `QStringList` but only accessed from user actions, not hot paths +- **Trackers**: `QHash`/`QSet` for tracker host dedup in filter widgets +- **RSS article IDs**: `QSet` — O(1) dedup +- **Search disabled plugins**: `QStringList` with O(N) contains, but N < 50 always + +No QList/QVector linear membership test found in any hot loop or +per-packet/per-torrent-update path.