Skip to content

Commit 33239ef

Browse files
committed
crypto: reject immutable random fill buffers
Prevent randomFillSync(), randomFill(), and getRandomValues() from modifying immutable ArrayBuffers or their views. Refs: #66346 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex
1 parent 727e0ca commit 33239ef

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

‎src/crypto/crypto_random.cc‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using ncrypto::BignumPointer;
1515
using ncrypto::ClearErrorOnReturn;
1616
using ncrypto::DataPointer;
1717
using v8::ArrayBuffer;
18+
using v8::ArrayBufferView;
1819
using v8::Boolean;
1920
using v8::FunctionCallbackInfo;
2021
using v8::Int32;
@@ -30,6 +31,13 @@ using v8::Value;
3031

3132
namespace crypto {
3233
namespace {
34+
bool IsImmutableBufferSource(Local<Value> buffer) {
35+
if (buffer->IsArrayBufferView()) {
36+
return buffer.As<ArrayBufferView>()->Buffer()->IsImmutable();
37+
}
38+
return buffer->IsArrayBuffer() && buffer.As<ArrayBuffer>()->IsImmutable();
39+
}
40+
3341
BignumPointer::PrimeCheckCallback getPrimeCheckCallback(Environment* env) {
3442
// The callback is used to check if the operation should be stopped.
3543
// Currently, the only check we perform is if env->is_stopping()
@@ -52,6 +60,13 @@ Maybe<void> RandomBytesTraits::AdditionalConfig(
5260
CHECK(args[offset + 1]->IsUint32()); // Offset
5361
CHECK(args[offset + 2]->IsUint32()); // Size
5462

63+
if (IsImmutableBufferSource(args[offset])) {
64+
THROW_ERR_INVALID_ARG_VALUE(
65+
Environment::GetCurrent(args),
66+
"The 'buf' argument must not be backed by an immutable ArrayBuffer");
67+
return Nothing<void>();
68+
}
69+
5570
ArrayBufferOrViewContents<unsigned char> in(args[offset]);
5671

5772
const uint32_t byte_offset = args[offset + 1].As<Uint32>()->Value();
@@ -220,6 +235,12 @@ static void RandomFillSync(const FunctionCallbackInfo<Value>& args) {
220235
CHECK(args[1]->IsUint32()); // Offset
221236
CHECK(args[2]->IsUint32()); // Size
222237

238+
if (IsImmutableBufferSource(args[0])) {
239+
return THROW_ERR_INVALID_ARG_VALUE(
240+
env,
241+
"The 'buf' argument must not be backed by an immutable ArrayBuffer");
242+
}
243+
223244
ArrayBufferOrViewContents<unsigned char> in(args[0]);
224245

225246
const uint32_t byte_offset = args[1].As<Uint32>()->Value();
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Flags: --js-immutable-arraybuffer
2+
'use strict';
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
if (typeof ArrayBuffer.prototype.transferToImmutable !== 'function')
8+
common.skip('ArrayBuffer.prototype.transferToImmutable is not available');
9+
10+
const assert = require('assert');
11+
const crypto = require('crypto');
12+
13+
const bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
14+
const expectedError = {
15+
code: 'ERR_INVALID_ARG_VALUE',
16+
name: 'TypeError',
17+
message: "The 'buf' argument must not be backed by an immutable ArrayBuffer",
18+
};
19+
20+
function immutable() {
21+
return Uint8Array.from(bytes).buffer.transferToImmutable();
22+
}
23+
24+
const factories = [
25+
(ab) => ab,
26+
(ab) => Buffer.from(ab, 4, 8),
27+
(ab) => new Uint8Array(ab, 4, 8),
28+
(ab) => new DataView(ab, 4, 8),
29+
(ab) => new Uint16Array(ab, 4, 4),
30+
];
31+
32+
for (const create of factories) {
33+
const ab = immutable();
34+
const input = create(ab);
35+
const length = input.length ?? input.byteLength;
36+
37+
// Reject full and partial fills, including each callback overload, before
38+
// writing to any part of the backing store.
39+
for (const args of [[], [1], [1, 1]]) {
40+
assert.throws(() => crypto.randomFillSync(input, ...args), expectedError);
41+
assert.throws(() => crypto.randomFill(input, ...args, common.mustNotCall()),
42+
expectedError);
43+
}
44+
45+
// Zero-byte fills remain no-ops, including an omitted size at the end.
46+
for (const args of [[0, 0], [length]]) {
47+
assert.strictEqual(crypto.randomFillSync(input, ...args), input);
48+
crypto.randomFill(input, ...args, common.mustSucceed((result) => {
49+
assert.strictEqual(result, input);
50+
}));
51+
}
52+
assert.strictEqual(ab.immutable, true);
53+
assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
54+
}
55+
56+
// Shared backing stores, including those accessed through views, remain writable.
57+
for (const create of factories) {
58+
const input = create(new SharedArrayBuffer(16));
59+
assert.strictEqual(crypto.randomFillSync(input), input);
60+
crypto.randomFill(input, common.mustSucceed((result) => {
61+
assert.strictEqual(result, input);
62+
}));
63+
}
64+
for (const api of [crypto, globalThis.crypto]) {
65+
const ab = immutable();
66+
for (const input of [
67+
Buffer.from(ab, 4, 8),
68+
new Uint8Array(ab, 4, 8),
69+
new Uint16Array(ab, 4, 4),
70+
]) {
71+
assert.throws(() => api.getRandomValues(input), expectedError);
72+
}
73+
assert.strictEqual(ab.immutable, true);
74+
assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
75+
}
76+
77+
// Empty immutable buffers and views do not require a writable backing store.
78+
{
79+
const ab = new ArrayBuffer(0).transferToImmutable();
80+
for (const input of [ab, Buffer.from(ab), new Uint8Array(ab),
81+
new DataView(ab), new Uint16Array(ab)]) {
82+
assert.strictEqual(crypto.randomFillSync(input), input);
83+
crypto.randomFill(input, common.mustSucceed((result) => {
84+
assert.strictEqual(result, input);
85+
}));
86+
}
87+
for (const api of [crypto, globalThis.crypto]) {
88+
for (const input of [Buffer.from(ab), new Uint8Array(ab)]) {
89+
assert.strictEqual(api.getRandomValues(input), input);
90+
}
91+
}
92+
}
93+
94+
// Inspect the real backing store, regardless of shadowed public properties.
95+
{
96+
const ab = immutable();
97+
const input = new Uint8Array(ab, 4, 8);
98+
Object.defineProperty(ab, 'immutable', { value: false });
99+
Object.defineProperties(input, {
100+
buffer: { value: new ArrayBuffer(ab.byteLength) },
101+
immutable: { value: false },
102+
});
103+
assert.throws(() => crypto.randomFillSync(input), expectedError);
104+
assert.throws(() => crypto.randomFill(input, common.mustNotCall()),
105+
expectedError);
106+
assert.deepStrictEqual([...new Uint8Array(ab)], bytes);
107+
}

0 commit comments

Comments
 (0)