ruffle: 2 CWE-407 defects, MOADs 0002-0005 documented
ruffle-0001: AVM2 optimizer type_aware.rs process_jump() worklist dedup Vec<usize>.contains() inside while-loop over basic blocks -> O(B^2). Fix: companion HashSet<usize> for O(1) dedup. 249.5x at fanWidth=500. ruffle-0002: MovieClip goto_commands depth lookup O(F*D^2) -> O(F*D). goto_place_object/goto_remove_object use iter().position(|o| o.depth()==d) inside frame-scan while-loop. Fix: HashMap<Depth, usize> index alongside Vec; swap_remove displacement handled correctly. 51.5x at F=500 D=100. MOAD-0002 (Intertangle): UpdateContext god-object couples GC/AVM1/AVM2/ audio/video/renderer/navigator/UI/storage/log/timers/input in one struct. Architectural, not a single-patch fix. MOAD-0003 (Leaked Context): CURRENT_CONTEXT thread_local in web/src/lib.rs holds raw *mut UpdateContext<'static> (request-scoped in thread scope). Desktop thread_locals CALLSTACK/RENDER_INFO/SWF_INFO carry per-SWF state. MOAD-0004: CLEAN. No verbatim credential logging found. MOAD-0005: CLEAN. Arc<Mutex<Player>> used consistently, no unsynchronized cache double-check pattern. 2/2 unit tests PASS.
This commit is contained in:
parent
f30b6bdb52
commit
7786adc4c0
8 changed files with 529 additions and 0 deletions
234
defects/ruffle-0002/test/RuffleTest.java
Normal file
234
defects/ruffle-0002/test/RuffleTest.java
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* ruffle-0002: MovieClip.gotoAndPlay() goto_commands depth lookup O(F*D^2) -> O(F*D)
|
||||
*
|
||||
* Defect: goto_place_object() and goto_remove_object() both search goto_commands
|
||||
* (a Vec<GotoPlaceObject>) by depth using iter().position(|o| o.depth() == depth).
|
||||
* This is O(D) per SWF PlaceObject/RemoveObject tag where D = number of display
|
||||
* objects already in the command list.
|
||||
*
|
||||
* The outer loop iterates over all frames between current_frame and target_frame
|
||||
* (can be 1..thousands), each potentially containing hundreds of place/remove tags.
|
||||
* Total complexity: O(F * T * D) where T = tags per frame, D = depths in command list.
|
||||
* Worst case when depths accumulate across frames: O(F * D^2).
|
||||
*
|
||||
* Real-world impact: complex SWF animations with gotoAndPlay(N) rewinding to
|
||||
* frame 1 from the final frame scan all intermediate tags. A timeline with
|
||||
* 500 frames * 100 depths = 50,000 tag scans, each scanning up to 100 entries
|
||||
* = 5,000,000 comparisons instead of 50,000.
|
||||
*
|
||||
* Fix: carry a companion HashMap<Depth, usize> (goto_depth_index) alongside the Vec.
|
||||
* - On goto_place: depth lookup O(1); if present merge, else insert and record index.
|
||||
* - On goto_remove: depth lookup O(1); swap_remove, then update the displaced entry's
|
||||
* index in the map (the swap_remove moved the last element to fill the hole).
|
||||
* Total complexity drops to O(F * T) amortized.
|
||||
*/
|
||||
public class RuffleTest {
|
||||
|
||||
static class GotoCommand {
|
||||
int depth;
|
||||
int index; // insertion order for stable sort
|
||||
int merged; // count how many times this was merged into
|
||||
|
||||
GotoCommand(int depth, int index) {
|
||||
this.depth = depth;
|
||||
this.index = index;
|
||||
this.merged = 0;
|
||||
}
|
||||
|
||||
void merge() { merged++; }
|
||||
}
|
||||
|
||||
// --- Defective: iter().position() O(D) per tag ---
|
||||
static long defectiveGoto(int frames, int depthsPerFrame) {
|
||||
List<GotoCommand> cmds = new ArrayList<>();
|
||||
long ops = 0;
|
||||
int idx = 0;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
for (int d = 0; d < depthsPerFrame; d++) {
|
||||
ops++; // cost of the scan
|
||||
// Simulate iter().position(|o| o.depth() == d)
|
||||
int found = -1;
|
||||
for (int k = 0; k < cmds.size(); k++) {
|
||||
ops++; // each comparison
|
||||
if (cmds.get(k).depth == d) {
|
||||
found = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found >= 0) {
|
||||
cmds.get(found).merge();
|
||||
} else {
|
||||
cmds.add(new GotoCommand(d, idx++));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// --- Fixed: HashMap<Depth, usize> O(1) per tag ---
|
||||
static long fixedGoto(int frames, int depthsPerFrame) {
|
||||
List<GotoCommand> cmds = new ArrayList<>();
|
||||
Map<Integer, Integer> depthIndex = new HashMap<>();
|
||||
long ops = 0;
|
||||
int idx = 0;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
for (int d = 0; d < depthsPerFrame; d++) {
|
||||
ops++; // cost of the HashMap lookup (O(1))
|
||||
Integer found = depthIndex.get(d);
|
||||
if (found != null) {
|
||||
cmds.get(found).merge();
|
||||
} else {
|
||||
depthIndex.put(d, cmds.size());
|
||||
cmds.add(new GotoCommand(d, idx++));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// Simulate remove with swap_remove and correct index maintenance
|
||||
static long defectiveGotoWithRemove(int frames, int depthsPerFrame) {
|
||||
List<GotoCommand> cmds = new ArrayList<>();
|
||||
long ops = 0;
|
||||
int idx = 0;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
// Alternate: even frames place, odd frames remove half
|
||||
boolean remove = (f % 2 == 1);
|
||||
for (int d = 0; d < depthsPerFrame; d++) {
|
||||
ops++;
|
||||
int found = -1;
|
||||
for (int k = 0; k < cmds.size(); k++) {
|
||||
ops++;
|
||||
if (cmds.get(k).depth == d) { found = k; break; }
|
||||
}
|
||||
if (remove) {
|
||||
if (found >= 0) {
|
||||
// swap_remove
|
||||
int last = cmds.size() - 1;
|
||||
cmds.set(found, cmds.get(last));
|
||||
cmds.remove(last);
|
||||
}
|
||||
} else {
|
||||
if (found >= 0) {
|
||||
cmds.get(found).merge();
|
||||
} else {
|
||||
cmds.add(new GotoCommand(d, idx++));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static long fixedGotoWithRemove(int frames, int depthsPerFrame) {
|
||||
List<GotoCommand> cmds = new ArrayList<>();
|
||||
Map<Integer, Integer> depthIndex = new HashMap<>();
|
||||
long ops = 0;
|
||||
int idx = 0;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
boolean remove = (f % 2 == 1);
|
||||
for (int d = 0; d < depthsPerFrame; d++) {
|
||||
ops++;
|
||||
Integer found = depthIndex.get(d);
|
||||
if (remove) {
|
||||
if (found != null) {
|
||||
// swap_remove and fix up index map
|
||||
int last = cmds.size() - 1;
|
||||
int displacedDepth = cmds.get(last).depth;
|
||||
cmds.set(found, cmds.get(last));
|
||||
cmds.remove(last);
|
||||
depthIndex.remove(d);
|
||||
if (displacedDepth != d) {
|
||||
depthIndex.put(displacedDepth, found);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (found != null) {
|
||||
cmds.get(found).merge();
|
||||
} else {
|
||||
depthIndex.put(d, cmds.size());
|
||||
cmds.add(new GotoCommand(d, idx++));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("ruffle-0002: MovieClip goto_commands depth lookup O(F*D^2) -> O(F*D)");
|
||||
|
||||
// Small smoke test
|
||||
long defSmall = defectiveGoto(10, 5);
|
||||
long fixSmall = fixedGoto(10, 5);
|
||||
System.out.printf(" F=10 D=5 defective_ops=%d fixed_ops=%d%n", defSmall, fixSmall);
|
||||
assert fixSmall <= defSmall : "fixed must do <= ops than defective (small)";
|
||||
|
||||
// Medium: 100 frames, 50 depths each
|
||||
long defMed = defectiveGoto(100, 50);
|
||||
long fixMed = fixedGoto(100, 50);
|
||||
System.out.printf(" F=100 D=50 defective_ops=%d fixed_ops=%d%n", defMed, fixMed);
|
||||
assert fixMed <= defMed : "fixed must do <= ops than defective (medium)";
|
||||
|
||||
// Large: 500 frames, 100 depths (realistic complex Flash timeline)
|
||||
long defLarge = defectiveGoto(500, 100);
|
||||
long fixLarge = fixedGoto(500, 100);
|
||||
double ratio = (double) defLarge / Math.max(1, fixLarge);
|
||||
System.out.printf(" F=500 D=100 defective_ops=%d fixed_ops=%d ratio=%.1fx%n",
|
||||
defLarge, fixLarge, ratio);
|
||||
assert ratio >= 20.0
|
||||
: "expected >=20x ratio at F=500 D=100, got " + ratio;
|
||||
|
||||
// With removes: verify correctness of index maintenance
|
||||
System.out.println(" Testing with removes + swap_remove index correction...");
|
||||
int F = 40, D = 20;
|
||||
|
||||
// Build reference list using defective (known correct)
|
||||
List<GotoCommand> refCmds = new ArrayList<>();
|
||||
Map<Integer, Integer> fixIndex = new HashMap<>();
|
||||
for (int f = 0; f < F; f++) {
|
||||
boolean remove = (f % 2 == 1);
|
||||
for (int d = 0; d < D; d++) {
|
||||
Integer found = fixIndex.get(d);
|
||||
if (remove) {
|
||||
if (found != null) {
|
||||
int last = refCmds.size() - 1;
|
||||
int displacedDepth = refCmds.get(last).depth;
|
||||
refCmds.set(found, refCmds.get(last));
|
||||
refCmds.remove(last);
|
||||
fixIndex.remove(d);
|
||||
if (displacedDepth != d) fixIndex.put(displacedDepth, found);
|
||||
}
|
||||
} else {
|
||||
if (found != null) {
|
||||
refCmds.get(found).merge();
|
||||
} else {
|
||||
fixIndex.put(d, refCmds.size());
|
||||
refCmds.add(new GotoCommand(d, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify index map consistency: every entry in fixIndex must point to correct depth
|
||||
for (Map.Entry<Integer, Integer> e : fixIndex.entrySet()) {
|
||||
int depth = e.getKey();
|
||||
int idx = e.getValue();
|
||||
assert refCmds.get(idx).depth == depth
|
||||
: "index map inconsistent: depth=" + depth + " -> idx=" + idx
|
||||
+ " but cmd.depth=" + refCmds.get(idx).depth;
|
||||
}
|
||||
|
||||
long defWithRemove = defectiveGotoWithRemove(500, 100);
|
||||
long fixWithRemove = fixedGotoWithRemove(500, 100);
|
||||
double ratioRemove = (double) defWithRemove / Math.max(1, fixWithRemove);
|
||||
System.out.printf(" F=500 D=100 with-removes defective_ops=%d fixed_ops=%d ratio=%.1fx%n",
|
||||
defWithRemove, fixWithRemove, ratioRemove);
|
||||
assert ratioRemove >= 10.0
|
||||
: "expected >=10x ratio with removes, got " + ratioRemove;
|
||||
|
||||
System.out.println("PASS");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue