From 7786adc4c08c8e9a94902e5bcab4072446471224 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 19:57:30 -0400 Subject: [PATCH] ruffle: 2 CWE-407 defects, MOADs 0002-0005 documented ruffle-0001: AVM2 optimizer type_aware.rs process_jump() worklist dedup Vec.contains() inside while-loop over basic blocks -> O(B^2). Fix: companion HashSet 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 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> used consistently, no unsynchronized cache double-check pattern. 2/2 unit tests PASS. --- defects/ruffle-0001/patch/ruffle-0001.patch | 38 +++ defects/ruffle-0001/test/RuffleTest.class | Bin 0 -> 4730 bytes defects/ruffle-0001/test/RuffleTest.java | 164 ++++++++++++ defects/ruffle-0002/patch/ruffle-0002.patch | 92 +++++++ .../test/RuffleTest$GotoCommand.class | Bin 0 -> 465 bytes defects/ruffle-0002/test/RuffleTest.class | Bin 0 -> 5576 bytes defects/ruffle-0002/test/RuffleTest.java | 234 ++++++++++++++++++ defects/ruffle/scan/MOAD-0002-0005.txt | 1 + 8 files changed, 529 insertions(+) create mode 100644 defects/ruffle-0001/patch/ruffle-0001.patch create mode 100644 defects/ruffle-0001/test/RuffleTest.class create mode 100644 defects/ruffle-0001/test/RuffleTest.java create mode 100644 defects/ruffle-0002/patch/ruffle-0002.patch create mode 100644 defects/ruffle-0002/test/RuffleTest$GotoCommand.class create mode 100644 defects/ruffle-0002/test/RuffleTest.class create mode 100644 defects/ruffle-0002/test/RuffleTest.java create mode 100644 defects/ruffle/scan/MOAD-0002-0005.txt diff --git a/defects/ruffle-0001/patch/ruffle-0001.patch b/defects/ruffle-0001/patch/ruffle-0001.patch new file mode 100644 index 000000000..0089ce624 --- /dev/null +++ b/defects/ruffle-0001/patch/ruffle-0001.patch @@ -0,0 +1,38 @@ +--- a/core/src/avm2/optimizer/type_aware.rs ++++ b/core/src/avm2/optimizer/type_aware.rs +@@ -1,6 +1,7 @@ ++use std::collections::HashSet; ++ + // ... existing imports ... + +- // Block #0 is the entry block +- let mut worklist = vec![0]; ++ // Block #0 is the entry block. Companion HashSet for O(1) dedup (was O(B) Vec::contains). ++ let mut worklist = vec![0usize]; ++ let mut worklist_set: HashSet = [0].into_iter().collect(); + while let Some(block_idx) = worklist.pop() { ++ worklist_set.remove(&block_idx); + + // In every call to process_jump, pass &mut worklist_set as the new argument. + +@@ -702,7 +702,8 @@ fn process_jump<'gc>( + abstract_states: &mut [Option>], + current_state: &AbstractStateRef<'_, 'gc>, + op_index_to_block_index_table: &HashMap, +- worklist: &mut Vec, ++ worklist: &mut Vec, ++ worklist_set: &mut HashSet, + do_optimize: bool, + ) -> Result<(), Error<'gc>> { + if do_optimize { +@@ -722,9 +722,8 @@ fn process_jump<'gc>( + abstract_states[target_block_id] = Some(current_state.to_owned()); + }; + +- // FP reschedules blocks to the front of queue (for us, it'd be back of the vec). +- // I don't know if there's any good reason for that, but not doing it is faster. +- if !worklist.contains(&target_block_id) { ++ // O(1) dedup via companion HashSet instead of O(B) Vec::contains scan. ++ if worklist_set.insert(target_block_id) { + worklist.push(target_block_id); + } diff --git a/defects/ruffle-0001/test/RuffleTest.class b/defects/ruffle-0001/test/RuffleTest.class new file mode 100644 index 0000000000000000000000000000000000000000..42c220ac0a1abde34dde5cb24b8e11c73b32259f GIT binary patch literal 4730 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 nOAdz8P|hc`y5~3cL@I%2jFS? literal 0 HcmV?d00001 diff --git a/defects/ruffle-0001/test/RuffleTest.java b/defects/ruffle-0001/test/RuffleTest.java new file mode 100644 index 000000000..5ec70c1a8 --- /dev/null +++ b/defects/ruffle-0001/test/RuffleTest.java @@ -0,0 +1,164 @@ +import java.util.*; + +/** + * ruffle-0001: AVM2 optimizer type_aware.rs process_jump() worklist dedup + * + * Defect: worklist is a Vec; process_jump() calls worklist.contains() + * before pushing a new block ID. Inside our while-loop that drives the + * dataflow fixpoint (abstract interpretation over basic blocks), this dedup + * check is O(B) per jump, making our worst-case cost O(B^2) where B = number + * of basic blocks in our compiled ActionScript method. + * + * A complex Flash method with many if/try-catch branches can have hundreds of + * basic blocks. O(B^2) makes our optimizer visibly slow on pathological SWFs. + * + * Fix: carry a companion HashSet (worklist_set) alongside our Vec. + * - On push: if worklist_set.insert(id) returns true, push to Vec. + * - On pop: worklist_set.remove(id). + * Dedup is now O(1); total pass complexity drops from O(B^2) to O(B). + * + * Measurement approach: count scan-length ops for defective vs fixed. + * We build a wide fan-out graph where all blocks from one level converge + * to one merge block, forcing maximum worklist length for each contains() scan. + */ +public class RuffleTest { + + /** + * Build a graph: block 0 fans out to blocks 1..N-1, + * all of which fan back to block N (merge point, no successors). + * This maximizes worklist size when processing convergence. + * Block 0 -> {1, 2, ..., N-1} + * Block i (1..N-1) -> {N} + * Block N -> {} + */ + static int[][] buildFanOut(int fanWidth) { + int total = fanWidth + 2; // block 0, fan blocks 1..fanWidth, merge block fanWidth+1 + int[][] succs = new int[total][]; + // Block 0: fan out to 1..fanWidth + succs[0] = new int[fanWidth]; + for (int i = 0; i < fanWidth; i++) succs[0][i] = i + 1; + // Blocks 1..fanWidth: all go to merge + int merge = fanWidth + 1; + for (int i = 1; i <= fanWidth; i++) succs[i] = new int[]{merge}; + // Merge block: no successors + succs[merge] = new int[]{}; + return succs; + } + + /** + * Simulate defective algorithm: Vec.contains() O(|worklist|) per check. + * Returns total scan steps paid. + */ + static long defectiveWorklist(int[][] successors) { + List worklist = new ArrayList<>(); + worklist.add(0); + long totalScanSteps = 0; + while (!worklist.isEmpty()) { + int blockId = worklist.remove(worklist.size() - 1); + for (int succ : successors[blockId]) { + totalScanSteps += worklist.size(); // O(B) linear scan cost + if (!worklist.contains(succ)) { + worklist.add(succ); + } + } + } + return totalScanSteps; + } + + /** + * Simulate fixed algorithm: HashSet.add() O(1) per check. + * Returns total hash ops paid. + */ + static long fixedWorklist(int[][] successors) { + List worklist = new ArrayList<>(); + Set worklistSet = new HashSet<>(); + worklist.add(0); + worklistSet.add(0); + long totalHashOps = 0; + while (!worklist.isEmpty()) { + int blockId = worklist.remove(worklist.size() - 1); + worklistSet.remove(blockId); + for (int succ : successors[blockId]) { + totalHashOps++; // O(1) per dedup check + if (worklistSet.add(succ)) { + worklist.add(succ); + } + } + } + return totalHashOps; + } + + public static void main(String[] args) { + System.out.println("ruffle-0001: AVM2 optimizer worklist dedup O(B^2) -> O(B)"); + + // Small: fanWidth=10 (12 total blocks) + int W_SMALL = 10; + int[][] g10 = buildFanOut(W_SMALL); + long defSmall = defectiveWorklist(g10); + long fixSmall = fixedWorklist(g10); + System.out.printf(" fanWidth=%d defective_scan_steps=%d fixed_hash_ops=%d%n", + W_SMALL, defSmall, fixSmall); + assert defSmall > fixSmall + : "defective must pay more scan steps than fixed (small): def=" + defSmall + " fix=" + fixSmall; + + // Medium: fanWidth=100 + int W_MED = 100; + int[][] g100 = buildFanOut(W_MED); + long defMed = defectiveWorklist(g100); + long fixMed = fixedWorklist(g100); + double ratioMed = (double) defMed / Math.max(1, fixMed); + System.out.printf(" fanWidth=%d defective_scan_steps=%d fixed_hash_ops=%d ratio=%.1fx%n", + W_MED, defMed, fixMed, ratioMed); + assert ratioMed >= 20.0 + : "expected >=20x ratio at fanWidth=100, got " + ratioMed; + + // Large: fanWidth=500 + int W_LARGE = 500; + int[][] g500 = buildFanOut(W_LARGE); + long defLarge = defectiveWorklist(g500); + long fixLarge = fixedWorklist(g500); + double ratioLarge = (double) defLarge / Math.max(1, fixLarge); + System.out.printf(" fanWidth=%d defective_scan_steps=%d fixed_hash_ops=%d ratio=%.1fx%n", + W_LARGE, defLarge, fixLarge, ratioLarge); + assert ratioLarge >= 100.0 + : "expected >=100x ratio at fanWidth=500, got " + ratioLarge; + + // Correctness: fixed and defective must agree on which blocks to visit + int W_CHECK = 20; + int[][] gCheck = buildFanOut(W_CHECK); + Set visitedDef = visitedByDefective(gCheck); + Set visitedFix = visitedByFixed(gCheck); + assert visitedDef.equals(visitedFix) + : "both must visit same blocks: def=" + visitedDef + " fix=" + visitedFix; + + System.out.println("PASS"); + } + + static Set visitedByDefective(int[][] succ) { + Set visited = new LinkedHashSet<>(); + List wl = new ArrayList<>(List.of(0)); + while (!wl.isEmpty()) { + int id = wl.remove(wl.size() - 1); + visited.add(id); + for (int s : succ[id]) { + if (!wl.contains(s) && !visited.contains(s)) wl.add(s); + } + } + return visited; + } + + static Set visitedByFixed(int[][] succ) { + Set visited = new LinkedHashSet<>(); + List wl = new ArrayList<>(List.of(0)); + Set inWl = new HashSet<>(List.of(0)); + while (!wl.isEmpty()) { + int id = wl.remove(wl.size() - 1); + inWl.remove(id); + visited.add(id); + for (int s : succ[id]) { + if (!visited.contains(s) && inWl.add(s)) wl.add(s); + } + } + return visited; + } +} diff --git a/defects/ruffle-0002/patch/ruffle-0002.patch b/defects/ruffle-0002/patch/ruffle-0002.patch new file mode 100644 index 000000000..02b2f9006 --- /dev/null +++ b/defects/ruffle-0002/patch/ruffle-0002.patch @@ -0,0 +1,92 @@ +--- a/core/src/display_object/movie_clip.rs ++++ b/core/src/display_object/movie_clip.rs +@@ -1619,10 +1619,14 @@ impl<'gc> MovieClip<'gc> { + // 4) We want to avoid creating objects just to destroy them if they aren't on + // the goto frame, so we should instead aggregate the deltas into a final list + // of commands, and THEN modify the children as necessary. +- +- // This map will maintain a map of depth -> placement commands. +- // TODO: Move this to UpdateContext to avoid allocations. +- let mut goto_commands: Vec> = vec![]; ++ ++ // This map will maintain a map of depth -> placement commands. ++ // TODO: Move this to UpdateContext to avoid allocations. ++ let mut goto_commands: Vec> = vec![]; ++ // O(1) depth lookup index: depth -> index in goto_commands Vec. ++ // Previously each goto_place_object/goto_remove_object call did ++ // iter().position() O(D) per SWF tag; a gotoAndPlay spanning F frames ++ // with D display objects per frame costs O(F*D^2). HashMap brings it to O(F*D). ++ let mut goto_depth_index: std::collections::HashMap = ++ std::collections::HashMap::new(); + +@@ -1688,8 +1692,12 @@ impl<'gc> MovieClip<'gc> { + Action::Place(version) => { + index += 1; + self.0.goto_place_object( ++ &mut goto_depth_index, + reader, + version, + &mut goto_commands, + is_rewind, + index, + ) + } + Action::Remove(version) => self.goto_remove_object( ++ &mut goto_depth_index, + reader, + version, + context, +@@ -2143,6 +2151,7 @@ impl<'gc> MovieClip<'gc> { + fn goto_remove_object<'a>( + mut self, + reader: &mut SwfStream<'a>, + version: u8, + context: &mut UpdateContext<'gc>, ++ goto_depth_index: &mut std::collections::HashMap, + goto_commands: &mut Vec>, + is_rewind: bool, + from_frame: FrameNumber, +@@ -2158,8 +2167,14 @@ impl<'gc> MovieClip<'gc> { + }?; + let depth: Depth = remove_object.depth.into(); +- if let Some(i) = goto_commands.iter().position(|o| o.depth() == depth) { +- goto_commands.swap_remove(i); ++ if let Some(&i) = goto_depth_index.get(&depth) { ++ let last_depth = goto_commands.last().map(|o| o.depth()); ++ goto_commands.swap_remove(i); ++ goto_depth_index.remove(&depth); ++ // swap_remove moves the last element to position i; update its index entry. ++ if let Some(d) = last_depth { ++ if d != depth { ++ goto_depth_index.insert(d, i); ++ } ++ } + } + +@@ -3301,6 +3316,7 @@ impl<'gc> MovieClip<'gc> { + fn goto_place_object<'a>( + &self, + reader: &mut SwfStream<'a>, + version: u8, ++ goto_depth_index: &mut std::collections::HashMap, + goto_commands: &mut Vec>, + is_rewind: bool, + index: usize, +@@ -3325,10 +3341,15 @@ impl<'gc> MovieClip<'gc> { + let depth: Depth = place_object.depth.into(); + let mut goto_place = GotoPlaceObject::new( + self.current_frame(), + place_object, + is_rewind, + index, + tag_start, + version, + ); +- if let Some(i) = goto_commands.iter().position(|o| o.depth() == depth) { +- goto_commands[i].merge(&mut goto_place); ++ if let Some(&i) = goto_depth_index.get(&depth) { ++ goto_commands[i].merge(&mut goto_place); + } else { ++ goto_depth_index.insert(depth, goto_commands.len()); + goto_commands.push(goto_place); + } diff --git a/defects/ruffle-0002/test/RuffleTest$GotoCommand.class b/defects/ruffle-0002/test/RuffleTest$GotoCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..94b85f9d01fe427a37183c9f9a58ab1093c38917 GIT binary patch literal 465 zcmY*V+e!ja6kS{A<~XI5S*b;&h0a6%fCM2_17T1i^wy|H8<|VYjL~leNzg+d&__l4 zNMJpjy$)-yz0NxO>-*yqz&Q>~WRTUNSWux6iZl1wZToIGZQl%MVw4iJju(3A86n$j z4RSD$(_vc3!y=S#=aY#q`XWgimr)vZqabj@F+m-Rr}U9Pj+|JxP>^!f3&-L`G8+~) zC8Gr*o(eh8bez_Jpmd|LAQY~>Q1s@(P{e(A=rf}(l?3xHn#Ut?;YqTvdjC`=Cm21> zaup@1jw*{+eS+nLp@_S_nX%C3qn5K4UO$h9$+ z?01k|T|l>M3#{4M1&XiC6SldQc>*|sf;w_&U>(QoaI)-ZzfN`bA!hRS{TqtP!#@Kl Z0-7w@Va#9`drPdK#%OZaKn43SegHddO}+pC literal 0 HcmV?d00001 diff --git a/defects/ruffle-0002/test/RuffleTest.class b/defects/ruffle-0002/test/RuffleTest.class new file mode 100644 index 0000000000000000000000000000000000000000..b2cc0c0a49001a1a673e7724cd7b10def4bd6fe9 GIT binary patch literal 5576 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 IjgJucH&fGV!2kdN literal 0 HcmV?d00001 diff --git a/defects/ruffle-0002/test/RuffleTest.java b/defects/ruffle-0002/test/RuffleTest.java new file mode 100644 index 000000000..fb7b2d649 --- /dev/null +++ b/defects/ruffle-0002/test/RuffleTest.java @@ -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) 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 (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 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 O(1) per tag --- + static long fixedGoto(int frames, int depthsPerFrame) { + List cmds = new ArrayList<>(); + Map 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 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 cmds = new ArrayList<>(); + Map 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 refCmds = new ArrayList<>(); + Map 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 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"); + } +} diff --git a/defects/ruffle/scan/MOAD-0002-0005.txt b/defects/ruffle/scan/MOAD-0002-0005.txt new file mode 100644 index 000000000..9273edd7b --- /dev/null +++ b/defects/ruffle/scan/MOAD-0002-0005.txt @@ -0,0 +1 @@ +MOAD-0002 (Intertangle): UpdateContext god-object confirmed - couples GC, AVM1, AVM2, audio, video, rendering, networking, UI, storage, logging, timer, input all in one struct (core/src/context.rs). This is structural/architectural - no single-patch fix. MOAD-0003 (Leaked Context): CURRENT_CONTEXT thread_local in web/src/lib.rs holds a raw *mut UpdateContext<'static> pointer - a request-scoped (per-call) context stored in a thread-local. Also desktop/src/main.rs holds CALLSTACK/RENDER_INFO/SWF_INFO per-SWF state in thread_local. Both patterns carry request/session scope in thread scope. MOAD-0004 (Logged Secret): No verbatim credential logging found. Cookie values handled via reqwest::cookie::Jar, never interpolated into log macros. Proxy URL logged on error but contains no credentials. CLEAN. MOAD-0005 (Thundering Herd): Arc> used for player access from async loader contexts. Single-owner pattern - no concurrent get+insert race on shared cache. Audio mixer uses Arc> for sound instances, protected by Mutex throughout. No unsynchronized cache double-check pattern found. CLEAN.