forgejo-0001/snort3-0001: 2 CWE-407 defects from MOAD multi-scan

forgejo-0001: Results.RepoIDs() slices.Contains dedup O(N^2) in code search — 107x at N=5000
snort3-0001: ServiceDiscovery service_candidates std::find dedup O(M*C) in AppID — 59x at M=10000
This commit is contained in:
russell@unturf.com 2026-03-30 18:46:38 -04:00
parent 2798f926fc
commit 8660303f5a
6 changed files with 254 additions and 0 deletions

View file

@ -0,0 +1,18 @@
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -50,12 +50,13 @@ type Results []*Result
// Get the set of repo IDs from a list of search results
func (res Results) RepoIDs() []int64 {
- ids := make([]int64, len(res))
+ seen := make(map[int64]struct{}, len(res))
+ ids := make([]int64, 0, len(res))
for _, r := range res {
- if !slices.Contains(ids, r.RepoID) {
+ if _, ok := seen[r.RepoID]; !ok {
+ seen[r.RepoID] = struct{}{}
ids = append(ids, r.RepoID)
}
}
return ids
}

View file

@ -0,0 +1,92 @@
import java.util.*;
/**
* Unit test for Forgejo CWE-407 defect: Results.RepoIDs() in
* modules/indexer/code/search.go uses slices.Contains() for dedup,
* creating O(N^2) complexity on code search results.
*
* Additionally, the original code uses make([]int64, len(res)) which
* pre-fills the slice with N zeros, making Contains scan even more
* data than necessary.
*
* Fix: replace slices.Contains with a map[int64]struct{} set lookup.
*/
public class ForgejoSearchRepoIDsDedupTest {
// --- DEFECTIVE: slices.Contains O(N^2) ---
static List<Long> repoIDsDefective(long[] repoIDs) {
// Simulates: ids := make([]int64, len(res)) pre-filled with zeros!
List<Long> ids = new ArrayList<>(Collections.nCopies(repoIDs.length, 0L));
for (long repoID : repoIDs) {
if (!ids.contains(repoID)) {
ids.add(repoID);
}
}
return ids;
}
// --- FIXED: map-based O(N) ---
static List<Long> repoIDsFixed(long[] repoIDs) {
Set<Long> seen = new HashSet<>(repoIDs.length);
List<Long> ids = new ArrayList<>(repoIDs.length);
for (long repoID : repoIDs) {
if (seen.add(repoID)) {
ids.add(repoID);
}
}
return ids;
}
public static void main(String[] args) {
// Simulate code search returning N results from N/2 distinct repos
int[] sizes = {100, 500, 1000, 5000};
System.out.println("forgejo-0001: Results.RepoIDs() slices.Contains dedup");
System.out.println("N\tDefect(ms)\tFixed(ms)\tRatio");
for (int N : sizes) {
long[] repoIDs = new long[N];
Random rng = new Random(42);
for (int i = 0; i < N; i++) {
repoIDs[i] = rng.nextInt(N / 2) + 1;
}
// Warmup
for (int w = 0; w < 3; w++) {
repoIDsDefective(repoIDs);
repoIDsFixed(repoIDs);
}
int iters = Math.max(1, 200000 / N);
long t0 = System.nanoTime();
for (int i = 0; i < iters; i++) {
repoIDsDefective(repoIDs);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iters; i++) {
repoIDsFixed(repoIDs);
}
long fixedNs = System.nanoTime() - t0;
double defectMs = defectNs / 1e6;
double fixedMs = fixedNs / 1e6;
double ratio = defectMs / fixedMs;
System.out.printf("%d\t%.1f\t\t%.1f\t\t%.1fx%n", N, defectMs, fixedMs, ratio);
// Correctness: both must produce same unique set
List<Long> dResult = repoIDsDefective(repoIDs);
List<Long> fResult = repoIDsFixed(repoIDs);
// Remove the leading zeros from defective version
dResult.removeIf(id -> id == 0L);
Set<Long> dSet = new HashSet<>(dResult);
Set<Long> fSet = new HashSet<>(fResult);
assert dSet.equals(fSet) : "Results differ at N=" + N;
assert ratio > 1.5 || N < 200 : "Expected speedup at N=" + N + " but got ratio=" + ratio;
}
System.out.println("ALL PASS");
}
}

View file

@ -0,0 +1,46 @@
--- a/src/network_inspectors/appid/service_plugins/service_discovery.cc
+++ b/src/network_inspectors/appid/service_plugins/service_discovery.cc
@@ -1,5 +1,6 @@
#include <algorithm>
#include <cstdlib>
+#include <unordered_set>
#include <vector>
// ... (includes)
@@ -262,13 +263,15 @@
ServiceMatch* match_list = nullptr;
patterns->find_all((const char*)pkt->data, pkt->dsize, &pattern_match, false,
(void*)&match_list);
std::vector<ServiceMatch*> smOrderedList;
for (ServiceMatch* sm = match_list; sm; sm = sm->next)
smOrderedList.emplace_back(sm);
if (!smOrderedList.empty() )
{
std::sort(smOrderedList.begin(), smOrderedList.end(), AppIdPatternPrecedence);
+ std::unordered_set<ServiceDetector*> seen(asd.service_candidates.begin(),
+ asd.service_candidates.end());
for ( auto& sm : smOrderedList )
{
- if ( std::find(asd.service_candidates.begin(), asd.service_candidates.end(),
- sm->service) == asd.service_candidates.end() )
+ if ( seen.insert(sm->service).second )
{
asd.service_candidates.emplace_back(sm->service);
}
@@ -348,9 +351,11 @@
{
asd.service_candidates = it1->second;
if (it2 != services.end() && it2 != it1)
{
+ std::unordered_set<ServiceDetector*> seen(asd.service_candidates.begin(),
+ asd.service_candidates.end());
for (ServiceDetector* candidate : it2->second)
{
- if (std::find(asd.service_candidates.begin(), asd.service_candidates.end(),
- candidate) == asd.service_candidates.end())
+ if (seen.insert(candidate).second)
asd.service_candidates.push_back(candidate);
}
}

View file

@ -0,0 +1,98 @@
import java.util.*;
/**
* Unit test for Snort3 CWE-407 defect: ServiceDiscovery.match_by_pattern()
* and get_port_based_services() use std::find on service_candidates vector
* for dedup, creating O(M*C) complexity per AppID pattern match.
*
* File: src/network_inspectors/appid/service_plugins/service_discovery.cc
* Lines: 273-281 (match_by_pattern), 351-356 (get_port_based_services)
*
* Fix: replace std::find with std::unordered_set for O(1) lookup.
*/
public class Snort3ServiceCandidateDedupTest {
// Simulate ServiceDetector pointers as Integer IDs
// --- DEFECTIVE: std::find O(M*C) ---
static List<Integer> matchByPatternDefective(List<Integer> existingCandidates, int[] matchedServices) {
List<Integer> candidates = new ArrayList<>(existingCandidates);
for (int service : matchedServices) {
if (!candidates.contains(service)) {
candidates.add(service);
}
}
return candidates;
}
// --- FIXED: unordered_set O(M+C) ---
static List<Integer> matchByPatternFixed(List<Integer> existingCandidates, int[] matchedServices) {
List<Integer> candidates = new ArrayList<>(existingCandidates);
Set<Integer> seen = new HashSet<>(candidates);
for (int service : matchedServices) {
if (seen.add(service)) {
candidates.add(service);
}
}
return candidates;
}
public static void main(String[] args) {
// Simulate: many pattern matches with overlapping service detectors
// In real Snort: custom AppID ODP with many detectors can produce large match lists
// In real Snort, custom ODP rule sets can have hundreds of service detectors;
// with deep packet inspection + multiple pattern matches per flow, M can be large.
int[] sizes = {500, 2000, 5000, 10000};
System.out.println("snort3-0001: ServiceDiscovery service_candidates std::find dedup");
System.out.println("M\tDefect(ms)\tFixed(ms)\tRatio");
for (int M : sizes) {
// Existing candidates (from port-based detection)
List<Integer> existing = new ArrayList<>();
for (int i = 0; i < M / 4; i++) {
existing.add(i);
}
// Pattern matches: half overlap, half new
Random rng = new Random(42);
int[] matches = new int[M];
for (int i = 0; i < M; i++) {
matches[i] = rng.nextInt(M);
}
// Warmup
for (int w = 0; w < 3; w++) {
matchByPatternDefective(existing, matches);
matchByPatternFixed(existing, matches);
}
int iters = Math.max(1, 200000 / M);
long t0 = System.nanoTime();
for (int i = 0; i < iters; i++) {
matchByPatternDefective(existing, matches);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iters; i++) {
matchByPatternFixed(existing, matches);
}
long fixedNs = System.nanoTime() - t0;
double defectMs = defectNs / 1e6;
double fixedMs = fixedNs / 1e6;
double ratio = defectMs / fixedMs;
System.out.printf("%d\t%.1f\t\t%.1f\t\t%.1fx%n", M, defectMs, fixedMs, ratio);
// Correctness
List<Integer> dResult = matchByPatternDefective(existing, matches);
List<Integer> fResult = matchByPatternFixed(existing, matches);
assert new HashSet<>(dResult).equals(new HashSet<>(fResult)) : "Results differ at M=" + M;
assert dResult.size() == fResult.size() : "Sizes differ at M=" + M;
assert ratio > 1.5 || M < 100 : "Expected speedup at M=" + M + " but got ratio=" + ratio;
}
System.out.println("ALL PASS");
}
}