blender+darktable: 3 new defects, MOADs 0002-0005 scan complete
blender-0004: MOAD-0001 CWE-407 anim_channels_edit.cc rearrange_animchannel_islands() calls BLI_findptr(anim_data_visible, channel, ...) inside the channel-grouping loop — O(C*V) where C=channels, V=visible channels. In a complex rig: C=V=1000, 1,000,000 pointer comparisons per reorder. Fix: build blender::Set<void*> from anim_data_visible before loop, O(1) lookup. Measured: 250x op-count ratio at C=V=1000. darktable-0004: MOAD-0004 CWE-312 pwstorage backends backend_kwallet.c and backend_apple_keychain.c log credential key/value pairs verbatim via dt_print(DT_DEBUG_PWSTORAGE, "storing (%s, %s)", key, value). `value` for Piwigo export is JSON including plaintext password. Triggered by `darktable -d pwstorage` or `-d all` (common debugging mode). Fix: replace value argument with "[REDACTED]" in all four dt_print calls. darktable-0005: MOAD-0001 CWE-407 modulegroups test_visible O(M*G*P) _lib_modulegroups_update_iop_visibility() iterates M=80 IOP modules, calling _lib_modulegroups_test_visible() which iterates G=8 groups doing g_list_find_custom (linear scan of P=10 module names per group) — O(M*G*P) per UI refresh. Called on every search keystroke, module toggle, and group switch. Fix: precompute GHashTable of all visible module names; test_visible = O(1). Measured: 37.8x op-count ratio at M=80, G=8, P=10. MOADs 0002/0003/0005 CLEAN for blender; MOADs 0002/0003/0005 CLEAN for darktable. All 4 blender + 5 darktable unit tests PASS.
This commit is contained in:
parent
595e96ffe1
commit
7d135aa6a1
12 changed files with 467 additions and 7 deletions
7
defects/blender/patch/MOAD-0002-CLEAN.txt
Normal file
7
defects/blender/patch/MOAD-0002-CLEAN.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
MOAD-0002 (Intertangle): CLEAN
|
||||
|
||||
Blender uses G.main (the global Main database) extensively, but this is
|
||||
intentional architectural design: G.main is the single source of truth for
|
||||
all scene data. It is not an "accidental" shared mutable god object coupling
|
||||
independent subsystems. Each subsystem (depsgraph, render, physics) accesses
|
||||
G.main through well-defined APIs (BKE_*). No Intertangle defect found.
|
||||
8
defects/blender/patch/MOAD-0003-CLEAN.txt
Normal file
8
defects/blender/patch/MOAD-0003-CLEAN.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
MOAD-0003 (Leaked Context): CLEAN
|
||||
|
||||
Blender has several thread_local uses (gpu_context.cc active_ctx,
|
||||
gpu_immediate.cc imm, draw_context.cc DRWContext::g_context). These are
|
||||
per-thread GPU rendering contexts, which is the correct use of thread-local
|
||||
storage for thread-affine GPU resources. They do not hold request-scoped
|
||||
identity (e.g., user session, render job ID) that should be passed explicitly.
|
||||
No Leaked Context defect found.
|
||||
7
defects/blender/patch/MOAD-0004-CLEAN.txt
Normal file
7
defects/blender/patch/MOAD-0004-CLEAN.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
MOAD-0004 (Logged Secret / CWE-312): CLEAN
|
||||
|
||||
Searched blenkernel/, editors/, render/ for credentials (token, api_key,
|
||||
password, auth) in CLOG_* and printf calls. Blender does not have built-in
|
||||
render farm API key or cloud credential logging paths in the C/C++ core.
|
||||
Network render (Flamenco) is a separate service; credentials are not handled
|
||||
in the blenkernel/editors source tree. No CWE-312 defect found.
|
||||
7
defects/blender/patch/MOAD-0005-CLEAN.txt
Normal file
7
defects/blender/patch/MOAD-0005-CLEAN.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
MOAD-0005 (Thundering Herd / CWE-362): CLEAN
|
||||
|
||||
Searched blenkernel/ for cache patterns (get+null+compute+put without lock).
|
||||
Blender's main data structures (ID caches, depsgraph) are single-threaded by
|
||||
design. The render pipeline has explicit per-thread data (render tiles). No
|
||||
concurrent cache get+compute+put without synchronization pattern found in
|
||||
blenkernel or depsgraph. No Thundering Herd defect found.
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# UNDF: (leave blank)
|
||||
# CWE-407: anim_channels_edit.cc rearrange_animchannel_islands BLI_findptr O(C*V)
|
||||
#
|
||||
# rearrange_animchannel_islands() groups animation channels into islands for
|
||||
# reordering (Ctrl+PgUp/PgDn in the NLA editor, Dope Sheet, etc.). For each
|
||||
# channel in the channel list (length C), it calls:
|
||||
#
|
||||
# BLI_findptr(anim_data_visible, channel, offsetof(bAnimListElem, data))
|
||||
#
|
||||
# BLI_findptr is an O(V) linear scan over the anim_data_visible ListBase
|
||||
# (length V = visible animation channels). This makes the island-grouping loop
|
||||
# O(C * V).
|
||||
#
|
||||
# In a complex animation scene: 100 objects * 10 NLA tracks = 1000 NLA
|
||||
# channels (C), with a similarly-sized anim_data_visible list (V~1000).
|
||||
# Each call to rearrange_nla_tracks() costs O(1,000,000) pointer comparisons
|
||||
# instead of O(1,000).
|
||||
#
|
||||
# The function is templated and called 4 times per rearrange operation:
|
||||
# rearrange_nla_tracks(), rearrange_driver_channels(),
|
||||
# rearrange_nla_curve_channels(), and the action slot reorder path.
|
||||
#
|
||||
# Fix: before the channel loop, build a blender::Set<void*> of the `.data`
|
||||
# pointers from anim_data_visible. Each BLI_findptr becomes O(1) hash lookup.
|
||||
#
|
||||
# Severity: MEDIUM — triggered on every NLA track reorder in the Dope Sheet /
|
||||
# NLA editor. Overhead: ~500x at C=V=1000 channels.
|
||||
#
|
||||
--- a/source/blender/editors/animation/anim_channels_edit.cc
|
||||
+++ b/source/blender/editors/animation/anim_channels_edit.cc
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_math_base.h"
|
||||
+#include "BLI_set.hh"
|
||||
#include "BLI_span.hh"
|
||||
#include "BLI_string_utf8.h"
|
||||
#include "BLI_utildefines.h"
|
||||
@@ -1443,6 +1444,16 @@ static bool rearrange_animchannel_islands(ListBaseT<T> *list,
|
||||
Link *channel, *chanNext = nullptr;
|
||||
bool done = false;
|
||||
|
||||
+ /* Build a hash set of visible channel data-pointers for O(1) membership
|
||||
+ * test below. Without this, BLI_findptr() makes the island-grouping loop
|
||||
+ * O(C * V) — a quadratic scan over up to thousands of animation channels.
|
||||
+ */
|
||||
+ blender::Set<const void *> visible_data_set;
|
||||
+ LISTBASE_FOREACH (bAnimListElem *, ale, anim_data_visible) {
|
||||
+ visible_data_set.add(ale->data);
|
||||
+ }
|
||||
+
|
||||
/* don't waste effort on an empty list */
|
||||
if (BLI_listbase_is_empty(list)) {
|
||||
return false;
|
||||
@@ -1453,7 +1464,7 @@ static bool rearrange_animchannel_islands(ListBaseT<T> *list,
|
||||
for (channel = static_cast<Link *>(list->first); channel; channel = chanNext) {
|
||||
/* find out whether this channel is present in anim_data_visible or not! */
|
||||
const bool is_hidden =
|
||||
- (BLI_findptr(anim_data_visible, channel, offsetof(bAnimListElem, data)) == nullptr);
|
||||
+ (!visible_data_set.contains(static_cast<const void *>(channel)));
|
||||
chanNext = channel->next;
|
||||
rearrange_animchannel_add_to_islands(&islands, list, channel, type, is_hidden);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import java.util.*;
|
|||
* blender-0001: node_runtime.cc socket chain cycle detection Vector.contains() O(D^2)
|
||||
* blender-0002: usd_skel_convert.cc used_indices dedup std::find O(J^2)
|
||||
* blender-0003: shader_tool.cc visited_files std::find O(D*V)
|
||||
* blender-0004: anim_channels_edit.cc rearrange_animchannel_islands BLI_findptr O(C*V)
|
||||
*/
|
||||
public class BlenderTest {
|
||||
|
||||
|
|
@ -193,6 +194,55 @@ public class BlenderTest {
|
|||
return ratio > 2.0;
|
||||
}
|
||||
|
||||
// ========== blender-0004: anim channel rearrange island BLI_findptr ==========
|
||||
|
||||
/**
|
||||
* Simulates rearrange_animchannel_islands: for each channel in list,
|
||||
* check membership in anim_data_visible.
|
||||
* Defective: linear scan (BLI_findptr = List.contains).
|
||||
*/
|
||||
static int rearrangeIslandsDefective(List<Integer> channelList, List<Integer> visibleChannels) {
|
||||
int ops = 0;
|
||||
for (int channel : channelList) {
|
||||
// BLI_findptr: O(V) linear scan
|
||||
for (int vis : visibleChannels) {
|
||||
ops++;
|
||||
if (vis == channel) break;
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/** Fixed: Set<> from anim_data_visible for O(1) lookup per channel. */
|
||||
static int rearrangeIslandsFixed(List<Integer> channelList, List<Integer> visibleChannels) {
|
||||
int ops = 0;
|
||||
Set<Integer> visibleSet = new HashSet<>(visibleChannels);
|
||||
ops += visibleChannels.size(); // one-time build
|
||||
for (int channel : channelList) {
|
||||
visibleSet.contains(channel); // O(1)
|
||||
ops++;
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static boolean testAnimChannelRearrangeIslands() {
|
||||
// 100 objects * 10 NLA tracks = 1000 channels; all visible
|
||||
int C = 1000;
|
||||
int V = 1000;
|
||||
List<Integer> channelList = new ArrayList<>();
|
||||
List<Integer> visibleChannels = new ArrayList<>();
|
||||
for (int i = 0; i < C; i++) channelList.add(i);
|
||||
for (int i = 0; i < V; i++) visibleChannels.add(i); // all visible, worst case
|
||||
|
||||
int defectOps = rearrangeIslandsDefective(channelList, visibleChannels);
|
||||
int fixedOps = rearrangeIslandsFixed(channelList, visibleChannels);
|
||||
double ratio = (double) defectOps / fixedOps;
|
||||
|
||||
System.out.printf(" blender-0004 anim island rearrange: defect=%d fixed=%d ratio=%.1fx%n",
|
||||
defectOps, fixedOps, ratio);
|
||||
return ratio > 10.0;
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
|
@ -202,15 +252,17 @@ public class BlenderTest {
|
|||
boolean p1 = testSocketChainCycleDetection();
|
||||
boolean p2 = testUsedIndicesDedup();
|
||||
boolean p3 = testShaderToolVisited();
|
||||
boolean p4 = testAnimChannelRearrangeIslands();
|
||||
|
||||
System.out.println();
|
||||
System.out.printf("blender-0001 socket chain cycle: %s%n", p1 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0002 USD skel dedup: %s%n", p2 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0003 shader visited: %s%n", p3 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0001 socket chain cycle: %s%n", p1 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0002 USD skel dedup: %s%n", p2 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0003 shader visited: %s%n", p3 ? "PASS" : "FAIL");
|
||||
System.out.printf("blender-0004 anim island rearrange: %s%n", p4 ? "PASS" : "FAIL");
|
||||
|
||||
if (!p1 || !p2 || !p3) {
|
||||
if (!p1 || !p2 || !p3 || !p4) {
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("\nAll 3 tests PASS");
|
||||
System.out.println("\nAll 4 tests PASS");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
defects/darktable/patch/MOAD-0002-CLEAN.txt
Normal file
8
defects/darktable/patch/MOAD-0002-CLEAN.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
MOAD-0002 (Intertangle): CLEAN
|
||||
|
||||
darktable uses a global `darktable` struct that holds all subsystem pointers
|
||||
(darktable.db, darktable.undo, darktable.develop, darktable.pwstorage, etc.).
|
||||
This is intentional architecture: darktable is a single-process application
|
||||
with tight subsystem coupling by design. The subsystems do not independently
|
||||
evolve and share a single GTK main thread. No accidental Intertangle coupling
|
||||
between independently-deployable subsystems found.
|
||||
8
defects/darktable/patch/MOAD-0003-CLEAN.txt
Normal file
8
defects/darktable/patch/MOAD-0003-CLEAN.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
MOAD-0003 (Leaked Context): CLEAN
|
||||
|
||||
darktable uses `static __thread int32_t threadid` in control/jobs.c to track
|
||||
numeric worker thread pool indices. This is not request-scoped identity: it is
|
||||
a pool slot number used for selecting per-thread OpenCL queues and cache
|
||||
entries. It carries no user-visible identity (image ID, job ID, session). The
|
||||
correct fix pattern (ScopedValue / ContextVar) does not apply here. No Leaked
|
||||
Context defect found.
|
||||
8
defects/darktable/patch/MOAD-0005-CLEAN.txt
Normal file
8
defects/darktable/patch/MOAD-0005-CLEAN.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
MOAD-0005 (Thundering Herd / CWE-362): CLEAN
|
||||
|
||||
darktable's image cache (src/common/cache.c) uses a single dt_pthread_mutex_t
|
||||
covering all get/insert/release operations. The dt_cache_get function acquires
|
||||
the lock before hash lookup and holds it through the entire miss path. No
|
||||
concurrent get+null+compute+put without synchronization pattern found.
|
||||
The mipmap/thumbnail cache follows the same pattern. No Thundering Herd
|
||||
defect found.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# UNDF: (leave blank)
|
||||
# CWE-312: darktable pwstorage backends log credentials verbatim when -d pwstorage
|
||||
#
|
||||
# darktable's password storage system (pwstorage) manages service credentials
|
||||
# for cloud photo sharing services such as Piwigo. When a user saves their
|
||||
# account to Piwigo, _piwigo_set_account() serializes their credentials into:
|
||||
#
|
||||
# {"server":"example.com","username":"user","password":"s3cr3t"}
|
||||
#
|
||||
# This JSON value flows into both pwstorage backends:
|
||||
#
|
||||
# 1. backend_kwallet.c dt_pwstorage_kwallet_set() line 368:
|
||||
# dt_print(DT_DEBUG_PWSTORAGE, "...storing (%s, %s)", key, value);
|
||||
# `value` is the full JSON blob including the plaintext password.
|
||||
#
|
||||
# 2. backend_kwallet.c dt_pwstorage_kwallet_get() line 555:
|
||||
# dt_print(DT_DEBUG_PWSTORAGE, "...reading (%s, %s)", key, value);
|
||||
# Same exposure on read.
|
||||
#
|
||||
# 3. backend_apple_keychain.c line 65:
|
||||
# dt_print(DT_DEBUG_PWSTORAGE, "...storing (%s, %s)", key, value);
|
||||
# Same JSON value including password.
|
||||
#
|
||||
# 4. backend_apple_keychain.c line 239:
|
||||
# dt_print(DT_DEBUG_PWSTORAGE, "...reading (%s, %s)", server, json_data);
|
||||
# json_data contains the reconstructed JSON with the password.
|
||||
#
|
||||
# The DT_DEBUG_PWSTORAGE flag is enabled at runtime with `darktable -d pwstorage`
|
||||
# or `darktable -d all`. Debug logs go to stdout and optionally to the log file
|
||||
# (~/.xsession-errors, journald, or a redirected terminal session). Any debug
|
||||
# session — including those run to diagnose KWallet/Keychain connectivity — will
|
||||
# expose the user's Piwigo password in plaintext in the terminal output.
|
||||
#
|
||||
# Lua scripts using darktable.password.save() are also affected: backend_kwallet
|
||||
# and backend_apple_keychain receive the raw password string via the same path.
|
||||
#
|
||||
# Fix: log only the key (service name / username), never the value (which may
|
||||
# contain the password). Replace `(%s, %s)` with `(%s, [REDACTED])` in all
|
||||
# four dt_print calls.
|
||||
#
|
||||
# Severity: MEDIUM — requires the debug flag `darktable -d pwstorage` or
|
||||
# `-d all` to be active. However, `-d all` is commonly used by users diagnosing
|
||||
# OpenCL or other issues, which silently exposes credentials. Piwigo credentials
|
||||
# are real username+password (not OAuth tokens), so exposure is direct account
|
||||
# compromise.
|
||||
#
|
||||
--- a/src/common/pwstorage/backend_kwallet.c
|
||||
+++ b/src/common/pwstorage/backend_kwallet.c
|
||||
@@ -365,7 +365,7 @@ gboolean dt_pwstorage_kwallet_set(const backend_kwallet_context_t *context, con
|
||||
|
||||
while(g_hash_table_iter_next(&iter, &key, &value))
|
||||
{
|
||||
- dt_print(DT_DEBUG_PWSTORAGE, "[pwstorage_kwallet_set] storing (%s, %s)", (gchar *)key, (gchar *)value);
|
||||
+ dt_print(DT_DEBUG_PWSTORAGE, "[pwstorage_kwallet_set] storing (%s, [REDACTED])", (gchar *)key);
|
||||
gsize length;
|
||||
gchar *new_key = char2qstring(key, &length);
|
||||
|
||||
@@ -552,7 +552,7 @@ GHashTable *dt_pwstorage_kwallet_get(const backend_kwallet_context_t *context,
|
||||
|
||||
dt_print(DT_DEBUG_PWSTORAGE,
|
||||
- "[pwstorage_kwallet_get] reading (%s, %s)", (gchar *)key, (gchar *)value);
|
||||
+ "[pwstorage_kwallet_get] reading (%s, [REDACTED])", (gchar *)key);
|
||||
|
||||
g_hash_table_insert(table, key, value);
|
||||
}
|
||||
--- a/src/common/pwstorage/backend_apple_keychain.c
|
||||
+++ b/src/common/pwstorage/backend_apple_keychain.c
|
||||
@@ -62,7 +62,7 @@ gboolean pwstorage_apple_keychain_set(const char *slot, GHashTable *table)
|
||||
while(g_hash_table_iter_next(&iter, &key, &value))
|
||||
{
|
||||
- dt_print(DT_DEBUG_PWSTORAGE, "[pwstorage_apple_keychain_set] storing (%s, %s)", (gchar *) key, (gchar *) value);
|
||||
+ dt_print(DT_DEBUG_PWSTORAGE, "[pwstorage_apple_keychain_set] storing (%s, [REDACTED])", (gchar *) key);
|
||||
|
||||
gchar *lbl = g_strconcat("darktable - ", slot, NULL);
|
||||
|
||||
@@ -236,7 +236,7 @@ GHashTable *pwstorage_apple_keychain_get(const char *slot)
|
||||
|
||||
dt_print(DT_DEBUG_PWSTORAGE,
|
||||
- "[pwstorage_apple_keychain_get] reading (%s, %s)", server, json_data);
|
||||
+ "[pwstorage_apple_keychain_get] reading (%s, [REDACTED])", server);
|
||||
|
||||
g_hash_table_insert(table, g_strdup(server), g_strdup(json_data));
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# UNDF: (leave blank)
|
||||
# CWE-407: modulegroups.c _lib_modulegroups_test_visible O(M*G*P) per module visibility check
|
||||
#
|
||||
# In src/libs/modulegroups.c, _lib_modulegroups_update_iop_visibility() iterates
|
||||
# all IOP modules (length M) to determine whether each should be shown or hidden.
|
||||
# For the DT_MODULEGROUP_NONE case, it calls _lib_modulegroups_test_visible() for
|
||||
# each module. That function iterates all module groups (G) and for each group
|
||||
# calls g_list_find_custom(gr->modules, module_op, _iop_compare) — a linear scan
|
||||
# of the group's module list (length P).
|
||||
#
|
||||
# Total cost per UI refresh: O(M * G * P)
|
||||
#
|
||||
# Typical values: M=80 iop modules, G=8 groups, P=10 modules/group = 6,400
|
||||
# g_strcmp0 calls per update. With the search text entry callback wired directly
|
||||
# to _lib_modulegroups_update_iop_visibility, every keystroke triggers this.
|
||||
# A user typing a 10-character search string incurs 64,000 string comparisons
|
||||
# instead of 80.
|
||||
#
|
||||
# Fix: build a GHashTable (keyed on module op-name string) that covers all
|
||||
# module names across all groups. Populate it once, then test_visible becomes
|
||||
# a single g_hash_table_contains call — O(1).
|
||||
# Invalidate/rebuild the hash table whenever d->groups is updated.
|
||||
#
|
||||
# The hash table can be stored in dt_lib_modulegroups_t and rebuilt in
|
||||
# _lib_modulegroups_update (called whenever groups are loaded/saved).
|
||||
#
|
||||
# Severity: MEDIUM — 80x per-module savings; triggered on every text search
|
||||
# keystroke, module group switch, and image load in the darkroom panel.
|
||||
# Overhead ratio: ~80x at M=80 modules, G=8 groups, P=10 per group.
|
||||
#
|
||||
--- a/src/libs/modulegroups.c
|
||||
+++ b/src/libs/modulegroups.c
|
||||
@@ -107,6 +107,8 @@ typedef struct dt_lib_modulegroups_t
|
||||
GList *groups;
|
||||
GList *edit_groups;
|
||||
+ /* Flat hash set of all module op-names visible in any group (for test_visible).
|
||||
+ * Rebuilt whenever d->groups changes. */
|
||||
+ GHashTable *visible_modules_set;
|
||||
int current;
|
||||
gboolean show_search;
|
||||
|
||||
@@ -277,12 +279,20 @@ static gboolean _lib_modulegroups_test_visible(dt_lib_module_t *self, gchar *mo
|
||||
{
|
||||
dt_lib_modulegroups_t *d = self->data;
|
||||
- for(const GList *l = d->groups; l; l = g_list_next(l))
|
||||
- {
|
||||
- dt_lib_modulegroups_group_t *gr = l->data;
|
||||
- if(g_list_find_custom(gr->modules, module, _iop_compare) != NULL)
|
||||
- {
|
||||
- return TRUE;
|
||||
- }
|
||||
- }
|
||||
- return FALSE;
|
||||
+ if(d->visible_modules_set)
|
||||
+ return g_hash_table_contains(d->visible_modules_set, module);
|
||||
+ /* Fallback to linear scan if hash not yet built (should not happen). */
|
||||
+ for(const GList *l = d->groups; l; l = g_list_next(l))
|
||||
+ {
|
||||
+ dt_lib_modulegroups_group_t *gr = l->data;
|
||||
+ if(g_list_find_custom(gr->modules, module, _iop_compare) != NULL)
|
||||
+ return TRUE;
|
||||
+ }
|
||||
+ return FALSE;
|
||||
}
|
||||
|
||||
+/* Rebuild the visible-modules hash set from d->groups.
|
||||
+ * Call whenever groups are loaded, saved, or modified. */
|
||||
+static void _lib_modulegroups_rebuild_visible_set(dt_lib_modulegroups_t *d)
|
||||
+{
|
||||
+ if(d->visible_modules_set)
|
||||
+ g_hash_table_destroy(d->visible_modules_set);
|
||||
+ d->visible_modules_set = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
+ for(const GList *l = d->groups; l; l = g_list_next(l))
|
||||
+ {
|
||||
+ dt_lib_modulegroups_group_t *gr = l->data;
|
||||
+ for(const GList *m = gr->modules; m; m = g_list_next(m))
|
||||
+ g_hash_table_add(d->visible_modules_set, m->data);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
/* Call _lib_modulegroups_rebuild_visible_set after every d->groups assignment. */
|
||||
@@ -1458,2 +1472,3 @@ static void _lib_modulegroups_update(dt_lib_module_t *self, ...)
|
||||
d->groups = res;
|
||||
+ _lib_modulegroups_rebuild_visible_set(d);
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* CWE-407 simulation tests for darktable defects.
|
||||
* CWE-407 / CWE-312 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
|
||||
* darktable-0004: pwstorage backends log credentials verbatim (CWE-312)
|
||||
* darktable-0005: modulegroups test_visible O(M*G*P) per UI refresh
|
||||
*/
|
||||
public class DarktableTest {
|
||||
|
||||
|
|
@ -151,12 +153,129 @@ public class DarktableTest {
|
|||
System.out.println(" PASS");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// darktable-0004: pwstorage credential logged verbatim (CWE-312)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Defective: credential value logged verbatim.
|
||||
* Simulates dt_print(DT_DEBUG_PWSTORAGE, "storing (%s, %s)", key, value)
|
||||
* where value = {"server":"...","username":"...","password":"s3cr3t"}.
|
||||
*/
|
||||
static String logCredentialDefective(String key, String credentialJson) {
|
||||
// Mirrors: dt_print(DT_DEBUG_PWSTORAGE, "storing (%s, %s)", key, value)
|
||||
return String.format("[pwstorage_kwallet_set] storing (%s, %s)", key, credentialJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed: only log the key (service name), never the value (which contains password).
|
||||
* Mirrors: dt_print(DT_DEBUG_PWSTORAGE, "storing (%s, [REDACTED])", key)
|
||||
*/
|
||||
static String logCredentialFixed(String key, String credentialJson) {
|
||||
return String.format("[pwstorage_kwallet_set] storing (%s, [REDACTED])", key);
|
||||
}
|
||||
|
||||
static void testPwstorageCredentialLog() {
|
||||
String key = "example.com";
|
||||
String credJson = "{\"server\":\"example.com\",\"username\":\"alice\",\"password\":\"s3cr3t\"}";
|
||||
|
||||
String defectLog = logCredentialDefective(key, credJson);
|
||||
String fixedLog = logCredentialFixed(key, credJson);
|
||||
|
||||
boolean defectExposesPassword = defectLog.contains("s3cr3t");
|
||||
boolean fixedExposesPassword = fixedLog.contains("s3cr3t");
|
||||
|
||||
System.out.printf("darktable-0004 pwstorage CWE-312: defect_exposes=%b fixed_exposes=%b%n",
|
||||
defectExposesPassword, fixedExposesPassword);
|
||||
|
||||
assert defectExposesPassword : "Defective log should expose password";
|
||||
assert !fixedExposesPassword : "Fixed log must NOT expose password";
|
||||
assert fixedLog.contains("[REDACTED]") : "Fixed log must contain [REDACTED]";
|
||||
System.out.println(" PASS");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// darktable-0005: modulegroups test_visible O(M*G*P) vs O(M)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Defective: for each of M modules, iterate G groups, each doing linear scan of P items.
|
||||
* Mirrors _lib_modulegroups_test_visible called inside _lib_modulegroups_update_iop_visibility.
|
||||
*/
|
||||
static int modulegroupsTestVisibleDefective(List<String> iopModules,
|
||||
List<List<String>> groups) {
|
||||
int ops = 0;
|
||||
for (String module : iopModules) {
|
||||
// _lib_modulegroups_test_visible inner loop
|
||||
for (List<String> group : groups) {
|
||||
for (String gm : group) {
|
||||
ops++;
|
||||
if (gm.equals(module)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed: precompute a Set of all visible module names across groups.
|
||||
* test_visible = O(1) per module.
|
||||
*/
|
||||
static int modulegroupsTestVisibleFixed(List<String> iopModules,
|
||||
List<List<String>> groups) {
|
||||
int ops = 0;
|
||||
// Rebuild visible set once (done when groups change)
|
||||
Set<String> visibleSet = new HashSet<>();
|
||||
for (List<String> group : groups) {
|
||||
for (String gm : group) {
|
||||
visibleSet.add(gm);
|
||||
ops++; // set build cost
|
||||
}
|
||||
}
|
||||
// Per-update: O(M) hash lookups
|
||||
for (String module : iopModules) {
|
||||
visibleSet.contains(module);
|
||||
ops++;
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static void testModulegroupsTestVisible() {
|
||||
// 80 iop modules, 8 groups of 10 modules each
|
||||
int M = 80;
|
||||
int G = 8;
|
||||
int P = 10;
|
||||
List<String> iopModules = new ArrayList<>();
|
||||
for (int i = 0; i < M; i++) iopModules.add("module_" + i);
|
||||
|
||||
List<List<String>> groups = new ArrayList<>();
|
||||
for (int g = 0; g < G; g++) {
|
||||
List<String> group = new ArrayList<>();
|
||||
for (int p = 0; p < P; p++) {
|
||||
// Distribute modules evenly across groups
|
||||
group.add("module_" + (g * P + p));
|
||||
}
|
||||
groups.add(group);
|
||||
}
|
||||
|
||||
int defectOps = modulegroupsTestVisibleDefective(iopModules, groups);
|
||||
int fixedOps = modulegroupsTestVisibleFixed(iopModules, groups);
|
||||
double ratio = (double) defectOps / fixedOps;
|
||||
|
||||
System.out.printf("darktable-0005 modulegroups test_visible: defect=%d fixed=%d ratio=%.1fx%n",
|
||||
defectOps, fixedOps, ratio);
|
||||
assert ratio > 3.0 : "Expected >3x 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.");
|
||||
testPwstorageCredentialLog();
|
||||
testModulegroupsTestVisible();
|
||||
System.out.println("\nAll 5 darktable tests PASSED.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue