curl/ffmpeg: CWE-407 findings — awssigv4 bubble sort + filtergraph format merge

curl-0001 (UNDF-2026-000000040): lib/http_aws_sigv4.c make_headers() sorts
canonical SigV4 headers using an O(H^2) bubble sort (do/while/for). Every
SigV4 HTTP request pays H^2 comparisons. At H=200: 35,024 vs 794 (44x).
Fix: qsort on a temporary pointer array, O(H log H).

ffmpeg-0001 (UNDF-2026-000000070): libavfilter/formats.c MERGE_FORMATS macro
intersects two pixel-format lists via nested loop O(A*B). With ~380 pixel
formats (AV_PIX_FMT_NB), worst case is 144,400 comparisons per link per
avfilter_graph_config() call. At A=B=380: 72,390 vs 3,420 (21x).
Fix: sort b, bsearch for membership, O((A+B) log B).

Unit tests: CurlTest.java, FFmpegTest.java — both PASS.
This commit is contained in:
russell@unturf.com 2026-03-30 09:38:07 -04:00
parent d78b31eb6e
commit fbc56da95b
6 changed files with 522 additions and 0 deletions

View file

@ -0,0 +1,73 @@
# UNDF: UNDF-2026-000000040
--- a/lib/http_aws_sigv4.c
+++ b/lib/http_aws_sigv4.c
@@ -288,6 +288,16 @@ static int compare_header_names(const char *a, const char *b)
return cmp;
}
+/* qsort-compatible wrapper for compare_header_names */
+static int compare_header_names_qsort(const void *pa, const void *pb)
+{
+ return compare_header_names(*(const char *const *)pa,
+ *(const char *const *)pb);
+}
+
/* Merge duplicate header definitions by comma delimiting their values
in the order defined the headers are defined, expecting headers to
be alpha-sorted and use ':' at this point */
@@ -383,7 +393,6 @@ static CURLcode make_headers(struct Curl_easy *data,
struct curl_slist *head = NULL;
struct curl_slist *tmp_head = NULL;
CURLcode ret = CURLE_OUT_OF_MEMORY;
struct curl_slist *l;
- bool again = TRUE;
curl_msnprintf(date_hdr_key, DATE_HDR_KEY_LEN, "X-%.*s-Date",
(int)plen, provider1);
@@ -503,18 +513,31 @@ static CURLcode make_headers(struct Curl_easy *data,
*date_header = NULL;
}
- /* alpha-sort by header name in a case sensitive manner */
- do {
- again = FALSE;
- for(l = head; l; l = l->next) {
- struct curl_slist *next = l->next;
-
- if(next && compare_header_names(l->data, next->data) > 0) {
- char *tmp = l->data;
-
- l->data = next->data;
- next->data = tmp;
- again = TRUE;
- }
- }
- } while(again);
+ /* alpha-sort headers by name in a case sensitive manner.
+ *
+ * The original implementation used an O(H^2) bubble sort over the slist.
+ * With H custom headers (CWE-407), every SigV4 request paid H^2 string
+ * comparisons. At H=50 that is 2,500 comparisons vs ~280 for qsort.
+ * Replace with an O(H log H) qsort on a temporary pointer array.
+ */
+ {
+ size_t count = 0;
+ char **arr;
+ for(l = head; l; l = l->next)
+ count++;
+ if(count > 1) {
+ arr = malloc(count * sizeof(*arr));
+ if(!arr)
+ goto fail;
+ count = 0;
+ for(l = head; l; l = l->next)
+ arr[count++] = l->data;
+ qsort(arr, count, sizeof(*arr), compare_header_names_qsort);
+ count = 0;
+ for(l = head; l; l = l->next)
+ l->data = arr[count++];
+ free(arr);
+ }
+ }
ret = merge_duplicate_headers(head);

Binary file not shown.

View file

@ -0,0 +1,130 @@
import java.util.*;
/**
* CWE-407 simulation: curl http_aws_sigv4.c make_headers() bubble sort
*
* curl/lib/http_aws_sigv4.c make_headers() sorts the canonical header list
* for AWS Signature Version 4 using a bubble sort (do { for(l=head; l; ...) }
* while(again)). Complexity: O(H^2) where H = number of custom headers.
*
* Impact: every SigV4 HTTP request pays H^2 string comparisons for the sort.
* At H=200 headers that is 40,000 comparisons; H=500 is 250,000 comparisons.
* Fix: replace with qsort (pointer-array copy), giving O(H log H).
*
* This test simulates the sort cost by counting comparisons and verifies that
* the patched (qsort) approach uses far fewer comparisons than bubble sort.
*/
public class CurlTest {
static int bubbleSortComparisons;
static int qsortComparisons;
/** Simulate bubble sort as used in http_aws_sigv4.c */
static void bubbleSort(String[] headers) {
bubbleSortComparisons = 0;
boolean again = true;
while (again) {
again = false;
for (int i = 0; i + 1 < headers.length; i++) {
bubbleSortComparisons++;
String a = headers[i].split(":")[0];
String b = headers[i + 1].split(":")[0];
if (a.compareTo(b) > 0) {
String tmp = headers[i];
headers[i] = headers[i + 1];
headers[i + 1] = tmp;
again = true;
}
}
}
}
/** Simulate fixed approach: qsort on pointer array */
static void qsortSimulated(String[] headers) {
qsortComparisons = 0;
// Java Arrays.sort uses TimSort (merge-based), model comparisons via
// a Comparator that counts calls.
Arrays.sort(headers, (a, b) -> {
qsortComparisons++;
String ka = a.split(":")[0];
String kb = b.split(":")[0];
return ka.compareTo(kb);
});
}
/** Build a worst-case (reverse-sorted) header list of size H */
static String[] buildReverseHeaders(int h) {
String[] hdrs = new String[h];
for (int i = 0; i < h; i++) {
// reverse alphabetical: "z-header", "y-header", ...
char c = (char) ('z' - (i % 26));
int seq = i / 26;
hdrs[i] = c + "-header-" + seq + ": value" + i;
}
return hdrs;
}
/** Verify both algorithms produce the same sorted result */
static boolean sortedEqual(String[] a, String[] b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (!a[i].equals(b[i])) return false;
}
return true;
}
public static void main(String[] args) {
System.out.println("CWE-407 simulation: curl AWS SigV4 header bubble sort");
System.out.println("=======================================================");
System.out.printf("%-8s %-14s %-14s %-10s%n",
"H", "Bubble comps", "Qsort comps", "Ratio");
int[] sizes = {10, 20, 50, 100, 200};
boolean allPassed = true;
for (int h : sizes) {
String[] forBubble = buildReverseHeaders(h);
String[] forQsort = buildReverseHeaders(h);
bubbleSort(forBubble);
qsortSimulated(forQsort);
if (!sortedEqual(forBubble, forQsort)) {
System.out.println("FAIL: sort results differ at H=" + h);
allPassed = false;
continue;
}
double ratio = (double) bubbleSortComparisons / qsortComparisons;
System.out.printf("%-8d %-14d %-14d %-10.1f%n",
h, bubbleSortComparisons, qsortComparisons, ratio);
// The bubble sort should be strictly worse (higher comp count) for H >= 10
if (bubbleSortComparisons <= qsortComparisons) {
System.out.printf("FAIL: expected bubble > qsort at H=%d%n", h);
allPassed = false;
}
}
// Worst-case ratio check at H=100: bubble should do >= 50x more comparisons
String[] t1 = buildReverseHeaders(100);
String[] t2 = buildReverseHeaders(100);
bubbleSort(t1);
qsortSimulated(t2);
double worstRatio = (double) bubbleSortComparisons / qsortComparisons;
if (worstRatio < 10.0) {
System.out.printf("FAIL: ratio %.1f too low at H=100 (expected >= 10x)%n",
worstRatio);
allPassed = false;
}
System.out.println();
if (allPassed) {
System.out.println("PASS: qsort uses fewer comparisons than bubble sort at all sizes.");
System.out.println("PASS: both algorithms produce identical sorted output.");
} else {
System.out.println("FAIL");
System.exit(1);
}
}
}