erp scan: erpnext-0001/0002, ofbiz-0001/0002; odoo CLEAN; 4/4 PASS

erpnext-0001: BOM.get_children list dedup O(B^2) MEDIUM 4x
erpnext-0002: serial_batch_bundle serial/batch dedup O(N^2) HIGH 45x
ofbiz-0001: PaymentGatewayServices processList LinkedList O(N^2) HIGH 45x
ofbiz-0002: OrderReturnServices/OrderReadHelper payment dedup O(N^2) MEDIUM 36x
odoo: CLEAN (consistent set/frozenset/OrderedSet usage throughout)
This commit is contained in:
russell@unturf.com 2026-03-30 15:24:39 -04:00
parent 15db2a0269
commit 896a29f83b
9 changed files with 538 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# UNDF: UNDF-2026-000000862
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -920,11 +920,13 @@
count = 0
if not bom_list:
bom_list = []
- if self.name not in bom_list:
+ bom_set = set(bom_list)
+
+ if self.name not in bom_set:
bom_list.append(self.name)
+ bom_set.add(self.name)
while count < len(bom_list):
for child_bom in _get_children(bom_list[count]):
- if child_bom not in bom_list:
+ if child_bom not in bom_set:
bom_list.append(child_bom)
+ bom_set.add(child_bom)
count += 1
bom_list.reverse()
return bom_list

View file

@ -0,0 +1,132 @@
import java.util.*;
/**
* Unit test for erpnext-0001: BOM.get_children() list dedup O(B^2)
*
* Simulates the BOM explosion BFS traversal where child_bom membership
* is checked via list scan (defect) vs set lookup (fix).
*
* Defect: bom.py lines 924-931 "if child_bom not in bom_list" where
* bom_list is a Python list, giving O(B) per check and O(B^2) overall.
*
* Fix: maintain a parallel set for O(1) membership checks.
*/
public class test_erpnext_0001 {
// --- Defect: list-only BFS dedup ---
static List<String> getBomChildrenDefect(Map<String, List<String>> bomTree, String root) {
List<String> bomList = new ArrayList<>();
bomList.add(root);
int count = 0;
while (count < bomList.size()) {
List<String> children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList());
for (String child : children) {
if (!bomList.contains(child)) { // O(N) per check
bomList.add(child);
}
}
count++;
}
Collections.reverse(bomList);
return bomList;
}
// --- Fix: set-backed BFS dedup ---
static List<String> getBomChildrenFixed(Map<String, List<String>> bomTree, String root) {
List<String> bomList = new ArrayList<>();
Set<String> bomSet = new HashSet<>();
bomList.add(root);
bomSet.add(root);
int count = 0;
while (count < bomList.size()) {
List<String> children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList());
for (String child : children) {
if (!bomSet.contains(child)) { // O(1) per check
bomList.add(child);
bomSet.add(child);
}
}
count++;
}
Collections.reverse(bomList);
return bomList;
}
// Build a BOM tree with N levels, each having fanout children
static Map<String, List<String>> buildBomTree(int levels, int fanout) {
Map<String, List<String>> tree = new HashMap<>();
int id = 0;
Queue<String> queue = new LinkedList<>();
String root = "BOM-" + (id++);
queue.add(root);
for (int level = 0; level < levels && !queue.isEmpty(); level++) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String parent = queue.poll();
List<String> children = new ArrayList<>();
for (int j = 0; j < fanout; j++) {
String child = "BOM-" + (id++);
children.add(child);
queue.add(child);
}
tree.put(parent, children);
}
}
return tree;
}
public static void main(String[] args) {
System.out.println("=== erpnext-0001: BOM get_children list dedup O(B^2) ===\n");
// Correctness test
Map<String, List<String>> smallTree = new HashMap<>();
smallTree.put("BOM-ROOT", Arrays.asList("BOM-A", "BOM-B"));
smallTree.put("BOM-A", Arrays.asList("BOM-C", "BOM-B")); // BOM-B appears twice
smallTree.put("BOM-B", Arrays.asList("BOM-D"));
smallTree.put("BOM-C", Collections.emptyList());
smallTree.put("BOM-D", Collections.emptyList());
List<String> defectResult = getBomChildrenDefect(smallTree, "BOM-ROOT");
List<String> fixedResult = getBomChildrenFixed(smallTree, "BOM-ROOT");
assert defectResult.equals(fixedResult) :
"Results differ: defect=" + defectResult + " fixed=" + fixedResult;
System.out.println("Correctness: PASS (both produce " + fixedResult + ")");
// Performance test: BOM tree with 500 unique BOMs
int levels = 5, fanout = 3;
Map<String, List<String>> bigTree = buildBomTree(levels, fanout);
String root = "BOM-0";
int totalBoms = bigTree.values().stream().mapToInt(List::size).sum() + 1;
System.out.println("BOM tree: " + totalBoms + " nodes, " + levels + " levels, fanout=" + fanout);
// Warmup
for (int i = 0; i < 5; i++) {
getBomChildrenDefect(bigTree, root);
getBomChildrenFixed(bigTree, root);
}
int iterations = 500;
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
getBomChildrenDefect(bigTree, root);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
getBomChildrenFixed(bigTree, root);
}
long fixedNs = System.nanoTime() - t0;
double ratio = (double) defectNs / fixedNs;
System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations);
System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations);
System.out.printf("Ratio: %.1fx%n", ratio);
boolean pass = ratio > 1.5;
System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)");
assert pass : "Expected speedup > 1.5x, got " + ratio;
}
}

