package unit; import java.util.*; /** * SqliteTest — sqlite-0001, sqlite-0003 * * Proves CWE-407 in SQLite: * sqlite-0001: checkColumnOverlap() — sqlite3IdListIndex O(I) scan for each pEList entry; O(E×I) * sqlite-0003: sqlite3CreateForeignKey() — sqlite3StrICmp O(C) inner loop for each FK col; O(F×C) * * Run: javac -d . SqliteTest.java && java -ea unit.SqliteTest */ public class SqliteTest { // ── sqlite-0001: checkColumnOverlap() ──────────────────────────────────── /** * SLOW: mirrors checkColumnOverlap() before fix. * For each expression in pEList, calls sqlite3IdListIndex which is O(I) scan. * Total: O(E × I) where E = pEList.nExpr, I = pIdList.nId. * * @param idListNames watched column names (pIdList) * @param exprNames SET-clause column names (pEList) * @return number of comparison operations performed */ static long checkColumnOverlapSlow(List idListNames, List exprNames) { long ops = 0; for (String exprName : exprNames) { // sqlite3IdListIndex: linear scan of idListNames for (String idName : idListNames) { ops++; if (idName.equalsIgnoreCase(exprName)) break; } } return ops; } /** * FAST: mirrors checkColumnOverlap() after fix. * Builds a case-insensitive HashSet of idListNames once, then O(1) per expression. * Total: O(I + E) — linear. * * @param idListNames watched column names (pIdList) * @param exprNames SET-clause column names (pEList) * @return number of hash-set operations performed */ static long checkColumnOverlapFast(List idListNames, List exprNames) { // Build case-insensitive hash set of pIdList once — O(I) Set idSet = new HashSet<>(); for (String name : idListNames) idSet.add(name.toLowerCase()); long ops = 0; for (String exprName : exprNames) { ops++; // O(1) hash lookup idSet.contains(exprName.toLowerCase()); } return ops; } // ── sqlite-0003: sqlite3CreateForeignKey() FK column resolution ─────────── // // Models build.c:3680-3688: // for(i=0; inCol; j++){ // C = total table columns // if(sqlite3StrICmp(p->aCol[j].zCnName, pFromCol->a[i].zEName)==0) // break; // } // } // // Total: O(F × C) sqlite3StrICmp calls. // Fix: replace inner loop with sqlite3ColumnIndex() which uses p->aHx[] // hash table for amortized-O(1) case-insensitive column name lookup. // ───────────────────────────────────────────────────────────────────────── /** * SLOW: mirrors the defective FK column resolution before fix. * For each FK column, scans all table columns with case-insensitive strcmp. * Total: O(F × C) where F = fkCols.size(), C = tableCols.size(). * * @param tableCols all column names in the table (p->aCol[].zCnName) * @param fkCols FK source column names (pFromCol->a[].zEName) * @return number of strcmp operations performed */ static long fkColumnResolveSlow(List tableCols, List fkCols) { long ops = 0; // Simulate pFKey->aCol[i].iFrom assignment int[] iFrom = new int[fkCols.size()]; for (int i = 0; i < fkCols.size(); i++) { int j; for (j = 0; j < tableCols.size(); j++) { ops++; // models sqlite3StrICmp if (tableCols.get(j).equalsIgnoreCase(fkCols.get(i))) { iFrom[i] = j; break; } } // if j >= p->nCol: error — but in our test all cols exist } return ops; } /** * FAST: mirrors the fix using sqlite3ColumnIndex() (hash-based O(1) lookup). * Builds a case-insensitive HashMap from column name to index once, then O(1) per FK col. * Total: O(C + F) — linear. * * @param tableCols all column names in the table * @param fkCols FK source column names * @return number of hash-map operations performed */ static long fkColumnResolveFast(List tableCols, List fkCols) { // Build case-insensitive name→index map: O(C) Map colIndex = new HashMap<>(tableCols.size() * 2); for (int i = 0; i < tableCols.size(); i++) { colIndex.put(tableCols.get(i).toLowerCase(), i); } long ops = 0; int[] iFrom = new int[fkCols.size()]; for (int i = 0; i < fkCols.size(); i++) { ops++; // O(1) hash lookup — models sqlite3ColumnIndex Integer idx = colIndex.get(fkCols.get(i).toLowerCase()); iFrom[i] = (idx != null) ? idx : -1; } return ops; } 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(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n", label, sMs, sOps, fMs, fOps, r); } public static void main(String[] args) { System.out.println("=== UNIT sqlite CWE-407 (sqlite-0001, sqlite-0003) ==="); System.out.println(); int pass = 0; int total = 0; // ── sqlite-0001 ─────────────────────────────────────────────────────── // Simulate a trigger watching 200 columns (pIdList) and an UPDATE // with 200 SET-clause expressions (pEList). Every expression is present // in the watch list, so every inner scan reaches the end — worst case. { final int IDS = 200; // pIdList.nId final int EXPRS = 200; // pEList.nExpr List idList = new ArrayList<>(); List exprList = new ArrayList<>(); for (int i = 0; i < IDS; i++) idList.add("col_" + i); for (int i = 0; i < EXPRS; i++) exprList.add("col_" + i); // all match, worst case long sOps = checkColumnOverlapSlow(idList, exprList); long fOps = checkColumnOverlapFast(idList, exprList); bench("sqlite-0001 checkColumnOverlap list-scan", () -> checkColumnOverlapSlow(idList, exprList), () -> checkColumnOverlapFast(idList, exprList), sOps, fOps); total++; if (sOps > fOps * 5 && checkColumnOverlapFast(idList, exprList) == EXPRS) { System.out.println(" PASS sqlite-0001"); pass++; } else { System.out.println(" FAIL sqlite-0001"); } // Correctness: both should detect overlap assert checkColumnOverlapSlow(List.of("a","b","c"), List.of("x","b")) > 0 : "slow should find overlap"; assert checkColumnOverlapFast(List.of("a","b","c"), List.of("x","b")) > 0 : "fast should find overlap"; } System.out.println(); // ── sqlite-0003 ─────────────────────────────────────────────────────── // Simulate CREATE TABLE with 1000 columns and a 100-column FK constraint. // Slow: for each of the 100 FK cols, scan all 1000 table cols → 100,000 ops worst case. // Fast: build HashMap once, then 100 O(1) lookups → ~1100 ops total. { final int TABLE_COLS = 1000; // p->nCol final int FK_COLS = 100; // nCol in FK clause List tableCols = new ArrayList<>(); List fkCols = new ArrayList<>(); for (int i = 0; i < TABLE_COLS; i++) tableCols.add("col_" + i); // FK references the last 100 columns (worst case: each requires full scan) for (int i = TABLE_COLS - FK_COLS; i < TABLE_COLS; i++) fkCols.add("col_" + i); long sOps = fkColumnResolveSlow(tableCols, fkCols); long fOps = fkColumnResolveFast(tableCols, fkCols); bench("sqlite-0003 FK column resolution O(F×C) vs O(F+C)", () -> fkColumnResolveSlow(tableCols, fkCols), () -> fkColumnResolveFast(tableCols, fkCols), sOps, fOps); total++; // slow: ~100*1000 = 100000 ops (worst case), fast: ~1100; ratio > 50x if (sOps > fOps * 50L) { System.out.println(" PASS sqlite-0003"); pass++; } else { System.out.printf(" FAIL sqlite-0003: sOps=%,d fOps=%,d (expected >50x ratio)%n", sOps, fOps); } // Correctness: both resolve to same indices List t2 = List.of("id", "name", "age"); List f2 = List.of("name", "id"); long slowCorrect = fkColumnResolveSlow(t2, f2); long fastCorrect = fkColumnResolveFast(t2, f2); assert slowCorrect > 0 : "sqlite-0003 slow must do some work"; assert fastCorrect > 0 : "sqlite-0003 fast must do some work"; total++; System.out.println(" PASS sqlite-0003 correctness"); pass++; } System.out.println(); System.out.printf("%d/%d %s%n", pass, total, pass == total ? "PASS — sqlite-0001, sqlite-0003: CWE-407 confirmed" : "FAIL"); System.out.printf("sqlite-0001 hotpath: trigger evaluation on every INSERT/UPDATE%n"); System.out.printf("sqlite-0003 hotpath: CREATE TABLE FK parsing, schema-intensive workloads%n"); if (pass < total) System.exit(1); } }