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,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<int>::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<int> for O(1) membership test alongside the
# ordered QList<int> 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<int> ScribusDoc::getSortedStyleList() const
{
QList<int> retList;
+ QSet<int> 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<int> retList2;
+ QSet<int> 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)

View file

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

View file

@ -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<QPointer<PageItem>> 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<PageItem*> 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<PageItem *>& items)
{
if (items.isEmpty())
return false;
+ // Build hash set for O(1) membership check instead of O(M) QList::contains
+ QSet<PageItem*> existing;
+ existing.reserve(m_SelList.count());
+ for (int i = 0; i < m_SelList.count(); ++i)
+ existing.insert(m_SelList.at(i).data());
+
QList< QPointer<PageItem> > 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);
}