578/240 — rustc-0004/vertx-0001/asterisk-0003/bitcoin-0001/actix-web-0003/hazelcast-0001+0002/rabbitmq-0005/nginx-0003/haproxy-0003/envoy-0003/istio-0003
This commit is contained in:
parent
dde5ec97fb
commit
e4ee168b1e
50 changed files with 4775 additions and 5 deletions
|
|
@ -0,0 +1,117 @@
|
|||
# asterisk-0003: CDR Variable Merge O(B×V) Quadratic Membership Scan
|
||||
|
||||
## Classification
|
||||
- **CWE**: CWE-407 (Inefficient Algorithmic Complexity)
|
||||
- **Severity**: MEDIUM
|
||||
- **Component**: Call Detail Records (CDR)
|
||||
- **Location**: `main/cdr.c`, function `cdr_object_create_public_records()`, ~line 1458
|
||||
|
||||
## Description
|
||||
|
||||
When publishing CDR records at call teardown, Asterisk merges party_b channel
|
||||
variables into the CDR varshead. For each party_b variable, it performs a full
|
||||
linear scan of `varshead` (already containing party_a variables) to check for
|
||||
duplicates before inserting.
|
||||
|
||||
This is an O(B × V) operation where:
|
||||
- B = number of party_b variables
|
||||
- V = number of variables already in varshead (party_a variables)
|
||||
|
||||
In a dialplan with many channel variables (common in IVR-heavy or CRM-integrated
|
||||
deployments), every call teardown pays this quadratic cost.
|
||||
|
||||
## Defective Code
|
||||
|
||||
```c
|
||||
/* main/cdr.c ~line 1458 — cdr_object_create_public_records() */
|
||||
AST_LIST_TRAVERSE(&it_cdr->party_b.variables, it_var, entries) {
|
||||
int found = 0;
|
||||
struct ast_var_t *newvariable;
|
||||
AST_LIST_TRAVERSE(&cdr_copy->varshead, it_copy_var, entries) { /* O(V) per it_var */
|
||||
if (!strcasecmp(ast_var_name(it_var), ast_var_name(it_copy_var))) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found && (newvariable = ast_var_assign(ast_var_name(it_var), ast_var_value(it_var)))) {
|
||||
AST_LIST_INSERT_TAIL(&cdr_copy->varshead, newvariable, entries);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
Build a `case-insensitive hash set` of already-present variable names before the
|
||||
merge loop, then do O(1) membership checks:
|
||||
|
||||
```c
|
||||
/* Build a hash set of existing variable names (lowercased) */
|
||||
struct ao2_container *existing_names = ao2_container_alloc_hash(
|
||||
AO2_ALLOC_OPT_LOCK_NOLOCK, 0, 31, str_hash_fn, NULL, str_cmp_fn);
|
||||
|
||||
AST_LIST_TRAVERSE(&cdr_copy->varshead, it_copy_var, entries) {
|
||||
char *lower = ast_strdupa(ast_var_name(it_copy_var));
|
||||
ast_str_to_lower(lower);
|
||||
ao2_link(existing_names, lower);
|
||||
}
|
||||
|
||||
AST_LIST_TRAVERSE(&it_cdr->party_b.variables, it_var, entries) {
|
||||
char *lower = ast_strdupa(ast_var_name(it_var));
|
||||
ast_str_to_lower(lower);
|
||||
if (!ao2_find(existing_names, lower, OBJ_SEARCH_KEY | OBJ_NOLOCK)) {
|
||||
struct ast_var_t *newvariable = ast_var_assign(
|
||||
ast_var_name(it_var), ast_var_value(it_var));
|
||||
if (newvariable) {
|
||||
AST_LIST_INSERT_TAIL(&cdr_copy->varshead, newvariable, entries);
|
||||
ao2_link(existing_names, lower);
|
||||
}
|
||||
}
|
||||
}
|
||||
ao2_ref(existing_names, -1);
|
||||
```
|
||||
|
||||
Alternatively, since CDR variable counts are moderate (typically < 100), a
|
||||
simpler approach uses `ast_hashtab` which is already available in Asterisk:
|
||||
|
||||
```c
|
||||
struct ast_hashtab *seen = ast_hashtab_create(31, ast_hashtab_compare_strings_nocase,
|
||||
ast_hashtab_resize_java, ast_hashtab_newsize_java,
|
||||
ast_hashtab_hash_string_nocase, 0);
|
||||
|
||||
AST_LIST_TRAVERSE(&cdr_copy->varshead, it_copy_var, entries) {
|
||||
ast_hashtab_insert_safe(seen, (void *)ast_var_name(it_copy_var));
|
||||
}
|
||||
AST_LIST_TRAVERSE(&it_cdr->party_b.variables, it_var, entries) {
|
||||
if (!ast_hashtab_lookup(seen, ast_var_name(it_var))) {
|
||||
struct ast_var_t *newvariable = ast_var_assign(
|
||||
ast_var_name(it_var), ast_var_value(it_var));
|
||||
if (newvariable) {
|
||||
AST_LIST_INSERT_TAIL(&cdr_copy->varshead, newvariable, entries);
|
||||
ast_hashtab_insert_safe(seen, ast_var_name(it_var));
|
||||
}
|
||||
}
|
||||
}
|
||||
ast_hashtab_destroy(seen, NULL);
|
||||
```
|
||||
|
||||
## Complexity
|
||||
|
||||
| Metric | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Variable merge | O(B × V) | O(B + V) |
|
||||
| Per-lookup | O(V) linear scan | O(1) hash lookup |
|
||||
|
||||
## Speedup Estimate
|
||||
|
||||
At V=50 party_a vars and B=50 party_b vars:
|
||||
- Before: 50 × 50 = 2,500 strcmp operations
|
||||
- After: 50 + 50 = 100 hash operations
|
||||
- Ratio: ~25x
|
||||
|
||||
At V=200, B=200 (large IVR/CRM deployment):
|
||||
- Before: 200 × 200 = 40,000 operations
|
||||
- After: 400 operations
|
||||
- Ratio: ~100x
|
||||
|
||||
The defect scales quadratically with the number of channel variables, which
|
||||
grows with dialplan complexity and integration depth.
|
||||
180
defects/asterisk/unit/CdrVarMergeAlgorithm.java
Normal file
180
defects/asterisk/unit/CdrVarMergeAlgorithm.java
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package unit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CWE-407 unit test: asterisk-0003
|
||||
*
|
||||
* Models cdr_object_create_public_records() variable merge.
|
||||
* SLOW: nested list traversal with strcasecmp — O(B × V)
|
||||
* FAST: hash set membership check — O(B + V)
|
||||
*/
|
||||
public class CdrVarMergeAlgorithm {
|
||||
|
||||
static long slowOps = 0;
|
||||
static long fastOps = 0;
|
||||
|
||||
/** Simulated variable: name -> value pair */
|
||||
static class Var {
|
||||
String name;
|
||||
String value;
|
||||
Var(String name, String value) { this.name = name; this.value = value; }
|
||||
}
|
||||
|
||||
/**
|
||||
* SLOW path: nested linear scan to deduplicate variables.
|
||||
* Models the defective AST_LIST_TRAVERSE inside AST_LIST_TRAVERSE.
|
||||
*
|
||||
* @param partyAVars already in varshead
|
||||
* @param partyBVars party_b variables to merge in
|
||||
* @return merged list
|
||||
*/
|
||||
static List<Var> mergeVarsSlow(List<Var> partyAVars, List<Var> partyBVars) {
|
||||
List<Var> varshead = new ArrayList<>(partyAVars);
|
||||
|
||||
for (Var bVar : partyBVars) { // outer: B iterations
|
||||
boolean found = false;
|
||||
for (Var existing : varshead) { // inner: V iterations — O(B×V)
|
||||
slowOps++;
|
||||
if (bVar.name.equalsIgnoreCase(existing.name)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
varshead.add(new Var(bVar.name, bVar.value));
|
||||
}
|
||||
}
|
||||
return varshead;
|
||||
}
|
||||
|
||||
/**
|
||||
* FAST path: hash set membership check — O(B + V).
|
||||
* Fix: build a HashMap of existing names first, then check in O(1).
|
||||
*
|
||||
* @param partyAVars already in varshead
|
||||
* @param partyBVars party_b variables to merge in
|
||||
* @return merged list
|
||||
*/
|
||||
static List<Var> mergeVarsFast(List<Var> partyAVars, List<Var> partyBVars) {
|
||||
List<Var> varshead = new ArrayList<>(partyAVars);
|
||||
HashMap<String, Boolean> existingNames = new HashMap<>();
|
||||
|
||||
for (Var v : partyAVars) { // O(V) build
|
||||
fastOps++;
|
||||
existingNames.put(v.name.toLowerCase(), Boolean.TRUE);
|
||||
}
|
||||
|
||||
for (Var bVar : partyBVars) { // outer: B iterations
|
||||
fastOps++; // O(1) lookup
|
||||
if (!existingNames.containsKey(bVar.name.toLowerCase())) {
|
||||
varshead.add(new Var(bVar.name, bVar.value));
|
||||
existingNames.put(bVar.name.toLowerCase(), Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
return varshead;
|
||||
}
|
||||
|
||||
static boolean runTest(int numVarsA, int numVarsB, int overlap) {
|
||||
// Build party_a vars: var_a_0 ... var_a_(numVarsA-1)
|
||||
List<Var> partyA = new ArrayList<>();
|
||||
for (int i = 0; i < numVarsA; i++) {
|
||||
partyA.add(new Var("var_a_" + i, "val_a_" + i));
|
||||
}
|
||||
|
||||
// Build party_b vars: first 'overlap' vars share names with party_a
|
||||
// rest are unique to party_b
|
||||
List<Var> partyB = new ArrayList<>();
|
||||
for (int i = 0; i < overlap; i++) {
|
||||
partyB.add(new Var("var_a_" + i, "val_b_" + i)); // duplicate
|
||||
}
|
||||
for (int i = 0; i < numVarsB - overlap; i++) {
|
||||
partyB.add(new Var("var_b_" + i, "val_b_" + i)); // unique
|
||||
}
|
||||
|
||||
long slowBefore = slowOps;
|
||||
long fastBefore = fastOps;
|
||||
|
||||
List<Var> slowResult = mergeVarsSlow(partyA, partyB);
|
||||
List<Var> fastResult = mergeVarsFast(partyA, partyB);
|
||||
|
||||
long slowCount = slowOps - slowBefore;
|
||||
long fastCount = fastOps - fastBefore;
|
||||
|
||||
// Both should produce same merged count: numVarsA + (numVarsB - overlap) unique vars
|
||||
int expectedSize = numVarsA + (numVarsB - overlap);
|
||||
if (slowResult.size() != expectedSize) {
|
||||
System.out.println("FAIL: slow result size " + slowResult.size() + " expected " + expectedSize);
|
||||
return false;
|
||||
}
|
||||
if (fastResult.size() != expectedSize) {
|
||||
System.out.println("FAIL: fast result size " + fastResult.size() + " expected " + expectedSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Slow ops should be at least numVarsB (inner loop on each), approx numVarsB * numVarsA
|
||||
// Fast ops should be at most numVarsA + numVarsB
|
||||
double ratio = (double) slowCount / (double) fastCount;
|
||||
|
||||
System.out.printf(" N=%d B=%d overlap=%d | slowOps=%d fastOps=%d ratio=%.1fx%n",
|
||||
numVarsA, numVarsB, overlap, slowCount, fastCount, ratio);
|
||||
|
||||
if (ratio < 5.0) {
|
||||
System.out.printf("FAIL: ratio %.1fx < 5x threshold%n", ratio);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int pass = 0;
|
||||
int fail = 0;
|
||||
|
||||
System.out.println("=== asterisk-0003: CDR Variable Merge O(B×V) ===");
|
||||
System.out.println();
|
||||
|
||||
// Test cases: (numVarsA, numVarsB, overlap)
|
||||
int[][] tests = {
|
||||
{20, 20, 5},
|
||||
{50, 50, 10},
|
||||
{100, 100, 20},
|
||||
{200, 200, 50},
|
||||
{500, 500, 100},
|
||||
};
|
||||
|
||||
for (int[] t : tests) {
|
||||
slowOps = 0;
|
||||
fastOps = 0;
|
||||
boolean ok = runTest(t[0], t[1], t[2]);
|
||||
if (ok) { pass++; } else { fail++; }
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
|
||||
// Verify quadratic growth in slow path
|
||||
System.out.println("Quadratic growth verification (slow path):");
|
||||
for (int n : new int[]{10, 50, 100, 200}) {
|
||||
slowOps = 0;
|
||||
fastOps = 0;
|
||||
mergeVarsSlow(buildVarList("a", n), buildVarList("b", n));
|
||||
System.out.printf(" N=%d slow_ops=%d (expected ~%d quadratic)%n",
|
||||
n, slowOps, n * n);
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.printf("%d/%d PASS%n", pass, pass + fail);
|
||||
if (fail > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Var> buildVarList(String prefix, int n) {
|
||||
List<Var> list = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
list.add(new Var(prefix + "_var_" + i, "val_" + i));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue