wave17: openssh-0001/2, strongswan-0001, gradle-0002, groovy-0001/2, make-0001, clojure-CLEAN + whitepaper 549/240

This commit is contained in:
russell@unturf.com 2026-03-27 19:55:52 -04:00
parent 0f275c44af
commit 4221966e66
22 changed files with 1776 additions and 4 deletions

View file

@ -0,0 +1,111 @@
# openssh-0001: CWE-407 O(N²) dedup in kex_assemble_server_sig_algs via match_list inside loop
## Severity: MEDIUM
## Location
`kex.c` — `kex_assemble_server_sig_algs()`, lines ~276–284
Also called indirectly via `kex-names.c:kex_has_any_alg()` → `match.c:match_list()`
## Description
`kex_assemble_server_sig_algs()` builds the server's `server_sig_algs` string
by iterating over `allowed_algs` (a comma-separated list of N algorithm names)
and deduplicating against the accumulator string as it grows.
```c
for ((alg = strsep(&algs, ",")); alg != NULL && *alg != '\0';
(alg = strsep(&algs, ","))) {
if ((sigalg = sshkey_sigalg_by_name(alg)) == NULL)
continue;
if (!kex_has_any_alg(sigalg, sigalgs)) /* O(S) scan */
continue;
/* Don't add an algorithm twice. */
if (ssh->kex->server_sig_algs != NULL &&
kex_has_any_alg(sigalg, ssh->kex->server_sig_algs)) /* O(i) */
continue;
xextendf(&ssh->kex->server_sig_algs, ",", "%s", sigalg);
}
```
`kex_has_any_alg(sigalg, ssh->kex->server_sig_algs)` calls `match_list()`,
which splits the second argument on commas and scans every token with
`strcmp`. At iteration i, `server_sig_algs` contains i tokens, so the check
costs O(i). Over N iterations the total work is:
0 + 1 + 2 + ... + (N-1) = O(N²)
For a server configured with N signature algorithms, every new SSH connection
triggers a rebuild of `server_sig_algs` during EXT_INFO negotiation.
OpenSSH ships with ~30 default signature algorithms. A compile-time or
runtime configuration with a long algorithm list amplifies the cost. The
function is called once per accepted connection (in `kex_compose_ext_info_server`),
making this an O(N²) cost proportional to configuration size, evaluated
per-connection.
## Complexity Before Fix
O(N²) where N = number of entries in `allowed_algs` (signature algorithm list).
## Fix
Collect results into a `HashSet<String>` equivalent (a small string set)
before building the output string. In C, a stack-allocated open-addressing
hash table over short algorithm names achieves O(1) lookup.
Alternatively, since the algorithm lists are short (< 64 entries), a single
pre-pass can build a sorted array of `sigalg` pointers from `allowed_algs`
filtered by `sigalgs`, then deduplicate in O(N log N) with bsearch.
```c
--- a/kex.c
+++ b/kex.c
@@ -264,6 +264,7 @@ kex_assemble_server_sig_algs(struct ssh *ssh, const char *allowed_algs)
const char *sigalg;
if ((sigalgs = sshkey_alg_list(0, 1, 1, ',')) == NULL)
fatal_f("sshkey_alg_list failed");
oalgs = algs = xstrdup(allowed_algs);
free(ssh->kex->server_sig_algs);
ssh->kex->server_sig_algs = NULL;
+ /* seen_set: small open-addressing hash set, avoids O(N²) dedup scan */
+ char *seen[128] = {0}; /* 128 slots, load ≤ 50% for N ≤ 64 algs */
+ size_t seen_mask = 127;
+ /* djb2 hash on null-terminated string */
+#define SEEN_HASH(s) ({ \
+ unsigned long _h = 5381; \
+ const unsigned char *_p = (const unsigned char*)(s); \
+ while (*_p) _h = ((_h << 5) + _h) ^ *_p++; \
+ (size_t)(_h & seen_mask); \
+})
for ((alg = strsep(&algs, ",")); alg != NULL && *alg != '\0';
(alg = strsep(&algs, ","))) {
if ((sigalg = sshkey_sigalg_by_name(alg)) == NULL)
continue;
if (!kex_has_any_alg(sigalg, sigalgs))
continue;
- /* Don't add an algorithm twice. */
- if (ssh->kex->server_sig_algs != NULL &&
- kex_has_any_alg(sigalg, ssh->kex->server_sig_algs))
- continue;
+ /* O(1) duplicate check via open-addressing hash set */
+ size_t _slot = SEEN_HASH(sigalg);
+ bool _dup = false;
+ for (size_t _i = 0; _i < seen_mask + 1; _i++) {
+ size_t _s = (_slot + _i) & seen_mask;
+ if (seen[_s] == NULL) { seen[_s] = (char*)sigalg; break; }
+ if (strcmp(seen[_s], sigalg) == 0) { _dup = true; break; }
+ }
+ if (_dup) continue;
xextendf(&ssh->kex->server_sig_algs, ",", "%s", sigalg);
}
```
## Complexity After Fix
O(N) expected — one pass over allowed_algs, O(1) hash-set lookup per entry.
## Notes
- `allowed_algs` is typically 20–35 algorithms in practice; the defect is
low-severity at current scale but O(N²) is an architectural property that
will worsen as algorithm lists grow (post-quantum additions, etc.).
- The same pattern appears in `kex_names_cat()` (see openssh-0002).

