2.2 KiB
jami-daemon-0001: std::find on replies vector O(n) per git commit in conversation history load
Target: savoirfairelinux/jami-daemon
Severity: MEDIUM
CWE: CWE-407 (Inefficient Algorithmic Complexity)
File: src/jamidht/conversation.cpp
Lines: 832, 837
Status: PATCHED
Description
Conversation::loadMessages() walks the conversation git log via a callback
(emplaceCb). For each commit it performs two std::find scans on the
replies vector:
- Line 832: Check if
message["reply-to"]is already tracked. - Line 837: Remove
message["id"]from the tracked set.
replies is a std::vector<std::string> that grows as threaded messages
accumulate. If the conversation contains R reply-chains the cost per commit
is O(R), and with C total commits the history load is O(C × R).
Conversations with long threaded discussions (common in P2P group chats) will exhibit quadratic load times when loading or syncing history.
// conversation.cpp:783
std::vector<std::string> replies;
...
// emplaceCb called once per git commit:
if (message.find("reply-to") != message.end()) {
auto it = std::find(replies.begin(), replies.end(), message.at("reply-to")); // O(R)
if (it == replies.end()) {
replies.emplace_back(message.at("reply-to"));
}
}
auto it = std::find(replies.begin(), replies.end(), message.at("id")); // O(R)
if (it != replies.end()) {
replies.erase(it);
}
Fix
Replace std::vector<std::string> replies with std::unordered_set<std::string>:
std::unordered_set<std::string> replies;
...
if (message.find("reply-to") != message.end()) {
replies.insert(message.at("reply-to")); // O(1) avg
}
auto eraseIt = replies.find(message.at("id")); // O(1) avg
if (eraseIt != replies.end()) {
replies.erase(eraseIt);
}
Erase-by-iterator on unordered_set is O(1) amortized, eliminating the O(R)
scan entirely.
Complexity
| Before | After | |
|---|---|---|
| Per commit | O(R) | O(1) amortized |
| Full load (C commits, R replies) | O(C × R) | O(C) |
For a conversation with 1 000 commits and 200 reply-chains: 200 000 → 1 000 operations — 200× speedup.