From 8d4d60b421ad12ff06be2d6ff4a3653a456e005c Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 20:13:01 -0400 Subject: [PATCH] ollama: 1 CWE-407 defect (ollama-0001); dosbox-staging: all 5 MOADs CLEAN ollama-0001: kvcache/causal.go buildMask() Except []int linear scan O(B*E). For Gemma3 multi-image prompts, slices.Contains(opts.Except, i) is called per batch token. With N images x 256 tokens/image, B and |except| both scale as N*256 giving quadratic prefill cost. Fix: convert Except to map[int]struct{} before the outer loop. 3.4x speedup at N=10 images. Unit test PASS. dosbox-staging scanned for all 5 MOADs; all CLEAN. Linear scans operate on fixed small palettes (16 CGA colors), hardcoded 3-item lists, or single-call config paths. Mixer mutex consistent. No credential logging. Updated defects/ollama/CLEAN.md to note the missed defect from prior scan. --- defects/dosbox-staging/CLEAN.md | 36 +++++++ ...kvcache-buildmask-except-linear-scan.patch | 50 +++++++++ .../OllamaKvcacheBuildmaskExceptTest.class | Bin 0 -> 3367 bytes .../OllamaKvcacheBuildmaskExceptTest.java | 100 ++++++++++++++++++ defects/ollama/CLEAN.md | 28 ++--- 5 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 defects/dosbox-staging/CLEAN.md create mode 100644 defects/ollama-0001/patch/ollama-0001-kvcache-buildmask-except-linear-scan.patch create mode 100644 defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.class create mode 100644 defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.java diff --git a/defects/dosbox-staging/CLEAN.md b/defects/dosbox-staging/CLEAN.md new file mode 100644 index 000000000..df6d1caee --- /dev/null +++ b/defects/dosbox-staging/CLEAN.md @@ -0,0 +1,36 @@ +# DOSBox-Staging — All 5 MOADs CLEAN + +**Date:** 2026-03-31 +**Target:** https://github.com/dosbox-staging/dosbox-staging (C++) +**Scanner:** Agent Blackops 5-MOAD sweep +**Note:** DOSBox-X (a different fork) has dosbox-x-0003 and dosbox-x-0004 defects. This is the maintained upstream staging fork. + +## MOAD-0001 (CWE-407): CLEAN + +No O(N²) linear scan defects found. All membership checks operate on small fixed-size collections: + +- **vga_dac.cpp** `is_cga_color` / `is_ega_color`: `std::find` on `cga16[16]` and `ega[16]` — fixed 16-element arrays, O(16) constant +- **messages.cpp** `ImportantMessagesList`: `contains()` on a hardcoded 3-element vector — constant +- **config.cpp** `loaded_config_paths_canonical`: `contains()` on config file paths — called once per file load, not hot +- **vga_dac.cpp** outer loop: `for i in NumCgaColors(16)` calls `vga_dac_send_color` which calls `is_cga_color` — O(16*16) = 256 ops constant +- **support.h** `contains()` wrapper: used only for small enum-like checks throughout codebase + +## MOAD-0002 (Intertangle): CLEAN + +No shared mutable god object coupling. DOSBox uses a section-based configuration system (`Config::sections`) with clean interfaces between subsystems. Each hardware subsystem (VGA, sound, CPU) has its own module with minimal shared state. + +## MOAD-0003 (Leaked Context): CLEAN + +DOSBox-Staging is fundamentally single-threaded (main emulation loop). No thread-local or request-scoped identity leaks. The mixer uses a `std::recursive_mutex` correctly for the audio callback thread boundary. + +## MOAD-0004 (CWE-312): CLEAN + +No credential logging found: + +- **softmodem.cpp**: Logs connection host/port and modem ATDT command buffer — hostnames only, no passwords +- **ethernet_slirp.cpp**: Logs port forward rules — TCP/UDP ports only, no credentials +- **No HTTP auth headers, API keys, or bearer tokens** in codebase (DOSBox is a DOS emulator, not a network service) + +## MOAD-0005 (Thundering Herd): CLEAN + +No concurrent cache races. Audio mixer uses `std::recursive_mutex` with `std::lock_guard` consistently. All hardware state is single-threaded. No map/cache get+null+put patterns. diff --git a/defects/ollama-0001/patch/ollama-0001-kvcache-buildmask-except-linear-scan.patch b/defects/ollama-0001/patch/ollama-0001-kvcache-buildmask-except-linear-scan.patch new file mode 100644 index 000000000..9d0fad9a9 --- /dev/null +++ b/defects/ollama-0001/patch/ollama-0001-kvcache-buildmask-except-linear-scan.patch @@ -0,0 +1,50 @@ +--- a/kvcache/causal.go ++++ b/kvcache/causal.go +@@ -362,12 +362,17 @@ func (c *Causal) buildMask(ctx ml.Context) ml.Tensor { + length := c.curCellRange.max - c.curCellRange.min + 1 + + mask := make([]float32, c.curBatchSize*length) + ++ // Build an O(1) lookup set from the Except slice to avoid O(B*E) quadratic ++ // scan in the inner loop. For Gemma3 multi-image prompts, |except| equals ++ // the total number of image tokens (num_images * tokensPerImage = N), and ++ // curBatchSize also scales with N, giving O(N^2) without this fix. ++ exceptSet := make(map[int]struct{}, len(c.opts.Except)) ++ for _, idx := range c.opts.Except { ++ exceptSet[idx] = struct{}{} ++ } ++ + for i := range c.curBatchSize { +- enabled := !slices.Contains(c.opts.Except, i) ++ _, excluded := exceptSet[i] ++ enabled := !excluded + for j := c.curCellRange.min; j <= c.curCellRange.max; j++ { + if !slices.Contains(c.cells[j].sequences, c.curSequences[i]) || + (enabled && c.cells[j].pos > c.curPositions[i]) || + c.chunkSize > 0 && c.cells[j].pos < c.curPositions[i]-c.curPositions[i]%c.chunkSize || + c.cells[j].pos < c.curPositions[i]-c.swaWindowSize { + mask[i*length+(j-c.curCellRange.min)] = float32(math.Inf(-1)) + } + } + } +# +# CWE-407: kvcache.Causal.buildMask Except []int linear scan O(B*E) +# +# In buildMask() (kvcache/causal.go), the outer loop iterates over all batch +# tokens (curBatchSize = B) and calls slices.Contains(c.opts.Except, i) per +# iteration. Except is a []int slice populated by multimodal models when image +# token positions must be excluded from the causal mask. +# +# For Gemma3 multi-image inference (model/models/gemma3/model_text.go): +# - Each image contributes tokensPerImage (= 256) entries to except +# - curBatchSize also equals the total number of image+text tokens +# - For N images: |except| = N*256, B = N*256 + text_tokens ≈ N*256 +# - Total mask build cost: O(B * E) = O(N^2 * 256^2) — quadratic in image count +# +# Fix: convert Except []int to a map[int]struct{} before the outer loop. +# The map is O(|except|) to build and O(1) per lookup, reducing buildMask +# from O(B*E) to O(B + E). +# +# Severity: LOW-MEDIUM — triggered only during Gemma3 multi-image PREFILL, not +# per-token generation. Noticeable for 10+ images; significant at 100+ images. +# 10 images: 2560^2 = 6.5M ops saved; 100 images: 655M ops saved per prefill. diff --git a/defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.class b/defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.class new file mode 100644 index 0000000000000000000000000000000000000000..298ad56f6161effb6f8094cfc630eecf7e9386b3 GIT binary patch literal 3367 zcmai1U2q#$75=WY(yrz8pJK^>QMzgEASbdN*QwI@kN7W*659%uiEArKyO!4W)~j7r zyD~O}e?v?7YYPmJ4m04F4l|tr3X`~LFs}@6Jo3Wu00X@6!VJ^t5Sw#XvMo0T*xOsr+JyfdTOq%Iq}?TlHbqw1zj zyG7H9MCZJST80MKSkcjqp3ADeoYI^CYEaKzX(FQPh%)3}MokJP4pHF;I;bw2EkxaU2nbnggq3k-}25S%y%wLbDY5K<7MHeL{x9 z^`uNW2#&#%GM+*^Lu1Cu<%^CsVX6y;mYvj1P31mC6X`@}dY+4(lJPMv8agamj<|{> zI%S;38HQu+s%>ipN4HFST({jF85r%-g>Df&GR~rRFGntHBZY#x&QaA~DXIod#Atdx zJs9E-fQUgE zLm*02&?U8QJ6ew5w2B03sD!3&^;|3Hrjv3CnwlF366Y?;M7vRjrkuK>5ic^TGpjqx zG_xJmbSMT&L;}dwEcwwrhHKXS=w?eK+m|+-x zPRr%gJ|$N)9KB1=sY_bF(vel%nXRnxxnr$prmYMqqeJv|TXSpXHqARs9_n=&vv`60 zaw8&k4^}d?98mfQ(jJMtgn5QjiZZS(Qp%NR`X<4o7%p9TNMWgP}ywY}QdBA^BB4*Jq2nK@_jEMLV(K2EZ zENU&+MpCH&UO_>GEyF>Pq3$5eF@x%mc$n2}y`W|HYbWKXPLV!PBP2A1F)K?R)$y92 zF6I`r!VIriYA__1S(#Gv?z@OjGkDTek+=DnAjKTDLQeC_?CmpD#1{zSBR&z+1~Zf~ zw-*OeRYDobo{?A+|6SnJV!0dv7LgNpVFLVD{D*mDmzT}=+0el_b5b;e8{98Yvqii?Fh3U4QLc*k4#SE6ElO2giVG*H{dQrcmEdqIf;4PjlyILXe7lDo#ummRJGyDaf z(h@3`=mxRQ*oWko#4iSAk8Y3=KW)^0dMdU(?2QW{A+m|4SV-uO`$E39AHyfSvmua6 zfA_w#?-H4!`Ehb8LeJxP5>L49)WW+1iJV~U*azgc#E;dorN5AlNLnwwkD9qN+rH)d zsGB-7-L)MTyxdes*hE`b$k!eB$3p%s1m8om2eTVKvi)bmYua^=5L3EB0{Q1kobccP zIPT@2x(L!GlD2ypz#)^k^`88f0yv|mym05T?anEiG5gG;oJXM ziR_`(N^t4mDu+A+uM1RbYuoa}08e;qdlNOWNF?cI`JazvxrdT*7PaW5Y@VeY_qk5W z@H{HP`XLdj3ZKR2N)|kCP>5k-+*|eagW|)$oY$M26NKcP&!3$0OUbFW*cMJs^JhoD z@Ghcv5$*g~RT z3W+TeZP#B_nsoFB+#Q=*s#*dSTTbyUz47W$wV&u+R+=sA%x+Xumj2p+-^Q!AkUTGY zSlY98&ESH`WTVC}uE*G+iySbc4#_GIfmvUME8R5L5UGrtwpn|BNm*zryo)4=MbfH2#3=_#@p! z{zc5*p-Y5_G;72S)=T3vxXG^I7JHGd2@YKV-oygCi;PRF8C30(X`hJCml(cIEdto3 zt*Z~MF;@Bg-|$cyY1^THE(Dr1?x0R^Cy(%!;H8@XqTBm;zdJ`X<-dlYbPxHxO8q6G x2cYb=NnVUTc;t(8cH>L*;=z}xUxJFkgRjv(UvZzW;@h+qp!e7DCccZ7{{Yhm7t#O# literal 0 HcmV?d00001 diff --git a/defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.java b/defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.java new file mode 100644 index 000000000..dde5a3c68 --- /dev/null +++ b/defects/ollama-0001/test/OllamaKvcacheBuildmaskExceptTest.java @@ -0,0 +1,100 @@ +import java.util.*; + +/** + * Unit test for ollama-0001: kvcache.Causal.buildMask Except []int O(B*E) scan. + * + * Models the Ollama defect where buildMask() calls slices.Contains(opts.Except, i) + * inside a loop over all batch tokens (curBatchSize = B). Except is populated by + * Gemma3 for multimodal image token positions. For N images with 256 tokens each: + * |except| = N * 256, B = N * 256 + text_tokens ≈ N * 256 + * Cost of Except check alone: O(B * E) = O((N*256)^2) — quadratic in image count. + * + * Fix: convert Except to a HashSet before the outer loop, giving O(1) per lookup + * and O(B + E) total cost for the Except component. + */ +public class OllamaKvcacheBuildmaskExceptTest { + + // --- Defect: slices.Contains on []int Except, O(B*E) --- + // Isolates just the enabled[] computation (the Except scan), not the full mask fill. + static boolean[] computeEnabledLinear(int batchSize, int[] except) { + boolean[] enabled = new boolean[batchSize]; + for (int i = 0; i < batchSize; i++) { + enabled[i] = !listContains(except, i); + } + return enabled; + } + + static boolean listContains(int[] arr, int val) { + for (int v : arr) { + if (v == val) return true; + } + return false; + } + + // --- Fix: map[int]struct{} lookup, O(B+E) --- + static boolean[] computeEnabledHashSet(int batchSize, int[] except) { + Set exceptSet = new HashSet<>(except.length * 2); + for (int idx : except) exceptSet.add(idx); + boolean[] enabled = new boolean[batchSize]; + for (int i = 0; i < batchSize; i++) { + enabled[i] = !exceptSet.contains(i); + } + return enabled; + } + + // Build an Except array simulating N images * tokensPerImage = 256 each + static int[] buildExcept(int numImages, int tokensPerImage) { + int[] except = new int[numImages * tokensPerImage]; + for (int i = 0; i < except.length; i++) { + except[i] = i; + } + return except; + } + + public static void main(String[] args) { + // Correctness: small case + int smallBatch = 16; + int[] smallExcept = {0, 1, 3, 5}; + boolean[] enabledLinear = computeEnabledLinear(smallBatch, smallExcept); + boolean[] enabledHash = computeEnabledHashSet(smallBatch, smallExcept); + assert Arrays.equals(enabledLinear, enabledHash) : "Correctness failed"; + System.out.println("Correctness OK: batch=" + smallBatch + " except=" + smallExcept.length); + + // Performance: simulate 10-image Gemma3 prefill + // tokensPerImage=256, 10 images => batchSize=2560, except=2560 + int numImages = 10; + int tokensPerImage = 256; + int batchSize = numImages * tokensPerImage; + int[] except = buildExcept(numImages, tokensPerImage); + + // Correctness at full scale + boolean[] mL = computeEnabledLinear(batchSize, except); + boolean[] mH = computeEnabledHashSet(batchSize, except); + assert Arrays.equals(mL, mH) : "Correctness failed at full scale"; + + // Warmup + for (int w = 0; w < 3; w++) { + computeEnabledLinear(batchSize, except); + computeEnabledHashSet(batchSize, except); + } + + int reps = 200; + long t0 = System.nanoTime(); + for (int r = 0; r < reps; r++) computeEnabledLinear(batchSize, except); + long linearNs = System.nanoTime() - t0; + + long t1 = System.nanoTime(); + for (int r = 0; r < reps; r++) computeEnabledHashSet(batchSize, except); + long hashNs = System.nanoTime() - t1; + + double ratio = (double) linearNs / hashNs; + System.out.printf("Gemma3 multi-image: %d images x %d tokens = B=%d |except|=%d%n", + numImages, tokensPerImage, batchSize, except.length); + System.out.printf(" Defect ([]int linear scan): %,d ns%n", linearNs); + System.out.printf(" Fix (HashSet O(1) lookup): %,d ns%n", hashNs); + System.out.printf(" speedup: %.1fx%n", ratio); + + assert ratio > 3.0 : "Expected >3x speedup, got " + ratio; + System.out.println("PASS"); + } +} diff --git a/defects/ollama/CLEAN.md b/defects/ollama/CLEAN.md index e32a1d9aa..c799ec935 100644 --- a/defects/ollama/CLEAN.md +++ b/defects/ollama/CLEAN.md @@ -1,19 +1,21 @@ -# Ollama — CWE-407 Scan Result: CLEAN +# Ollama — Previous CWE-407 Scan Result: CLEAN (superseded) **Date:** 2026-03-30 -**Target:** https://github.com/ollama/ollama (Go) -**Scanner:** Agent Blackops CWE-407 sweep -**Focus:** model registry dedup, runner/scheduler membership, layer dedup, kvcache sequences +**Note:** Superseded by deeper scan on 2026-03-31 that found **ollama-0001**. +See `defects/ollama-0001/` for the Gemma3 multi-image kvcache buildMask defect. -## Findings +## Original Findings (2026-03-30, MOADs 0002-0005) -No CWE-407 defects found. All `slices.Contains` calls operate on bounded slices: +- **MOAD-0002 (Intertangle):** CLEAN. Server struct is lean (4 fields). Scheduler is separate. No god object. +- **MOAD-0003 (Leaked Context):** CLEAN. Go has no goroutine-local storage. context.Context used correctly. +- **MOAD-0004 (CWE-312):** CLEAN. `inference_request_log.go` logs request body + Content-Type only, NOT Authorization headers. curl replay script omits auth headers. +- **MOAD-0005 (Thundering Herd):** CLEAN. `loadedMu sync.Mutex` consistently guards all accesses to the `loaded` map in sched.go. -- **kvcache/causal.go**: `slices.Contains(cell.sequences, seq)` — sequences per cell is 1-4 (parallel inference slots), effectively O(1) -- **server/images.go**: Capabilities checks on slices of 3-7 items (model capabilities enum) -- **server/sched.go**: Capability/family string checks against small constant lists -- **ml/backend/ggml/ggml.go**: Device/buffer type checks bounded by hardware count (1-8 GPUs) -- **server/images.go PullModel/PruneLayers**: Uses `map[string]struct{}` for layer dedup — correct O(1) lookup -- **convert/**: Tensor name matching on split strings — bounded by name segments +## What Was Missed in Original CWE-407 Scan -The codebase uses maps for all data-proportional dedup (blob digests, layer tracking) and reserves `slices.Contains` for small enum-like checks. Well-engineered. +`kvcache/causal.go:buildMask` line 371: `enabled := !slices.Contains(c.opts.Except, i)` +inside `for i := range c.curBatchSize`. + +`opts.Except` is a `[]int` slice populated by Gemma3 for multimodal image token positions +(model/models/gemma3/model_text.go lines 230-237). For N images x 256 tokens/image, both `B` +and `|except|` scale as N*256, giving O(B*E) = O(N^2) total cost during prefill. See ollama-0001.