Commit 250037174ee for nodejs
commit 250037174eebbd78461154858b3dd46494c491aa
Author: Filip Skokan <panva.ip@gmail.com>
Date: Fri Oct 30 08:25:47 2026 +0100
crypto: reject immutable random fill buffers
Prevent randomFillSync(), randomFill(), and getRandomValues() from
modifying immutable ArrayBuffers or their views.
Refs: https://github.com/nodejs/node/issues/66346
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66944
Reviewed-By: Richard Lau <richard.lau@ibm.com>
diff --git a/src/crypto/crypto_random.cc b/src/crypto/crypto_random.cc
index c80eb2c800a..79c52a7d08c 100644
--- a/src/crypto/crypto_random.cc
+++ b/src/crypto/crypto_random.cc
@@ -15,6 +15,7 @@ using ncrypto::BignumPointer;
using ncrypto::ClearErrorOnReturn;
using ncrypto::DataPointer;
using v8::ArrayBuffer;
+using v8::ArrayBufferView;
using v8::Boolean;
using v8::FunctionCallbackInfo;
using v8::Int32;
@@ -30,6 +31,13 @@ using v8::Value;
namespace crypto {
namespace {
+bool IsImmutableBufferSource(Local<Value> buffer) {
+ if (buffer->IsArrayBufferView()) {
+ return buffer.As<ArrayBufferView>()->Buffer()->IsImmutable();
+ }
+ return buffer->IsArrayBuffer() && buffer.As<ArrayBuffer>()->IsImmutable();
+}
+
BignumPointer::PrimeCheckCallback getPrimeCheckCallback(Environment* env) {
// The callback is used to check if the operation should be stopped.
// Currently, the only check we perform is if env->is_stopping()
@@ -52,6 +60,13 @@ Maybe<void> RandomBytesTraits::AdditionalConfig(
CHECK(args[offset + 1]->IsUint32()); // Offset
CHECK(args[offset + 2]->IsUint32()); // Size
+ if (IsImmutableBufferSource(args[offset])) {
+ THROW_ERR_INVALID_ARG_VALUE(
+ Environment::GetCurrent(args),
+ "The 'buf' argument must not be backed by an immutable ArrayBuffer");
+ return Nothing<void>();
+ }
+
ArrayBufferOrViewContents<unsigned char> in(args[offset]);
const uint32_t byte_offset = args[offset + 1].As<Uint32>()->Value();
@@ -220,6 +235,12 @@ static void RandomFillSync(const FunctionCallbackInfo<Value>& args) {
CHECK(args[1]->IsUint32()); // Offset
CHECK(args[2]->IsUint32()); // Size
+ if (IsImmutableBufferSource(args[0])) {
+ return THROW_ERR_INVALID_ARG_VALUE(
+ env,
+ "The 'buf' argument must not be backed by an immutable ArrayBuffer");
+ }
+
ArrayBufferOrViewContents<unsigned char> in(args[0]);
const uint32_t byte_offset = args[1].As<Uint32>()->Value();
diff --git a/test/parallel/test-crypto-randomfill-immutable.js b/test/parallel/test-crypto-randomfill-immutable.js
new file mode 100644
index 00000000000..a935651978b
--- /dev/null
+++ b/test/parallel/test-crypto-randomfill-immutable.js
@@ -0,0 +1,107 @@
+// Flags: --js-immutable-arraybuffer
+'use strict';
+const common = require('../common');
+
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+if (typeof ArrayBuffer.prototype.transferToImmutable !== 'function')
+ common.skip('ArrayBuffer.prototype.transferToImmutable is not available');
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+const bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
+const expectedError = {
+ code: 'ERR_INVALID_ARG_VALUE',
+ name: 'TypeError',
+ message: "The 'buf' argument must not be backed by an immutable ArrayBuffer",
+};
+
+function immutable() {
+ return Uint8Array.from(bytes).buffer.transferToImmutable();
+}
+
+const factories = [
+ (ab) => ab,
+ (ab) => Buffer.from(ab, 4, 8),
+ (ab) => new Uint8Array(ab, 4, 8),
+ (ab) => new DataView(ab, 4, 8),
+ (ab) => new Uint16Array(ab, 4, 4),
+];
+
+for (const create of factories) {
+ const ab = immutable();
+ const input = create(ab);
+ const length = input.length ?? input.byteLength;
+
+ // Reject full and partial fills, including each callback overload, before
+ // writing to any part of the backing store.
+ for (const args of [[], [1], [1, 1]]) {
+ assert.throws(() => crypto.randomFillSync(input, ...args), expectedError);
+ assert.throws(() => crypto.randomFill(input, ...args, common.mustNotCall()),
+ expectedError);
+ }
+
+ // Zero-byte fills remain no-ops, including an omitted size at the end.
+ for (const args of [[0, 0], [length]]) {
+ assert.strictEqual(crypto.randomFillSync(input, ...args), input);
+ crypto.randomFill(input, ...args, common.mustSucceed((result) => {
+ assert.strictEqual(result, input);
+ }));
+ }
+ assert.strictEqual(ab.immutable, true);
+ assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
+}
+
+// Shared backing stores, including those accessed through views, remain writable.
+for (const create of factories) {
+ const input = create(new SharedArrayBuffer(16));
+ assert.strictEqual(crypto.randomFillSync(input), input);
+ crypto.randomFill(input, common.mustSucceed((result) => {
+ assert.strictEqual(result, input);
+ }));
+}
+for (const api of [crypto, globalThis.crypto]) {
+ const ab = immutable();
+ for (const input of [
+ Buffer.from(ab, 4, 8),
+ new Uint8Array(ab, 4, 8),
+ new Uint16Array(ab, 4, 4),
+ ]) {
+ assert.throws(() => api.getRandomValues(input), expectedError);
+ }
+ assert.strictEqual(ab.immutable, true);
+ assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
+}
+
+// Empty immutable buffers and views do not require a writable backing store.
+{
+ const ab = new ArrayBuffer(0).transferToImmutable();
+ for (const input of [ab, Buffer.from(ab), new Uint8Array(ab),
+ new DataView(ab), new Uint16Array(ab)]) {
+ assert.strictEqual(crypto.randomFillSync(input), input);
+ crypto.randomFill(input, common.mustSucceed((result) => {
+ assert.strictEqual(result, input);
+ }));
+ }
+ for (const api of [crypto, globalThis.crypto]) {
+ for (const input of [Buffer.from(ab), new Uint8Array(ab)]) {
+ assert.strictEqual(api.getRandomValues(input), input);
+ }
+ }
+}
+
+// Inspect the real backing store, regardless of shadowed public properties.
+{
+ const ab = immutable();
+ const input = new Uint8Array(ab, 4, 8);
+ Object.defineProperty(ab, 'immutable', { value: false });
+ Object.defineProperties(input, {
+ buffer: { value: new ArrayBuffer(ab.byteLength) },
+ immutable: { value: false },
+ });
+ assert.throws(() => crypto.randomFillSync(input), expectedError);
+ assert.throws(() => crypto.randomFill(input, common.mustNotCall()),
+ expectedError);
+ assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
+}