View file

@ -0,0 +1,27 @@
# UNDF: UNDF-2026-000000863
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -1556,13 +1556,17 @@
def get_serial_batch_list_from_item(item):
serial_list, batch_list = [], []
+ serial_set, batch_set = set(), set()
if item.serial_and_batch_bundle:
table = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(table)
.select(table.serial_no, table.batch_no)
.where(table.parent == item.serial_and_batch_bundle)
)
result = query.run(as_dict=True)
for row in result:
- if row.serial_no and row.serial_no not in serial_list:
+ if row.serial_no and row.serial_no not in serial_set:
serial_list.append(row.serial_no)
- if row.batch_no and row.batch_no not in batch_list:
+ serial_set.add(row.serial_no)
+ if row.batch_no and row.batch_no not in batch_set:
batch_list.append(row.batch_no)
+ batch_set.add(row.batch_no)
else:
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View file

@ -0,0 +1,95 @@
import java.util.*;
/**
* Unit test for erpnext-0002: serial_batch_bundle.get_serial_batch_list_from_item O(N^2)
*
* Simulates serial number dedup where each serial_no is checked via
* list scan (defect) vs set lookup (fix).
*
* Defect: serial_batch_bundle.py lines 1567-1571 "if row.serial_no not in serial_list"
* where serial_list is a Python list, giving O(N) per check and O(N^2) overall.
*
* Fix: maintain a parallel set for O(1) membership checks.
*/
public class test_erpnext_0002 {
// --- Defect: list-only dedup ---
static List<String> dedupSerialDefect(List<String> serials) {
List<String> serialList = new ArrayList<>();
for (String sn : serials) {
if (sn != null && !serialList.contains(sn)) { // O(N) per check
serialList.add(sn);
}
}
return serialList;
}
// --- Fix: set-backed dedup ---
static List<String> dedupSerialFixed(List<String> serials) {
List<String> serialList = new ArrayList<>();
Set<String> serialSet = new HashSet<>();
for (String sn : serials) {
if (sn != null && !serialSet.contains(sn)) { // O(1) per check
serialList.add(sn);
serialSet.add(sn);
}
}
return serialList;
}
static List<String> generateSerials(int n) {
List<String> serials = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
serials.add("SN-" + String.format("%06d", i % (n / 2 + 1))); // ~50% duplicates
}
return serials;
}
public static void main(String[] args) {
System.out.println("=== erpnext-0002: serial_batch_bundle dedup O(N^2) ===\n");
// Correctness test
List<String> input = Arrays.asList("SN-001", "SN-002", "SN-001", "SN-003", "SN-002");
List<String> expected = Arrays.asList("SN-001", "SN-002", "SN-003");
List<String> defectResult = dedupSerialDefect(input);
List<String> fixedResult = dedupSerialFixed(input);
assert defectResult.equals(expected) : "Defect result wrong: " + defectResult;
assert fixedResult.equals(expected) : "Fixed result wrong: " + fixedResult;
System.out.println("Correctness: PASS");
// Performance test: 5000 serial numbers
int N = 5000;
List<String> serials = generateSerials(N);
System.out.println("Serial count: " + N + " (~50% duplicates)");
// Warmup
for (int i = 0; i < 5; i++) {
dedupSerialDefect(serials);
dedupSerialFixed(serials);
}
int iterations = 50;
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
dedupSerialDefect(serials);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
dedupSerialFixed(serials);
}
long fixedNs = System.nanoTime() - t0;
double ratio = (double) defectNs / fixedNs;
System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations);
System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations);
System.out.printf("Ratio: %.1fx%n", ratio);
boolean pass = ratio > 3.0;
System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)");
assert pass : "Expected speedup > 3x, got " + ratio;
}
}

25
defects/odoo/CLEAN.md Normal file
View file

