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:
@@ -16,6 +16,9 @@ void c_reraise_kj_exception() {
|
|||||||
}
|
}
|
||||||
catch (kj::Exception& exn) {
|
catch (kj::Exception& exn) {
|
||||||
auto obj = wrap_kj_exception_for_reraise(exn);
|
auto obj = wrap_kj_exception_for_reraise(exn);
|
||||||
|
if (obj == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
PyErr_SetObject((PyObject*)obj->ob_type, obj);
|
PyErr_SetObject((PyObject*)obj->ob_type, obj);
|
||||||
Py_DECREF(obj);
|
Py_DECREF(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
cdef api object wrap_kj_exception_for_reraise(capnp.Exception & exception) with gil:
|
||||||
|
PyErr_Clear()
|
||||||
wrapper = _KjExceptionWrapper()._init(exception)
|
wrapper = _KjExceptionWrapper()._init(exception)
|
||||||
|
|
||||||
ret = KjException(wrapper=wrapper)
|
ret = KjException(wrapper=wrapper)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
msg = all_types.TestAllTypes.from_bytes_packed(data, traversal_limit_in_words=2**62)
|
||||||
for i in range(0, size):
|
for i in range(0, size):
|
||||||
assert msg.structList[i].uInt8Field == 0
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user