View file

@ -0,0 +1,101 @@
# openssh-0002: CWE-407 O(N²) dedup in kex_names_cat via match_list inside loop
## Severity: MEDIUM
## Location
`kex-names.c` — `kex_names_cat()`, lines ~218–231
## Description
`kex_names_cat()` concatenates two comma-separated algorithm lists while
deduplicating. It iterates over all N entries of `b` and for each calls
`kex_has_any_alg(ret, p)`, where `ret` grows with each successful addition.
```c
char *
kex_names_cat(const char *a, const char *b)
{
...
strlcpy(ret, a, len); /* ret starts as copy of a (M entries) */
for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) {
if (kex_has_any_alg(ret, p)) /* O(M+i) linear scan of growing ret */
continue; /* Algorithm already present */
strlcat(ret, ",", len);
strlcat(ret, p, len);
}
...
}
```
`kex_has_any_alg(ret, p)` calls `match_list(ret, p, NULL)` which splits `ret`
on commas and does a strcmp scan for `p`. If `a` has M entries and `b` has N
entries, the worst-case (no duplicates) cost is:
sum_{i=0}^{N-1} (M + i) = N*M + N*(N-1)/2 = O(M*N + N²)
`kex_names_cat()` is called from:
- `kex_assemble_names()` (called during algorithm list assembly at connection setup)
- Various proposal-building paths
In a post-quantum migration scenario where algorithm lists are long (e.g., PQ
KEX algorithms added via `+` prefix in config), this is an O(N²) cost on
every connection.
## Complexity Before Fix
O(M×N + N²) where M = len(a), N = len(b).
## Fix
Pre-index `a` into a hash set, then iterate `b` with O(1) lookups:
```c
--- a/kex-names.c
+++ b/kex-names.c
@@ kex_names_cat
strlcpy(ret, a, len);
- for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) {
- if (kex_has_any_alg(ret, p))
- continue; /* Algorithm already present */
+ /* Build a seen-set from a's tokens — O(M) */
+ /* (small stack hash table, 256 slots, sufficient for < 128 algs) */
+ const char *seen[256] = {0};
+ size_t smask = 255;
+ char *scan = xstrdup(a);
+ char *scp = scan, *sp;
+ for ((sp = strsep(&scp, ",")); sp && *sp; (sp = strsep(&scp, ","))) {
+ size_t h = djb2(sp) & smask;
+ for (size_t i = 0; i < smask+1; i++) {
+ size_t s = (h + i) & smask;
+ if (!seen[s]) { seen[s] = sp; break; }
+ }
+ }
+ for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) {
+ /* O(1) hash set lookup instead of O(M+i) match_list scan */
+ size_t h = djb2(p) & smask, found = 0;
+ for (size_t i = 0; i < smask+1; i++) {
+ size_t s = (h + i) & smask;
+ if (!seen[s]) break;
+ if (strcmp(seen[s], p) == 0) { found = 1; break; }
+ }
+ if (found) continue;
+ /* Mark new entry in set */
+ size_t h2 = djb2(p) & smask;
+ for (size_t i = 0; i < smask+1; i++) {
+ size_t s = (h2 + i) & smask;
+ if (!seen[s]) { seen[s] = p; break; }
+ }
strlcat(ret, ",", len);
strlcat(ret, p, len);
}
+ free(scan);
```
## Complexity After Fix
O(M + N) expected.
## Notes
- The two dedup defects (openssh-0001, openssh-0002) share the same root
cause: `kex_has_any_alg()` / `match_list()` used as a "contains" check
inside a growing-accumulator loop.
- Both sites should be fixed together. A shared `kex_alg_set_t` helper
(init-from-string, O(1) contains, O(1) add) would eliminate both at once.