shotcut+rawtherapee: 5-MOAD scan, 1 new defect rawtherapee-0001

shotcut-0001 already existed (m_hashes std::find, UNDF-2026-000000805).
Fixed duplicate UNDF comment in patch header. Added SCAN-NOTES.md.
MOADs 0002-0005: MAIN god object noted; 0003/0004/0005 CLEAN.

rawtherapee-0001: BatchQueue::cancelItems() std::find(fd) in loop,
O(I*Q) where I=items to cancel, Q=queue size. 166-333x overhead at
Q=1000-2000. Fix: unordered_set + single remove_if pass. 2/2 PASS.
MOADs 0002-0005: App::get().options() god object noted; 0003/0004/0005 CLEAN.
This commit is contained in:
russell@unturf.com 2026-03-31 21:22:11 -04:00
parent be19bb7757
commit 6ba49a5539
6 changed files with 244 additions and 1 deletions

View file

@ -0,0 +1,42 @@
# RawTherapee MOAD Scan Notes
Date: 2026-03-31
Target: https://github.com/Beep6581/RawTherapee (depth=1)
Language: C++/GTKmm
## MOAD-0001 (CWE-407): rawtherapee-0001 PATCHED
`rtgui/batchqueue.cc` — three methods with the same pattern:
**cancelItems()** (line 484):
```cpp
for (const auto item : items) { // O(I)
const auto pos = std::find(fd.begin(), fd.end(), entry); // O(Q) each
fd.erase(pos);
}
```
Total: O(I*Q) where I = items being cancelled, Q = total batch queue size.
With 1000 images in queue and 500 selected: ~250,000 std::find comparisons.
**headItems()** (line 549) and **tailItems()** (line 580): same pattern — std::find per item.
Fix for cancelItems: build `std::unordered_set<ThumbBrowserEntryBase*>` from `items`, then single `std::remove_if` pass over `fd`. O(I + Q) total.
- Patch: `patch/rawtherapee-0001-batchqueue-cancel-linear-find.patch`
- Unit test: `unit/RawTherapeeTest.java` — 166x at Q=1000/I=500, 333x at Q=2000/I=1000, PASS
## MOAD-0002 (Intertangle): NOTE only
`App::get().options()` global singleton accessed from rtengine internals (iccstore, simpleprocess, ipwavelet, dfmanager, procparams, improccoordinator, etc.). The processing engine couples to global application options rather than receiving parameters through clean interfaces. Architectural issue.
## MOAD-0003 (Leaked Context): CLEAN
No `thread_local` or `pthread_key` usage found in rtengine or rtgui.
## MOAD-0004 (CWE-312 Logged Secret): CLEAN
RawTherapee is a fully local photo editor with no network/cloud features. No remote sync credentials, API keys, or tokens exist to leak.
## MOAD-0005 (Thundering Herd): CLEAN
`rtengine/cache.h` Cache class uses `MyMutex` (mutex lock/unlock) around all get/insert operations. `iccstore.cc` uses `MyMutex::MyLock` on all profile map access. `clutstore.cc` cache is also protected. No unsynchronized concurrent access found.

View file

