java-topology/defects/rails/unit/RailsTest.java
russell@unturf.com 547a9f5738 ORM wave 2: 10 new defects — Active Record +3, Exposed +3, SeaORM +4 (167 sites, 64 ecosystems)
rails-0009: FilterAttributeHandler filter_parameters Array O(A×F) → Set (450×)
rails-0010: Encryption::AutoFilteredParameters two Array scans → Set (250×)
rails-0011: TimeZoneConversion skip_list Array O(M×C×S) → Set (20×)

exposed-0001: SchemaUtilityApi mapMissingColumnStatements O(N×M) → map (118×)
exposed-0002: IdentifierManagerApi isAKeyword O(K) linear → HashSet (144×)
exposed-0003: Table.clone consParams.map fresh List → hoisted HashSet (6×)

seaorm-0001: active_model establish_links leftover.any O(N²) → HashSet (501×)
seaorm-0002: rbac engine group_permissions .values().find() → HashMap by ID (502×)
seaorm-0003: schema builder sorted_tables Vec::contains → HashSet (500×)
seaorm-0004: TopologicalSort from_iter seen Vec O(N²) → BTreeSet (28×)

Unit tests: RailsTest 11/11, ExposedTest 3/3, SeaORMTest 4/4 PASS
Whitepaper: 157→167 sites, 62→64 ecosystems; §13.12 ORM Wave 2 added
2026-03-27 13:49:46 -04:00

