diff --git a/doc/api/buffer.md b/doc/api/buffer.md index db80254ef11..48615414dad 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -793,11 +793,14 @@ data that might not have been allocated for `Buffer`s. A `TypeError` will be thrown if `size` is not a number. -### Static method: `Buffer.allocUnsafe(size)` +### Static method: `Buffer.allocUnsafe(size[, alignment])` * `size` {integer} The desired length of the new `Buffer`. +* `alignment` {integer} If given, the memory backing the new `Buffer` will start + at an address that is a multiple of `alignment`. Must be a power of two no + larger than `2 ** 30`. See [Aligned allocations][]. * Returns: {Buffer} Allocates a new `Buffer` of `size` bytes. If `size` is larger than @@ -867,11 +873,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal difference is subtle but can be important when an application requires the additional performance that [`Buffer.allocUnsafe()`][] provides. -### Static method: `Buffer.allocUnsafeSlow(size)` +### Static method: `Buffer.allocUnsafeSlow(size[, alignment])` * `size` {integer} The desired length of the new `Buffer`. +* `alignment` {integer} If given, the memory backing the new `Buffer` will start + at an address that is a multiple of `alignment`. Must be a power of two no + larger than `2 ** 30`. See [Aligned allocations][]. * Returns: {Buffer} Allocates a new `Buffer` of `size` bytes. If `size` is larger than @@ -5602,16 +5614,92 @@ While there are clear performance advantages to using [`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid introducing security vulnerabilities into an application. +### Aligned allocations + +Some operating system interfaces require the memory they operate on to be +aligned, and on some hardware alignment is merely faster. The most common +example of the former is unbuffered ("direct") file I/O, which on Linux requires +the buffer address, the file offset and the transfer length to all be multiples +of the logical block size of the underlying device: + +```mjs +import { open } from 'node:fs/promises'; +import { constants } from 'node:fs'; +import { Buffer } from 'node:buffer'; + +const blockSize = 4096; + +// The buffer address must be block-aligned for O_DIRECT to accept it. +const buf = Buffer.allocUnsafeSlow(blockSize, blockSize); + +const file = await open('/dev/sda', constants.O_RDONLY | constants.O_DIRECT); +try { + await file.read(buf, 0, blockSize, 0); +} finally { + await file.close(); +} +``` + +```cjs +const fs = require('node:fs'); +const { Buffer } = require('node:buffer'); + +const blockSize = 4096; + +// The buffer address must be block-aligned for O_DIRECT to accept it. +const buf = Buffer.allocUnsafeSlow(blockSize, blockSize); + +const flags = fs.constants.O_RDONLY | fs.constants.O_DIRECT; +fs.open('/dev/sda', flags, (err, fd) => { + if (err) throw err; + fs.read(fd, buf, 0, blockSize, 0, (err) => { + fs.close(fd, () => {}); + if (err) throw err; + }); +}); +``` + +Alignment can also be worth requesting purely for performance, even when no +interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on +most contemporary CPUs) keeps it from straddling one more cache line than it +needs to, so that a small structure is fetched with one cache miss instead of +two, and page-aligned (4096 bytes) allocations similarly help interfaces that map +or pin memory. These are micro-optimizations: measure before reaching for them, +since the extra bytes are not free. + +Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes +have to be allocated or skipped to reach an aligned address. +[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and +positions the returned `Buffer` at the first suitably aligned byte within them. +[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool, +whose start is always aligned to 64 bytes, and only falls back to an allocation +of its own when `alignment` is larger than that. Either way, +[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`, +so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must +take the offset into account, as it must for pooled `Buffer`s. + +The alignment is a property of the returned `Buffer` and is preserved for its +whole lifetime, but it is not inherited by other views: [`buf.subarray`][], +[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s. + +Alignment also does not survive being captured in a startup snapshot: memory does +not keep its address across serialization, so a `Buffer` allocated while +[`--build-snapshot`][] is in effect is not aligned in the deserialized process. +Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback, +or after startup, if the alignment has to hold at run time. + [ASCII]: https://en.wikipedia.org/wiki/ASCII +[Aligned allocations]: #aligned-allocations [Base64]: https://en.wikipedia.org/wiki/Base64 [ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1 [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5 [UTF-16]: https://en.wikipedia.org/wiki/UTF-16 [UTF-8]: https://en.wikipedia.org/wiki/UTF-8 [WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/ +[`--build-snapshot`]: cli.md#--build-snapshot [`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding -[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize -[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize +[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment +[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment [`Buffer.concat()`]: #static-method-bufferconcatlist-totallength [`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length [`Buffer.from(array)`]: #static-method-bufferfromarray @@ -5633,6 +5721,7 @@ introducing security vulnerabilities into an application. [`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray [`blob.stream()`]: #blobstream [`buf.buffer`]: #bufbuffer +[`buf.byteOffset`]: #bufbyteoffset [`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend [`buf.entries()`]: #bufentries [`buf.fill()`]: #buffillvalue-offset-end-encoding @@ -5647,6 +5736,7 @@ introducing security vulnerabilities into an application. [`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length [`buffer.kMaxLength`]: #bufferkmaxlength [`util.inspect()`]: util.md#utilinspectobject-options +[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data [`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6 [base64url]: https://tools.ietf.org/html/rfc4648#section-5 [endianness]: https://en.wikipedia.org/wiki/Endianness diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 0868f0fae4c..59931d34573 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -4727,7 +4727,7 @@ calling or overriding `_listen2`. [`--pending-deprecation`]: cli.md#--pending-deprecation [`--throw-deprecation`]: cli.md#--throw-deprecation [`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode -[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize +[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment [`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray [`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer [`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj @@ -4862,7 +4862,7 @@ calling or overriding `_listen2`. [`writable.writableLength`]: stream.md#writablewritablelength [`zlib.bytesWritten`]: zlib.md#zlibbyteswritten [alloc]: buffer.md#static-method-bufferallocsize-fill-encoding -[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize +[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment [caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks [from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length [from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index e137722674b..d812bce8fd0 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes. [`--max-old-space-size`]: cli.md#--max-old-space-sizesize-in-mib [`--max-semi-space-size`]: cli.md#--max-semi-space-sizesize-in-mib [`AsyncResource`]: async_hooks.md#class-asyncresource -[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize +[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize-alignment [`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list [`ERR_WORKER_MESSAGING_ERRORED`]: errors.md#err_worker_messaging_errored [`ERR_WORKER_MESSAGING_FAILED`]: errors.md#err_worker_messaging_failed diff --git a/lib/buffer.js b/lib/buffer.js index cc213f12dac..19574064ad1 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -140,6 +140,7 @@ const { markAsUntransferable, addBufferPrototypeMethods, createUnsafeBuffer, + createUnsafeAlignedBuffer, asciiWrite, latin1Write, utf8Write, @@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, { }, }); +// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible +// I/O alignment requirement (logical block size, memory page size, huge page +// size) is well below this. +const kMaxAlignment = 2 ** 30; + +// Slices handed out of the pool are 8 byte aligned relative to the start of the +// pool, so aligning the pool itself to a cache line keeps them from straddling +// one more cache line than their size requires. +const kPoolAlignment = 64; + Buffer.poolSize = 64 * 1024; -let poolSize, poolOffset, allocPool, allocBuffer; +// `poolOffset` is relative to `poolBase`, which is where the pool starts inside +// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase` +// is not necessarily 0. +let poolSize, poolOffset, poolBase, allocPool, allocBuffer; function createPool() { poolSize = Buffer.poolSize; - allocBuffer = createUnsafeBuffer(poolSize); + allocBuffer = createUnsafeAlignedBuffer(poolSize, kPoolAlignment); allocPool = allocBuffer.buffer; + poolBase = TypedArrayPrototypeGetByteOffset(allocBuffer); markAsUntransferable(allocPool); poolOffset = 0; } @@ -444,25 +459,58 @@ Buffer.alloc = function alloc(size, fill, encoding) { /** * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer * instance. If `--zero-fill-buffers` is set, will zero-fill the buffer. + * + * If `alignment` is given, the memory backing the returned buffer starts at an + * address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`. + * @param {number} size + * @param {number} [alignment] A power of two, at most 2 ** 30 * @returns {FastBuffer} */ -Buffer.allocUnsafe = function allocUnsafe(size) { +Buffer.allocUnsafe = function allocUnsafe(size, alignment) { validateNumber(size, 'size', 0, kMaxLength); - return allocate(size); + if (alignment === undefined) { + return allocate(size); + } + validateAlignment(size, alignment); + return allocateAligned(size, alignment); }; /** * By default creates a non-zero-filled Buffer instance that is not allocated * off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill * the buffer. + * + * If `alignment` is given, the memory backing the returned buffer starts at an + * address that is a multiple of `alignment`, which is required by e.g. reads + * and writes on file descriptors opened with `O_DIRECT`. Note that up to + * `alignment - 1` extra bytes are allocated to satisfy the request, and that + * the returned buffer's `byteOffset` is therefore usually non-zero. * @param {number} size - * @returns {FastBuffer|undefined} + * @param {number} [alignment] A power of two, at most 2 ** 30 + * @returns {FastBuffer} */ -Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) { +Buffer.allocUnsafeSlow = function allocUnsafeSlow(size, alignment) { validateNumber(size, 'size', 0, kMaxLength); - return createUnsafeBuffer(size); + if (alignment === undefined) { + return createUnsafeBuffer(size); + } + validateAlignment(size, alignment); + return createUnsafeAlignedBuffer(size, alignment); }; +function validateAlignment(size, alignment) { + validateInteger(alignment, 'alignment', 1, kMaxAlignment); + if ((alignment & (alignment - 1)) !== 0) { + throw new ERR_INVALID_ARG_VALUE( + 'alignment', alignment, 'must be a power of two'); + } + // Satisfying the alignment costs up to `alignment - 1` extra bytes. + if (size > kMaxLength - (alignment - 1)) { + throw new ERR_OUT_OF_RANGE( + 'size', `<= ${kMaxLength - (alignment - 1)}`, size); + } +} + function allocate(size) { if (size <= 0) { return new FastBuffer(); @@ -470,7 +518,7 @@ function allocate(size) { if (size < (Buffer.poolSize >>> 1)) { if (size > (poolSize - poolOffset)) createPool(); - const b = new FastBuffer(allocPool, poolOffset, size); + const b = new FastBuffer(allocPool, poolBase + poolOffset, size); poolOffset += size; alignPool(); return b; @@ -478,6 +526,25 @@ function allocate(size) { return createUnsafeBuffer(size); } +function allocateAligned(size, alignment) { + if (size <= 0) { + return new FastBuffer(); + } + // The pool starts at a `kPoolAlignment` aligned address, so any alignment up + // to that can be satisfied by padding the offset into the pool. Stricter + // alignments need an allocation of their own. + if (alignment > kPoolAlignment || size >= (Buffer.poolSize >>> 1)) { + return createUnsafeAlignedBuffer(size, alignment); + } + poolOffset = (poolOffset + alignment - 1) & ~(alignment - 1); + if (size > (poolSize - poolOffset)) + createPool(); + const b = new FastBuffer(allocPool, poolBase + poolOffset, size); + poolOffset += size; + alignPool(); + return b; +} + function fromStringFast(string, ops) { const maxLength = Buffer.poolSize >>> 1; @@ -498,7 +565,7 @@ function fromStringFast(string, ops) { createPool(); const actual = ops.write(allocBuffer, string, poolOffset, length); - const b = new FastBuffer(allocPool, poolOffset, actual); + const b = new FastBuffer(allocPool, poolBase + poolOffset, actual); poolOffset += actual; alignPool(); @@ -560,7 +627,7 @@ function fromArrayLike(obj) { if (length < (Buffer.poolSize >>> 1)) { if (length > (poolSize - poolOffset)) createPool(); - const b = new FastBuffer(allocPool, poolOffset, length); + const b = new FastBuffer(allocPool, poolBase + poolOffset, length); TypedArrayPrototypeSet(b, obj, 0); poolOffset += length; alignPool(); diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js index d23f5d0ab6a..5029f60e7ba 100644 --- a/lib/internal/buffer.js +++ b/lib/internal/buffer.js @@ -33,6 +33,7 @@ const { hexWrite, ucs2Write, utf8WriteStatic, + arrayBufferAlignedOffset, createUnsafeArrayBuffer, setDetachKey, } = internalBinding('buffer'); @@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) { return new FastBuffer(createUnsafeArrayBuffer(size)); } +// Returns an uninitialized buffer of `size` bytes whose first byte is located at +// a memory address that is a multiple of `alignment`. `alignment` must be a +// 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. +function createUnsafeAlignedBuffer(size, alignment) { + if (size === 0) { + return new FastBuffer(); + } + + const ab = createUnsafeArrayBuffer(size + alignment - 1); + return new FastBuffer(ab, arrayBufferAlignedOffset(ab, alignment), size); +} + module.exports = { FastBuffer, addBufferPrototypeMethods, markAsUntransferable, isMarkedAsUntransferable, createUnsafeBuffer, + createUnsafeAlignedBuffer, readUInt16BE, readUInt32BE, asciiWrite, diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 19c28609660..4389ca8bf87 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1653,6 +1653,32 @@ inline size_t CheckNumberToSize(Local number) { return size; } +// Allocates an ArrayBuffer of `size` bytes. Its contents are left +// uninitialized, unless zero-filling is required. +MaybeLocal AllocateUnsafeArrayBuffer(Environment* env, + size_t size) { + Isolate* isolate = env->isolate(); + + // 0-length, or zero-fill flag is set, or building snapshot + if (size == 0 || per_process::cli_options->zero_fill_all_buffers || + env->isolate_data()->is_building_snapshot()) { + return ArrayBuffer::New(isolate, size); + } + + std::unique_ptr store = ArrayBuffer::NewBackingStore( + isolate, + size, + BackingStoreInitializationMode::kUninitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + + if (!store) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(env); + return MaybeLocal(); + } + + return ArrayBuffer::New(isolate, std::move(store)); +} + void CreateUnsafeArrayBuffer(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); if (args.Length() != 1) { @@ -1662,30 +1688,49 @@ void CreateUnsafeArrayBuffer(const FunctionCallbackInfo& args) { size_t size = CheckNumberToSize(args[0]); - Isolate* isolate = env->isolate(); - Local buf; + if (AllocateUnsafeArrayBuffer(env, size).ToLocal(&buf)) { + args.GetReturnValue().Set(buf); + } +} - // 0-length, or zero-fill flag is set, or building snapshot - if (size == 0 || per_process::cli_options->zero_fill_all_buffers || - env->isolate_data()->is_building_snapshot()) { - buf = ArrayBuffer::New(isolate, size); - } else { - std::unique_ptr store = ArrayBuffer::NewBackingStore( - isolate, - size, - BackingStoreInitializationMode::kUninitialized, - v8::BackingStoreOnFailureMode::kReturnNull); +// arrayBufferAlignedOffset(arrayBuffer, alignment) +// +// Returns the offset of the first byte of `arrayBuffer` that is located at a +// memory address which is a multiple of `alignment`. V8 does not let us choose +// the address of a backing store, so an aligned view is obtained by +// over-allocating `alignment - 1` bytes and skipping to that offset. The +// backing store of a non-resizable ArrayBuffer never moves, so the offset stays +// aligned for the lifetime of the ArrayBuffer. +void ArrayBufferAlignedOffset(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 2); + CHECK(args[0]->IsArrayBuffer()); + Local ab = args[0].As(); - if (!store) [[unlikely]] { - THROW_ERR_MEMORY_ALLOCATION_FAILED(env); - return; - } + size_t alignment = CheckNumberToSize(args[1]); - buf = ArrayBuffer::New(isolate, std::move(store)); + // Validated in JS land. + CHECK_GT(alignment, 0); + CHECK_EQ(alignment & (alignment - 1), 0); + + // A backing store does not keep its address across snapshot serialization, so + // an offset computed here would not be aligned after deserialization anyway + // -- and worse, baking one in would make the snapshot depend on where this + // process happened to allocate, i.e. no longer reproducible. Report no + // padding instead. The buffer pool recreates itself in a deserialize + // callback, so it is properly aligned once the deserialized process runs. + if (env->isolate_data()->is_building_snapshot()) { + args.GetReturnValue().Set(0.0); + return; } - args.GetReturnValue().Set(buf); + uintptr_t start = reinterpret_cast(ab->Data()); + size_t offset = (alignment - (start & (alignment - 1))) & (alignment - 1); + CHECK_EQ((start + offset) & (alignment - 1), 0); + CHECK_LE(offset, ab->ByteLength()); + + args.GetReturnValue().Set(static_cast(offset)); } template @@ -1817,6 +1862,8 @@ void Initialize(Local target, SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer); SetMethodNoSideEffect( context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer); + SetMethodNoSideEffect( + context, target, "arrayBufferAlignedOffset", ArrayBufferAlignedOffset); SetFastMethod(context, target, "swap16", Swap16, &fast_swap16); SetFastMethod(context, target, "swap32", Swap32, &fast_swap32); @@ -1925,6 +1972,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(CopyArrayBuffer); registry->Register(CreateUnsafeArrayBuffer); + registry->Register(ArrayBufferAlignedOffset); registry->Register(Atob); registry->Register(Btoa); diff --git a/test/parallel/test-buffer-alloc-alignment.js b/test/parallel/test-buffer-alloc-alignment.js new file mode 100644 index 00000000000..a1a3890ddc2 --- /dev/null +++ b/test/parallel/test-buffer-alloc-alignment.js @@ -0,0 +1,158 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { Buffer, constants } = require('buffer'); +const { internalBinding } = require('internal/test/binding'); +const { arrayBufferAlignedOffset } = internalBinding('buffer'); + +// Buffer.allocUnsafe(size, alignment) and Buffer.allocUnsafeSlow(size, +// alignment) return a buffer whose memory starts at an address that is a +// multiple of `alignment`. + +// Addresses are not observable from JS, so alignment cannot be checked against +// `byteOffset` alone: the padding an allocation needs depends on where its +// backing store happens to land. `arrayBufferAlignedOffset(ab, alignment)` +// returns an offset into `ab` that is known to be aligned, so any other offset +// is aligned exactly when it is congruent to that one. +function assertAligned(buf, alignment) { + const aligned = arrayBufferAlignedOffset(buf.buffer, alignment); + // `aligned` is in [0, alignment), so adding `alignment` keeps this positive. + const skew = (buf.byteOffset - aligned + alignment) % alignment; + assert.strictEqual(skew, 0, + `byteOffset ${buf.byteOffset} is not ${alignment} byte ` + + `aligned (aligned offsets are ${aligned} mod ${alignment})`); +} + +const alignments = [1, 2, 4, 8, 16, 64, 512, 4096, 65536]; +const sizes = [0, 1, 7, 64, 65, 512, 4096, 100000]; + +for (const alloc of [Buffer.allocUnsafe, Buffer.allocUnsafeSlow]) { + for (const alignment of alignments) { + for (const size of sizes) { + const buf = alloc(size, alignment); + assert.strictEqual(buf.length, size); + if (size > 0) { + assertAligned(buf, alignment); + } + // The view must fit inside the (over-allocated) ArrayBuffer. + assert.ok(buf.byteOffset + size <= buf.buffer.byteLength); + // The whole buffer must be writable through the aligned view. + buf.fill(0x61); + if (size > 0) { + assert.strictEqual(buf[0], 0x61); + assert.strictEqual(buf[size - 1], 0x61); + } + } + } + + // A zero length buffer is returned for size 0, whatever the alignment. + assert.strictEqual(alloc(0, 4096).length, 0); + + // An aligned buffer is a normal Buffer. + { + const buf = alloc(32, 4096); + assert.ok(Buffer.isBuffer(buf)); + buf.write('hello'); + assert.strictEqual(buf.toString('latin1', 0, 5), 'hello'); + assert.strictEqual(buf.subarray(1, 3).length, 2); + } + + // Consecutive allocations do not overlap. + { + const a = alloc(64, 64).fill(0x01); + const b = alloc(64, 64).fill(0x02); + assert.strictEqual(a[0], 0x01); + assert.strictEqual(a[63], 0x01); + assert.strictEqual(b[0], 0x02); + assert.strictEqual(b[63], 0x02); + } + + // Invalid alignments. + for (const alignment of [0, -1, -4096, 3, 5, 100, 1000, 2 ** 30 + 1]) { + assert.throws(() => alloc(10, alignment), { + name: /^(RangeError|TypeError)$/, + }); + } + + for (const alignment of [1.5, NaN, Infinity]) { + assert.throws(() => alloc(10, alignment), { code: 'ERR_OUT_OF_RANGE' }); + } + + for (const alignment of [null, '64', 64n, {}, [], true]) { + assert.throws(() => alloc(10, alignment), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } + + // Alignments that are not a power of two report the reason. + assert.throws(() => alloc(10, 3), { + code: 'ERR_INVALID_ARG_VALUE', + message: /must be a power of two/, + }); + + // `size` plus the padding must still fit within the maximum buffer length. + assert.throws(() => alloc(constants.MAX_LENGTH, 4096), { + code: 'ERR_OUT_OF_RANGE', + }); + + // `size` itself is validated before `alignment`. + assert.throws(() => alloc(-1, 64), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => alloc(constants.MAX_LENGTH + 1, 64), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +// Omitting the alignment keeps the previous behaviour. +{ + const buf = Buffer.allocUnsafeSlow(100); + assert.strictEqual(buf.length, 100); + assert.strictEqual(buf.byteOffset, 0); + assert.strictEqual(buf.buffer.byteLength, 100); +} + +// Buffer.allocUnsafeSlow() is never pooled, even when aligned. +{ + const a = Buffer.allocUnsafeSlow(64, 64); + const b = Buffer.allocUnsafeSlow(64, 64); + assert.notStrictEqual(a.buffer, b.buffer); +} + +// Buffer.allocUnsafe() serves alignments up to the pool alignment from the pool, +// and allocates on its own beyond that. +{ + const bufs = []; + for (let i = 0; i < 8; i++) { + bufs.push(Buffer.allocUnsafe(64, 64)); + } + // Pooled, so consecutive allocations share an ArrayBuffer. A pool may be + // exhausted in between, hence checking that any two neighbours share one. + assert.ok(bufs.some((buf, i) => i > 0 && buf.buffer === bufs[i - 1].buffer)); + for (const buf of bufs) { + assertAligned(buf, 64); + } + + // Stricter than the pool alignment, so this gets its own ArrayBuffer. + const own = Buffer.allocUnsafe(64, 128); + assert.ok(bufs.every((buf) => buf.buffer !== own.buffer)); +} + +// Aligned pooled allocations do not disturb unaligned ones. Interleave the two +// and make sure every buffer keeps its own contents. +{ + const bufs = []; + for (let i = 0; i < 256; i++) { + const buf = i % 2 === 0 ? + Buffer.allocUnsafe(24) : + Buffer.allocUnsafe(24, 16); + if (i % 2 === 1) { + assertAligned(buf, 16); + } + buf.fill(i % 256); + bufs.push(buf); + } + for (let i = 0; i < bufs.length; i++) { + assert.strictEqual(bufs[i][0], i % 256); + assert.strictEqual(bufs[i][23], i % 256); + } +}