New diamond recursion defects (O(2^D) → O(N)): - godot-0009: Font::_is_cyclic no visited set — CJK fallback diamond, 2648x at F=4,D=8 - godot-0010: Font::_update_rids_fb no visited set — duplicate RIDs + O(N^2) hot path - meson-0002: get_internal_static_libraries_recurse link_whole guard missing — 132x at D=10 - typescript-0003: hasBaseType inner check() no visited set — 1024x at D=10; hot on instanceof New O(N²) defects: - typeorm-0004: SubjectTopologicalSorter Array.indexOf dedup — 200x at N=400 - typeorm-0005: DepGraph.createDFS result.indexOf + addDependency edge dedup — 300x at N=600 CLEAN confirmed (diamond recursion sweep): bazel, cargo, cmake, composer, dgl, diesel, doctrine-orm, efcore, helm, mybatis, networkx-deeper, ninja, npm-arborist, peewee, pip, rubygems, seaorm, sqlalchemy, swift UNDF: 571→578 assigned; MOAD count: 629→635
332 lines
14 KiB
Java
332 lines
14 KiB
Java
package unit;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* GodotFontCyclicTest — godot-0009 / godot-0010
|
|
*
|
|
* Standalone Java proof of the CWE-407 diamond-recursion patterns in Godot's
|
|
* font fallback graph traversal.
|
|
*
|
|
* Two defects:
|
|
* godot-0009: Font::_is_cyclic() — no visited set, O(F^D) exponential
|
|
* re-traversal of shared fallback nodes in a diamond graph;
|
|
* fix: HashSet<Font*> visited passed through recursion → O(N).
|
|
*
|
|
* godot-0010: Font::_update_rids_fb() — no visited set, O(N^2) re-traversal
|
|
* of shared fonts; rids[] accumulates duplicate RIDs; called on
|
|
* every text render op; fix: HashSet<Font*> visited → O(N),
|
|
* each font RID collected exactly once.
|
|
*
|
|
* Run: javac -d . GodotFontCyclicTest.java && java -ea unit.GodotFontCyclicTest
|
|
*/
|
|
public class GodotFontCyclicTest {
|
|
|
|
// ── Minimal Font model ───────────────────────────────────────────────────
|
|
|
|
static class Font {
|
|
final String name;
|
|
final List<Font> fallbacks = new ArrayList<>();
|
|
final int rid; // simulated RID (renderer handle)
|
|
|
|
Font(String name, int rid) {
|
|
this.name = name;
|
|
this.rid = rid;
|
|
}
|
|
|
|
void addFallback(Font f) { fallbacks.add(f); }
|
|
|
|
@Override public String toString() { return name; }
|
|
}
|
|
|
|
// ── godot-0009: _is_cyclic without visited set ───────────────────────────
|
|
|
|
/**
|
|
* SLOW: Simulates Font::_is_cyclic without a visited set (the defect).
|
|
* Counts nodes visited. Returns {isCyclic, nodesVisited}.
|
|
*/
|
|
static long[] isCyclicSlow(Font thisFont, Font p_f, int depth, int maxDepth) {
|
|
if (depth > maxDepth) return new long[]{1, 1};
|
|
if (p_f == null) return new long[]{0, 1};
|
|
if (p_f == thisFont) return new long[]{1, 1};
|
|
|
|
long visits = 1;
|
|
for (Font f : p_f.fallbacks) {
|
|
long[] result = isCyclicSlow(thisFont, f, depth + 1, maxDepth);
|
|
visits += result[1];
|
|
if (result[0] == 1) return new long[]{1, visits};
|
|
}
|
|
return new long[]{0, visits};
|
|
}
|
|
|
|
/**
|
|
* FAST: Simulates Font::_is_cyclic_internal with a visited set (the fix).
|
|
* Returns {isCyclic, nodesVisited}.
|
|
*/
|
|
static long[] isCyclicFast(Font thisFont, Font p_f, int depth, int maxDepth,
|
|
Set<Font> visited) {
|
|
if (depth > maxDepth) return new long[]{1, 1};
|
|
if (p_f == null) return new long[]{0, 1};
|
|
if (p_f == thisFont) return new long[]{1, 1};
|
|
if (visited.contains(p_f)) return new long[]{0, 0}; // skip — already proven safe
|
|
|
|
visited.add(p_f);
|
|
long visits = 1;
|
|
for (Font f : p_f.fallbacks) {
|
|
long[] result = isCyclicFast(thisFont, f, depth + 1, maxDepth, visited);
|
|
visits += result[1];
|
|
if (result[0] == 1) return new long[]{1, visits};
|
|
}
|
|
return new long[]{0, visits};
|
|
}
|
|
|
|
/**
|
|
* Simulate set_fallbacks(p_fallbacks) for a diamond graph.
|
|
* thisFont wants to add F new fallbacks; each new fallback shares D shared fonts
|
|
* in a diamond topology below it.
|
|
*
|
|
* Diamond structure (depth=3, fanout=2):
|
|
* new[0] new[1] ... new[F-1]
|
|
* / \ / \
|
|
* mid0 mid1 ... (fanout fonts at depth 1)
|
|
* \ /
|
|
* leaf0 leaf1 ... (shared leafs — same Font objects)
|
|
*
|
|
* With no visited set, each leaf is visited 2^(depth-1) times per call.
|
|
*/
|
|
static long setFallbacksOpsSlowDiamond(int F, int fanout, int depth) {
|
|
// Build diamond: fanout^0 nodes at top, fanout^1 mid, ...
|
|
// All paths converge to `fanout^(depth-1)` shared leaf nodes.
|
|
|
|
// Create shared leaves
|
|
int numLeaves = fanout;
|
|
Font[] leaves = new Font[numLeaves];
|
|
for (int i = 0; i < numLeaves; i++) {
|
|
leaves[i] = new Font("leaf" + i, 100 + i);
|
|
}
|
|
|
|
// Build a diamond of given depth over the leaves
|
|
Font[] currentLevel = leaves;
|
|
for (int d = 1; d < depth; d++) {
|
|
Font[] nextLevel = new Font[currentLevel.length]; // same width (shared convergence)
|
|
for (int i = 0; i < nextLevel.length; i++) {
|
|
nextLevel[i] = new Font("mid_d" + d + "_" + i, 200 + d * 10 + i);
|
|
for (Font leaf : currentLevel) {
|
|
nextLevel[i].addFallback(leaf); // all mids point to all leaves → diamond
|
|
}
|
|
}
|
|
currentLevel = nextLevel;
|
|
}
|
|
Font[] topLevel = currentLevel;
|
|
|
|
// Create F new fonts, each pointing to all top-level nodes
|
|
long totalOps = 0;
|
|
Font thisFont = new Font("this", 0);
|
|
for (int f = 0; f < F; f++) {
|
|
Font newFb = new Font("newFb" + f, 300 + f);
|
|
for (Font top : topLevel) {
|
|
newFb.addFallback(top);
|
|
}
|
|
// Simulate set_fallbacks calling _is_cyclic(newFb, 0) — no visited set
|
|
long[] result = isCyclicSlow(thisFont, newFb, 0, 64);
|
|
totalOps += result[1];
|
|
}
|
|
return totalOps;
|
|
}
|
|
|
|
static long setFallbacksOpsFastDiamond(int F, int fanout, int depth) {
|
|
int numLeaves = fanout;
|
|
Font[] leaves = new Font[numLeaves];
|
|
for (int i = 0; i < numLeaves; i++) {
|
|
leaves[i] = new Font("leaf" + i, 100 + i);
|
|
}
|
|
Font[] currentLevel = leaves;
|
|
for (int d = 1; d < depth; d++) {
|
|
Font[] nextLevel = new Font[currentLevel.length];
|
|
for (int i = 0; i < nextLevel.length; i++) {
|
|
nextLevel[i] = new Font("mid_d" + d + "_" + i, 200 + d * 10 + i);
|
|
for (Font leaf : currentLevel) {
|
|
nextLevel[i].addFallback(leaf);
|
|
}
|
|
}
|
|
currentLevel = nextLevel;
|
|
}
|
|
Font[] topLevel = currentLevel;
|
|
|
|
long totalOps = 0;
|
|
Font thisFont = new Font("this", 0);
|
|
for (int f = 0; f < F; f++) {
|
|
Font newFb = new Font("newFb" + f, 300 + f);
|
|
for (Font top : topLevel) {
|
|
newFb.addFallback(top);
|
|
}
|
|
// Fix: per-call visited set — shared across the entire _is_cyclic traversal
|
|
Set<Font> visited = new HashSet<>();
|
|
long[] result = isCyclicFast(thisFont, newFb, 0, 64, visited);
|
|
totalOps += result[1];
|
|
}
|
|
return totalOps;
|
|
}
|
|
|
|
// ── godot-0010: _update_rids_fb without visited set ─────────────────────
|
|
|
|
/**
|
|
* SLOW: Simulates _update_rids_fb without visited set.
|
|
* Returns list of collected RIDs (may contain duplicates).
|
|
*/
|
|
static List<Integer> updateRidsSlow(Font p_f, int depth, int maxDepth) {
|
|
List<Integer> rids = new ArrayList<>();
|
|
updateRidsFbSlow(p_f, depth, maxDepth, rids);
|
|
return rids;
|
|
}
|
|
|
|
static void updateRidsFbSlow(Font p_f, int depth, int maxDepth, List<Integer> rids) {
|
|
if (depth > maxDepth || p_f == null) return;
|
|
if (p_f.rid > 0) rids.add(p_f.rid);
|
|
for (Font fb : p_f.fallbacks) {
|
|
updateRidsFbSlow(fb, depth + 1, maxDepth, rids); // ← no visited guard
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FAST: Simulates _update_rids_fb with visited set (the fix).
|
|
* Returns list of collected RIDs (each font at most once).
|
|
*/
|
|
static List<Integer> updateRidsFast(Font p_f, int depth, int maxDepth) {
|
|
List<Integer> rids = new ArrayList<>();
|
|
Set<Font> visited = new HashSet<>();
|
|
updateRidsFbFast(p_f, depth, maxDepth, rids, visited);
|
|
return rids;
|
|
}
|
|
|
|
static void updateRidsFbFast(Font p_f, int depth, int maxDepth,
|
|
List<Integer> rids, Set<Font> visited) {
|
|
if (depth > maxDepth || p_f == null) return;
|
|
if (visited.contains(p_f)) return; // skip — already collected
|
|
visited.add(p_f);
|
|
if (p_f.rid > 0) rids.add(p_f.rid);
|
|
for (Font fb : p_f.fallbacks) {
|
|
updateRidsFbFast(fb, depth + 1, maxDepth, rids, visited);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a diamond font graph and count operations and RID list size.
|
|
* Diamond: A → {B, C}, B → D, C → D (D is shared CJK fallback font).
|
|
*/
|
|
static Font buildDiamondGraph(int sharedFonts) {
|
|
Font root = new Font("root", 1);
|
|
Font[] shared = new Font[sharedFonts];
|
|
for (int i = 0; i < sharedFonts; i++) {
|
|
shared[i] = new Font("cjk" + i, 10 + i);
|
|
}
|
|
// Create N "language" fonts, each referencing ALL shared CJK fonts
|
|
int numLang = 4;
|
|
for (int l = 0; l < numLang; l++) {
|
|
Font lang = new Font("lang" + l, 50 + l);
|
|
for (Font s : shared) lang.addFallback(s);
|
|
root.addFallback(lang);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
static void bench(String label, long slowOps, long fastOps) {
|
|
double ratio = fastOps > 0 ? (double) slowOps / fastOps : 999;
|
|
System.out.printf(" %-48s slow: %,8d ops fast: %,8d ops ratio: %.0fx%n",
|
|
label, slowOps, fastOps, ratio);
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("=== UNIT godot-0009 / godot-0010: Godot Font Fallback CWE-407 ===");
|
|
System.out.println();
|
|
|
|
// ── godot-0009: _is_cyclic diamond test ─────────────────────────────
|
|
System.out.println("godot-0009: Font::_is_cyclic — diamond fallback graph");
|
|
|
|
// Diamond parameters
|
|
int F = 5; // number of new fallbacks to validate
|
|
int fanout = 3; // fonts at each level
|
|
int depth = 6; // depth of the diamond graph
|
|
|
|
long slowOps0009 = setFallbacksOpsSlowDiamond(F, fanout, depth);
|
|
long fastOps0009 = setFallbacksOpsFastDiamond(F, fanout, depth);
|
|
|
|
bench(String.format("isCyclic(F=%d,fanout=%d,depth=%d)", F, fanout, depth),
|
|
slowOps0009, fastOps0009);
|
|
|
|
// Deeper diamond
|
|
int F2 = 4; int fanout2 = 4; int depth2 = 8;
|
|
long slowOps0009b = setFallbacksOpsSlowDiamond(F2, fanout2, depth2);
|
|
long fastOps0009b = setFallbacksOpsFastDiamond(F2, fanout2, depth2);
|
|
bench(String.format("isCyclic(F=%d,fanout=%d,depth=%d)", F2, fanout2, depth2),
|
|
slowOps0009b, fastOps0009b);
|
|
|
|
System.out.println();
|
|
|
|
// Assertions
|
|
assert slowOps0009 > fastOps0009 * 10 :
|
|
"godot-0009: expected slow >> fast; slow=" + slowOps0009 + " fast=" + fastOps0009;
|
|
assert slowOps0009b > fastOps0009b * 100 :
|
|
"godot-0009 deep: expected slow >> fast; slow=" + slowOps0009b + " fast=" + fastOps0009b;
|
|
|
|
// ── godot-0010: _update_rids_fb diamond test ─────────────────────────
|
|
System.out.println("godot-0010: Font::_update_rids_fb — duplicate RIDs in diamond graph");
|
|
|
|
int sharedFonts = 8; // 8 shared CJK fallback fonts
|
|
Font root = buildDiamondGraph(sharedFonts);
|
|
|
|
List<Integer> slowRids = updateRidsSlow(root, 0, 64);
|
|
List<Integer> fastRids = updateRidsFast(root, 0, 64);
|
|
|
|
System.out.printf(" %-48s slow rids.size(): %d fast rids.size(): %d duplicates eliminated: %d%n",
|
|
String.format("updateRids(sharedFonts=%d, langFonts=4)", sharedFonts),
|
|
slowRids.size(), fastRids.size(), slowRids.size() - fastRids.size());
|
|
|
|
// Verify fast has no duplicates
|
|
Set<Integer> fastRidSet = new HashSet<>(fastRids);
|
|
assert fastRidSet.size() == fastRids.size() :
|
|
"godot-0010: fast path must have no duplicate RIDs";
|
|
|
|
// Verify slow has duplicates
|
|
Set<Integer> slowRidSet = new HashSet<>(slowRids);
|
|
assert slowRids.size() > slowRidSet.size() :
|
|
"godot-0010: slow path must produce duplicate RIDs (diamond graph)";
|
|
|
|
// Count ops (traversal steps)
|
|
long[] slowResult = new long[]{0};
|
|
long[] fastResult = new long[]{0};
|
|
countOpsUpdateRidsSlow(root, 0, 64, slowResult);
|
|
Set<Font> vis = new HashSet<>();
|
|
countOpsUpdateRidsFast(root, 0, 64, fastResult, vis);
|
|
|
|
bench(String.format("updateRids(sharedFonts=%d, langFonts=4)", sharedFonts),
|
|
slowResult[0], fastResult[0]);
|
|
|
|
assert slowResult[0] > fastResult[0] * 2 :
|
|
"godot-0010: expected slow >> fast; slow=" + slowResult[0] + " fast=" + fastResult[0];
|
|
|
|
System.out.println();
|
|
System.out.println("ALL ASSERTIONS PASS");
|
|
System.out.println();
|
|
System.out.println("Summary:");
|
|
System.out.println(" godot-0009: Font::_is_cyclic O(F^D) → O(N) — visited set eliminates");
|
|
System.out.println(" exponential re-traversal of shared fallback fonts");
|
|
System.out.println(" godot-0010: Font::_update_rids_fb — no visited set causes duplicate");
|
|
System.out.println(" RIDs and O(N^2) traversal; fix collects each font once");
|
|
}
|
|
|
|
static void countOpsUpdateRidsSlow(Font p_f, int depth, int maxDepth, long[] ops) {
|
|
if (depth > maxDepth || p_f == null) return;
|
|
ops[0]++;
|
|
for (Font fb : p_f.fallbacks) countOpsUpdateRidsSlow(fb, depth + 1, maxDepth, ops);
|
|
}
|
|
|
|
static void countOpsUpdateRidsFast(Font p_f, int depth, int maxDepth,
|
|
long[] ops, Set<Font> visited) {
|
|
if (depth > maxDepth || p_f == null) return;
|
|
if (visited.contains(p_f)) return;
|
|
visited.add(p_f);
|
|
ops[0]++;
|
|
for (Font fb : p_f.fallbacks) countOpsUpdateRidsFast(fb, depth + 1, maxDepth, ops, visited);
|
|
}
|
|
}
|