From 13db08af50112dcef470979dc43e35e1290f5b23 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 11:28:25 -0400 Subject: [PATCH] =?UTF-8?q?darktable+scribus:=20CWE-407=20scan=20=E2=80=94?= =?UTF-8?q?=206=20defects=20(3=20darktable,=203=20scribus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit darktable-0001: map_locations image diff g_list_find O(N*M) MEDIUM 375x darktable-0002: tags _tag_add_tags_to_list g_list_find O(T*L) MEDIUM 375x darktable-0003: map view clustering g_list_find(sel_imgs) O(I²×S) HIGH 160x scribus-0001: getSortedStyleList retList.contains O(N²) MEDIUM 167x (×4 copies) scribus-0002: getUsedPatterns results.contains O(I×R) MEDIUM 98x scribus-0003: Selection::addItems m_SelList.contains O(N×M) MEDIUM 2100x --- ...1-map-locations-image-diff-list-find.patch | 90 ++++++++ ...arktable-0002-tags-add-to-list-dedup.patch | 85 ++++++++ ...ktable-0003-map-view-selection-check.patch | 58 ++++++ defects/darktable/unit/DarktableTest.class | Bin 0 -> 4871 bytes defects/darktable/unit/DarktableTest.java | 162 +++++++++++++++ ...ibus-0001-sorted-style-list-contains.patch | 75 +++++++ ...ibus-0002-get-used-patterns-contains.patch | 67 ++++++ ...bus-0003-selection-addItems-contains.patch | 44 ++++ defects/scribus/unit/ScribusTest.class | Bin 0 -> 5289 bytes defects/scribus/unit/ScribusTest.java | 195 ++++++++++++++++++ 10 files changed, 776 insertions(+) create mode 100644 defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch create mode 100644 defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch create mode 100644 defects/darktable/patch/darktable-0003-map-view-selection-check.patch create mode 100644 defects/darktable/unit/DarktableTest.class create mode 100644 defects/darktable/unit/DarktableTest.java create mode 100644 defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch create mode 100644 defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch create mode 100644 defects/scribus/patch/scribus-0003-selection-addItems-contains.patch create mode 100644 defects/scribus/unit/ScribusTest.class create mode 100644 defects/scribus/unit/ScribusTest.java diff --git a/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch b/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch new file mode 100644 index 000000000..f24632c44 --- /dev/null +++ b/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch @@ -0,0 +1,90 @@ +# UNDF: UNDF-2026-000000795 +# UNDF: (leave blank) +# darktable-0001: dt_map_location_update_images g_list_find O(N*M) image diff +# +# In src/common/map_locations.c, dt_map_location_update_images() computes the +# symmetric difference between old images (imgs) and new images (new_imgs) by +# calling g_list_find() inside two sequential for-loops. Each g_list_find() is +# O(M) on a GList, making each loop O(N*M). For a location covering N=1000 +# geotagged images with M=1000 new images, this is O(N*M) = 1,000,000 +# pointer comparisons instead of O(N+M) with a hash set. +# +# Similarly, dt_map_location_update_locations() does the same pattern with +# old_tags vs tags lists: two loops with g_list_find() inside each. +# +# Fix: build a GHashTable from one list, then iterate the other with O(1) +# hash lookups. +# +# Severity: MEDIUM — triggered on every map location update; scales with +# collection size. +--- a/src/common/map_locations.c ++++ b/src/common/map_locations.c +@@ -535,14 +535,22 @@ + // clean up locations which are not valid anymore ++ GHashTable *tag_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *tag = (GList *)tags; tag; tag = g_list_next(tag)) ++ g_hash_table_add(tag_set, tag->data); ++ + for(GList *tag = old_tags; tag; tag = g_list_next(tag)) + { +- if(!g_list_find((GList *)tags, tag->data)) ++ if(!g_hash_table_contains(tag_set, tag->data)) + { + dt_tag_detach(GPOINTER_TO_INT(tag->data), imgid, + FALSE, FALSE); + } + } ++ g_hash_table_destroy(tag_set); + + // add new locations ++ GHashTable *old_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *tag = old_tags; tag; tag = g_list_next(tag)) ++ g_hash_table_add(old_set, tag->data); ++ + for(GList *tag = (GList *)tags; tag; tag = g_list_next(tag)) + { +- if(!g_list_find(old_tags, tag->data)) ++ if(!g_hash_table_contains(old_set, tag->data)) + { + dt_tag_attach(GPOINTER_TO_INT(tag->data), imgid, + FALSE, FALSE); + } + } ++ g_hash_table_destroy(old_set); + g_list_free(old_tags); + } + +@@ -558,14 +566,22 @@ + gboolean res = FALSE; + // detach images which are not in location anymore ++ GHashTable *new_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *img = new_imgs; img; img = g_list_next(img)) ++ g_hash_table_add(new_set, img->data); ++ + for(GList *img = imgs; img; img = g_list_next(img)) + { +- if(!g_list_find(new_imgs, img->data)) ++ if(!g_hash_table_contains(new_set, img->data)) + { + dt_tag_detach(ld->id, GPOINTER_TO_INT(img->data), FALSE, FALSE); + res = TRUE; + } + } ++ g_hash_table_destroy(new_set); + + // add new images to location ++ GHashTable *old_img_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *img = imgs; img; img = g_list_next(img)) ++ g_hash_table_add(old_img_set, img->data); ++ + for(GList *img = new_imgs; img; img = g_list_next(img)) + { +- if(!g_list_find(imgs, img->data)) ++ if(!g_hash_table_contains(old_img_set, img->data)) + { + dt_tag_attach(ld->id, GPOINTER_TO_INT(img->data), FALSE, FALSE); + res = TRUE; + } + } ++ g_hash_table_destroy(old_img_set); + g_list_free(new_imgs); diff --git a/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch b/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch new file mode 100644 index 000000000..67d699e8a --- /dev/null +++ b/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch @@ -0,0 +1,85 @@ +# UNDF: UNDF-2026-000000796 +# UNDF: (leave blank) +# darktable-0002: _tag_add_tags_to_list g_list_find O(T*L) dedup +# +# In src/common/tags.c, _tag_add_tags_to_list() iterates over a list of tags +# to add, calling g_list_find(*list, t->data) on the accumulator list for each. +# As the accumulator grows, each find is O(L), making the overall complexity +# O(T*L) ≈ O(N²) when adding N tags. +# +# The same file also has _get_tb_removed_tag_string_values() and +# _get_tb_added_tag_string_values() which call g_list_find(a, b->data) and +# g_list_find(b, a->data) inside their respective loops — O(B*A) for the +# before/after tag diff used in undo operations. +# +# Fix: for _tag_add_tags_to_list, maintain a companion GHashTable for O(1) +# membership. For the tb_ functions, build a hash set from one side, iterate +# the other. +# +# Severity: MEDIUM — triggered during batch tag operations (e.g. applying tags +# to multiple selected images). With T=500 tags, ~125,000 comparisons instead +# of ~500. +--- a/src/common/tags.c ++++ b/src/common/tags.c +@@ -38,10 +38,13 @@ + static gchar *_get_tb_removed_tag_string_values(GList *before, + GList *after) + { +- GList *a = after; + gchar *tag_list = NULL; ++ GHashTable *after_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *a = after; a; a = g_list_next(a)) ++ g_hash_table_add(after_set, a->data); ++ + for(GList *b = before; b; b = g_list_next(b)) + { +- if(!g_list_find(a, b->data)) ++ if(!g_hash_table_contains(after_set, b->data)) + { + dt_util_str_cat(&tag_list, "%d,", GPOINTER_TO_INT(b->data)); + } + } ++ g_hash_table_destroy(after_set); + if(tag_list) tag_list[strlen(tag_list) - 1] = '\0'; +@@ -54,10 +57,13 @@ + static gchar *_get_tb_added_tag_string_values(const dt_imgid_t img, + GList *before, + GList *after) + { +- GList *b = before; + gchar *tag_list = NULL; ++ GHashTable *before_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *b = before; b; b = g_list_next(b)) ++ g_hash_table_add(before_set, b->data); ++ + for(GList *a = after; a; a = g_list_next(a)) + { +- if(!g_list_find(b, a->data)) ++ if(!g_hash_table_contains(before_set, a->data)) + { + // ...tag_list building... + } + } ++ g_hash_table_destroy(before_set); +@@ -378,6 +384,9 @@ + static gboolean _tag_add_tags_to_list(GList **list, + const GList *tags) + { + gboolean res = FALSE; ++ GHashTable *seen = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *existing = *list; existing; existing = g_list_next(existing)) ++ g_hash_table_add(seen, existing->data); ++ + for(const GList *t = tags; t; t = g_list_next(t)) + { +- if(!g_list_find(*list, t->data)) ++ if(!g_hash_table_contains(seen, t->data)) + { + *list = g_list_prepend(*list, t->data); ++ g_hash_table_add(seen, t->data); + res = TRUE; + } + } ++ g_hash_table_destroy(seen); + return res; + } diff --git a/defects/darktable/patch/darktable-0003-map-view-selection-check.patch b/defects/darktable/patch/darktable-0003-map-view-selection-check.patch new file mode 100644 index 000000000..8e4663e9a --- /dev/null +++ b/defects/darktable/patch/darktable-0003-map-view-selection-check.patch @@ -0,0 +1,58 @@ +# UNDF: UNDF-2026-000000797 +# UNDF: (leave blank) +# darktable-0003: map view clustering g_list_find(sel_imgs) inside O(I*J) loop +# +# In src/views/map.c, the map clustering code iterates over all images (i-loop) +# and for each cluster, iterates again over all images (j-loop). Inside the +# j-loop, g_list_find(sel_imgs, GINT_TO_POINTER(p[j].imgid)) performs a linear +# scan of the selected-images list (length S). Total complexity: O(I*J*S) +# which for 5000 map images with 500 selected is O(5000*5000*500) = 12.5 +# billion pointer comparisons. +# +# The outer i-loop also calls g_list_find(sel_imgs, ...) for NOISE points +# and cluster heads — each O(S), contributing O(I*S) additional. +# +# Fix: build a GHashTable from sel_imgs before the loop, replace g_list_find +# with g_hash_table_contains for O(1) lookup. +# +# Severity: HIGH — map view is redrawn on every pan/zoom with the full image +# collection; O(I²×S) collapses to O(I²+S). +--- a/src/views/map.c ++++ b/src/views/map.c +@@ -1506,6 +1506,12 @@ + gboolean *processed = calloc(num_clusters+1,sizeof(gboolean)); + GList *sel_imgs = dt_act_on_get_images(FALSE, FALSE, FALSE); ++ ++ // Build hash set for O(1) selection membership test ++ GHashTable *sel_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *si = sel_imgs; si; si = g_list_next(si)) ++ g_hash_table_add(sel_set, si->data); ++ + int group = -1; + for(i = 0; i< img_count; i++) + { +@@ -1523,8 +1529,7 @@ + if(sel_imgs) +- entry->selected_in_group = g_list_find((GList *)sel_imgs, +- GINT_TO_POINTER(entry->imgid)) +- ? TRUE : FALSE; ++ entry->selected_in_group = ++ g_hash_table_contains(sel_set, GINT_TO_POINTER(entry->imgid)); + lib->images = g_slist_prepend(lib->images, entry); +@@ -1540,8 +1545,7 @@ +- entry->selected_in_group = (sel_imgs && g_list_find((GList *)sel_imgs, +- GINT_TO_POINTER(p[i].imgid))) +- ? TRUE : FALSE; ++ entry->selected_in_group = sel_imgs ++ && g_hash_table_contains(sel_set, GINT_TO_POINTER(p[i].imgid)); +@@ -1557,7 +1561,7 @@ + if(sel_imgs && !entry->selected_in_group) + { +- if(g_list_find((GList *)sel_imgs, GINT_TO_POINTER(p[j].imgid))) ++ if(g_hash_table_contains(sel_set, GINT_TO_POINTER(p[j].imgid))) + entry->selected_in_group = TRUE; + } + } + free(processed); ++ g_hash_table_destroy(sel_set); + g_list_free(sel_imgs); diff --git a/defects/darktable/unit/DarktableTest.class b/defects/darktable/unit/DarktableTest.class new file mode 100644 index 0000000000000000000000000000000000000000..db76a04ae9e2bfb7e5d678e9a78cdbca18efb09a GIT binary patch literal 4871 zcmb7IZFCgn8NIVRv$I);kAzG}0)Yh*nvhRG7EK@$l28^yL^mb|v@m2RVa;YI-JKx# zZ5yj%wMxYgEQ+=g+p4YDLJ)2H)8oJXvh6wcchBkZU;oGv=zV8qv$FxT<>buQ`}N%W z-1olE^7)mI{{)~5e~ci2pbA-of{=v8_Zx?e*0hlsYrW&({pN@*A+#lxN!gteg7po9 zHzAA?6%h?LL6fl1={{wr(ye`|96N_owwX0-D=VR*zM-$s9QVF=+=PW#q++p#Qj|$3 zyS9r2^^lS4H;-^WuDMr?DA%B)LV}!eK9}d`_C?yEqEbR>0YE%sn`34+f~Ba|unfyZ zco}=pNKcv0%yAVf@*^A!JB{3-glR`mgQy^6m4vE#ABb)%ohBzKE7Rdpu|`9!m{-oF z9yG;5Yc+fi^+a%3ta8Vwpds$@;Kb?$MH<$jQALx6X0%9HP@sVUFwhYzV;iYVPJ-@5 z;D_3vs_lCHbeT#;zV(RjFQ|(9{F_R^@ z4ck<7Y1oeLB9SGI=4^9*WHT}5H^SW_fJaNXli zmixIQf69s68hMJc>4))G>AmxcrexK6E+ z%b8h`lw40LC(@OaAngrptL5J1KNa^dcLl+B<-7N0vsSi@J@#tYhkGR~oiN7DZY#qS z52frwe9GBI#&*B*>uZ(2&l|8|Tg2 zFev0172@A0RFl$hzfg^C8)IF`1)8MU+9p-Uw_=sW`0R0UXI|!!ZSCXOSs+G?h*hbN#+}T;Pb|K@AV#VTawx z!Ib$xcY2Cy&d$Z}Z2tM%Wna8uml*u0hR4L{2rLtynPWTDiu%|?>x~(L|xTr6cG5e<`4w~5kv3=-# z>@hMmrg?BghT zg0?S|F;JT+PS{%i0hF}(J{-8N@2WP)V^t6`=Stj6Txrso{IN{w|pR>wvd)o z=CCzxwz|=}cC*76VKs16!mEB;^g6mh?KCpUw3(~zv#jx{NsnJgq8@Bt8j1!E9Gx_; z)7?*;Tf}tuCGJhpZKTr)Rx2G6w0I_CW*tqLITasDsJjN!^+Hwgk%SfB-j#;o;=(F! zi~t=+T3jjoolhE#hgBD^ukfvgcZqjp-FZkKijH`dSJ4bac%bv><^=32E#O_{*LQBej95VYn*^K5LZ)8wURzC_c|RU%zrjE&07bOm4#b6rKkDN)427m7_ufts;G+{a(@vslRVt>VvTm z_Yn6~yIQ4s@~9I}1$l(Ut_kbPX_Uw@cjJ7ahfZQqUN(Z>nX7O5lI`lglY!mFUryTy zRsh|O5p)w?4~vdo2igjRu5cv>*upDVBMI%-Q}|zFF|WS7vnujno+3;kr6E`9LZ$ul zMOwhSVIE#$$%jp~`#_92AU^kjSmDTP7ni-A`RZdvcHn_pFT)+p77-bQ&-LBvkWzlB~8IjS8mR&C~gR_&x; zwb>h~ww*skLBo=XSHmD#xQD46#tt5Zebj3L_wi{z#&Ce8;eJ+l22SB1-eIBzJFB2v zqg?$PNYW)Nof9x?N-wjYa-j%^jg)K;#uQ!Q4jyjOm6lj27*lnXuRTq=+Mc%=@}Zc zMR)okQD=xRO;l43p5;(4Ly@J+SSU%~pn_su>xfmk#FNeSVY z(R%7#O!bPiye!mPiqE$AOm^9I^!ZiwCz1YUT4t`gsd*MhH_Oibk7n%`##Q*>R literal 0 HcmV?d00001 diff --git a/defects/darktable/unit/DarktableTest.java b/defects/darktable/unit/DarktableTest.java new file mode 100644 index 000000000..b8e69e514 --- /dev/null +++ b/defects/darktable/unit/DarktableTest.java @@ -0,0 +1,162 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for darktable defects. + * + * darktable-0001: map_locations image diff via g_list_find O(N*M) + * darktable-0002: _tag_add_tags_to_list dedup via g_list_find O(T*L) + * darktable-0003: map view clustering sel_imgs g_list_find inside O(I*J) loop + */ +public class DarktableTest { + + // --------------------------------------------------------------- + // darktable-0001: map_locations symmetric diff O(N*M) vs O(N+M) + // --------------------------------------------------------------- + + /** Defective: g_list_find inside loop — O(N*M) */ + static int mapLocationDiffDefective(List oldImgs, List newImgs) { + int ops = 0; + // detach old not in new + for (int id : oldImgs) { + for (int nid : newImgs) { ops++; if (nid == id) break; } + } + // attach new not in old + for (int id : newImgs) { + for (int oid : oldImgs) { ops++; if (oid == id) break; } + } + return ops; + } + + /** Fixed: GHashTable for O(1) lookup — O(N+M) */ + static int mapLocationDiffFixed(List oldImgs, List newImgs) { + int ops = 0; + Set newSet = new HashSet<>(newImgs); ops += newImgs.size(); + for (int id : oldImgs) { newSet.contains(id); ops++; } + Set oldSet = new HashSet<>(oldImgs); ops += oldImgs.size(); + for (int id : newImgs) { oldSet.contains(id); ops++; } + return ops; + } + + static void testMapLocationDiff() { + int N = 1000; + List oldImgs = new ArrayList<>(); + List newImgs = new ArrayList<>(); + for (int i = 0; i < N; i++) { oldImgs.add(i); newImgs.add(i + N / 2); } + + int defectOps = mapLocationDiffDefective(oldImgs, newImgs); + int fixedOps = mapLocationDiffFixed(oldImgs, newImgs); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0001 map_locations diff: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // darktable-0002: tag dedup via g_list_find O(T*L) ≈ O(N²) + // --------------------------------------------------------------- + + /** Defective: linear scan dedup */ + static int tagAddToListDefective(List existingTags, List newTags) { + int ops = 0; + List list = new ArrayList<>(existingTags); + for (int tag : newTags) { + boolean found = false; + for (int t : list) { ops++; if (t == tag) { found = true; break; } } + if (!found) list.add(tag); + } + return ops; + } + + /** Fixed: HashSet companion */ + static int tagAddToListFixed(List existingTags, List newTags) { + int ops = 0; + Set seen = new HashSet<>(existingTags); ops += existingTags.size(); + for (int tag : newTags) { + ops++; + if (!seen.contains(tag)) { seen.add(tag); } + } + return ops; + } + + static void testTagAddToList() { + int N = 500; + List existing = new ArrayList<>(); + List toAdd = new ArrayList<>(); + // all unique — worst case for dedup + for (int i = 0; i < N; i++) existing.add(i); + for (int i = N; i < 2 * N; i++) toAdd.add(i); + + int defectOps = tagAddToListDefective(existing, toAdd); + int fixedOps = tagAddToListFixed(existing, toAdd); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0002 tag dedup: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 50 : "Expected >50x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // darktable-0003: map view sel_imgs inside O(I*J) clustering loop + // --------------------------------------------------------------- + + /** Defective: g_list_find(sel_imgs, imgid) inside nested I×J loop — O(I²×S) */ + static long mapViewClusterDefective(int imgCount, List selImgs, int[] clusterIds) { + long ops = 0; + for (int i = 0; i < imgCount; i++) { + int group = clusterIds[i]; + for (int j = 0; j < imgCount; j++) { + if (clusterIds[j] == group) { + // linear scan of sel_imgs + for (int s : selImgs) { ops++; if (s == j) break; } + } + } + } + return ops; + } + + /** Fixed: HashSet for O(1) selection check — O(I²+S) */ + static long mapViewClusterFixed(int imgCount, List selImgs, int[] clusterIds) { + long ops = 0; + Set selSet = new HashSet<>(selImgs); ops += selImgs.size(); + for (int i = 0; i < imgCount; i++) { + int group = clusterIds[i]; + for (int j = 0; j < imgCount; j++) { + if (clusterIds[j] == group) { + selSet.contains(j); ops++; + } + } + } + return ops; + } + + static void testMapViewCluster() { + int I = 500; + int S = 200; + List selImgs = new ArrayList<>(); + for (int i = 0; i < S; i++) selImgs.add(i); + // All in one cluster — worst case + int[] clusterIds = new int[I]; + Arrays.fill(clusterIds, 1); + + long defectOps = mapViewClusterDefective(I, selImgs, clusterIds); + long fixedOps = mapViewClusterFixed(I, selImgs, clusterIds); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0003 map view cluster sel: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + + public static void main(String[] args) { + testMapLocationDiff(); + testTagAddToList(); + testMapViewCluster(); + System.out.println("\nAll 3 darktable CWE-407 tests PASSED."); + } +} diff --git a/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch b/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch new file mode 100644 index 000000000..cef229503 --- /dev/null +++ b/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch @@ -0,0 +1,75 @@ +# UNDF: UNDF-2026-000000802 +# UNDF: (leave blank) +# scribus-0001: getSortedStyleList retList.contains O(N²) dedup +# +# In scribus/scribusdoc.cpp, four identical functions — getSortedStyleList(), +# getSortedCharStyleList(), getSortedTableStyleList(), getSortedCellStyleList() +# — walk the style parent chain and accumulate indices into retList using +# QList::contains() to dedup. QList::contains is O(N) per call, and the +# outer loop is O(N) over all styles, making overall complexity O(N²). +# +# Additionally, the inner while-loop walking up the parent chain accumulates +# into retList2 with retList2.contains(pp) — a minor O(depth²) per style. +# +# For a document with N=500 paragraph styles (plausible in long-form publishing +# with inherited house styles), this is ~125,000 linear scans instead of ~500 +# hash lookups. +# +# Fix: maintain a companion QSet for O(1) membership test alongside the +# ordered QList retList. +# +# Severity: MEDIUM — 4 identical defect sites; triggered on style +# reorder/display; scales with style count. +--- a/scribus/scribusdoc.cpp ++++ b/scribus/scribusdoc.cpp +@@ -1198,6 +1198,7 @@ + QList ScribusDoc::getSortedStyleList() const + { + QList retList; ++ QSet retSet; + for (int i = 0; i < m_docParagraphStyles.count(); ++i) + { + if (m_docParagraphStyles[i].parent().isEmpty()) + { +- if (!retList.contains(i)) ++ if (!retSet.contains(i)) ++ { + retList.append(i); ++ retSet.insert(i); ++ } + continue; + } + + QList retList2; ++ QSet retSet2; + ... + retList2.prepend(i); ++ retSet2.insert(i); + while ((!par.isEmpty()) && (par != name)) + { + int pp = m_docParagraphStyles.find(par); +- if ((pp >= 0) && (!retList2.contains(pp))) ++ if ((pp >= 0) && (!retSet2.contains(pp))) ++ { + retList2.prepend(pp); ++ retSet2.insert(pp); ++ } + par = (pp >= 0) ? m_docParagraphStyles[pp].parent() : QString(); + } + for (int r = 0; r < retList2.count(); ++r) + { +- if (!retList.contains(retList2[r])) ++ if (!retSet.contains(retList2[r])) ++ { + retList.append(retList2[r]); ++ retSet.insert(retList2[r]); ++ } + } + } + return retList; + } + + // Same fix applies identically to: + // getSortedCharStyleList() (line 1230) + // getSortedTableStyleList() (line 1262) + // getSortedCellStyleList() (line 1294) diff --git a/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch b/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch new file mode 100644 index 000000000..fe40fe08a --- /dev/null +++ b/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch @@ -0,0 +1,67 @@ +# UNDF: UNDF-2026-000000803 +# UNDF: (leave blank) +# scribus-0002: getUsedPatterns results.contains O(I×R) pattern collection +# +# In scribus/scribusdoc.cpp, getUsedPatterns() iterates over all page items +# and for each checks results.contains(pattern) — a QStringList linear scan +# O(R) where R is the number of results accumulated so far. With I page items +# each potentially contributing 3 pattern names (fill, stroke, mask), the +# overall complexity is O(3×I×R). For a document with 2000 items and 200 +# patterns, that is ~1.2M string comparisons. +# +# The same pattern repeats in getUsedPatternsSelection() and +# getUsedPatternsHelper() and getPatternDependencyList(). +# +# Fix: maintain a companion QSet for O(1) membership alongside the +# QStringList for ordered results. +# +# Severity: MEDIUM — triggered during document save, export, and pattern +# cleanup; scales with item count × pattern count. +--- a/scribus/scribusdoc.cpp ++++ b/scribus/scribusdoc.cpp +@@ -3933,6 +3933,7 @@ + QStringList ScribusDoc::getUsedPatterns() const + { + QStringList results; ++ QSet resultSet; + + for (PageItemIterator it(this, ...); *it; ++it) + { + const PageItem* currItem = *it; +- if ((!results.contains(currItem->pattern())) && ...) ++ if ((!resultSet.contains(currItem->pattern())) && ...) ++ { + results.append(currItem->pattern()); ++ resultSet.insert(currItem->pattern()); ++ } + if (!currItem->strokePattern().isEmpty()) + { +- if (!results.contains(currItem->strokePattern())) ++ if (!resultSet.contains(currItem->strokePattern())) ++ { + results.append(currItem->strokePattern()); ++ resultSet.insert(currItem->strokePattern()); ++ } + } + if (!currItem->patternMask().isEmpty()) + { +- if (!results.contains(currItem->patternMask())) ++ if (!resultSet.contains(currItem->patternMask())) ++ { + results.append(currItem->patternMask()); ++ resultSet.insert(currItem->patternMask()); ++ } + } + } + + // Same fix for the inner docPatterns loop (lines 3955-3983) +- // Replace results.contains(...) with resultSet.contains(...) ++ // and add resultSet.insert(...) after each results.append(...) + + return results; + } + + // Same fix applies to: + // getUsedPatternsSelection() (line 3988) + // getUsedPatternsHelper() (line 4036) + // getPatternDependencyList() (line 4087) diff --git a/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch b/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch new file mode 100644 index 000000000..6150c150b --- /dev/null +++ b/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch @@ -0,0 +1,44 @@ +# UNDF: UNDF-2026-000000804 +# UNDF: (leave blank) +# scribus-0003: Selection::addItems m_SelList.contains O(N×M) +# +# In scribus/selection.cpp, addItems() iterates over the items to add (N) and +# for each calls m_SelList.contains(item), which is O(M) on the existing +# QList> selection list. Total: O(N×M). For a "Select All" +# on a 5000-item document adding to an existing selection of 2000, this is +# ~10M pointer comparisons. +# +# The single-item addItem() also calls m_SelList.contains(item) — O(M) per +# call — but is less severe since it adds one item at a time. +# +# Fix: build a QSet from m_SelList before the loop for O(1) lookup, +# or maintain a companion QSet as a class member. +# +# Severity: MEDIUM — triggered on every multi-select operation (rubber-band, +# Select All, group selection); scales with document item count. +--- a/scribus/selection.cpp ++++ b/scribus/selection.cpp +@@ -194,6 +194,12 @@ + bool Selection::addItems(const QList& items) + { + if (items.isEmpty()) + return false; + ++ // Build hash set for O(1) membership check instead of O(M) QList::contains ++ QSet existing; ++ existing.reserve(m_SelList.count()); ++ for (int i = 0; i < m_SelList.count(); ++i) ++ existing.insert(m_SelList.at(i).data()); ++ + QList< QPointer > toAdd; + toAdd.reserve(items.count()); + for (int i = 0; i < items.count(); ++i) + { + PageItem* item = items.at(i); +- if (m_SelList.contains(item)) ++ if (existing.contains(item)) + continue; + toAdd.append(item); ++ existing.insert(item); + item->setSelected(true); + } diff --git a/defects/scribus/unit/ScribusTest.class b/defects/scribus/unit/ScribusTest.class new file mode 100644 index 0000000000000000000000000000000000000000..fb366b131d9faf3b1f3dd700f498567472d3d340 GIT binary patch literal 5289 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 literal 0 HcmV?d00001 diff --git a/defects/scribus/unit/ScribusTest.java b/defects/scribus/unit/ScribusTest.java new file mode 100644 index 000000000..1aa432b97 --- /dev/null +++ b/defects/scribus/unit/ScribusTest.java @@ -0,0 +1,195 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Scribus defects. + * + * scribus-0001: getSortedStyleList retList.contains O(N²) dedup + * scribus-0002: getUsedPatterns results.contains O(I×R) pattern accumulation + * scribus-0003: Selection::addItems m_SelList.contains O(N×M) selection dedup + */ +public class ScribusTest { + + // --------------------------------------------------------------- + // scribus-0001: getSortedStyleList — QList::contains inside loop + // Simulates walking parent chains and deduplicating into retList + // --------------------------------------------------------------- + + /** Defective: List.contains O(N) inside O(N) loop */ + static int sortedStyleListDefective(int styleCount, int[] parentIdx) { + int ops = 0; + List retList = new ArrayList<>(); + for (int i = 0; i < styleCount; i++) { + // Check if already in retList + for (int x : retList) { ops++; if (x == i) break; } + if (!retList.contains(i)) { + // Walk parent chain + List chain = new ArrayList<>(); + chain.add(i); + int p = parentIdx[i]; + while (p >= 0) { + boolean found = false; + for (int x : chain) { ops++; if (x == p) { found = true; break; } } + if (!found) chain.add(0, p); + p = parentIdx[p]; + } + // Merge chain into retList with contains check + for (int idx : chain) { + boolean found = false; + for (int x : retList) { ops++; if (x == idx) { found = true; break; } } + if (!found) retList.add(idx); + } + } + } + return ops; + } + + /** Fixed: QSet companion */ + static int sortedStyleListFixed(int styleCount, int[] parentIdx) { + int ops = 0; + List retList = new ArrayList<>(); + Set retSet = new HashSet<>(); + for (int i = 0; i < styleCount; i++) { + ops++; + if (!retSet.contains(i)) { + List chain = new ArrayList<>(); + Set chainSet = new HashSet<>(); + chain.add(i); chainSet.add(i); ops++; + int p = parentIdx[i]; + while (p >= 0) { + ops++; + if (!chainSet.contains(p)) { chain.add(0, p); chainSet.add(p); } + p = parentIdx[p]; + } + for (int idx : chain) { + ops++; + if (!retSet.contains(idx)) { retList.add(idx); retSet.add(idx); } + } + } + } + return ops; + } + + static void testSortedStyleList() { + int N = 500; + int[] parentIdx = new int[N]; + // Linear parent chain: 0←1←2←...←N-1 (worst case) + parentIdx[0] = -1; + for (int i = 1; i < N; i++) parentIdx[i] = i - 1; + + int defectOps = sortedStyleListDefective(N, parentIdx); + int fixedOps = sortedStyleListFixed(N, parentIdx); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0001 sorted style list: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // scribus-0002: getUsedPatterns — QStringList::contains inside item loop + // --------------------------------------------------------------- + + /** Defective: linear scan for each pattern check */ + static int getUsedPatternsDefective(String[][] itemPatterns) { + int ops = 0; + List results = new ArrayList<>(); + for (String[] pats : itemPatterns) { + for (String pat : pats) { + if (pat == null || pat.isEmpty()) continue; + boolean found = false; + for (String r : results) { ops++; if (r.equals(pat)) { found = true; break; } } + if (!found) results.add(pat); + } + } + return ops; + } + + /** Fixed: HashSet companion */ + static int getUsedPatternsFixed(String[][] itemPatterns) { + int ops = 0; + Set resultSet = new HashSet<>(); + for (String[] pats : itemPatterns) { + for (String pat : pats) { + if (pat == null || pat.isEmpty()) continue; + ops++; + if (!resultSet.contains(pat)) resultSet.add(pat); + } + } + return ops; + } + + static void testGetUsedPatterns() { + int items = 2000; + int patsPerItem = 3; // fill, stroke, mask + int uniquePatterns = 200; + Random rng = new Random(42); + String[][] itemPatterns = new String[items][patsPerItem]; + for (int i = 0; i < items; i++) + for (int j = 0; j < patsPerItem; j++) + itemPatterns[i][j] = "pattern_" + rng.nextInt(uniquePatterns); + + int defectOps = getUsedPatternsDefective(itemPatterns); + int fixedOps = getUsedPatternsFixed(itemPatterns); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0002 used patterns: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // scribus-0003: Selection::addItems — m_SelList.contains inside loop + // --------------------------------------------------------------- + + /** Defective: QList::contains O(M) for each of N items */ + static int selectionAddItemsDefective(int existing, int toAdd) { + int ops = 0; + List selList = new ArrayList<>(); + for (int i = 0; i < existing; i++) selList.add(i); + + for (int i = existing; i < existing + toAdd; i++) { + // linear scan + for (int s : selList) { ops++; if (s == i) break; } + selList.add(i); + } + return ops; + } + + /** Fixed: QSet companion for O(1) check */ + static int selectionAddItemsFixed(int existing, int toAdd) { + int ops = 0; + Set selSet = new HashSet<>(); + for (int i = 0; i < existing; i++) { selSet.add(i); ops++; } + + for (int i = existing; i < existing + toAdd; i++) { + ops++; + if (!selSet.contains(i)) selSet.add(i); + } + return ops; + } + + static void testSelectionAddItems() { + int existing = 2000; + int toAdd = 3000; + + int defectOps = selectionAddItemsDefective(existing, toAdd); + int fixedOps = selectionAddItemsFixed(existing, toAdd); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0003 selection addItems: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + + public static void main(String[] args) { + testSortedStyleList(); + testGetUsedPatterns(); + testSelectionAddItems(); + System.out.println("\nAll 3 Scribus CWE-407 tests PASSED."); + } +}