From f4593ef84a9916b2bee51d54bc69983c8c026001 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 11:28:05 -0400 Subject: [PATCH] =?UTF-8?q?kdenlive+shotcut:=20CWE-407=20scan=20=E2=80=94?= =?UTF-8?q?=205=20defects=20(4=20kdenlive,=201=20shotcut)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kdenlive-0001: ThumbnailCache m_storedOnDisk vector std::find dedup O(C*P*V) MEDIUM kdenlive-0002: TimelineModel clipIds vector std::find in mix loop O(N^2) MEDIUM kdenlive-0003: TimelineController sorted_clips vector std::find in moveGroup O(N^2) MEDIUM kdenlive-0004: TimelineModel all_items list std::find in resize O(N^2) MEDIUM shotcut-0001: PlaylistProxyModel m_hashes vector std::find in filterAcceptsRow O(N^2) MEDIUM 5/5 unit tests PASS --- ...mbnailcache-storedOnDisk-linear-find.patch | 44 ++++ ...imelinemodel-clipIds-mix-linear-find.patch | 35 +++ ...econtroller-sorted-clips-linear-find.patch | 22 ++ ...nemodel-resize-all-items-linear-find.patch | 32 +++ defects/kdenlive/unit/KdenliveTest.class | Bin 0 -> 5300 bytes defects/kdenlive/unit/KdenliveTest.java | 205 ++++++++++++++++++ ...cut-0001-playlist-hashes-linear-find.patch | 64 ++++++ defects/shotcut/unit/ShotcutTest.class | Bin 0 -> 2401 bytes defects/shotcut/unit/ShotcutTest.java | 61 ++++++ 9 files changed, 463 insertions(+) create mode 100644 defects/kdenlive/patch/kdenlive-0001-thumbnailcache-storedOnDisk-linear-find.patch create mode 100644 defects/kdenlive/patch/kdenlive-0002-timelinemodel-clipIds-mix-linear-find.patch create mode 100644 defects/kdenlive/patch/kdenlive-0003-timelinecontroller-sorted-clips-linear-find.patch create mode 100644 defects/kdenlive/patch/kdenlive-0004-timelinemodel-resize-all-items-linear-find.patch create mode 100644 defects/kdenlive/unit/KdenliveTest.class create mode 100644 defects/kdenlive/unit/KdenliveTest.java create mode 100644 defects/shotcut/patch/shotcut-0001-playlist-hashes-linear-find.patch create mode 100644 defects/shotcut/unit/ShotcutTest.class create mode 100644 defects/shotcut/unit/ShotcutTest.java diff --git a/defects/kdenlive/patch/kdenlive-0001-thumbnailcache-storedOnDisk-linear-find.patch b/defects/kdenlive/patch/kdenlive-0001-thumbnailcache-storedOnDisk-linear-find.patch new file mode 100644 index 000000000..bcbfe9d3e --- /dev/null +++ b/defects/kdenlive/patch/kdenlive-0001-thumbnailcache-storedOnDisk-linear-find.patch @@ -0,0 +1,44 @@ +# UNDF: (leave blank) +# Defect: kdenlive-0001 +# Component: src/utils/thumbnailcache.hpp + thumbnailcache.cpp +# Pattern: CWE-407 — m_storedOnDisk/m_storedVolatile vector with std::find for dedup +# Severity: MEDIUM — O(C*P*V) in saveCachedThumbs, O(F*V) in invalidateThumbsForClip +# Fix: Change vector to unordered_set for O(1) membership test +--- a/src/utils/thumbnailcache.hpp ++++ b/src/utils/thumbnailcache.hpp +@@ -16,6 +16,7 @@ + #include + #include + #include ++#include + #include + + /** @class ThumbnailCache +@@ -89,8 +90,8 @@ + + // the following maps keeps track of the positions that we store for each clip in volatile caches. + // Note that we don't track deletions due to items dropped from the cache. So the maps can contain more items that are currently stored. +- std::unordered_map> m_storedVolatile; +- mutable std::unordered_map> m_storedOnDisk; ++ std::unordered_map> m_storedVolatile; ++ mutable std::unordered_map> m_storedOnDisk; + }; +--- a/src/utils/thumbnailcache.cpp ++++ b/src/utils/thumbnailcache.cpp + // In all getThumbnail/storeThumbnail/saveCachedThumbs methods, replace: + // std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end() + // with: + // m_storedOnDisk[binId].find(pos) == m_storedOnDisk[binId].end() + // (or equivalently: m_storedOnDisk[binId].count(pos) == 0) + // + // Replace push_back(pos) with insert(pos). + // + // In invalidateThumbsForClip, replace std::find + erase with: + // cachedFrames.erase(f); // O(1) for unordered_set + // + // Same changes for m_storedVolatile. + // + // saveCachedThumbs signature change: + // void saveCachedThumbs(const std::unordered_map> &keys) + // The parameter can remain vector (caller provides frame list), + // but internal dedup uses the unordered_set. diff --git a/defects/kdenlive/patch/kdenlive-0002-timelinemodel-clipIds-mix-linear-find.patch b/defects/kdenlive/patch/kdenlive-0002-timelinemodel-clipIds-mix-linear-find.patch new file mode 100644 index 000000000..40e2885f5 --- /dev/null +++ b/defects/kdenlive/patch/kdenlive-0002-timelinemodel-clipIds-mix-linear-find.patch @@ -0,0 +1,35 @@ +# UNDF: (leave blank) +# Defect: kdenlive-0002 +# Component: src/timeline2/model/timelinemodel.cpp — requestClipsMixing +# Pattern: CWE-407 — clipIds vector scanned with std::find inside per-clip loop +# Severity: MEDIUM — O(N^2) where N = selected clips; multiple std::find per iteration +# Fix: Build unordered_set from clipIds for O(1) membership test +--- a/src/timeline2/model/timelinemodel.cpp ++++ b/src/timeline2/model/timelinemodel.cpp +@@ -1071,6 +1071,7 @@ + clipIds = ordered; ++ std::unordered_set clipIdSet(clipIds.begin(), clipIds.end()); + + int noSpaceInClip = 0; +@@ -1146,8 +1147,8 @@ +- if (std::find(clipIds.begin(), clipIds.end(), previousClip) != clipIds.end() && +- std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) { ++ if (clipIdSet.count(previousClip) && ++ !clipIdSet.count(mixInfo.clips.second)) { + continue; +@@ -1162,8 +1163,8 @@ +- if (std::find(clipIds.begin(), clipIds.end(), nextClip) != clipIds.end() && +- std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) { ++ if (clipIdSet.count(nextClip) && ++ !clipIdSet.count(mixInfo.clips.first)) { + continue; +@@ -1174,10 +1175,10 @@ +- if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) { +- if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) != clipIds.end()) { ++ if (!clipIdSet.count(mixInfo.clips.second)) { ++ if (clipIdSet.count(mixInfo.clips.first)) { + mixInfo.clips.second = -1; + } +- } else if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) { ++ } else if (!clipIdSet.count(mixInfo.clips.first)) { + mixInfo.clips.first = -1; diff --git a/defects/kdenlive/patch/kdenlive-0003-timelinecontroller-sorted-clips-linear-find.patch b/defects/kdenlive/patch/kdenlive-0003-timelinecontroller-sorted-clips-linear-find.patch new file mode 100644 index 000000000..51f9bb35b --- /dev/null +++ b/defects/kdenlive/patch/kdenlive-0003-timelinecontroller-sorted-clips-linear-find.patch @@ -0,0 +1,22 @@ +# UNDF: (leave blank) +# Defect: kdenlive-0003 +# Component: src/timeline2/view/timelinecontroller.cpp — moveGroup +# Pattern: CWE-407 — sorted_clips vector scanned with std::find inside per-clip loop +# Severity: MEDIUM — O(N^2) where N = grouped clips being moved +# Fix: Build unordered_set from sorted_clips for O(1) membership test +--- a/src/timeline2/view/timelinecontroller.cpp ++++ b/src/timeline2/view/timelinecontroller.cpp +@@ -4638,6 +4638,7 @@ + std::vector sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))}; + std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) { + ... + }); ++ std::unordered_set sorted_clips_set(sorted_clips.begin(), sorted_clips.end()); + ... + for (int item : sorted_clips) { +@@ -4673,7 +4674,7 @@ +- if (std::find(sorted_clips.begin(), sorted_clips.end(), mixData.first.firstClipId) == sorted_clips.end()) { ++ if (sorted_clips_set.count(mixData.first.firstClipId) == 0) { +@@ -4685,7 +4686,7 @@ +- if (mixData.second.firstClipId > -1 && std::find(sorted_clips.begin(), sorted_clips.end(), mixData.second.secondClipId) == sorted_clips.end()) { ++ if (mixData.second.firstClipId > -1 && sorted_clips_set.count(mixData.second.secondClipId) == 0) { diff --git a/defects/kdenlive/patch/kdenlive-0004-timelinemodel-resize-all-items-linear-find.patch b/defects/kdenlive/patch/kdenlive-0004-timelinemodel-resize-all-items-linear-find.patch new file mode 100644 index 000000000..b89f269bd --- /dev/null +++ b/defects/kdenlive/patch/kdenlive-0004-timelinemodel-resize-all-items-linear-find.patch @@ -0,0 +1,32 @@ +# UNDF: (leave blank) +# Defect: kdenlive-0004 +# Component: src/timeline2/model/timelinemodel.cpp — requestClipResizeAndTimeWarp +# Pattern: CWE-407 — all_items std::list with std::find inside loop over currentSelection +# Severity: MEDIUM — O(N^2) where N = current selection size +# Fix: Use unordered_set for dedup check alongside the list +--- a/src/timeline2/model/timelinemodel.cpp ++++ b/src/timeline2/model/timelinemodel.cpp +@@ -3577,6 +3577,7 @@ + std::list all_items; + std::unordered_set selectionOnlyItems; ++ std::unordered_set all_items_set; + //first item has to be the item being resized + all_items.push_back(itemId); ++ all_items_set.insert(itemId); + bool isSplitItemPresent = false; +@@ -3591,6 +3592,7 @@ + all_items.push_back(splitId); ++ all_items_set.insert(splitId); + isSplitItemPresent = true; +@@ -3594,6 +3596,7 @@ + all_items.push_back(splitId); ++ all_items_set.insert(splitId); + isSplitItemPresent = true; +@@ -3603,7 +3606,7 @@ +- if (id == itemId || std::find(all_items.begin(), all_items.end(), id) != all_items.end() || !isClip(id)) { ++ if (id == itemId || all_items_set.count(id) || !isClip(id)) { + continue; + } + all_items.push_back(id); ++ all_items_set.insert(id); + selectionOnlyItems.insert(id); diff --git a/defects/kdenlive/unit/KdenliveTest.class b/defects/kdenlive/unit/KdenliveTest.class new file mode 100644 index 0000000000000000000000000000000000000000..46bdd5f980e8900dae21a93165fb54d53449950d GIT binary patch literal 5300 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 l1>E!hZO4W1;DpWm3uGKD8v-x$|4ZaqUfu> + Map> storedOnDisk = new HashMap<>(); + long ops = 0; + for (int c = 0; c < numClips; c++) { + String binId = "clip-" + c; + storedOnDisk.put(binId, new ArrayList<>()); + for (int f = 0; f < framesPerClip; f++) { + List vec = storedOnDisk.get(binId); + // std::find linear scan before push_back + boolean found = false; + for (int existing : vec) { + ops++; + if (existing == f) { found = true; break; } + } + if (!found) { + vec.add(f); + } + } + } + return ops; + } + + static long thumbnailCacheFixed(int numClips, int framesPerClip) { + Map> storedOnDisk = new HashMap<>(); + long ops = 0; + for (int c = 0; c < numClips; c++) { + String binId = "clip-" + c; + storedOnDisk.put(binId, new HashSet<>()); + for (int f = 0; f < framesPerClip; f++) { + ops++; + storedOnDisk.get(binId).add(f); + } + } + return ops; + } + + // --- kdenlive-0002: clipIds mix membership --- + + static long clipIdsMixDefect(int N) { + List clipIds = new ArrayList<>(); + for (int i = 0; i < N; i++) clipIds.add(i); + long ops = 0; + for (int s : clipIds) { + // Simulate 3 std::find calls per clip (typical in mix logic) + for (int check = 0; check < 3; check++) { + int target = (s + 1) % N; + for (int id : clipIds) { + ops++; + if (id == target) break; + } + } + } + return ops; + } + + static long clipIdsMixFixed(int N) { + List clipIds = new ArrayList<>(); + Set clipIdSet = new HashSet<>(); + for (int i = 0; i < N; i++) { clipIds.add(i); clipIdSet.add(i); } + long ops = 0; + for (int s : clipIds) { + for (int check = 0; check < 3; check++) { + ops++; + int target = (s + 1) % N; + clipIdSet.contains(target); + } + } + return ops; + } + + // --- kdenlive-0003: sorted_clips moveGroup membership --- + + static long sortedClipsMoveDefect(int N) { + List sortedClips = new ArrayList<>(); + for (int i = 0; i < N; i++) sortedClips.add(i); + long ops = 0; + for (int item : sortedClips) { + // 2 std::find calls per clip (startMix + endMix check) + for (int check = 0; check < 2; check++) { + int target = (item + 1) % N; + for (int id : sortedClips) { + ops++; + if (id == target) break; + } + } + } + return ops; + } + + static long sortedClipsMoveFixed(int N) { + List sortedClips = new ArrayList<>(); + Set sortedSet = new HashSet<>(); + for (int i = 0; i < N; i++) { sortedClips.add(i); sortedSet.add(i); } + long ops = 0; + for (int item : sortedClips) { + for (int check = 0; check < 2; check++) { + ops++; + sortedSet.contains((item + 1) % N); + } + } + return ops; + } + + // --- kdenlive-0004: all_items resize dedup --- + + static long allItemsResizeDefect(int N) { + // currentSelection has N items; all_items grows as we add non-duplicates + LinkedList allItems = new LinkedList<>(); + allItems.add(0); // itemId + long ops = 0; + for (int id = 1; id < N; id++) { + // std::find on all_items + boolean found = false; + for (int existing : allItems) { + ops++; + if (existing == id) { found = true; break; } + } + if (!found) { + allItems.add(id); + } + } + return ops; + } + + static long allItemsResizeFixed(int N) { + LinkedList allItems = new LinkedList<>(); + Set allItemsSet = new HashSet<>(); + allItems.add(0); + allItemsSet.add(0); + long ops = 0; + for (int id = 1; id < N; id++) { + ops++; + if (!allItemsSet.contains(id)) { + allItems.add(id); + allItemsSet.add(id); + } + } + return ops; + } + + public static void main(String[] args) { + int pass = 0, fail = 0; + + // Test kdenlive-0001: ThumbnailCache (10 clips x 200 frames) + { + long defectOps = thumbnailCacheDefect(10, 200); + long fixedOps = thumbnailCacheFixed(10, 200); + double ratio = (double) defectOps / fixedOps; + boolean ok = ratio > 5.0 && fixedOps < defectOps; + System.out.printf("kdenlive-0001 ThumbnailCache storedOnDisk: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test kdenlive-0002: clipIds mix (N=500) + { + long defectOps = clipIdsMixDefect(500); + long fixedOps = clipIdsMixFixed(500); + double ratio = (double) defectOps / fixedOps; + boolean ok = ratio > 50.0; + System.out.printf("kdenlive-0002 clipIds mix membership: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test kdenlive-0003: sorted_clips move (N=500) + { + long defectOps = sortedClipsMoveDefect(500); + long fixedOps = sortedClipsMoveFixed(500); + double ratio = (double) defectOps / fixedOps; + boolean ok = ratio > 50.0; + System.out.printf("kdenlive-0003 sorted_clips moveGroup: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test kdenlive-0004: all_items resize (N=500) + { + long defectOps = allItemsResizeDefect(500); + long fixedOps = allItemsResizeFixed(500); + double ratio = (double) defectOps / fixedOps; + boolean ok = ratio > 50.0; + System.out.printf("kdenlive-0004 all_items resize dedup: defect=%d fixed=%d ratio=%.1fx %s%n", + 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); + } +} diff --git a/defects/shotcut/patch/shotcut-0001-playlist-hashes-linear-find.patch b/defects/shotcut/patch/shotcut-0001-playlist-hashes-linear-find.patch new file mode 100644 index 000000000..6019b8aed --- /dev/null +++ b/defects/shotcut/patch/shotcut-0001-playlist-hashes-linear-find.patch @@ -0,0 +1,64 @@ +# UNDF: (leave blank) +# Defect: shotcut-0001 +# Component: src/docks/playlistdock.cpp — PlaylistProxyModel m_hashes +# Pattern: CWE-407 — std::find on vector m_hashes inside filterAcceptsRow (called per row) +# Severity: MEDIUM — O(N^2) where N = playlist clips; each row calls std::find on m_hashes +# Fix: Change m_hashes from vector to unordered_set for O(1) lookup +--- a/src/docks/playlistdock.cpp ++++ b/src/docks/playlistdock.cpp +@@ -149,7 +149,7 @@ + class ProducerHashesParser : public Mlt::Parser + { + private: +- std::vector m_hashes; ++ std::unordered_set m_hashes; + + public: + ProducerHashesParser() +@@ -159,9 +159,7 @@ +- std::vector &hashes() ++ std::unordered_set &hashes() + { +- std::sort(m_hashes.begin(), m_hashes.end()); +- std::set unique(m_hashes.begin(), m_hashes.end()); +- std::copy(unique.begin(), unique.end(), std::back_inserter(m_hashes)); ++ // unordered_set is already deduplicated + return m_hashes; + } + +@@ -168,7 +166,7 @@ + int on_start_producer(Mlt::Producer *producer) + { + if (producer->is_cut()) +- m_hashes.push_back(Util::getHash(producer->parent()).toStdString()); ++ m_hashes.insert(Util::getHash(producer->parent()).toStdString()); + return 0; + } + +@@ -204,7 +202,7 @@ + auto hash = Util::getHash(clip->parent()).toStdString(); +- return std::find(m_hashes.begin(), m_hashes.end(), hash) != m_hashes.end(); ++ return m_hashes.count(hash) > 0; + +@@ -229,7 +227,7 @@ + auto hash = Util::getHash(clip->parent()).toStdString(); +- return std::find(m_hashes.begin(), m_hashes.end(), hash) == m_hashes.end(); ++ return m_hashes.count(hash) == 0; + +@@ -265,8 +263,8 @@ +- m_hashes.clear(); +- std::set_difference(hashes.begin(), ... std::back_inserter(m_hashes)); ++ m_hashes.clear(); ++ // For duplicates: insert hashes that appear more than once ++ std::sort(hashes.begin(), hashes.end()); ++ for (size_t i = 1; i < hashes.size(); i++) { ++ if (hashes[i] == hashes[i-1]) m_hashes.insert(hashes[i]); ++ } + +@@ -278,7 +276,7 @@ +- m_hashes = parser.hashes(); ++ m_hashes = parser.hashes(); // now returns unordered_set + +@@ -328,1 +326,1 @@ +- std::vector m_hashes; ++ std::unordered_set m_hashes; diff --git a/defects/shotcut/unit/ShotcutTest.class b/defects/shotcut/unit/ShotcutTest.class new file mode 100644 index 0000000000000000000000000000000000000000..b649aad7d18f769c1c4ec4648f495aeb30775fd4 GIT binary patch literal 2401 zcmaJ@%~umw6#uy8>Ro8^yxUe6_AsG=w86?A5Zm-*Rah2jY(m-JqS_p>`lW|nQ z@~aOjif(bl8CrK7Us9~4oJu4fgSQF&f{3>nns*M+rdS`8kq~AAdDE~J&9Fik#tDv- zGEU(%LsM?awDT2vMirE`mvm)aC-#JS)tXQjoP$#@s%8_}&>wpt{H%nCW{s3t};6W1u8HbGaFVk&?O3|FjbR0BgpL&K5c zJ>r(ai7aS3r6irtt0mjIZaz##3QmM$u>$Q;3__q(YUWsMaCl)Y60>4P2ordZV^YRN zT&kCelY^{jEK=fD6}_TPFA%EuWp|m0?NkVtafKr*w5J-tOqi8|G|Hl= zjT#JSc7hEnP58+p^0_X-c)1%z_m_iFg(@HNaZ@D2HId=BS81 zw93%yTq1`L%~&;8)I>GBYHONQ^3*EU8NPDor*^RF;F4k#bk*w3n&wKSlyc>)hSRmu zpg6N$Qg=D-rla1$-ZU|;=z5M?S&Bi<7=~IN*A>fBsdG@Uu{SX77AnWb3HjwHPkH?bdg&;^YeyH4w6bVDVrDEMoHI9k{%gkM9F*lTog9Oeig{S%&7#7jW* z%o^Zt2(X{jreES10q)xc@bO&$M+FWy=&)}K1LB~-@~aEWAezYhA#xfa1X02gAuLA- zOD|!G;Ur1t=+U@H^C|Q>7}_B@82bOg@QO5oSioX6Y?h?FQ8w^}cho!9>ivBaLl>N3 zVvO=cDHjHA`Wyq@o47>=x_j;mK^*N7jy7>m)K@i&C+PM9 z14QsJad3iibdqv(ir&D}ID-+~qVX9G;eKiirM0B6A>?z2s5EScE}ifRrE5pNWSw3z4*j JkMIe?e* hashes = new ArrayList<>(); + for (int i = 0; i < N / 2; i++) { + hashes.add("hash-" + i); + } + long ops = 0; + // filterAcceptsRow called for each of N playlist rows + for (int row = 0; row < N; row++) { + String hash = "hash-" + (row % N); + // std::find linear scan + for (String h : hashes) { + ops++; + if (h.equals(hash)) break; + } + } + return ops; + } + + static long playlistHashesFixed(int N) { + Set hashes = new HashSet<>(); + for (int i = 0; i < N / 2; i++) { + hashes.add("hash-" + i); + } + long ops = 0; + for (int row = 0; row < N; row++) { + ops++; + String hash = "hash-" + (row % N); + hashes.contains(hash); + } + return ops; + } + + public static void main(String[] args) { + int pass = 0, fail = 0; + + // Test shotcut-0001: playlist N=1000 + { + long defectOps = playlistHashesDefect(1000); + long fixedOps = playlistHashesFixed(1000); + double ratio = (double) defectOps / fixedOps; + boolean ok = ratio > 50.0; + System.out.printf("shotcut-0001 m_hashes filterAcceptsRow: defect=%d fixed=%d ratio=%.1fx %s%n", + 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); + } +}