buffer: pad aligned allocations by a multiple of 8
Project: nodejs/node | Language: JavaScript | Concepts: memory-management, data-structures | Difficulty: mid
Link: https://github.com/nodejs/node/pull/65605
The problem
When allocating buffers with alignment requirements, Node.js previously padded allocations by up to alignment - 1 bytes. This could leave the end of a pooled buffer misaligned with respect to the backing ArrayBuffer. As a result, creating typed array views (like Uint32Array or BigUint64Array) over the buffer could throw a RangeError, because the buffer might not end on a boundary that is a multiple of the element size. This regression started in Node.js v26.8.0.
Why it's non-trivial
A naive fix would be to just pad by alignment - 1 bytes, but this does not guarantee that the leftover memory after the buffer ends will be suitable for creating typed array views, especially when the element size is greater than the alignment. The underlying memory allocator may also require stricter alignment (e.g., 8 bytes), so simply matching the requested alignment isn't always sufficient to prevent partial elements at the end.
The approach
The PR changes the padding logic to always pad by a multiple of 8 bytes or the requested alignment, whichever is greater. This ensures that the leftover memory after an aligned buffer allocation is always a whole number of elements for any typed array, preventing partial elements and related errors.
1. Introduce a helper function for alignment padding:
+function alignmentPadding(alignment) {
+ return MathMax(alignment, 8);
+}
This function ensures that the padding is at least 8 bytes, or the requested alignment if it's greater. This avoids partial typed array elements after the buffer.
2. Use the new padding logic in buffer allocation:
- const ab = createUnsafeArrayBuffer(size + alignment - 1);
+ const ab = createUnsafeArrayBuffer(size + alignmentPadding(alignment));
Instead of padding by alignment - 1, the code now pads by the value returned from alignmentPadding, ensuring enough space for alignment and element boundaries.
3. Update validation logic to match the new padding:
- if (size > kMaxLength - (alignment - 1)) {
+ const padding = alignmentPadding(alignment);
+ if (size > kMaxLength - padding) {
throw new ERR_OUT_OF_RANGE(
- 'size', `<= ${kMaxLength - (alignment - 1)}`, size);
+ 'size', `<= ${kMaxLength - padding}`, size);
}
This ensures that the maximum allowed buffer size accounts for the new, possibly larger, padding.
4. Update documentation and comments:
-// power of two, and `size + alignment - 1` must not exceed the maximum buffer
-// length. Since the address of a backing store cannot be chosen, `alignment - 1`
-// extra bytes are allocated and skipped, which leaves the returned buffer with a
-// non-zero `byteOffset` into a larger ArrayBuffer.
+// power of two, and `size` plus its padding must not exceed the maximum buffer
+// length. Since the address of a backing store cannot be chosen, that padding is
+// allocated and skipped, which leaves the returned buffer with a non-zero
+// `byteOffset` into a larger ArrayBuffer.
Comments and documentation are updated to accurately describe the new behavior.
5. Add tests to verify no partial elements remain:
+// The padding an aligned allocation needs does not leave a partial element
+// behind it, so a typed array view running from the buffer to the end of its
+// ArrayBuffer can be created without an explicit length.
+{
+ const bufs = [Buffer.allocUnsafe(8), Buffer.allocUnsafeSlow(4096, 4096)];
+ for (const alignment of alignments) {
+ bufs.push(Buffer.allocUnsafe(8, alignment),
+ Buffer.allocUnsafeSlow(8, alignment));
+ }
+ for (const buf of bufs) {
+ const rest = buf.buffer.byteLength - buf.byteOffset;
+ assert.strictEqual(new BigUint64Array(buf.buffer, buf.byteOffset).byteLength,
+ rest);
+ assert.strictEqual(new Uint32Array(buf.buffer, buf.byteOffset).byteLength,
+ rest);
+ }
+}
This test ensures that after the buffer, the remaining memory is a whole number of typed array elements, preventing RangeError.
From the review thread
No notable review discussion on this PR.
Takeaways
- When aligning memory for buffers, always pad to the largest required alignment (including hardware or typed array requirements) to avoid partial elements and runtime errors.
- Helper functions for alignment logic centralize and clarify padding calculations, reducing subtle bugs.
- Tests for boundary conditions (like creating typed arrays over buffer ends) are essential when changing memory allocation or alignment logic.