CVE-2026-104874
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
5.3
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
- Record
CVE-2026-104874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-401
- Updated
- 2026-11-02
EUVDEUVD-2026-91800
5.3
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
- Record
EUVD-2026-91800- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-54p9-h82j-f925
5.3
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
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:
// 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_DECREFs 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
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
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)",…
- Record
GHSA-54p9-h82j-f925- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-401
- Updated
- 2026-11-02
NVD
5.3
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
- Record
CVE-2026-104874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-401
- Updated
- 2026-11-02
OSV
5.3
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
- Record
CVE-2026-104874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-401
- Updated
- 2026-11-02
OSVGHSA-54p9-h82j-f925
5.3
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
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:
// 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_DECREFs 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
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
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)",…
- Record
GHSA-54p9-h82j-f925- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-401
- Updated
- 2026-11-02
OSVPYSEC-2026-4244
5.3
Multidict: Reference leak in CIMultiDict/MultiDict items-view union and subtraction
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:
// 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_DECREFs 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
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
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)",…
- Record
PYSEC-2026-4244- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-104874
5.3
Multidict is an implementation of a multidict data structure. From 6.7.0 until 6.9.1, the C extension's items-view reflected union operation, operand | d.items(), in multidict_itemsview_or2_impl and subtraction operation, d.items() - operand, in multidict_itemsview_sub1_impl fail to release new key-identity and value references returned for each operand element. Applications that perform these operations over attacker-influenced sequences can leak two strong references per element, and garbage collection cannot reclaim them, so repeated operations can cause unbounded process memory growth and denial of service. Forward union, intersection, non-tuple operand elements, and pure-Python builds are not affected by this reference leak. This issue is fixed in version 6.9.1.
- Record
UBUNTU-CVE-2026-104874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
OSVopenSUSE-SU-2026:12117-1 No score
python313-multidict-7.0.0-1.1 on GA media
These are all security issues fixed in the python313-multidict-7.0.0-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:12117-1- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
multidictPyPI |
6.7.0 to 6.9.0 | 6.9.1 |
multidictPyPI |
6.7.0 up to 6.9.1, plus listed versions 6.7.16.8.06.9.0 |
6.9.1 |
python-multidictUbuntu:18.04:LTS |
All versions, plus listed versions 3.1.3-1build13.2.0-13.3.2-14.1.0-1 |
None known |
python-multidictUbuntu:20.04:LTS |
All versions, plus listed versions 4.5.2-14.6.1-14.7.3-14.7.3-1build1 |
None known |
python-multidictUbuntu:22.04:LTS |
All versions, plus listed versions 5.1.0-15.1.0-1build15.1.0-1build2 |
None known |
python-multidictUbuntu:24.04:LTS |
All versions, plus listed versions 6.0.4-1build16.0.4-1.16.0.4-1.1build1 |
None known |
python-multidictUbuntu:26.10 |
All versions, plus listed versions 6.4.3-1build26.4.3-2 |
None known |
python-multidictUbuntu:26.04:LTS |
All versions, plus listed versions 6.4.3-16.4.3-1build16.4.3-1build2 |
None known |
python-multidictopenSUSE:Tumbleweed |
before 7.0.0-1.1 | 7.0.0-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-104874 is a medium vulnerability (CVSS 5.3) in Aio-libs Multidict, published October 2, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.45%, the 39th percentile. Fixed in multidict 6.9.1.
References
github.com/aio-libs/multidict/security/advisories/GHSA-54p9-h82j-f925
github.com/aio-libs/multidict/commit/350b4a07bf8ff851b6d7544e81b1c748bdc74c40
github.com/aio-libs/multidict/releases/tag/v6.9.1
nvd.nist.gov/vuln/detail/CVE-2026-104874
github.com/advisories/GHSA-54p9-h82j-f925
github.com/CVEProject/cvelistV5/tree/main/cves/2026/104xxx/CVE-2026-104874.json
Left out here: the rest of 3 source descriptions. The JSON record has them.