darktable+scribus: CWE-407 scan — 6 defects (3 darktable, 3 scribus)

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
This commit is contained in:
russell@unturf.com 2026-03-30 11:28:25 -04:00
parent f4593ef84a
commit 13db08af50
10 changed files with 776 additions and 0 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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);