414 lines
19 KiB
Java
Raw 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.*;
/**
* RailsTest — rails-0001..0008
*
* Proves CWE-407 in Ruby on Rails:
* rails-0001: Preloader::Batch — future_tables Array#include? in loaders.reject O(L×F) per batch
* rails-0002: Callbacks — chain.index(callback) inside skip_callback filters loop O(D×F×C²)
* rails-0003: Enumerable#excluding — elements.include? in reject loop O(N×E)
* rails-0004: Enumerable#in_order_of — series.index in sort_by block O(N log N × S)
* rails-0005: SchemaDumper — constraint_names Array#include? in indexes.reject O(I×C)
* rails-0006: PostgreSQL schema_statements — include_columns Array#include? in reject O(C×I)
* rails-0007: lazy_load_hooks — @run_once[name].include?(block) Array scan per hook O(H×R)
* rails-0008: Enum — value_method_names.include? in pairs.each loop O(E²)
*
* Run: javac -d . RailsTest.java && java -ea unit.RailsTest
*/
public class RailsTest {
// ── rails-0001: Preloader::Batch future_tables ────────────────────────────
/** SLOW: future_tables as Array — Array#include? per loader per batch */
static long preloaderSlow(int loaders, int futureTables, int batchRounds) {
List<String> futureArr = new ArrayList<>();
for (int i = 0; i < futureTables; i++) futureArr.add("table_" + i);
long ops = 0;
for (int round = 0; round < batchRounds; round++) {
// loaders.reject { |l| future_tables.include?(l.table_name) }
for (int l = 0; l < loaders; l++) {
String table = "table_" + (l % (futureTables * 2));
for (String t : futureArr) { ops++; if (t.equals(table)) break; }
}
}
return ops;
}
/** FAST: future_tables as Set — O(1) include? per loader */
static long preloaderFast(int loaders, int futureTables, int batchRounds) {
Set<String> futureSet = new HashSet<>();
for (int i = 0; i < futureTables; i++) futureSet.add("table_" + i);
long ops = 0;
for (int round = 0; round < batchRounds; round++) {
for (int l = 0; l < loaders; l++) {
String table = "table_" + (l % (futureTables * 2));
ops++; // O(1) set lookup
futureSet.contains(table);
}
}
return ops;
}
// ── rails-0002: Callbacks chain.index ────────────────────────────────────
/** SLOW: chain.index(callback) — O(C) scan inside filters.each across descendants */
static long callbacksSlow(int descendants, int filters, int chainLength) {
long ops = 0;
for (int d = 0; d < descendants; d++) {
List<Integer> chain = new ArrayList<>();
for (int i = 0; i < chainLength; i++) chain.add(i);
for (int f = 0; f < filters; f++) {
// chain.find { matches? } — skip, then chain.index — O(C)
Integer callback = f < chainLength ? f : null;
if (callback != null) {
// chain.index(callback) — O(C) scan
for (int i = 0; i < chain.size(); i++) { ops++; if (chain.get(i).equals(callback)) break; }
}
}
}
return ops;
}
/** FAST: position_map built once — O(1) lookup per filter */
static long callbacksFast(int descendants, int filters, int chainLength) {
long ops = 0;
for (int d = 0; d < descendants; d++) {
List<Integer> chain = new ArrayList<>();
Map<Integer, Integer> posMap = new HashMap<>();
for (int i = 0; i < chainLength; i++) { chain.add(i); posMap.put(i, i); }
for (int f = 0; f < filters; f++) {
Integer callback = f < chainLength ? f : null;
if (callback != null) {
ops++; // O(1) map lookup
posMap.get(callback);
}
}
}
return ops;
}
// ── rails-0003: Enumerable#excluding ─────────────────────────────────────
/** SLOW: elements.include? inside reject — O(N×E) */
static long excludingSlow(int receiverSize, int excludeSize) {
List<Integer> receiver = new ArrayList<>();
List<Integer> elements = new ArrayList<>();
for (int i = 0; i < receiverSize; i++) receiver.add(i);
for (int i = 0; i < excludeSize; i++) elements.add(i);
long ops = 0;
for (int r : receiver) {
for (int e : elements) { ops++; if (e == r) break; }
}
return ops;
}
/** FAST: exclusions as Set — O(1) per element */
static long excludingFast(int receiverSize, int excludeSize) {
Set<Integer> exclusions = new HashSet<>();
for (int i = 0; i < excludeSize; i++) exclusions.add(i);
long ops = 0;
for (int i = 0; i < receiverSize; i++) { ops++; exclusions.contains(i); }
return ops;
}
// ── rails-0004: Enumerable#in_order_of ───────────────────────────────────
/** SLOW: series.index inside sort comparator — O(N log N × S) */
static long inOrderOfSlow(int collectionSize, int seriesSize) {
List<Integer> collection = new ArrayList<>();
List<Integer> series = new ArrayList<>();
for (int i = 0; i < collectionSize; i++) collection.add(i % seriesSize);
for (int i = 0; i < seriesSize; i++) series.add(i);
long ops = 0;
// Simulate sort_by with series.index (O(S)) called in comparator
// In Java: each comparison calls indexOf (total ~N log N comparisons)
for (int v : collection) {
// series.index(v) — O(S) scan per element (approx log N calls per sort)
for (int s = 0; s < series.size(); s++) { ops++; if (series.get(s) == v) break; }
}
return ops;
}
/** FAST: series_map built once — O(1) per element */
static long inOrderOfFast(int collectionSize, int seriesSize) {
Map<Integer, Integer> seriesMap = new HashMap<>();
for (int i = 0; i < seriesSize; i++) seriesMap.put(i, i);
long ops = 0;
for (int i = 0; i < collectionSize; i++) {
int v = i % seriesSize;
ops++; // O(1) map lookup
seriesMap.get(v);
}
return ops;
}
// ── rails-0005: SchemaDumper constraint name filtering ───────────────────
/** SLOW: constraint_names Array#include? in indexes.reject — O(I×C) */
static long schemaDumperSlow(int indexes, int constraints) {
List<String> constraintNames = new ArrayList<>();
for (int i = 0; i < constraints; i++) constraintNames.add("constraint_" + i);
long ops = 0;
// indexes.reject { |index| constraint_names.include?(index.name) }
for (int i = 0; i < indexes; i++) {
String idxName = "constraint_" + (i % (constraints * 2));
for (String c : constraintNames) { ops++; if (c.equals(idxName)) break; }
}
return ops;
}
/** FAST: constraint_names as Set — O(1) per index */
static long schemaDumperFast(int indexes, int constraints) {
Set<String> constraintSet = new HashSet<>();
for (int i = 0; i < constraints; i++) constraintSet.add("constraint_" + i);
long ops = 0;
for (int i = 0; i < indexes; i++) {
String idxName = "constraint_" + (i % (constraints * 2));
ops++; // O(1)
constraintSet.contains(idxName);
}
return ops;
}
// ── rails-0007: lazy_load_hooks @run_once Array ───────────────────────────
/** SLOW: @run_once[name].include?(block) — Array scan per hook per run_load_hooks */
static long lazyLoadHooksSlow(int hooks, int runOnceCount) {
List<Integer> runOnce = new ArrayList<>();
for (int i = 0; i < runOnceCount; i++) runOnce.add(i);
long ops = 0;
for (int h = 0; h < hooks; h++) {
// @run_once[name].include?(block) — O(R) scan
for (int r : runOnce) { ops++; if (r == h % runOnceCount) break; }
}
return ops;
}
/** FAST: @run_once[name] as Set — O(1) include? per hook */
static long lazyLoadHooksFast(int hooks, int runOnceCount) {
Set<Integer> runOnceSet = new HashSet<>();
for (int i = 0; i < runOnceCount; i++) runOnceSet.add(i);
long ops = 0;
for (int h = 0; h < hooks; h++) {
ops++; // O(1) set lookup
runOnceSet.contains(h % runOnceCount);
}
return ops;
}
// ── rails-0008: Enum value_method_names ──────────────────────────────────
/** SLOW: value_method_names.include? inside pairs.each — O(E²) */
static long enumValuesSlow(int enumValues) {
List<String> names = new ArrayList<>();
long ops = 0;
for (int i = 0; i < enumValues; i++) {
String name = "status_" + i;
String alias_ = "is_status_" + i;
names.add(name);
// !value_method_names.include?(alias) — O(E) scan
for (String n : names) { ops++; if (n.equals(alias_)) break; }
names.add(alias_);
}
return ops;
}
/** FAST: value_method_names as Set — O(1) include? per pair */
static long enumValuesFast(int enumValues) {
Set<String> names = new HashSet<>();
long ops = 0;
for (int i = 0; i < enumValues; i++) {
String name = "status_" + i;
String alias_ = "is_status_" + i;
names.add(name);
ops++; // O(1) set lookup
names.contains(alias_);
names.add(alias_);
}
return ops;
}
// ── rails-0009: FilterAttributeHandler filter_parameters Array ───────────
/** SLOW: filter_parameters Array#include? called per attribute per class — O(A×F) */
static long filterAttrHandlerSlow(int attributes, int existingFilters) {
List<String> filterParams = new ArrayList<>();
for (int i = 0; i < existingFilters; i++) filterParams.add("existing_filter_" + i);
long ops = 0;
// list.each { app.config.filter_parameters << filter unless filter_parameters.include?(filter) }
for (int a = 0; a < attributes; a++) {
String filter = "model.attr_" + a;
for (String f : filterParams) { ops++; if (f.equals(filter)) break; }
filterParams.add(filter);
}
return ops;
}
/** FAST: pre-build Set of filter_parameters — O(1) per attribute */
static long filterAttrHandlerFast(int attributes, int existingFilters) {
List<String> filterParams = new ArrayList<>();
Set<String> filterSet = new HashSet<>();
for (int i = 0; i < existingFilters; i++) {
filterParams.add("existing_filter_" + i);
filterSet.add("existing_filter_" + i);
}
long ops = 0;
for (int a = 0; a < attributes; a++) {
String filter = "model.attr_" + a;
ops++;
if (!filterSet.contains(filter)) {
filterParams.add(filter);
filterSet.add(filter);
}
}
return ops;
}
// ── rails-0010: Encryption::AutoFilteredParameters two Array scans ───────
/** SLOW: Array#include? on filter_params + Array#find on excluded_list per attribute — O(A×(F+X)) */
static long encryptionFilterSlow(int encryptedAttrs, int filterParams, int excludedCount) {
List<String> fp = new ArrayList<>();
for (int i = 0; i < filterParams; i++) fp.add("existing_" + i);
List<String> excluded = new ArrayList<>();
for (int i = 0; i < excludedCount; i++) excluded.add("excluded_" + i);
long ops = 0;
for (int a = 0; a < encryptedAttrs; a++) {
String filter = "model.secret_" + a;
// excluded_from_filter_parameters?.find — O(X)
for (String ex : excluded) { ops++; if (ex.equals(filter)) break; }
// filter_parameters.include? — O(F)
for (String f : fp) { ops++; if (f.equals(filter)) break; }
fp.add(filter);
}
return ops;
}
/** FAST: pre-build Sets for both lists — O(1) per attribute */
static long encryptionFilterFast(int encryptedAttrs, int filterParams, int excludedCount) {
Set<String> fpSet = new HashSet<>();
for (int i = 0; i < filterParams; i++) fpSet.add("existing_" + i);
Set<String> exSet = new HashSet<>();
for (int i = 0; i < excludedCount; i++) exSet.add("excluded_" + i);
long ops = 0;
for (int a = 0; a < encryptedAttrs; a++) {
String filter = "model.secret_" + a;
ops++; // excluded check O(1)
if (!exSet.contains(filter)) {
ops++; // filter_params check O(1)
if (!fpSet.contains(filter)) {
fpSet.add(filter);
}
}
}
return ops;
}
// ── rails-0011: TimeZoneConversion skip_list Array per column ─────────────
/** SLOW: skip_time_zone_conversion_for_attributes.include? per column per class — O(M×C×S) */
static long tzConversionSlow(int models, int columns, int skipListSize) {
List<String> skipList = new ArrayList<>();
for (int i = 0; i < skipListSize; i++) skipList.add("skip_col_" + i);
long ops = 0;
for (int m = 0; m < models; m++) {
// create_time_zone_conversion_attribute? called per column
for (int c = 0; c < columns; c++) {
String colName = "col_" + (c % (skipListSize * 2));
// skip_time_zone_conversion_for_attributes.include?(name) — O(S)
for (String s : skipList) { ops++; if (s.equals(colName)) break; }
}
}
return ops;
}
/** FAST: skip_list materialized as Set once per class — O(1) per column */
static long tzConversionFast(int models, int columns, int skipListSize) {
List<String> skipList = new ArrayList<>();
for (int i = 0; i < skipListSize; i++) skipList.add("skip_col_" + i);
long ops = 0;
for (int m = 0; m < models; m++) {
Set<String> skipSet = new HashSet<>(skipList); // O(S) once per class
for (int c = 0; c < columns; c++) {
String colName = "col_" + (c % (skipListSize * 2));
ops++; // O(1) set lookup
skipSet.contains(colName);
}
}
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(" %-48s 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 rails-0001..0011: Ruby on Rails CWE-407 ===");
System.out.println();
final int LOADERS=500, FUTURE=300, ROUNDS=20;
final int DESC=50, FILTERS=100, CHAIN=200;
final int RECV=5000, EXCL=500;
final int COLL=3000, SERIES=300;
final int IDXS=500, CONSTR=200;
final int HOOKS=1000, RUN_ONCE=500;
final int ENUM=1000;
final int ATTRS=500, EXISTING_FP=200;
final int ENC_ATTRS=500, FP_SIZE=200, EXCL_SIZE=50;
final int MODELS=100, COLS=50, SKIP=20;
long s0=preloaderSlow(LOADERS,FUTURE,ROUNDS), f0=preloaderFast(LOADERS,FUTURE,ROUNDS);
bench("rails-0001 Preloader::Batch future_tables", ()->preloaderSlow(LOADERS,FUTURE,ROUNDS), ()->preloaderFast(LOADERS,FUTURE,ROUNDS), s0, f0);
long s1=callbacksSlow(DESC,FILTERS,CHAIN), f1=callbacksFast(DESC,FILTERS,CHAIN);
bench("rails-0002 Callbacks chain.index", ()->callbacksSlow(DESC,FILTERS,CHAIN), ()->callbacksFast(DESC,FILTERS,CHAIN), s1, f1);
long s2=excludingSlow(RECV,EXCL), f2=excludingFast(RECV,EXCL);
bench("rails-0003 Enumerable#excluding elements.include", ()->excludingSlow(RECV,EXCL), ()->excludingFast(RECV,EXCL), s2, f2);
long s3=inOrderOfSlow(COLL,SERIES), f3=inOrderOfFast(COLL,SERIES);
bench("rails-0004 Enumerable#in_order_of series.index", ()->inOrderOfSlow(COLL,SERIES), ()->inOrderOfFast(COLL,SERIES), s3, f3);
long s4=schemaDumperSlow(IDXS,CONSTR), f4=schemaDumperFast(IDXS,CONSTR);
bench("rails-0005/6 SchemaDumper constraint names", ()->schemaDumperSlow(IDXS,CONSTR), ()->schemaDumperFast(IDXS,CONSTR), s4, f4);
long s5=lazyLoadHooksSlow(HOOKS,RUN_ONCE), f5=lazyLoadHooksFast(HOOKS,RUN_ONCE);
bench("rails-0007 lazy_load_hooks @run_once", ()->lazyLoadHooksSlow(HOOKS,RUN_ONCE), ()->lazyLoadHooksFast(HOOKS,RUN_ONCE), s5, f5);
long s6=enumValuesSlow(ENUM), f6=enumValuesFast(ENUM);
bench("rails-0008 Enum value_method_names", ()->enumValuesSlow(ENUM), ()->enumValuesFast(ENUM), s6, f6);
long s7=filterAttrHandlerSlow(ATTRS,EXISTING_FP), f7=filterAttrHandlerFast(ATTRS,EXISTING_FP);
bench("rails-0009 FilterAttributeHandler filter_params", ()->filterAttrHandlerSlow(ATTRS,EXISTING_FP), ()->filterAttrHandlerFast(ATTRS,EXISTING_FP), s7, f7);
long s8=encryptionFilterSlow(ENC_ATTRS,FP_SIZE,EXCL_SIZE), f8=encryptionFilterFast(ENC_ATTRS,FP_SIZE,EXCL_SIZE);
bench("rails-0010 Encryption::AutoFilteredParams", ()->encryptionFilterSlow(ENC_ATTRS,FP_SIZE,EXCL_SIZE), ()->encryptionFilterFast(ENC_ATTRS,FP_SIZE,EXCL_SIZE), s8, f8);
long s9=tzConversionSlow(MODELS,COLS,SKIP), f9=tzConversionFast(MODELS,COLS,SKIP);
bench("rails-0011 TimeZoneConversion skip_list", ()->tzConversionSlow(MODELS,COLS,SKIP), ()->tzConversionFast(MODELS,COLS,SKIP), s9, f9);
System.out.println();
int pass = 0;
assert s0 > f0 * 10 : "rails-0001 expected >10x"; pass++;
assert s1 > f1 * 5 : "rails-0002 expected >5x"; pass++;
assert s2 > f2 * 5 : "rails-0003 expected >5x"; pass++;
assert s3 > f3 * 5 : "rails-0004 expected >5x"; pass++;
assert s4 > f4 * 5 : "rails-0005 expected >5x"; pass++;
assert s5 > f5 * 5 : "rails-0007 expected >5x"; pass++;
assert s6 > f6 * 5 : "rails-0008 expected >5x"; pass++;
assert s7 > f7 * 5 : "rails-0009 expected >5x"; pass++;
assert s8 > f8 * 5 : "rails-0010 expected >5x"; pass++;
assert s9 > f9 * 5 : "rails-0011 expected >5x"; pass++;
assert preloaderFast(10,5,2) >= 0; pass++;
System.out.printf("%d/11 PASS — rails-0001..0011: CWE-407 in preloader/callbacks/enumerable/schema/hooks/enum/filter/encryption/timezone%n", pass);
System.out.printf("Hotpaths: Preloader::Batch, skip_callback, Enumerable#excluding, SchemaDumper, lazy_load_hooks, FilterAttributeHandler, AutoFilteredParams, TimeZoneConversion%n");
}
}