From 494d1c82a30dd378c75915e9ea07944a677848ad Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 16:46:38 -0400 Subject: [PATCH] undf: assign UNDF numbers, stamp patches; 878 total --- UNDF-REGISTRY.json | 5 +- .../redmine-0001-issue-blocks-bfs-array.patch | 1 + ...002-issue-would_reschedule-bfs-array.patch | 1 + ...edmine-0003-user-project_ids-include.patch | 18 ++ defects/redmine/test/RedmineCWE407Test.class | Bin 0 -> 7046 bytes defects/redmine/test/RedmineCWE407Test.java | 192 ++++++++++++++++++ 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 defects/redmine/patch/redmine-0003-user-project_ids-include.patch create mode 100644 defects/redmine/test/RedmineCWE407Test.class create mode 100644 defects/redmine/test/RedmineCWE407Test.java diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 4f8d0b68f..5e09678be 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -873,5 +873,8 @@ "suitecrm-0002": "UNDF-2026-000000872", "suitecrm-0003": "UNDF-2026-000000873", "vllm-0001": "UNDF-2026-000000874", - "suricata-0001-0001": "UNDF-2026-000000875" + "suricata-0001-0001": "UNDF-2026-000000875", + "redmine-0001": "UNDF-2026-000000876", + "redmine-0002": "UNDF-2026-000000877", + "redmine-0003": "UNDF-2026-000000878" } diff --git a/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch b/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch index de1ffb772..c3b99cb2f 100644 --- a/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch +++ b/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch @@ -1,3 +1,4 @@ +# UNDF: UNDF-2026-000000876 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1332,15 +1332,16 @@ diff --git a/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch b/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch index a93d8b05e..93c4866e1 100644 --- a/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch +++ b/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch @@ -1,3 +1,4 @@ +# UNDF: UNDF-2026-000000877 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1352,7 +1352,7 @@ diff --git a/defects/redmine/patch/redmine-0003-user-project_ids-include.patch b/defects/redmine/patch/redmine-0003-user-project_ids-include.patch new file mode 100644 index 000000000..ed7ab8cd2 --- /dev/null +++ b/defects/redmine/patch/redmine-0003-user-project_ids-include.patch @@ -0,0 +1,18 @@ +# UNDF: UNDF-2026-000000878 +--- a/app/models/user.rb ++++ b/app/models/user.rb +@@ -700,9 +700,10 @@ + members = Member.joins(:project, :member_roles). + where("#{Project.table_name}.status <> 9"). + where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, group_id). + pluck(:user_id, :role_id, :project_id) + ++ project_ids_set = project_ids.to_set # O(1) lookup instead of O(P) Array#include? + hash = {} + members.each do |user_id, role_id, project_id| + # Ignore the roles of the builtin group if the user is a member of the project +- next if user_id != id && project_ids.include?(project_id) ++ next if user_id != id && project_ids_set.include?(project_id) + + hash[role_id] ||= [] + hash[role_id] << project_id diff --git a/defects/redmine/test/RedmineCWE407Test.class b/defects/redmine/test/RedmineCWE407Test.class new file mode 100644 index 0000000000000000000000000000000000000000..fb97c55ad0dbcd4fdc64f0003dea6fba5ad7dfbd GIT binary patch literal 7046 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 literal 0 HcmV?d00001 diff --git a/defects/redmine/test/RedmineCWE407Test.java b/defects/redmine/test/RedmineCWE407Test.java new file mode 100644 index 000000000..82470b196 --- /dev/null +++ b/defects/redmine/test/RedmineCWE407Test.java @@ -0,0 +1,192 @@ +import java.util.*; + +/** + * CWE-407 unit tests for Redmine defects. + * + * redmine-0001: issue.rb blocks?() BFS uses Array for 'all' visited set — O(V^2) subtraction + * redmine-0002: issue.rb would_reschedule?() same BFS Array pattern — O(V^2) + * redmine-0003: user.rb project_ids_by_role project_ids.include? in members.each — O(M*P) + */ +public class RedmineCWE407Test { + + // ---- redmine-0001: blocks? BFS with Array visited set ---- + + /** Unpatched: Array 'all' grows, Array subtraction O(C*A) per BFS layer */ + static boolean blocksBfsUnpatched(int startId, int targetId, Map> graph) { + List all = new ArrayList<>(); + all.add(startId); + List last = new ArrayList<>(); + last.add(startId); + + while (!last.isEmpty()) { + Set currentSet = new LinkedHashSet<>(); + for (int node : last) { + List neighbors = graph.getOrDefault(node, Collections.emptyList()); + currentSet.addAll(neighbors); + } + List current = new ArrayList<>(currentSet); + + // Array subtraction: current -= last, current -= all (both O(N) per element) + current.removeAll(last); + Iterator it = current.iterator(); + while (it.hasNext()) { + if (all.contains(it.next())) { // O(A) per element + it.remove(); + } + } + + if (current.contains(targetId)) return true; + + last = current; + all.addAll(last); + } + return false; + } + + /** Patched: Set 'all' for O(1) membership — O(V+E) total */ + static boolean blocksBfsPatched(int startId, int targetId, Map> graph) { + Set all = new HashSet<>(); + all.add(startId); + List last = new ArrayList<>(); + last.add(startId); + + while (!last.isEmpty()) { + Set currentSet = new LinkedHashSet<>(); + for (int node : last) { + List neighbors = graph.getOrDefault(node, Collections.emptyList()); + currentSet.addAll(neighbors); + } + List current = new ArrayList<>(currentSet); + + // Set-based removal: O(1) per element + current.removeIf(all::contains); + + if (current.contains(targetId)) return true; + + last = current; + all.addAll(last); + } + return false; + } + + // ---- redmine-0003: user project_ids membership ---- + + /** Unpatched: project_ids.include? in members.each — O(M*P) */ + static Map> projectIdsByRoleUnpatched( + int userId, List members, List projectIds) { + Map> hash = new HashMap<>(); + for (int[] member : members) { + int memberUserId = member[0]; + int roleId = member[1]; + int projectId = member[2]; + + // Skip builtin group roles if user is a member of this project + if (memberUserId != userId && projectIds.contains(projectId)) { // O(P) per member + continue; + } + hash.computeIfAbsent(roleId, k -> new ArrayList<>()).add(projectId); + } + return hash; + } + + /** Patched: project_ids.to_set for O(1) lookup — O(M+P) */ + static Map> projectIdsByRolePatched( + int userId, List members, List projectIds) { + Set projectIdsSet = new HashSet<>(projectIds); // O(P) once + Map> hash = new HashMap<>(); + for (int[] member : members) { + int memberUserId = member[0]; + int roleId = member[1]; + int projectId = member[2]; + + if (memberUserId != userId && projectIdsSet.contains(projectId)) { // O(1) per member + continue; + } + hash.computeIfAbsent(roleId, k -> new ArrayList<>()).add(projectId); + } + return hash; + } + + // ---- Test harness ---- + + static void testRedmine0001and0002() { + System.out.println("=== redmine-0001/0002: issue BFS blocks?/would_reschedule? ==="); + // Build a chain graph: 0 -> 1 -> 2 -> ... -> V-1 + int V = 20000; + Map> graph = new HashMap<>(); + for (int i = 0; i < V - 1; i++) { + graph.put(i, List.of(i + 1)); + } + int targetId = V - 1; + + // Warmup + blocksBfsPatched(0, targetId, graph); + + long t0 = System.nanoTime(); + boolean r1 = blocksBfsUnpatched(0, targetId, graph); + long unpatched = System.nanoTime() - t0; + + t0 = System.nanoTime(); + boolean r2 = blocksBfsPatched(0, targetId, graph); + long patched = System.nanoTime() - t0; + + assert r1 == r2 : "Result mismatch"; + assert r1 == true : "Expected true"; + + double ratio = (double) unpatched / patched; + System.out.printf(" V=%d unpatched=%dms patched=%dms ratio=%.1fx%n", + V, unpatched / 1_000_000, patched / 1_000_000, ratio); + assert ratio > 3.0 : "Expected >3x ratio, got " + ratio; + System.out.println(" PASS (covers both blocks? and would_reschedule?)"); + } + + static void testRedmine0003() { + System.out.println("=== redmine-0003: user project_ids_by_role membership ==="); + int M = 50000; // membership rows + int P = 5000; // user's projects + int userId = 1; + + List projectIds = new ArrayList<>(P); + for (int i = 0; i < P; i++) { + projectIds.add(i); + } + + // Generate member rows: mix of user's own and builtin group entries + List members = new ArrayList<>(M); + Random rng = new Random(42); + for (int i = 0; i < M; i++) { + int uid = rng.nextBoolean() ? userId : 999; // 50% user, 50% builtin group + int roleId = rng.nextInt(10); + int projectId = rng.nextInt(P * 2); // some in user's projects, some not + members.add(new int[]{uid, roleId, projectId}); + } + + // Warmup + projectIdsByRolePatched(userId, members, projectIds); + + long t0 = System.nanoTime(); + Map> r1 = projectIdsByRoleUnpatched(userId, members, projectIds); + long unpatched = System.nanoTime() - t0; + + t0 = System.nanoTime(); + Map> r2 = projectIdsByRolePatched(userId, members, projectIds); + long patched = System.nanoTime() - t0; + + // Verify same results + int totalR1 = r1.values().stream().mapToInt(List::size).sum(); + int totalR2 = r2.values().stream().mapToInt(List::size).sum(); + assert totalR1 == totalR2 : "Result mismatch: " + totalR1 + " vs " + totalR2; + + double ratio = (double) unpatched / patched; + System.out.printf(" M=%d P=%d unpatched=%dms patched=%dms ratio=%.1fx%n", + M, P, unpatched / 1_000_000, patched / 1_000_000, ratio); + assert ratio > 3.0 : "Expected >3x ratio, got " + ratio; + System.out.println(" PASS"); + } + + public static void main(String[] args) { + testRedmine0001and0002(); + testRedmine0003(); + System.out.println("\nAll 3 Redmine CWE-407 tests PASSED."); + } +}