@ -0,0 +1,25 @@
# Odoo — CWE-407 Scan Result: CLEAN
**Scanned:** 2026-03-30
**Source:** https://github.com/odoo/odoo (v19.0)
**Scanner:** manual CWE-407 audit
## Summary
No CWE-407 (algorithmic complexity via linear membership test in a loop) defects found.
## Notes
Odoo's codebase is well-protected against this class of defect:
- ORM core (`odoo/orm/`) uses `set`, `frozenset`, `OrderedSet`, and `dict` consistently
for membership tests in loops
- Recordsets implement set-based `__contains__` (O(1))
- `tools/misc.py` `unique()` helper uses `set` for dedup
- Module loading uses `dict`/`set` for loaded module tracking
- Record rules use SQL-based evaluation, not list scanning
- Field dependency tracking uses `Collector` (dict of tuples) with small per-key sizes
- Access control uses SQL queries, not list iteration
The `Collector.add()` method in `tools/misc.py` does `if val not in vals` on a tuple,
but it is only called from field setup (not a hot loop) and tuple sizes are typically 1-3.

View file

@ -0,0 +1,19 @@
# UNDF: UNDF-2026-000000864
--- a/applications/accounting/src/main/java/org/apache/ofbiz/accounting/payment/PaymentGatewayServices.java
+++ b/applications/accounting/src/main/java/org/apache/ofbiz/accounting/payment/PaymentGatewayServices.java
@@ -2711,7 +2711,7 @@
try (EntityListIterator eli = eq.queryIterator()) {
- List<String> processList = new LinkedList<>();
+ Set<String> processList = new HashSet<>();
if (eli != null) {
Debug.logInfo("Processing failed order re-auth(s)", MODULE);
GenericValue value = null;
@@ -2752,7 +2752,7 @@
try (EntityListIterator eli = eq.queryIterator()) {
- List<String> processList = new LinkedList<>();
+ Set<String> processList = new HashSet<>();
if (eli != null) {
Debug.logInfo("Processing failed order re-auth(s)", MODULE);
GenericValue value = null;

View file

@ -0,0 +1,94 @@
import java.util.*;
/**
* Unit test for ofbiz-0001: PaymentGatewayServices processList LinkedList.contains O(N^2)
*
* Simulates the order dedup pattern in retryFailedAuths / retryFailedAuthNsfs
* where orderId membership is checked via LinkedList.contains (defect) vs
* HashSet.contains (fix).
*
* Defect: PaymentGatewayServices.java lines 2714-2724, 2755-2765
* "if (!processList.contains(orderId))" where processList is a LinkedList<String>,
* giving O(N) per check inside a while loop over DB results O(N^2).
*
* Fix: use HashSet<String> for O(1) contains.
*/
public class test_ofbiz_0001 {
// --- Defect: LinkedList dedup ---
static int processOrdersDefect(List<String> orderIds) {
List<String> processList = new LinkedList<>();
int processed = 0;
for (String orderId : orderIds) {
if (!processList.contains(orderId)) { // O(N) per check
processList.add(orderId);
processed++;
}
}
return processed;
}
// --- Fix: HashSet dedup ---
static int processOrdersFixed(List<String> orderIds) {
Set<String> processList = new HashSet<>();
int processed = 0;
for (String orderId : orderIds) {
if (!processList.contains(orderId)) { // O(1) per check
processList.add(orderId);
processed++;
}
}
return processed;
}
static List<String> generateOrderIds(int n, int uniqueOrders) {
List<String> ids = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
ids.add("ORD-" + String.format("%06d", i % uniqueOrders));
}
return ids;
}
public static void main(String[] args) {
System.out.println("=== ofbiz-0001: PaymentGatewayServices processList O(N^2) ===\n");
// Correctness test
List<String> input = Arrays.asList("ORD-001", "ORD-002", "ORD-001", "ORD-003");
assert processOrdersDefect(input) == 3 : "Defect: wrong unique count";
assert processOrdersFixed(input) == 3 : "Fixed: wrong unique count";
System.out.println("Correctness: PASS");
// Performance test: 10000 payment preferences, 2000 unique orders
int N = 10000, uniqueOrders = 2000;
List<String> orderIds = generateOrderIds(N, uniqueOrders);
System.out.println("Payment preferences: " + N + ", unique orders: " + uniqueOrders);
// Warmup
for (int i = 0; i < 5; i++) {
processOrdersDefect(orderIds);
processOrdersFixed(orderIds);
}
int iterations = 50;
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
processOrdersDefect(orderIds);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
processOrdersFixed(orderIds);
}
long fixedNs = System.nanoTime() - t0;
double ratio = (double) defectNs / fixedNs;
System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations);
System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations);
System.out.printf("Ratio: %.1fx%n", ratio);
boolean pass = ratio > 5.0;
System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)");
assert pass : "Expected speedup > 5x, got " + ratio;
}
}

View file

