From 277483b96384e84f739717082386f5c4c97144fd Mon Sep 17 00:00:00 2001 From: Jacob Alexander Date: Wed, 1 Jul 2026 23:06:40 -0700 Subject: [PATCH] Fix SIGSEGV on malformed Text field C++ helper c_reraise_kj_exception() (capnp/helpers/capabilityHelper.cpp) unconditionally dereferences the PyObject* returned by wrap_kj_exception_for_reraise() (capnp/lib/capnp.pyx). For a specific class of malformed input -- a Cap'n Proto Text field whose NUL terminator is corrupt -- the wrapper returns NULL, and the subsequent "obj->ob_type" access dereferences NULL (offset 0x8) inside the C extension, producing a deterministic, UNCATCHABLE SIGSEGV. libcapnp itself detects the corruption correctly and would raise a catchable KjException for the sibling code path; only this reraise helper crashes. The malformed bytes reach the crash through the documented public API Type.from_bytes(...) + lazy field access -- exactly how pycapnp consumers deserialize untrusted Cap'n Proto messages received over the network / RPC / from files. A single flipped byte in an attacker-controlled message takes down the consuming process; the crash cannot be caught with try/except, so no graceful degradation is possible. --- capnp/helpers/capabilityHelper.cpp | 3 +++ capnp/lib/capnp.pyx | 2 +- test/test_serialization.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/capnp/helpers/capabilityHelper.cpp b/capnp/helpers/capabilityHelper.cpp index 43a4c2c..5a0a3db 100644 --- a/capnp/helpers/capabilityHelper.cpp +++ b/capnp/helpers/capabilityHelper.cpp @@ -16,6 +16,9 @@ void c_reraise_kj_exception() { } catch (kj::Exception& exn) { auto obj = wrap_kj_exception_for_reraise(exn); + if (obj == nullptr) { + return; + } PyErr_SetObject((PyObject*)obj->ob_type, obj); Py_DECREF(obj); } diff --git a/capnp/lib/capnp.pyx b/capnp/lib/capnp.pyx index a6eddb0..538c46f 100644 --- a/capnp/lib/capnp.pyx +++ b/capnp/lib/capnp.pyx @@ -288,8 +288,8 @@ cdef api object wrap_kj_exception(capnp.Exception & exception) with gil: cdef api object wrap_kj_exception_for_reraise(capnp.Exception & exception) with gil: + PyErr_Clear() wrapper = _KjExceptionWrapper()._init(exception) - ret = KjException(wrapper=wrapper) return ret diff --git a/test/test_serialization.py b/test/test_serialization.py index 6c4bfa5..285edcf 100644 --- a/test/test_serialization.py +++ b/test/test_serialization.py @@ -252,3 +252,22 @@ def test_from_bytes_packed_traversal_limit(all_types): msg = all_types.TestAllTypes.from_bytes_packed(data, traversal_limit_in_words=2**62) for i in range(0, size): assert msg.structList[i].uInt8Field == 0 + +def test_malformed_text_field_reraise(): + SCHEMA = "@0xdbb9ad1f14bf0b36;\nstruct Person { name @0 :Text; age @1 :UInt32; }\n" + with tempfile.NamedTemporaryFile(suffix=".capnp", mode="w", delete=False) as f: + f.write(SCHEMA) + + Person = capnp.load(f.name).Person + + # Create valid payload and corrupt the NUL terminator + buf = bytearray(Person.new_message(name="alice", age=30).to_bytes()) + buf[37] ^= 0xFF + + # The process should raise an exception, not SIGSEGV + try: + with Person.from_bytes(bytes(buf), traversal_limit_in_words=2**20) as r: + _ = str(r.name) + except Exception as e: + # Success: We caught an exception cleanly + pass