mirror of
https://github.com/gentoo-mirror/gentoo.git
synced 2026-08-24 06:48:18 -07:00
Also, drop a patch we don't seem to need anymore. Closes: https://bugs.gentoo.org/978301 Signed-off-by: Sam James <sam@gentoo.org>
329 lines
11 KiB
Diff
329 lines
11 KiB
Diff
https://github.com/lericson/pylibmc/issues/302
|
|
https://github.com/lericson/pylibmc/pull/303
|
|
|
|
From 76c966a113a02e170926f622da4dc8a4f2ba6f21 Mon Sep 17 00:00:00 2001
|
|
From: "Bryan C. Mills" <bryan@duolingo.com>
|
|
Date: Tue, 10 Mar 2026 00:37:37 -0400
|
|
Subject: [PATCH 1/3] Fix doctests.txt for libmemcache 1.0.18
|
|
|
|
Fixes lericson/pylibmc#302.
|
|
---
|
|
tests/doctests.txt | 4 +++-
|
|
2 files changed, 6 insertions(+), 1 deletion(-)
|
|
|
|
diff --git a/tests/doctests.txt b/tests/doctests.txt
|
|
index 3bc5728..6ed44bd 100644
|
|
--- a/tests/doctests.txt
|
|
+++ b/tests/doctests.txt
|
|
@@ -209,11 +209,13 @@ Per-error exceptions
|
|
... except pylibmc.NotFound:
|
|
... print("ok")
|
|
ok
|
|
+>>> c.set_behaviors({"verify_keys": True})
|
|
>>> try:
|
|
... c.incr(chr(0))
|
|
-... except (pylibmc.ProtocolError, pylibmc.SocketCreateError):
|
|
+... except pylibmc.BadKeyProvided:
|
|
... print("ok")
|
|
ok
|
|
+>>> c.set_behaviors({"verify_keys": False})
|
|
|
|
Behaviors.
|
|
>>> c.set_behaviors({"tcp_nodelay": True, "hash": 6})
|
|
|
|
From de096912844a6df42da8f9ad2b826769ee643a01 Mon Sep 17 00:00:00 2001
|
|
From: "Bryan C. Mills" <bryan@duolingo.com>
|
|
Date: Tue, 10 Mar 2026 00:41:30 -0400
|
|
Subject: [PATCH 2/3] Fix a use-after-free bug in gets
|
|
|
|
Fixes lericson/pylibmc#300.
|
|
---
|
|
src/_pylibmcmodule.c | 16 +----
|
|
tests/test_gets_use_after_free.py | 107 ++++++++++++++++++++++++++++++
|
|
2 files changed, 110 insertions(+), 13 deletions(-)
|
|
create mode 100644 tests/test_gets_use_after_free.py
|
|
|
|
diff --git a/src/_pylibmcmodule.c b/src/_pylibmcmodule.c
|
|
index 545b658..83ca929 100644
|
|
--- a/src/_pylibmcmodule.c
|
|
+++ b/src/_pylibmcmodule.c
|
|
@@ -759,8 +759,6 @@ static PyObject *PylibMC_Client_gets(PylibMC_Client *self, PyObject *arg) {
|
|
*keys = PyBytes_AS_STRING(arg);
|
|
*keylengths = (size_t)PyBytes_GET_SIZE(arg);
|
|
|
|
- Py_DECREF(arg);
|
|
-
|
|
Py_BEGIN_ALLOW_THREADS;
|
|
|
|
rc = memcached_mget(self->mc, keys, keylengths, 1);
|
|
@@ -769,6 +767,8 @@ static PyObject *PylibMC_Client_gets(PylibMC_Client *self, PyObject *arg) {
|
|
|
|
Py_END_ALLOW_THREADS;
|
|
|
|
+ Py_DECREF(arg);
|
|
+
|
|
int miss = 0;
|
|
int fail = 0;
|
|
if (rc == MEMCACHED_SUCCESS && res != NULL) {
|
|
@@ -2463,14 +2463,6 @@ static int _key_normalized_obj(PyObject **key) {
|
|
key_str = PyBytes_AS_STRING(retval);
|
|
key_sz = PyBytes_GET_SIZE(retval);
|
|
rc = _key_normalized_str(&key_str, &key_sz);
|
|
- if (rc == 2) {
|
|
- retval = PyBytes_FromStringAndSize(key_str, key_sz);
|
|
- if (retval != NULL) {
|
|
- rc = 1;
|
|
- } else {
|
|
- rc = 0;
|
|
- }
|
|
- }
|
|
|
|
END:
|
|
if (retval != orig_key) {
|
|
@@ -2488,9 +2480,7 @@ static int _key_normalized_obj(PyObject **key) {
|
|
}
|
|
|
|
/**
|
|
- * Normalize memcached key.
|
|
- *
|
|
- * Returns 0 if invalid, 1 if already normalized, and 2 if mutated.
|
|
+ * Validate a memcached key. Returns 0 if invalid, 1 if valid.
|
|
*/
|
|
static int _key_normalized_str(char **str, Py_ssize_t *size) {
|
|
/* libmemcached pads max_key_size with one byte for null termination */
|
|
diff --git a/tests/test_gets_use_after_free.py b/tests/test_gets_use_after_free.py
|
|
new file mode 100644
|
|
index 0000000..fdcb861
|
|
--- /dev/null
|
|
+++ b/tests/test_gets_use_after_free.py
|
|
@@ -0,0 +1,107 @@
|
|
+"""Regression test for premature Py_DECREF in PylibMC_Client_gets.
|
|
+
|
|
+The bug only triggered with str keys: _key_normalized_obj encodes the
|
|
+str into a new bytes object, and the call to Py_DECREF
|
|
+deallocated it immediately (BEFORE it was passed to memcached_mget),
|
|
+causing a use-after-free and potential reads of arbitrary keys.
|
|
+
|
|
+Requires a running memcached (see MEMCACHED_HOST/PORT env vars).
|
|
+"""
|
|
+
|
|
+import os
|
|
+import subprocess
|
|
+import sys
|
|
+import textwrap
|
|
+
|
|
+import pytest
|
|
+
|
|
+from pylibmc.test import make_test_client
|
|
+from tests import PylibmcTestCase
|
|
+
|
|
+
|
|
+def _run_with_debug_malloc(script, timeout=10):
|
|
+ """Run a Python script in a subprocess with PYTHONMALLOC=debug.
|
|
+
|
|
+ The debug allocator fills freed memory with 0xDD, making
|
|
+ use-after-free bugs deterministically detectable.
|
|
+ """
|
|
+ return subprocess.run(
|
|
+ [sys.executable, "-c", script],
|
|
+ env={**os.environ, "PYTHONMALLOC": "debug"},
|
|
+ capture_output=True,
|
|
+ text=True,
|
|
+ timeout=timeout,
|
|
+ )
|
|
+
|
|
+
|
|
+class TestGetsUseAfterFree(PylibmcTestCase):
|
|
+
|
|
+ def test_gets_str_key_reads_freed_buffer(self):
|
|
+ """gets(str_key) sends garbage because the key buffer is freed first.
|
|
+
|
|
+ With PYTHONMALLOC=debug the freed buffer becomes 0xDD bytes, so
|
|
+ memcached_mget asks for a nonexistent key and returns a miss.
|
|
+ get(str_key) works correctly as a control.
|
|
+
|
|
+ This test runs in a subprocess so that PYTHONMALLOC=debug is
|
|
+ active regardless of how the outer test runner was invoked.
|
|
+ """
|
|
+ host = os.environ.get("MEMCACHED_HOST", "127.0.0.1")
|
|
+ port = os.environ.get("MEMCACHED_PORT", "11211")
|
|
+
|
|
+ result = _run_with_debug_malloc(textwrap.dedent(f"""\
|
|
+ import sys
|
|
+ import pylibmc
|
|
+
|
|
+ mc = pylibmc.Client(
|
|
+ ["{host}:{port}"], behaviors={{"cas": True}},
|
|
+ )
|
|
+
|
|
+ key_str = "test_gets_uaf"
|
|
+ key_bytes = key_str.encode("utf-8")
|
|
+ assert mc.set(key_bytes, 42)
|
|
+
|
|
+ # Control: get() Py_DECREFs AFTER memcached_get — no UAF.
|
|
+ val = mc.get(key_str)
|
|
+ if val != 42:
|
|
+ print(f"get() failed: {{val!r}}", file=sys.stderr)
|
|
+ sys.exit(2)
|
|
+
|
|
+ # Bug: gets() Py_DECREFs BEFORE memcached_mget — UAF.
|
|
+ value, cas = mc.gets(key_str)
|
|
+ if value != 42:
|
|
+ print(
|
|
+ f"BUG: gets({{key_str!r}}) = ({{value!r}}, {{cas!r}}), "
|
|
+ f"expected (42, <cas>)\\n"
|
|
+ f" get({{key_str!r}}) = 42 (correct)\\n"
|
|
+ f"Key buffer was used after free in PylibMC_Client_gets.",
|
|
+ file=sys.stderr,
|
|
+ )
|
|
+ sys.exit(1)
|
|
+ """))
|
|
+
|
|
+ if result.returncode == 1:
|
|
+ pytest.fail(
|
|
+ f"gets() use-after-free confirmed:\n{result.stderr.strip()}"
|
|
+ )
|
|
+ elif result.returncode == 2:
|
|
+ pytest.fail(
|
|
+ f"get() failed (is memcached running?):\n"
|
|
+ f"{result.stderr.strip()}"
|
|
+ )
|
|
+ elif result.returncode != 0:
|
|
+ pytest.fail(
|
|
+ f"Subprocess failed (exit {result.returncode}):\n"
|
|
+ f"{result.stderr.strip()}"
|
|
+ )
|
|
+
|
|
+ def test_gets_bytes_key_not_affected(self):
|
|
+ """gets(bytes_key) was not affected by the bug.
|
|
+
|
|
+ This serves as a control case for the test.
|
|
+ """
|
|
+ mc = make_test_client(binary=False, behaviors={"cas": True})
|
|
+ key = b"test_gets_bytes_ok"
|
|
+ assert mc.set(key, 99)
|
|
+ value, _cas = mc.gets(key)
|
|
+ assert value == 99
|
|
|
|
From fa897d09b8b2d084279bb2a635611f26d7023f61 Mon Sep 17 00:00:00 2001
|
|
From: "Bryan C. Mills" <bryan@duolingo.com>
|
|
Date: Tue, 10 Mar 2026 00:42:29 -0400
|
|
Subject: [PATCH 3/3] Fix a masked error in get_multi
|
|
|
|
Fixes lericson/pylibmc#301.
|
|
---
|
|
src/_pylibmcmodule.c | 13 +++--
|
|
tests/test_fetch_multi_error_masking.py | 65 +++++++++++++++++++++++++
|
|
2 files changed, 75 insertions(+), 3 deletions(-)
|
|
create mode 100644 tests/test_fetch_multi_error_masking.py
|
|
|
|
diff --git a/src/_pylibmcmodule.c b/src/_pylibmcmodule.c
|
|
index 83ca929..39293b8 100644
|
|
--- a/src/_pylibmcmodule.c
|
|
+++ b/src/_pylibmcmodule.c
|
|
@@ -1666,7 +1666,10 @@ static pylibmc_mget_res _fetch_multi(memcached_st *mc,
|
|
result = memcached_fetch_result(mc, result, &res.rc);
|
|
|
|
if (result == NULL || res.rc == MEMCACHED_END) {
|
|
- /* This is how libmecached signals EOF. */
|
|
+ /* memcached_fetch_result returns NULL once all servers are
|
|
+ * exhausted. res.rc distinguishes normal completion
|
|
+ * (MEMCACHED_END, MEMCACHED_NOTFOUND) from errors
|
|
+ * (e.g. MEMCACHED_CONNECTION_FAILURE). */
|
|
break;
|
|
} else if (res.rc == MEMCACHED_BAD_KEY_PROVIDED
|
|
|| res.rc == MEMCACHED_NO_KEY_PROVIDED) {
|
|
@@ -1678,7 +1681,6 @@ static pylibmc_mget_res _fetch_multi(memcached_st *mc,
|
|
}
|
|
}
|
|
|
|
- res.rc = MEMCACHED_SUCCESS;
|
|
return res;
|
|
}
|
|
|
|
@@ -1797,7 +1799,12 @@ static PyObject *PylibMC_Client_get_multi(
|
|
res = _fetch_multi(self->mc, req);
|
|
Py_END_ALLOW_THREADS;
|
|
|
|
- if (res.rc != MEMCACHED_SUCCESS) {
|
|
+ /* _fetch_multi returns the rc from the final memcached_fetch_result
|
|
+ * call. Per the libmemcached docs, that function sets rc to END on
|
|
+ * successful conclusion, or NOTFOUND if no keys matched — both are
|
|
+ * normal outcomes for get_multi. Any other code (e.g.
|
|
+ * CONNECTION_FAILURE) is an actual error. */
|
|
+ if (res.rc != MEMCACHED_END && res.rc != MEMCACHED_NOTFOUND) {
|
|
PylibMC_ErrFromMemcached(self, res.err_func, res.rc);
|
|
goto earlybird;
|
|
}
|
|
diff --git a/tests/test_fetch_multi_error_masking.py b/tests/test_fetch_multi_error_masking.py
|
|
new file mode 100644
|
|
index 0000000..ef66833
|
|
--- /dev/null
|
|
+++ b/tests/test_fetch_multi_error_masking.py
|
|
@@ -0,0 +1,65 @@
|
|
+"""Regression test for error masking in _fetch_multi.
|
|
+
|
|
+If the connection failed after the initial memcached_mget had already succeeded,
|
|
+_fetch_multi would interpret the NULL return from memcached_fetch_result as EOF
|
|
+and unconditionally overwrite the result's status with MEMCACHED_SUCCESS.
|
|
+"""
|
|
+
|
|
+import socket
|
|
+import threading
|
|
+import time
|
|
+import unittest
|
|
+
|
|
+import pytest
|
|
+
|
|
+import pylibmc
|
|
+
|
|
+
|
|
+def _mock_text_server():
|
|
+ """Start a mock memcached that sends one VALUE then drops the connection.
|
|
+
|
|
+ Returns (port, thread). The server socket is cleaned up by the
|
|
+ handler thread after a single accepted connection.
|
|
+ """
|
|
+ server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
+ server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
+ server_sock.bind(("127.0.0.1", 0))
|
|
+ port = server_sock.getsockname()[1]
|
|
+ server_sock.listen(1)
|
|
+ server_sock.settimeout(10)
|
|
+
|
|
+ def handler():
|
|
+ try:
|
|
+ conn, _ = server_sock.accept()
|
|
+ data = b""
|
|
+ while b"\r\n" not in data:
|
|
+ chunk = conn.recv(4096)
|
|
+ if not chunk:
|
|
+ break
|
|
+ data += chunk
|
|
+ conn.sendall(b"VALUE key1 0 5\r\nhello\r\n")
|
|
+ time.sleep(0.05)
|
|
+ conn.close()
|
|
+ except Exception:
|
|
+ pass
|
|
+ finally:
|
|
+ server_sock.close()
|
|
+
|
|
+ t = threading.Thread(target=handler, daemon=True)
|
|
+ t.start()
|
|
+ return port, t
|
|
+
|
|
+
|
|
+class TestFetchMultiErrorMasking(unittest.TestCase):
|
|
+
|
|
+ def test_get_multi_returns_error_on_connection_drop(self):
|
|
+ port, server_thread = _mock_text_server()
|
|
+
|
|
+ mc = pylibmc.Client([f"127.0.0.1:{port}"])
|
|
+ try:
|
|
+ result = mc.get_multi([b"key1", b"key2", b"key3"])
|
|
+ pytest.fail(f"get_multi failed to raise pylibmc.Error. Partial results: {result!r}")
|
|
+ except pylibmc.Error:
|
|
+ return # Correct: connection error raised.
|
|
+ finally:
|
|
+ server_thread.join()
|