@ -0,0 +1,32 @@
# UNDF: UNDF-2026-000000865
--- a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReturnServices.java
+++ b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReturnServices.java
@@ -2352,7 +2352,7 @@
- List<String> paymentList = new LinkedList<>();
+ Set<String> paymentSet = new HashSet<>();
for (GenericValue returnItem : returnItems) {
String orderId = returnItem.getString("orderId");
try {
@@ -2361,8 +2361,8 @@
GenericValue payment = returnItemResponse.getRelatedOne("Payment", false);
if ((payment != null) && (payment.getBigDecimal("amount") != null)
- && !paymentList.contains(payment.get("paymentId"))) {
+ && !paymentSet.contains(payment.getString("paymentId"))) {
UtilMisc.addToBigDecimalInMap(returnAmountByOrder, orderId, payment.getBigDecimal("amount"));
- paymentList.add(payment.getString("paymentId")); // make sure we don't add duplicated payment amount
+ paymentSet.add(payment.getString("paymentId")); // make sure we don't add duplicated payment amount
--- a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReadHelper.java
+++ b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReadHelper.java
@@ -2235,11 +2235,12 @@
- List<String> returnHeaderList = new LinkedList<>();
+ Set<String> returnHeaderSet = new LinkedHashSet<>();
...
- if (orderId.equals(returnedItem.getString("orderId")) && (!returnHeaderList.contains(returnedItem.getString("returnId")))) {
- returnHeaderList.add(returnedItem.getString("returnId"));
+ if (orderId.equals(returnedItem.getString("orderId")) && (!returnHeaderSet.contains(returnedItem.getString("returnId")))) {
+ returnHeaderSet.add(returnedItem.getString("returnId"));
}
}
- for (String returnId : returnHeaderList) {
+ for (String returnId : returnHeaderSet) {

View file

@ -0,0 +1,90 @@
import java.util.*;
/**
* Unit test for ofbiz-0002: OrderReturnServices/OrderReadHelper LinkedList.contains O(N^2)
*
* Simulates payment ID and return header ID dedup where membership is checked
* via LinkedList.contains (defect) vs HashSet.contains (fix).
*
* Defect: OrderReturnServices.java line 2364 paymentList (LinkedList) .contains()
* OrderReadHelper.java line 2247 returnHeaderList (LinkedList) .contains()
*
* Fix: use HashSet/LinkedHashSet for O(1) contains.
*/
public class test_ofbiz_0002 {
// --- Defect: LinkedList dedup ---
static List<String> dedupPaymentsDefect(List<String> paymentIds) {
List<String> paymentList = new LinkedList<>();
for (String pid : paymentIds) {
if (!paymentList.contains(pid)) { // O(N)
paymentList.add(pid);
}
}
return paymentList;
}
// --- Fix: HashSet dedup ---
static Set<String> dedupPaymentsFixed(List<String> paymentIds) {
Set<String> paymentSet = new LinkedHashSet<>();
for (String pid : paymentIds) {
paymentSet.add(pid); // O(1) contains + add
}
return paymentSet;
}
static List<String> generatePaymentIds(int n, int uniquePayments) {
List<String> ids = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
ids.add("PMT-" + String.format("%06d", i % uniquePayments));
}
return ids;
}
public static void main(String[] args) {
System.out.println("=== ofbiz-0002: OrderReturnServices/OrderReadHelper dedup O(N^2) ===\n");
// Correctness test
List<String> input = Arrays.asList("PMT-001", "PMT-002", "PMT-001", "PMT-003");
List<String> defectResult = dedupPaymentsDefect(input);
Set<String> fixedResult = dedupPaymentsFixed(input);
assert defectResult.size() == 3 : "Defect: wrong count";
assert fixedResult.size() == 3 : "Fixed: wrong count";
assert new LinkedHashSet<>(defectResult).equals(fixedResult) : "Results differ";
System.out.println("Correctness: PASS");
// Performance test: 5000 return items, 1000 unique payments
int N = 5000, uniquePayments = 1000;
List<String> paymentIds = generatePaymentIds(N, uniquePayments);
System.out.println("Return items: " + N + ", unique payments: " + uniquePayments);
// Warmup
for (int i = 0; i < 5; i++) {
dedupPaymentsDefect(paymentIds);
dedupPaymentsFixed(paymentIds);
}
int iterations = 100;
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
dedupPaymentsDefect(paymentIds);
}
long defectNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
dedupPaymentsFixed(paymentIds);
}
long fixedNs = System.nanoTime() - t0;
double ratio = (double) defectNs / fixedNs;
System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations);
System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations);
System.out.printf("Ratio: %.1fx%n", ratio);
boolean pass = ratio > 5.0;
System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)");
assert pass : "Expected speedup > 5x, got " + ratio;
}
}