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:
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.
not_64_aligned is true whenever byte_width is not a multiple of 8 (e.g. 1001).
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.
word_width_bytes = word_count << 3 = 8, so fits_in_stack is true regardless of how large byte_width is.
- Because
not_64_aligned is true and fits_in_stack is true, conv_buffer = stack_buffer (a 256-byte on-stack array).
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.
- 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.
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 fixed256-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 thenmemsets and latermemcpys usingbyte_width, which is the size of the caller-supplied output Buffer and iscompletely 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_widthis huge, so thememset(conv_buffer, 0, byte_width + overflow_len)writes far past the 256-byte stack buffer.File:
src/bigint-buffer.cFunction:
fromBigIntPath, line by line:
byte_widthis taken from the second argument vianapi_get_buffer_info(env, argv[1], ..., &byte_width)— it is the length of the destination Buffer, fully attacker-controlled.not_64_alignedis true wheneverbyte_widthis not a multiple of 8 (e.g. 1001).word_widthbecomes large for a large buffer, butword_countis the BigInt's word count. Passing a tiny BigInt (e.g.1n, one 64-bit word) keepsword_count == 1.word_width_bytes = word_count << 3 = 8, sofits_in_stackistrueregardless of how largebyte_widthis.not_64_alignedis true andfits_in_stackis true,conv_buffer = stack_buffer(a 256-byte on-stack array).memset(conv_buffer, 0, byte_width + overflow_len)then zeroesbyte_width + overflow_lenbytes (e.g. 1008) into the 256-byte stack buffer — a stack buffer overflow of arbitrary size, corrupting the return address / saved registers.memcpy(raw_buffer, ... conv_buffer ..., byte_width)also over-reads the same stack buffer, but thememsetalready smashes the stack.JS trigger:
Suggested fix: size the scratch buffer (and the
fits_in_stackdecision) frombyte_width, notfrom the clamped
word_count; i.e. computefits_in_stack = (byte_width + overflow_len) <= sizeof(stack_buffer)and allocate
malloc(byte_width + overflow_len)otherwise. Also validate thatbyte_widthis within a sane limit before touching the stack buffer.