diff --git a/src/bigint-buffer.c b/src/bigint-buffer.c index e57d516..51323f3 100644 --- a/src/bigint-buffer.c +++ b/src/bigint-buffer.c @@ -9,9 +9,10 @@ #define BIT_MASK(n) (~( ((~0ull) << ((n)-1)) << 1 )) -// The maximum size we'll store on the stack. If we need a larger temporary -// buffer malloc will be called. -#define BUFFER_STACK_SIZE 32 +// Number of uint64_t words to allocate on the stack for temporary buffers. +// If the required buffer exceeds this, malloc is used instead. +#define BUFFER_STACK_WORDS 32 +#define BUFFER_STACK_BYTES (BUFFER_STACK_WORDS * sizeof(uint64_t)) #if defined(_WIN16) || defined(_WIN32) || defined(_WIN64) #define bswap64(x) _byteswap_uint64(x) @@ -54,9 +55,9 @@ napi_value toBigInt (napi_env env, napi_callback_info info) { // Buffer is managed by VM, so copy it out (TODO: perhaps we can increase refcount?) size_t aligned_len = len + overflow_len; size_t len_in_words = not_64_aligned ? (len >> 3) + 1 : (len >> 3); - bool fits_in_stack = aligned_len <= BUFFER_STACK_SIZE; + bool fits_in_stack = aligned_len <= BUFFER_STACK_BYTES; - uint8_t copy[BUFFER_STACK_SIZE]; + uint8_t copy[BUFFER_STACK_BYTES]; uint8_t* bufTemp = fits_in_stack ? copy : malloc(aligned_len); if (overflow_len > 0) { memset(bufTemp + len, 0, overflow_len); @@ -149,11 +150,11 @@ napi_value fromBigInt (napi_env env, napi_callback_info info) { 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; + size_t buffer_size = byte_width + overflow_len; + bool fits_in_stack = buffer_size <= BUFFER_STACK_BYTES; uint64_t* conv_buffer = (uint64_t*) raw_buffer; - uint64_t stack_buffer[BUFFER_STACK_SIZE]; + uint64_t stack_buffer[BUFFER_STACK_WORDS]; if (not_64_aligned) { conv_buffer = fits_in_stack ? stack_buffer : malloc(byte_width + overflow_len); } diff --git a/src/index.spec.ts b/src/index.spec.ts index f9a6f84..3f2df98 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -285,4 +285,82 @@ describe('Try bigint conversion (big endian)', () => { 0xe0, 0xdd, 0xf0, 0x0d, 0xde, 0xad, 0xbe, 0xef ])); }); +}); + +describe('Buffer overflow regression (SNYK-JS-BIGINTBUFFER-3364597)', () => { + // Small BigInt into a non-aligned buffer wider than the native stack buffer. + // Before the fix, fromBigInt compared word_count bytes (small) against the + // stack limit, then memset the full byte_width+overflow into the undersized + // stack buffer, causing a stack buffer overflow. + + it('toBufferBE with small bigint and non-aligned width > 32 bytes', () => { + const buf = toBufferBE(BigInt(`0xdeadbeef`), 33); + buf.length.should.equal(33); + // First 29 bytes should be zero-padded, last 4 bytes hold the value + for (let i = 0; i < 29; i++) { + buf[i].should.equal(0, `byte ${i} should be 0`); + } + buf[29].should.equal(0xde); + buf[30].should.equal(0xad); + buf[31].should.equal(0xbe); + buf[32].should.equal(0xef); + }); + + it('toBufferLE with small bigint and non-aligned width > 32 bytes', () => { + const buf = toBufferLE(BigInt(`0xdeadbeef`), 33); + buf.length.should.equal(33); + buf[0].should.equal(0xef); + buf[1].should.equal(0xbe); + buf[2].should.equal(0xad); + buf[3].should.equal(0xde); + for (let i = 4; i < 33; i++) { + buf[i].should.equal(0, `byte ${i} should be 0`); + } + }); + + it('toBufferBE with small bigint and large non-aligned width (257 bytes)', () => { + const buf = toBufferBE(BigInt(`0x1`), 257); + buf.length.should.equal(257); + for (let i = 0; i < 256; i++) { + buf[i].should.equal(0, `byte ${i} should be 0`); + } + buf[256].should.equal(1); + }); + + it('toBufferLE with small bigint and large non-aligned width (257 bytes)', () => { + const buf = toBufferLE(BigInt(`0x1`), 257); + buf.length.should.equal(257); + buf[0].should.equal(1); + for (let i = 1; i < 257; i++) { + buf[i].should.equal(0, `byte ${i} should be 0`); + } + }); + + it('roundtrip BE with non-aligned width (33 bytes)', () => { + const val = BigInt(`0xbadc0ffee0ddf00ddeadbeef`); + const buf = toBufferBE(val, 33); + const result = toBigIntBE(buf); + assertEquals(result, val); + }); + + it('roundtrip LE with non-aligned width (33 bytes)', () => { + const val = BigInt(`0xbadc0ffee0ddf00ddeadbeef`); + const buf = toBufferLE(val, 33); + const result = toBigIntLE(buf); + assertEquals(result, val); + }); + + it('roundtrip BE with large non-aligned width (257 bytes)', () => { + const val = BigInt(`0xbadc0ffee0ddf00ddeadbeefbadc0ffee0ddf00ddeadbeef`); + const buf = toBufferBE(val, 257); + const result = toBigIntBE(buf); + assertEquals(result, val); + }); + + it('roundtrip LE with large non-aligned width (257 bytes)', () => { + const val = BigInt(`0xbadc0ffee0ddf00ddeadbeefbadc0ffee0ddf00ddeadbeef`); + const buf = toBufferLE(val, 257); + const result = toBigIntLE(buf); + assertEquals(result, val); + }); }); \ No newline at end of file