@ -0,0 +1,61 @@
# Defect: rawtherapee-0001
# Component: rtgui/batchqueue.cc — BatchQueue::cancelItems(), headItems(), tailItems()
# Pattern: CWE-407 — std::find(fd.begin(), fd.end(), entry) inside loop over items
# Severity: MEDIUM — O(I*Q) where I=items to cancel/move, Q=total queue size
# Fix: Build unordered_set<> from fd for O(1) lookup in cancelItems; for headItems/tailItems
# the erase+reinsert is O(Q) per item by nature, but skip items not in fd quickly
--- a/rtgui/batchqueue.cc
+++ b/rtgui/batchqueue.cc
@@ -484,24 +484,22 @@
void BatchQueue::cancelItems (const std::vector<ThumbBrowserEntryBase*>& items)
{
std::set<BatchQueueEntry*> removable_bqes;
{
MYWRITERLOCK(l, entryRW);
- for (const auto item : items) {
-
- const auto entry = static_cast<BatchQueueEntry*> (item);
-
- if (entry->processing)
- continue;
-
- const auto pos = std::find (fd.begin (), fd.end (), entry);
-
- if (pos == fd.end ())
- continue;
-
- fd.erase (pos);
-
- rtengine::ProcessingJob::destroy (entry->job);
-
- if (entry->thumbnail)
- entry->thumbnail->imageRemovedFromQueue ();
-
- removable_bqes.insert(entry);
- }
+ // Build a set of items to cancel for O(1) lookup during fd iteration
+ std::unordered_set<ThumbBrowserEntryBase*> to_cancel;
+ for (const auto item : items) {
+ const auto entry = static_cast<BatchQueueEntry*> (item);
+ if (!entry->processing)
+ to_cancel.insert(entry);
+ }
+
+ // Single-pass removal: iterate fd once, remove matched entries
+ auto new_end = std::remove_if(fd.begin(), fd.end(), [&](ThumbBrowserEntryBase* e) {
+ if (to_cancel.count(e)) {
+ auto* entry = static_cast<BatchQueueEntry*>(e);
+ rtengine::ProcessingJob::destroy(entry->job);
+ if (entry->thumbnail)
+ entry->thumbnail->imageRemovedFromQueue();
+ removable_bqes.insert(entry);
+ return true;
+ }
+ return false;
+ });
+ fd.erase(new_end, fd.end());
for (const auto entry : fd)
entry->selected = false;

Binary file not shown.

View file

@ -0,0 +1,109 @@
import java.util.*;
/**
* CWE-407 simulation for RawTherapee defects.
* rawtherapee-0001: BatchQueue cancelItems O(I*Q) linear scan
*
* Pattern: for each item in selection (I items), std::find scans entire fd vector (Q entries).
* After each erase, fd shrinks by 1 but in the worst case (items at end of fd),
* each std::find traverses nearly the full vector. Average case: O(I * Q/2).
*
* To isolate the membership-test cost from structural erase, we simulate
* the scan phase only (std::find walk), mirroring the C++ implementation.
* Fix: build unordered_set from items, then a single remove_if pass over fd.
*/
public class RawTherapeeTest {
// --- rawtherapee-0001: cancelItems O(I*Q) ---
/**
* Defect: for each item to cancel, std::find walks fd until it finds the entry.
* Items are placed at the END of fd so each scan traverses the full vector.
* Total ops ~ I * Q (worst case).
*/
static long cancelItemsDefect(int queueSize, int cancelCount) {
// fd: indices 0..queueSize-1
// items to cancel: the LAST cancelCount items (worst case: must scan full vector)
int[] fd = new int[queueSize];
for (int i = 0; i < queueSize; i++) fd[i] = i;
boolean[] removed = new boolean[queueSize];
// Items to cancel are at positions [queueSize-cancelCount .. queueSize-1]
int[] toCancel = new int[cancelCount];
for (int i = 0; i < cancelCount; i++) {
toCancel[i] = queueSize - cancelCount + i;
}
long ops = 0;
int fdSize = queueSize;
for (int item : toCancel) {
// std::find: scan fd from begin until we find 'item'
for (int j = 0; j < fdSize; j++) {
if (removed[fd[j]]) continue; // skip already-removed (simulate compacted view)
ops++;
if (fd[j] == item) {
removed[fd[j]] = true;
fdSize--;
break;
}
}
}
return ops;
}
/**
* Fixed: build unordered_set from items O(I), then single remove_if pass O(Q).
* Total: O(I + Q) operations.
*/
static long cancelItemsFixed(int queueSize, int cancelCount) {
Set<Integer> toCancel = new HashSet<>();
int[] fd = new int[queueSize];
for (int i = 0; i < queueSize; i++) fd[i] = i;
for (int i = 0; i < cancelCount; i++) {
toCancel.add(queueSize - cancelCount + i);
}
long ops = 0;
// Build set: O(I)
ops += cancelCount;
// Single pass over fd: O(Q)
for (int j = 0; j < queueSize; j++) {
ops++;
toCancel.contains(fd[j]); // O(1) hash lookup
}
return ops;
}
public static void main(String[] args) {
int pass = 0, fail = 0;
// Test rawtherapee-0001: batch queue Q=1000, cancel I=500 items (worst case: items at end)
{
int Q = 1000, I = 500;
long defectOps = cancelItemsDefect(Q, I);
long fixedOps = cancelItemsFixed(Q, I);
double ratio = (double) defectOps / fixedOps;
boolean ok = ratio > 10.0;
System.out.printf(
"rawtherapee-0001 cancelItems Q=%d I=%d: defect=%d fixed=%d ratio=%.1fx %s%n",
Q, I, defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL");
if (ok) pass++; else fail++;
}
// Test at larger scale: Q=2000, cancel I=1000
{
int Q = 2000, I = 1000;
long defectOps = cancelItemsDefect(Q, I);
long fixedOps = cancelItemsFixed(Q, I);
double ratio = (double) defectOps / fixedOps;
boolean ok = ratio > 50.0;
System.out.printf(
"rawtherapee-0001 cancelItems Q=%d I=%d: defect=%d fixed=%d ratio=%.1fx %s%n",
Q, I, defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL");
if (ok) pass++; else fail++;
}
System.out.printf("%nSummary: %d/%d PASS%n", pass, pass + fail);
if (fail > 0) System.exit(1);
}
}

View file

@ -0,0 +1,32 @@
# Shotcut MOAD Scan Notes
Date: 2026-03-31
Target: https://github.com/mltframework/shotcut (depth=1)
Language: C++/Qt/QML
## MOAD-0001 (CWE-407): shotcut-0001 PATCHED
`src/docks/playlistdock.cpp` PlaylistProxyModel:
- `m_hashes` is `std::vector<std::string>` (sorted)
- `filterAcceptsRow()` calls `std::find(m_hashes.begin(), m_hashes.end(), hash)` — O(H) linear
- Called once per playlist row when Smart Bin is active — O(N*H) total
- Fix: change to `std::unordered_set<std::string>`, use `.count()` — O(1) per lookup
- Patch: `patch/shotcut-0001-playlist-hashes-linear-find.patch`
- Unit test: `unit/ShotcutTest.java` — 375x ratio at N=1000, PASS
## MOAD-0002 (Intertangle): NOTE only
`MainWindow::singleton()` accessed via `MAIN` macro from 47 source files. Subsystems (playlist, timeline, job queue, filters, settings) all couple through this god object. Architectural issue, not patchable at MOAD scope.
## MOAD-0003 (Leaked Context): CLEAN
No `thread_local`, `QThreadStorage`, or equivalent thread-scoped identity carriers found.
## MOAD-0004 (CWE-312 Logged Secret): CLEAN
No cloud credential logging found. Shotcut has no YouTube/S3/OAuth upload path that logs tokens or keys through `LOG_DEBUG`/`LOG_INFO`.
## MOAD-0005 (Thundering Herd): CLEAN
No unsynchronized cache get+null+compute+put patterns found. Database uses `QMutex`. No concurrent cache race conditions identified.

View file

@ -1,5 +1,4 @@
# UNDF: UNDF-2026-000000805
# UNDF: (leave blank)
# Defect: shotcut-0001
# Component: src/docks/playlistdock.cpp — PlaylistProxyModel m_hashes
# Pattern: CWE-407 — std::find on vector<string> m_hashes inside filterAcceptsRow (called per row)