Skip to content

Possible stack out-of-bounds write in fromBigInt (caller-controlled output buffer size) #65

Description

@OvOhao

Possible stack out-of-bounds write in fromBigInt (caller-controlled output buffer size)

I found a possible OOB write in fromBigInt. The function decides whether to use a fixed
256-byte stack scratch buffer (stack_buffer) based only on the BigInt's word count
(fits_in_stack = word_width_bytes <= BUFFER_STACK_SIZE), but it then memsets and later
memcpys using byte_width, which is the size of the caller-supplied output Buffer and is
completely unbounded. A caller passing a small BigInt together with a large, non-8-aligned
output Buffer selects the stack path (small word count) while byte_width is huge, so the
memset(conv_buffer, 0, byte_width + overflow_len) writes far past the 256-byte stack buffer.

File: src/bigint-buffer.c

Function: fromBigInt

size_t word_width = (byte_width >> 3) + (not_64_aligned ? 1 : 0);
size_t original_word_width = word_width;
if (word_count > word_width) {
    word_count = word_width;
}
size_t word_width_bytes = (word_count << 3);
bool fits_in_stack = word_width_bytes <= BUFFER_STACK_SIZE;   // depends on BigInt size, NOT byte_width

uint64_t* conv_buffer = (uint64_t*) raw_buffer;
uint64_t stack_buffer[BUFFER_STACK_SIZE];                     // 32 * 8 = 256 bytes
if (not_64_aligned) {
    conv_buffer = fits_in_stack ? stack_buffer : malloc(byte_width + overflow_len);
}

memset(conv_buffer, 0, byte_width + overflow_len);           // <-- writes byte_width bytes into stack_buffer

Path, line by line:

  1. byte_width is taken from the second argument via napi_get_buffer_info(env, argv[1], ..., &byte_width) — it is the length of the destination Buffer, fully attacker-controlled.
  2. not_64_aligned is true whenever byte_width is not a multiple of 8 (e.g. 1001).
  3. word_width becomes large for a large buffer, but word_count is the BigInt's word count. Passing a tiny BigInt (e.g. 1n, one 64-bit word) keeps word_count == 1.
  4. word_width_bytes = word_count << 3 = 8, so fits_in_stack is true regardless of how large byte_width is.
  5. Because not_64_aligned is true and fits_in_stack is true, conv_buffer = stack_buffer (a 256-byte on-stack array).
  6. memset(conv_buffer, 0, byte_width + overflow_len) then zeroes byte_width + overflow_len bytes (e.g. 1008) into the 256-byte stack buffer — a stack buffer overflow of arbitrary size, corrupting the return address / saved registers.
  7. The later memcpy(raw_buffer, ... conv_buffer ..., byte_width) also over-reads the same stack buffer, but the memset already smashes the stack.

JS trigger:

const { fromBigInt } = require('bigint-buffer'); // native path
// small BigInt, large non-8-aligned output buffer -> stack smash
fromBigInt(1n, Buffer.alloc(1001), false);

Suggested fix: size the scratch buffer (and the fits_in_stack decision) from byte_width, not
from the clamped word_count; i.e. compute fits_in_stack = (byte_width + overflow_len) <= sizeof(stack_buffer)
and allocate malloc(byte_width + overflow_len) otherwise. Also validate that byte_width
is within a sane limit before touching the stack buffer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions