langchain: 1 CWE-407 defect; forgejo: 2 new CWE-407 defects, MOADs 0002-0005 CLEAN
langchain-0001: MultiVectorRetriever._get_relevant_documents() dedup IDs from vectorstore sub_docs uses list.contains() inside loop, O(k^2). k is unbounded in production RAG pipelines (configurable via search_kwargs). Fix: track seen IDs in a set, keep list for order. 499.5x at k=1000. forgejo-0002: LoadRepoConfig() license sort O(P*L) where L=776 licenses. Two SliceContainsString calls in back-to-back loops iterate full license list for each preferred license and vice versa. Fix: build lookup sets before loops. 19.5x at P=20 preferred licenses. forgejo-0003: synchronizePublicKeys() three O(N*M) scans per LDAP sync. Dedup of providedKeys is O(K^2), plus two O(P*G) set-difference loops. Runs per user per sync cycle. Fix: use maps for O(1) membership. 178.6x at K=G=500. forgejo-0001 (search.go RepoIDs) already patched in prior scan. MOADs 0002-0005 CLEAN for both targets.
This commit is contained in:
parent
efe58242e0
commit
cdff140a7c
9 changed files with 593 additions and 0 deletions
179
defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java
Normal file
179
defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Unit test simulating Forgejo forgejo-0003:
|
||||
* models/asymkey/ssh_key.go synchronizePublicKeys() has three O(N*M) scans:
|
||||
*
|
||||
* 1. Building providedKeys with dedup: O(K^2) where K = LDAP-provided key count
|
||||
* for _, v := range sshPublicKeys {
|
||||
* key = ...
|
||||
* if !util.SliceContainsString(providedKeys, key) { // O(|providedKeys|) each time
|
||||
*
|
||||
* 2. Finding new keys: O(P * G) where P = provided, G = DB keys
|
||||
* for _, key := range providedKeys {
|
||||
* if !util.SliceContainsString(giteaKeys, key) { // O(G) each
|
||||
*
|
||||
* 3. Finding deleted keys: O(G * P)
|
||||
* for _, giteaKey := range giteaKeys {
|
||||
* if !util.SliceContainsString(providedKeys, giteaKey) { // O(P) each
|
||||
*
|
||||
* This runs per user per LDAP sync cycle. With many users or large key sets it accumulates.
|
||||
* Fix: build maps for O(1) lookup before each loop.
|
||||
*/
|
||||
public class ForgejoSSHKeySyncTest {
|
||||
|
||||
// Defective: O(K^2) dedup + O(P*G) + O(G*P)
|
||||
static long countDefectiveOps(int rawKeyCount, int giteaKeyCount) {
|
||||
// Phase 1: dedup raw keys into providedKeys
|
||||
// Assume half are unique: K/2 unique keys, K/2 duplicates
|
||||
// Each insertion checks against growing list
|
||||
long ops = 0;
|
||||
int provided = 0;
|
||||
for (int i = 0; i < rawKeyCount; i++) {
|
||||
ops += provided; // SliceContainsString cost
|
||||
if (i % 2 == 0) provided++; // ~half unique
|
||||
}
|
||||
// Phase 2: find new keys (provided not in gitea)
|
||||
ops += (long) provided * giteaKeyCount;
|
||||
// Phase 3: find keys to delete (gitea not in provided)
|
||||
ops += (long) giteaKeyCount * provided;
|
||||
return ops;
|
||||
}
|
||||
|
||||
static long countFixedOps(int rawKeyCount, int giteaKeyCount) {
|
||||
// Phase 1: dedup with set — O(K) total
|
||||
long ops = rawKeyCount;
|
||||
int provided = rawKeyCount / 2; // ~half unique
|
||||
// Build giteaKeysSet: O(G)
|
||||
ops += giteaKeyCount;
|
||||
// Phase 2: find new keys — O(P)
|
||||
ops += provided;
|
||||
// Phase 3: find keys to delete — O(G)
|
||||
ops += giteaKeyCount;
|
||||
return ops;
|
||||
}
|
||||
|
||||
// Simulate the sync logic for correctness
|
||||
static Map<String, List<String>> syncKeysDefective(
|
||||
List<String> rawKeys, List<String> giteaKeys) {
|
||||
// Dedup rawKeys
|
||||
List<String> provided = new ArrayList<>();
|
||||
for (String k : rawKeys) {
|
||||
if (!provided.contains(k)) {
|
||||
provided.add(k);
|
||||
}
|
||||
}
|
||||
// Find new keys
|
||||
List<String> toAdd = new ArrayList<>();
|
||||
for (String k : provided) {
|
||||
if (!giteaKeys.contains(k)) {
|
||||
toAdd.add(k);
|
||||
}
|
||||
}
|
||||
// Find keys to delete
|
||||
List<String> toDelete = new ArrayList<>();
|
||||
for (String k : giteaKeys) {
|
||||
if (!provided.contains(k)) {
|
||||
toDelete.add(k);
|
||||
}
|
||||
}
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
result.put("toAdd", toAdd);
|
||||
result.put("toDelete", toDelete);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Map<String, List<String>> syncKeysFixed(
|
||||
List<String> rawKeys, List<String> giteaKeys) {
|
||||
// Dedup rawKeys with set
|
||||
Set<String> providedSet = new LinkedHashSet<>();
|
||||
List<String> provided = new ArrayList<>();
|
||||
for (String k : rawKeys) {
|
||||
if (providedSet.add(k)) {
|
||||
provided.add(k);
|
||||
}
|
||||
}
|
||||
// Build giteaKeys set
|
||||
Set<String> giteaSet = new HashSet<>(giteaKeys);
|
||||
// Find new keys
|
||||
List<String> toAdd = new ArrayList<>();
|
||||
for (String k : provided) {
|
||||
if (!giteaSet.contains(k)) {
|
||||
toAdd.add(k);
|
||||
}
|
||||
}
|
||||
// Find keys to delete
|
||||
List<String> toDelete = new ArrayList<>();
|
||||
for (String k : giteaKeys) {
|
||||
if (!providedSet.contains(k)) {
|
||||
toDelete.add(k);
|
||||
}
|
||||
}
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
result.put("toAdd", toAdd);
|
||||
result.put("toDelete", toDelete);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("forgejo-0003: synchronizePublicKeys() O(K^2+P*G) -> O(K+P+G)");
|
||||
System.out.println("=".repeat(60));
|
||||
|
||||
// Correctness test
|
||||
List<String> rawKeys = Arrays.asList(
|
||||
"ssh-rsa AAAA1", "ssh-rsa AAAA2", "ssh-rsa AAAA1", // dup
|
||||
"ssh-rsa AAAA3", "ssh-rsa AAAA2" // more dups
|
||||
);
|
||||
List<String> giteaKeys = Arrays.asList(
|
||||
"ssh-rsa AAAA2", "ssh-rsa AAAA4"
|
||||
);
|
||||
|
||||
Map<String, List<String>> def = syncKeysDefective(rawKeys, giteaKeys);
|
||||
Map<String, List<String>> fix = syncKeysFixed(rawKeys, giteaKeys);
|
||||
|
||||
// Expected: add AAAA1 and AAAA3 (new from LDAP), delete AAAA4 (no longer in LDAP)
|
||||
List<String> expectedAdd = Arrays.asList("ssh-rsa AAAA1", "ssh-rsa AAAA3");
|
||||
List<String> expectedDelete = Arrays.asList("ssh-rsa AAAA4");
|
||||
|
||||
if (!def.get("toAdd").equals(expectedAdd)) {
|
||||
throw new AssertionError("Defective toAdd wrong: " + def.get("toAdd"));
|
||||
}
|
||||
if (!def.get("toDelete").equals(expectedDelete)) {
|
||||
throw new AssertionError("Defective toDelete wrong: " + def.get("toDelete"));
|
||||
}
|
||||
if (!fix.get("toAdd").equals(expectedAdd)) {
|
||||
throw new AssertionError("Fixed toAdd wrong: " + fix.get("toAdd"));
|
||||
}
|
||||
if (!fix.get("toDelete").equals(expectedDelete)) {
|
||||
throw new AssertionError("Fixed toDelete wrong: " + fix.get("toDelete"));
|
||||
}
|
||||
System.out.println("PASS: correctness verified");
|
||||
|
||||
// Performance comparison
|
||||
System.out.printf("%n%-20s %15s %12s %10s%n",
|
||||
"scenario", "defective_ops", "fixed_ops", "ratio");
|
||||
System.out.println("-".repeat(58));
|
||||
|
||||
int[][] scenarios = {
|
||||
{10, 10}, {50, 50}, {100, 100}, {500, 500}, {1000, 1000}
|
||||
};
|
||||
for (int[] sc : scenarios) {
|
||||
int K = sc[0], G = sc[1];
|
||||
long dOps = countDefectiveOps(K, G);
|
||||
long fOps = countFixedOps(K, G);
|
||||
double ratio = (double) dOps / fOps;
|
||||
System.out.printf("K=%-4d G=%-4d %15d %12d %9.1fx%n",
|
||||
K, G, dOps, fOps, ratio);
|
||||
}
|
||||
|
||||
long dOps = countDefectiveOps(500, 500);
|
||||
long fOps = countFixedOps(500, 500);
|
||||
double ratio = (double) dOps / fOps;
|
||||
if (ratio < 10) {
|
||||
throw new AssertionError("Expected >10x ratio at K=500 G=500, got " + ratio);
|
||||
}
|
||||
|
||||
System.out.println("\nAll assertions PASS");
|
||||
System.out.println("Fix: use providedKeysSet(map) for O(1) dedup; giteaKeysSet for O(1) diff");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue