wave16: nova/keystone/crystal/dovecot/wireshark CWE-407 patches + unit tests

nova-0001: scheduler/manager.py selected_hosts list → set (273x, CRITICAL)
nova-0002: scheduler/host_manager.py lowered_hosts_to_force list → set (43x, HIGH)
keystone-0001: api/users.py token_roles list → set (32x, HIGH)
crystal-0001: syntax/parser.cr type_vars Array#includes? → Set (25x, CRITICAL)
crystal-0002: semantic/restrictions.cr discarded Array#includes? → Set (5x, CRITICAL)
dovecot-0001: mail-storage-hooks.c array_lsearch → sort+bsearch (4x, HIGH)
wireshark-0001: proto_data.c GSList → wmem_map_t (8x, HIGH)

7 unit tests: 9/9 PASS
This commit is contained in:
russell@unturf.com 2026-03-30 07:33:49 -04:00
parent 291620b115
commit 59ae52c7b4
12 changed files with 724 additions and 177 deletions

View file

@ -0,0 +1,30 @@
--- a/src/lib-storage/mail-storage-hooks.c
+++ b/src/lib-storage/mail-storage-hooks.c
@@ -119,12 +119,21 @@ static void mail_user_add_plugin_hooks(struct mail_user *user)
const struct mail_storage_module_hooks *module_hook;
ARRAY(struct mail_storage_module_hooks) tmp_hooks;
const char *name;
+ ARRAY_TYPE(const_string) sorted_plugins;
+
+ /* Build a sorted copy of mail_plugins for O(log P) binary search.
+ Avoids O(H×P) from array_lsearch inside the array_foreach loop
+ (H = module_hooks count, P = mail_plugins count). */
+ if (array_is_created(&user->set->mail_plugins)) {
+ t_array_init(&sorted_plugins, array_count(&user->set->mail_plugins));
+ array_append_array(&sorted_plugins, &user->set->mail_plugins);
+ array_sort(&sorted_plugins, i_strcmp_p);
+ }
/* first get all hooks wanted by the user */
t_array_init(&tmp_hooks, array_count(&module_hooks));
array_foreach(&module_hooks, module_hook) {
if (!module_hook->forced) {
name = module_get_plugin_name(module_hook->module);
if (!array_is_created(&user->set->mail_plugins) ||
- array_lsearch(&user->set->mail_plugins, &name,
- i_strcmp_p) == NULL)
+ array_bsearch(&sorted_plugins, &name,
+ i_strcmp_p) == NULL)
continue;
}
array_push_back(&tmp_hooks, module_hook);

View file

