libtorrent-0001/qbittorrent: CWE-407 scan — 1 defect, 1 CLEAN

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)
This commit is contained in:
russell@unturf.com 2026-03-30 10:39:32 -04:00
parent a0dde8b41a
commit 0b5409ff95
6 changed files with 195 additions and 0 deletions

View file

@ -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<string>) 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<string, path_index_t> 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<std::uint32_t>(
- 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<std::string, aux::path_index_t> m_path_index;

Binary file not shown.

View file

@ -0,0 +1,114 @@
import java.util.*;
/**
* CWE-407 simulation: libtorrent file_storage::get_or_add_path
*
* Reproduces the O(F*P) linear-scan deduplication defect in
* file_storage::get_or_add_path where std::find scans m_paths
* vector for every file added to a torrent.
*
* libtorrent-0001: get_or_add_path uses std::find on vector O(F*P)
* Fix: unordered_map for O(1) lookup O(F) total
*/
public class LibtorrentTest {
// === DEFECTIVE: linear scan dedup (std::find on vector) ===
static class FileStorageDefective {
private final List<String> 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<String> paths = new ArrayList<>();
private final Map<String, Integer> 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);
}
}
}

View file

@ -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<Tag>` (std::set) — O(log N) membership
- **Categories**: `QHash<QString, CategoryOptions>` — O(1) membership
- **Torrents**: `QHash<TorrentID, TorrentImpl*>` — 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<QString>` — 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.