java-topology/defects/sqlite/unit/SqliteTest.java

227 lines
10 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> idListNames, List<String> 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<String> idListNames, List<String> exprNames) {
// Build case-insensitive hash set of pIdList once — O(I)
Set<String> 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; i<nCol; i++){ // F = number of FK columns
// for(j=0; j<p->nCol; 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<String> tableCols, List<String> 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<String> tableCols, List<String> fkCols) {
// Build case-insensitive name→index map: O(C)
Map<String, Integer> 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<String> idList = new ArrayList<>();
List<String> 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<String> tableCols = new ArrayList<>();
List<String> 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<String> t2 = List.of("id", "name", "age");
List<String> 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);
}
}