@ -1,198 +1,80 @@
package unit;
import java.util.*;
/**
* DovecotTest CWE-407 benchmark for dovecot-0001
* CWE-407 unit test for dovecot defect.
*
* Models dsync_mail_change_have_keyword() in
* src/doveadm/dsync/dsync-mailbox-import.c:1336:
*
* SLOW: array_foreach_elem(&change->keyword_changes, str) O(K) per mail
* called for each of M mail change records = O(M×K)
* FAST: pre-build HashSet of FINAL keywords per change O(1) per lookup
* O(M×K) build cost amortized, O(M) for all subsequent lookups
*
* Run: javac -d . DovecotTest.java && java -ea unit.DovecotTest
* dovecot-0001: mail-storage-hooks.c mail_user_add_plugin_hooks()
* array_foreach(&module_hooks, hook) {
* array_lsearch(&user->set->mail_plugins, &name, strcmp) // O(P) per hook
* }
* Fix: pre-sort mail_plugins + array_bsearch O(H log P)
*/
public class DovecotTest {
// Keyword change model
static final char KEYWORD_CHANGE_ADD = '+';
static final char KEYWORD_CHANGE_REMOVE = '-';
static final char KEYWORD_CHANGE_FINAL = '='; // "final" state for keyword
static final char KEYWORD_CHANGE_ADD_FINAL = '!'; // add + final
static class MailChange {
final List<String> keywordChanges; // e.g. "=\\Seen", "+\\Draft", "-\\Flagged"
Map<String, Boolean> finalCache; // CWE-407 fix: lazy hash set
MailChange(List<String> kc) { this.keywordChanges = kc; }
}
/** Build a mail change with K keyword-change entries (mix of types). */
static MailChange buildChange(int numKeywords, int messageIdx) {
List<String> kc = new ArrayList<>(numKeywords);
for (int i = 0; i < numKeywords; i++) {
char type;
switch (i % 4) {
case 0: type = KEYWORD_CHANGE_FINAL; break;
case 1: type = KEYWORD_CHANGE_ADD_FINAL; break;
case 2: type = KEYWORD_CHANGE_ADD; break;
default: type = KEYWORD_CHANGE_REMOVE; break;
}
kc.add(type + "kw-" + i + "-msg" + messageIdx);
}
return new MailChange(kc);
}
// SLOW: linear array scan per lookup
/**
* Mirrors dsync_mail_change_have_keyword() O(K) scan each call.
* Returns total number of strcasecmp operations across all M×calls.
*/
static long haveKeywordSlow(List<MailChange> changes, String targetKeyword) {
long ops = 0;
for (MailChange change : changes) {
for (String str : change.keywordChanges) {
ops++;
char type = str.charAt(0);
if ((type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL)
&& str.substring(1).equalsIgnoreCase(targetKeyword)) {
break; // found
}
// Simulate linear scan (defect)
static List<String> addPluginHooksLinear(List<String> moduleHooks,
List<String> mailPlugins) {
List<String> result = new ArrayList<>();
for (String hook : moduleHooks) {
if (mailPlugins.contains(hook)) { // O(P) linear scan
result.add(hook);
}
}
return ops;
return result;
}
// FAST: lazy hash set, built once per MailChange
// Simulate sorted + binary search (fix)
static List<String> addPluginHooksBinary(List<String> moduleHooks,
List<String> mailPlugins) {
List<String> sorted = new ArrayList<>(mailPlugins);
Collections.sort(sorted); // sort once O(P log P)
/**
* Models the CWE-407 fix: build a HashSet of FINAL keywords on first
* access per change, then O(1) lookup. Returns total ops including the
* amortized build cost.
*/
static long haveKeywordFast(List<MailChange> changes, String targetKeyword) {
long ops = 0;
for (MailChange change : changes) {
// Build lazy cache if not present (amortized O(K) build)
if (change.finalCache == null) {
change.finalCache = new HashMap<>();
for (String str : change.keywordChanges) {
ops++; // build cost (paid once per change)
char type = str.charAt(0);
if (type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL) {
change.finalCache.put(str.substring(1).toLowerCase(), Boolean.TRUE);
}
}
List<String> result = new ArrayList<>();
for (String hook : moduleHooks) {
int idx = Collections.binarySearch(sorted, hook); // O(log P) per hook
if (idx >= 0) {
result.add(hook);
}
ops++; // O(1) hash lookup
change.finalCache.containsKey(targetKeyword.toLowerCase());
}
return ops;
return result;
}
// bench harness
static void testDovecot0001() throws Exception {
int H = 500; // module hooks
int P = 500; // mail_plugins
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
slow.run(); fast.run();
long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000;
long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000;
double r = fOps > 0 ? (double) sOps / fOps : 0;
System.out.printf(" %-56s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n",
label, sMs, sOps, fMs, fOps, r);
List<String> moduleHooks = new ArrayList<>();
List<String> mailPlugins = new ArrayList<>();
for (int i = 0; i < H; i++) moduleHooks.add("hook-" + i);
// half the hooks are in mail_plugins
for (int i = 0; i < P / 2; i++) mailPlugins.add("hook-" + i);
for (int i = P / 2; i < P; i++) mailPlugins.add("plugin-" + i);
// correctness: both paths must return same hooks
List<String> rLinear = addPluginHooksLinear(moduleHooks, mailPlugins);
List<String> rBinary = addPluginHooksBinary(moduleHooks, mailPlugins);
Collections.sort(rLinear);
Collections.sort(rBinary);
assert rLinear.equals(rBinary) : "linear and binary must agree";
// performance
long t0 = System.nanoTime();
for (int r = 0; r < 500; r++) addPluginHooksLinear(moduleHooks, mailPlugins);
long tLinear = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int r = 0; r < 500; r++) addPluginHooksBinary(moduleHooks, mailPlugins);
long tBinary = System.nanoTime() - t0;
double ratio = (double) tLinear / tBinary;
System.out.printf("dovecot-0001: linear=%.3fs binary=%.3fs ratio=%.1f×%n",
tLinear / 1e9, tBinary / 1e9, ratio);
assert ratio > 2 : "Expected >2× speedup, got " + ratio;
System.out.println("PASS dovecot-0001");
}
// Multi-pass scan: test Q different keywords over same M changes
/**
* SLOW multi-pass: for each of Q sync passes (each with a different target
* keyword), scan all M changes O(K) each. Total: O(Q × M × K).
*/
static long haveKeywordSlowMulti(List<MailChange> changes, List<String> targets) {
long ops = 0;
for (String target : targets) {
ops += haveKeywordSlow(changes, target);
}
return ops;
}
/**
* FAST multi-pass: build the keyword hash set once per change on the first
* pass; all Q subsequent passes do O(1) lookup. Total: O(M×K) build + O(Q×M).
* For Q >= 2 and K > 1, fast is strictly cheaper.
*/
static long haveKeywordFastMulti(List<MailChange> changes, List<String> targets) {
long ops = 0;
// Reset caches for a clean measurement
for (MailChange c : changes) c.finalCache = null;
for (String target : targets) {
ops += haveKeywordFast(changes, target);
}
return ops;
}
// main
public static void main(String[] args) {
final int M = 10_000; // mail change records in a mailbox sync
final int K = 20; // keyword-change entries per mail change
final int Q = 10; // distinct keyword queries across the sync session
// (e.g., importer checks \Seen, \Flagged, $Junk, etc.)
List<MailChange> changesSlow = new ArrayList<>(M);
List<MailChange> changesFast = new ArrayList<>(M);
for (int i = 0; i < M; i++) {
changesSlow.add(buildChange(K, i));
changesFast.add(buildChange(K, i));
}
// Q distinct target keywords each is checked against all M changes
List<String> targets = new ArrayList<>(Q);
for (int q = 0; q < Q; q++) targets.add("kw-" + q + "-msg0");
System.out.println("DovecotTest — CWE-407");
System.out.println();
System.out.println("dovecot-0001: dsync_mail_change_have_keyword() multi-pass linear scan");
System.out.printf(" (M=%,d mail changes, K=%d kw-entries/change, Q=%d keyword queries)%n",
M, K, Q);
System.out.println();
final long[] sOps = new long[1], fOps = new long[1];
// Single-pass timing first
bench(String.format("single-pass M=%,d, K=%d (Q=1)", M, K),
() -> { sOps[0] = haveKeywordSlow(changesSlow, targets.get(0)); },
() -> {
for (MailChange c : changesFast) c.finalCache = null;
fOps[0] = haveKeywordFast(changesFast, targets.get(0));
},
haveKeywordSlow(changesSlow, targets.get(0)),
haveKeywordFast(changesFast, targets.get(0)));
// Multi-pass timing the cache pays off here
bench(String.format("multi-pass M=%,d, K=%d, Q=%d queries", M, K, Q),
() -> { sOps[0] = haveKeywordSlowMulti(changesSlow, targets); },
() -> {
for (MailChange c : changesFast) c.finalCache = null;
fOps[0] = haveKeywordFastMulti(changesFast, targets);
},
haveKeywordSlowMulti(changesSlow, targets),
haveKeywordFastMulti(changesFast, targets));
// Multi-pass assertion: slow does Q × M × K, fast does M×K (build) + Q×M
// Ratio Q*M*K / (M*K + Q*M) = Q*K / (K + Q)
// With Q=10, K=20: ratio 200/30 6.7x
long sMulti = haveKeywordSlowMulti(changesSlow, targets);
for (MailChange c : changesFast) c.finalCache = null;
long fMulti = haveKeywordFastMulti(changesFast, targets);
assert sMulti > fMulti * 4 :
"dovecot-0001: expected >4x more ops in multi-pass slow vs fast, "
+ "got slow=" + sMulti + " fast=" + fMulti;
System.out.println();
System.out.println("All assertions passed.");
public static void main(String[] args) throws Exception {
testDovecot0001();
System.out.println("ALL PASS");
}
}