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.
This commit is contained in:
Jacob Alexander
2026-07-01 23:06:40 -07:00
parent 6f70b8d737
commit 277483b963
3 changed files with 23 additions and 1 deletions

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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