Skip to content

Possible heap buffer overflow when copying an unaligned Buffer in FromJsConverter::orphanFromJs #74

Description

@OvOhao

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>());
}
  1. 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).
  2. scratch = kj::heapArray<capnp::word>(buffer->size() / sizeof(capnp::word)) allocates
    floor(size/8) words = floor(size/8) * 8 bytes.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions