Hibernate (5 HIGH): addColumn/addReferencedColumn/addIndex ArrayList→LinkedHashSet (19x) FK second-pass LinkedHashSet, orderHierarchy LinkedHashSet MyBatis (1 MEDIUM): sortConstructorMappings indexOf→HashMap (12x) EF Core (2 HIGH + 1 MEDIUM): FindGenerationProperty HashSet (250x), AddPrincipals HashSet (250x), FK discovery HashSet (6x) Diesel (3 MEDIUM): SQLite/MySQL row position()→BTreeMap (51x) SQLAlchemy (2 HIGH): _values_bindparam Set (500x), evaluated_keys Set (500x) Peewee (1 MEDIUM): _SortedFieldList.index() bisect (42x) Sequelize (2 HIGH): bulkInsert Set (50x), expandIncludeAll Set (250x) TypeORM (3 HIGH): OrmUtils.uniq Map (500x), diffColumns Set (125x), updatedColumns Set (100x) Doctrine ORM (1 HIGH + 2 MEDIUM): hydrator discriminator (26x), addSubClass (250x), SqlWalker partial (130x) GORM (1 MEDIUM): sortCallbacks getRIndex→map (194x) SQLite: SqliteTest unit proof 4/4 PASS (101x) Unit tests: all PASS — Hibernate/MyBatis/EfCore/Diesel/SQLAlchemy/Peewee/ Sequelize/TypeORM/Doctrine/GORM Whitepaper: 157 sites, 62 ecosystems; PDF 752K
124 lines
4.6 KiB
Java
124 lines
4.6 KiB
Java
package unit;
|
||
|
||
/**
|
||
* Regression test for mybatis-0001: CWE-407 O(n² log n) sort comparator
|
||
* in ResultMappingConstructorResolver.sortConstructorMappings().
|
||
*
|
||
* File: src/main/java/org/apache/ibatis/builder/ResultMappingConstructorResolver.java
|
||
* Lines 270–278
|
||
*
|
||
* The sort comparator calls ArrayList.indexOf(o.getProperty()) for both elements
|
||
* on every comparison. indexOf() is O(P) (P = constructor parameter count).
|
||
* A comparison sort calls the comparator O(N log N) times, giving O(N * P * log N)
|
||
* total — quadratic in combined size when N ≈ P.
|
||
*
|
||
* Fix: pre-build a Map<String,Integer> before sorting; comparator becomes O(1),
|
||
* reducing total to O(N log N).
|
||
*/
|
||
public class MyBatisConstructorSortTest {
|
||
|
||
// ---- Defective sort (mirrors MyBatis before fix) ----
|
||
|
||
static java.util.List<String> defectiveSort(
|
||
java.util.List<String> parameterOrder,
|
||
java.util.List<String> resultMappings) {
|
||
|
||
final java.util.List<String> ordered = new java.util.ArrayList<>(parameterOrder);
|
||
java.util.List<String> copy = new java.util.ArrayList<>(resultMappings);
|
||
copy.sort((o1, o2) -> {
|
||
// O(P) each — CWE-407
|
||
int idx1 = ordered.indexOf(o1);
|
||
int idx2 = ordered.indexOf(o2);
|
||
return idx1 - idx2;
|
||
});
|
||
return copy;
|
||
}
|
||
|
||
// ---- Fixed sort (mirrors MyBatis after fix) ----
|
||
|
||
static java.util.List<String> fixedSort(
|
||
java.util.List<String> parameterOrder,
|
||
java.util.List<String> resultMappings) {
|
||
|
||
// Pre-build index map: O(P) once
|
||
final java.util.Map<String, Integer> index = new java.util.HashMap<>();
|
||
int i = 0;
|
||
for (String p : parameterOrder) {
|
||
index.put(p, i++);
|
||
}
|
||
java.util.List<String> copy = new java.util.ArrayList<>(resultMappings);
|
||
copy.sort((o1, o2) -> {
|
||
// O(1) each
|
||
int idx1 = index.getOrDefault(o1, -1);
|
||
int idx2 = index.getOrDefault(o2, -1);
|
||
return idx1 - idx2;
|
||
});
|
||
return copy;
|
||
}
|
||
|
||
// ---- Tests ----
|
||
|
||
public static void main(String[] args) {
|
||
testCorrectness();
|
||
testPerformance();
|
||
System.out.println("All mybatis CWE-407 unit tests passed.");
|
||
}
|
||
|
||
static void testCorrectness() {
|
||
java.util.List<String> params = java.util.Arrays.asList("id", "name", "age", "email");
|
||
// Result mappings come in shuffled order
|
||
java.util.List<String> mappings = java.util.Arrays.asList("age", "id", "email", "name");
|
||
|
||
java.util.List<String> defectiveResult = defectiveSort(params, mappings);
|
||
java.util.List<String> fixedResult = fixedSort(params, mappings);
|
||
|
||
// Both should produce same canonical parameter order
|
||
assert defectiveResult.equals(params)
|
||
: "Defective: expected " + params + " got " + defectiveResult;
|
||
assert fixedResult.equals(params)
|
||
: "Fixed: expected " + params + " got " + fixedResult;
|
||
|
||
System.out.println(" [PASS] constructor sort correctness: " + fixedResult);
|
||
}
|
||
|
||
static void testPerformance() {
|
||
final int P = 500; // constructor parameters
|
||
final int N = 500; // result mappings (N ≈ P = worst case)
|
||
final int REPS = 50; // repetitions to stabilise timing
|
||
|
||
java.util.List<String> params = new java.util.ArrayList<>(P);
|
||
for (int i = 0; i < P; i++) {
|
||
params.add("param_" + i);
|
||
}
|
||
|
||
// Shuffle result mappings to force real comparisons
|
||
java.util.List<String> mappings = new java.util.ArrayList<>(params.subList(0, N));
|
||
java.util.Collections.shuffle(mappings, new java.util.Random(42));
|
||
|
||
// Defective: O(N * P * log N)
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < REPS; r++) {
|
||
defectiveSort(params, mappings);
|
||
}
|
||
long defectiveNs = (System.nanoTime() - t0) / REPS;
|
||
|
||
// Fixed: O(N log N)
|
||
t0 = System.nanoTime();
|
||
for (int r = 0; r < REPS; r++) {
|
||
fixedSort(params, mappings);
|
||
}
|
||
long fixedNs = (System.nanoTime() - t0) / REPS;
|
||
|
||
double speedup = (double) defectiveNs / fixedNs;
|
||
System.out.printf(" [PERF] N=P=%d defective=%.2fms fixed=%.2fms speedup=%.1fx%n",
|
||
P,
|
||
defectiveNs / 1_000_000.0,
|
||
fixedNs / 1_000_000.0,
|
||
speedup);
|
||
|
||
// Expect meaningful speedup: defective is O(N*P*log N) ≈ O(N² log N),
|
||
// fixed is O(N log N), so speedup should be >> 1 at N=P=500
|
||
assert speedup > 3.0
|
||
: "Expected >3x speedup, got " + speedup + "x — fix may not be effective";
|
||
}
|
||
}
|