From 555896829ab5614a07186d225cf388275484d588 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 08:54:06 -0400 Subject: [PATCH] =?UTF-8?q?vertx:=201=20CWE-407=20defect=20=E2=80=94=20HAM?= =?UTF-8?q?anager=20nodeLeft=20List.contains=20O(N=C3=97M)=20in=20cluster?= =?UTF-8?q?=20failover=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ertx-0001-hamanger-nodelist-contains.patch | 24 ++ defects/vertx/unit/VertxTest.class | Bin 0 -> 5379 bytes defects/vertx/unit/VertxTest.java | 225 ++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 defects/vertx/patch/vertx-0001-hamanger-nodelist-contains.patch create mode 100644 defects/vertx/unit/VertxTest.class create mode 100644 defects/vertx/unit/VertxTest.java diff --git a/defects/vertx/patch/vertx-0001-hamanger-nodelist-contains.patch b/defects/vertx/patch/vertx-0001-hamanger-nodelist-contains.patch new file mode 100644 index 000000000..9c49f6cb1 --- /dev/null +++ b/defects/vertx/patch/vertx-0001-hamanger-nodelist-contains.patch @@ -0,0 +1,24 @@ +# UNDF: UNDF-2026-000000570 +--- a/vertx-core/src/main/java/io/vertx/core/impl/HAManager.java ++++ b/vertx-core/src/main/java/io/vertx/core/impl/HAManager.java +@@ -304,15 +304,15 @@ public class HAManager { + // We also check for and potentially resume any previous failovers that might have failed + // We can determine this if there any ids in the cluster map which aren't in the node list +- List nodes = clusterManager.getNodes(); ++ Set nodes = new HashSet<>(clusterManager.getNodes()); + + for (Map.Entry entry: clusterMap.entrySet()) { + if (!leftNodeID.equals(entry.getKey()) && !nodes.contains(entry.getKey())) { + JsonObject haInfo = new JsonObject(entry.getValue()); + checkFailover(entry.getKey(), haInfo); + } + } + } + } + + private void addHaInfoIfLost() { +- if (clusterManager.getNodes().contains(nodeID) && !clusterMap.containsKey(nodeID)) { ++ if (new HashSet<>(clusterManager.getNodes()).contains(nodeID) && !clusterMap.containsKey(nodeID)) { + synchronized (haInfo) { + clusterMap.put(nodeID, haInfo.encode()); + } diff --git a/defects/vertx/unit/VertxTest.class b/defects/vertx/unit/VertxTest.class new file mode 100644 index 0000000000000000000000000000000000000000..a2b431da51c48a54d4e13b306d153ba17cf16805 GIT binary patch literal 5379 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 OMTpYB!$.contains() inside a loop + * over clusterMap.entrySet() → O(N×M) where N = clusterMap size, M = node count. + * Fix: convert List to HashSet before the loop → O(N+M). + * + * Additionally: HAManager.addHaInfoIfLost() calls + * clusterManager.getNodes().contains(nodeID) on every nodeAdded/nodeLeft event + * → O(M) per event with List, O(1) with Set. + */ +public class VertxTest { + + // ----------------------------------------------------------------------- + // vertx-0001 helpers — simulate the HAManager.nodeLeft inner loop + // ----------------------------------------------------------------------- + + /** + * Defective: List.contains() called inside a for-each over + * clusterMapKeys → O(N × M) total. + * + * Mirrors HAManager lines 307-314: + * List nodes = clusterManager.getNodes(); + * for (Map.Entry entry : clusterMap.entrySet()) { + * if (!leftNodeID.equals(entry.getKey()) && !nodes.contains(entry.getKey())) { + * checkFailover(...) + * } + * } + */ + static long nodeLeftDefect(List clusterMapKeys, List nodes, String leftNodeID) { + long ops = 0; + for (String key : clusterMapKeys) { + ops++; + if (!leftNodeID.equals(key)) { + // List.contains = linear scan, O(M) worst case + boolean found = nodes.contains(key); + ops += nodes.size(); // account for full scan (worst case) + } + } + return ops; + } + + /** + * Fixed: wrap nodes in HashSet once, then O(1) membership test per entry. + */ + static long nodeLeftFixed(List clusterMapKeys, List nodes, String leftNodeID) { + Set nodeSet = new HashSet<>(nodes); // O(M) once + long ops = nodes.size(); // cost of building the set + for (String key : clusterMapKeys) { + ops++; + if (!leftNodeID.equals(key)) { + boolean found = nodeSet.contains(key); // O(1) + ops++; + } + } + return ops; + } + + // ----------------------------------------------------------------------- + // vertx-0001 test A — nodeLeft loop op-count ratio + // ----------------------------------------------------------------------- + + static void testVertx0001_opCount() { + int N = 1000; // clusterMap size + int M = 1000; // node count + + List clusterMapKeys = new ArrayList<>(N); + List nodes = new ArrayList<>(M); + + for (int i = 0; i < N; i++) clusterMapKeys.add("node-" + i); + for (int i = 0; i < M; i++) nodes.add("node-" + i); + + String leftNode = "node-0"; + + long defectOps = nodeLeftDefect(clusterMapKeys, nodes, leftNode); + long fixedOps = nodeLeftFixed(clusterMapKeys, nodes, leftNode); + double ratio = (double) defectOps / fixedOps; + + System.out.printf( + "vertx-0001 nodeLeft op-count: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio + ); + + if (ratio < 50.0) { + throw new AssertionError( + "vertx-0001: expected op-count ratio >= 50x at N=M=1000, got " + ratio); + } + System.out.println("vertx-0001 op-count: PASS"); + } + + // ----------------------------------------------------------------------- + // vertx-0001 test B — wall-clock timing + // ----------------------------------------------------------------------- + + static void testVertx0001_timing() { + int N = 500; + int M = 500; + int REPS = 200; + + List clusterMapKeys = new ArrayList<>(N); + List nodes = new ArrayList<>(M); + for (int i = 0; i < N; i++) clusterMapKeys.add("node-" + i); + for (int i = 0; i < M; i++) nodes.add("node-" + i); + + String leftNode = "node-0"; + + // Warm up + for (int r = 0; r < 10; r++) { + simulateNodeLeftDefect(clusterMapKeys, nodes, leftNode); + simulateNodeLeftFixed(clusterMapKeys, nodes, leftNode); + } + + long t0 = System.nanoTime(); + for (int r = 0; r < REPS; r++) simulateNodeLeftDefect(clusterMapKeys, nodes, leftNode); + long defectNs = System.nanoTime() - t0; + + long t1 = System.nanoTime(); + for (int r = 0; r < REPS; r++) simulateNodeLeftFixed(clusterMapKeys, nodes, leftNode); + long fixedNs = System.nanoTime() - t1; + + double ratio = (double) defectNs / fixedNs; + System.out.printf( + "vertx-0001 nodeLeft timing: defect=%.2fms fixed=%.2fms ratio=%.1fx%n", + defectNs / 1e6, fixedNs / 1e6, ratio + ); + + if (ratio < 5.0) { + throw new AssertionError( + "vertx-0001: expected wall-clock ratio >= 5x at N=M=500, got " + ratio); + } + System.out.println("vertx-0001 timing: PASS"); + } + + // Real simulation (no op counting) for timing test + static int simulateNodeLeftDefect(List clusterMapKeys, List nodes, String leftNodeID) { + int failovers = 0; + for (String key : clusterMapKeys) { + if (!leftNodeID.equals(key) && !nodes.contains(key)) { + failovers++; + } + } + return failovers; + } + + static int simulateNodeLeftFixed(List clusterMapKeys, List nodes, String leftNodeID) { + Set nodeSet = new HashSet<>(nodes); + int failovers = 0; + for (String key : clusterMapKeys) { + if (!leftNodeID.equals(key) && !nodeSet.contains(key)) { + failovers++; + } + } + return failovers; + } + + // ----------------------------------------------------------------------- + // vertx-0001 test C — correctness + // ----------------------------------------------------------------------- + + static void testVertx0001_correctness() { + List nodes = Arrays.asList("alpha", "beta", "gamma", "delta"); + + // HashSet must agree with List for all membership queries + Set nodeSet = new HashSet<>(nodes); + for (String n : nodes) { + if (nodes.contains(n) != nodeSet.contains(n)) { + throw new AssertionError("Membership mismatch for: " + n); + } + } + if (nodes.contains("omega") != nodeSet.contains("omega")) { + throw new AssertionError("Membership mismatch for omega"); + } + + // nodeLeft logic must produce identical failover sets + List clusterMapKeys = Arrays.asList("alpha", "beta", "gamma", "delta", "epsilon"); + String leftNode = "alpha"; + + // nodes = {alpha, beta, gamma, delta} clusterMap has "epsilon" extra + List defectiveFailovers = new ArrayList<>(); + for (String key : clusterMapKeys) { + if (!leftNode.equals(key) && !nodes.contains(key)) { + defectiveFailovers.add(key); + } + } + + Set fixedNodeSet = new HashSet<>(nodes); + List fixedFailovers = new ArrayList<>(); + for (String key : clusterMapKeys) { + if (!leftNode.equals(key) && !fixedNodeSet.contains(key)) { + fixedFailovers.add(key); + } + } + + if (!defectiveFailovers.equals(fixedFailovers)) { + throw new AssertionError( + "Failover lists differ: defect=" + defectiveFailovers + " fixed=" + fixedFailovers); + } + if (fixedFailovers.size() != 1 || !fixedFailovers.get(0).equals("epsilon")) { + throw new AssertionError("Expected [epsilon], got: " + fixedFailovers); + } + + // addHaInfoIfLost pattern: single .contains() call on list vs set + String targetNodeId = "delta"; + boolean listResult = nodes.contains(targetNodeId); + boolean setResult = nodeSet.contains(targetNodeId); + if (listResult != setResult) { + throw new AssertionError("addHaInfoIfLost: list/set mismatch for " + targetNodeId); + } + + System.out.println("vertx-0001 correctness: PASS"); + } + + // ----------------------------------------------------------------------- + // Main + // ----------------------------------------------------------------------- + + public static void main(String[] args) { + testVertx0001_correctness(); + testVertx0001_opCount(); + testVertx0001_timing(); + System.out.println("ALL PASS"); + } +}