java-topology/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java
russell@unturf.com cdff140a7c 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.
2026-03-31 20:12:14 -04:00

179 lines
6.7 KiB
Java

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");
}
}