Possible heap buffer overflow when copying an unaligned Buffer in FromJsConverter::orphanFromJs
I found a possible heap buffer overflow (OOB write) in the AnyPointer case of
FromJsConverter::orphanFromJs. When a JS Buffer is passed for an AnyPointer field and its
backing pointer is not word-aligned, the code copies it into a freshly allocated word array before
constructing a FlatArrayMessageReader. The scratch array is sized by rounding the buffer length
down to a whole number of capnp::words (buffer->size() / sizeof(capnp::word)), but the
memcpy copies the full, un-rounded buffer->size() bytes into it. If the buffer length is not a
multiple of 8, this writes up to 7 bytes past the end of the scratch allocation.
File: src/node-capnp/capnp.cc
Function: FromJsConverter::orphanFromJs (the capnp::schema::Type::ANY_POINTER /
non-CAPABILITY branch)
} else KJ_IF_MAYBE(buffer, unwrapBuffer(js)) {
kj::Array<capnp::word> scratch;
kj::ArrayPtr<const capnp::word> words;
if (reinterpret_cast<uintptr_t>(buffer->begin()) % sizeof(capnp::word) != 0) {
// Array is not aligned. We have to make a copy. :(
scratch = kj::heapArray<capnp::word>(buffer->size() / sizeof(capnp::word));
memcpy(scratch.begin(), buffer->begin(), buffer->size()); // <-- copies full size into a rounded-down allocation
words = scratch;
} else {
...
}
capnp::FlatArrayMessageReader reader(words);
return orphanage.newOrphanCopy(reader.getRoot<capnp::AnyPointer>());
}
unwrapBuffer(js) yields a kj::ArrayPtr<const byte> over a Node Buffer; both its base
pointer alignment and its byte length are attacker-controlled (a Buffer.subarray/slice can
start at an arbitrary, unaligned offset and have an arbitrary length).
scratch = kj::heapArray<capnp::word>(buffer->size() / sizeof(capnp::word)) allocates
floor(size/8) words = floor(size/8) * 8 bytes.
memcpy(scratch.begin(), buffer->begin(), buffer->size()) copies the full size bytes.
When size % 8 != 0, the copy writes size % 8 (1–7) bytes beyond the end of scratch —
a heap out-of-bounds write.
This is confirmed by the two sibling copy sites in the same file (the import/fromBytes-style
paths around lines 1894 and 1954), which handle the identical unaligned case correctly by copying
copy.asBytes().size() / array.asBytes().size() (the rounded-down allocation size) rather than
buffer.size(). Only this orphanFromJs site copies the un-rounded length.
JS trigger (if applicable):
// schema field typed as AnyPointer; pass an unaligned, non-multiple-of-8-length Buffer
const backing = Buffer.alloc(64);
const unaligned = backing.subarray(1, 1 + 17); // begin() unaligned, length 17 (17 % 8 = 1)
msg.anyPointerField = unaligned; // reaches orphanFromJs -> 1-byte heap overflow
Suggested fix: copy only the rounded-down number of bytes, matching the sibling sites, e.g.
memcpy(scratch.begin(), buffer->begin(), scratch.asBytes().size()); (or size scratch to
(buffer->size() + sizeof(capnp::word) - 1) / sizeof(capnp::word) words if the trailing partial
word must be preserved).
Possible heap buffer overflow when copying an unaligned Buffer in
FromJsConverter::orphanFromJsI found a possible heap buffer overflow (OOB write) in the
AnyPointercase ofFromJsConverter::orphanFromJs. When a JSBufferis passed for anAnyPointerfield and itsbacking pointer is not word-aligned, the code copies it into a freshly allocated word array before
constructing a
FlatArrayMessageReader. The scratch array is sized by rounding the buffer lengthdown to a whole number of
capnp::words (buffer->size() / sizeof(capnp::word)), but thememcpycopies the full, un-roundedbuffer->size()bytes into it. If the buffer length is not amultiple of 8, this writes up to 7 bytes past the end of the scratch allocation.
File:
src/node-capnp/capnp.ccFunction:
FromJsConverter::orphanFromJs(thecapnp::schema::Type::ANY_POINTER/non-CAPABILITY branch)
unwrapBuffer(js)yields akj::ArrayPtr<const byte>over a NodeBuffer; both its basepointer alignment and its byte length are attacker-controlled (a
Buffer.subarray/slice canstart at an arbitrary, unaligned offset and have an arbitrary length).
scratch = kj::heapArray<capnp::word>(buffer->size() / sizeof(capnp::word))allocatesfloor(size/8)words =floor(size/8) * 8bytes.memcpy(scratch.begin(), buffer->begin(), buffer->size())copies the fullsizebytes.When
size % 8 != 0, the copy writessize % 8(1–7) bytes beyond the end ofscratch—a heap out-of-bounds write.
This is confirmed by the two sibling copy sites in the same file (the
import/fromBytes-stylepaths around lines 1894 and 1954), which handle the identical unaligned case correctly by copying
copy.asBytes().size()/array.asBytes().size()(the rounded-down allocation size) rather thanbuffer.size(). Only thisorphanFromJssite copies the un-rounded length.JS trigger (if applicable):
Suggested fix: copy only the rounded-down number of bytes, matching the sibling sites, e.g.
memcpy(scratch.begin(), buffer->begin(), scratch.asBytes().size());(or sizescratchto(buffer->size() + sizeof(capnp::word) - 1) / sizeof(capnp::word)words if the trailing partialword must be preserved).