wave17: openssh-0001/2, strongswan-0001, gradle-0002, groovy-0001/2, make-0001, clojure-CLEAN + whitepaper 549/240
This commit is contained in:
parent
0f275c44af
commit
4221966e66
22 changed files with 1776 additions and 4 deletions
111
defects/openssh/patch/openssh-0001-server-sig-algs-dedup.md
Normal file
111
defects/openssh/patch/openssh-0001-server-sig-algs-dedup.md
Normal 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).
|
||||
101
defects/openssh/patch/openssh-0002-kex-names-cat-dedup.md
Normal file
101
defects/openssh/patch/openssh-0002-kex-names-cat-dedup.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue