Skip to content

Commit ab9a3f2

Browse files
panvaaduh95
authored andcommitted
crypto: use internal random buffer bounds
Read buffer lengths and element sizes from internal slots in randomFillSync() and randomFill(). Shadowed properties can otherwise skip filling, change the selected range, or fail a native bounds check. Use the intrinsic byte length for the getRandomValues() quota check and keep its delegation to randomFillSync(). Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: #66237 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
1 parent db2665b commit ab9a3f2

3 files changed

Lines changed: 201 additions & 11 deletions

File tree

‎lib/internal/crypto/random.js‎

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
BigInt,
1111
BigIntPrototypeToString,
1212
DataView,
13+
DataViewPrototypeGetByteLength,
1314
DataViewPrototypeGetUint8,
1415
DateNow,
1516
FunctionPrototypeBind,
@@ -21,6 +22,10 @@ const {
2122
StringFromCharCodeApply,
2223
StringPrototypePadStart,
2324
TypedArrayPrototypeGetBuffer,
25+
TypedArrayPrototypeGetByteLength,
26+
TypedArrayPrototypeGetLength,
27+
TypedArrayPrototypeGetSymbolToStringTag,
28+
Uint8Array,
2429
} = primordials;
2530

2631
const {
@@ -58,6 +63,8 @@ const {
5863
const {
5964
isArrayBufferView,
6065
isAnyArrayBuffer,
66+
isDataView,
67+
isSharedArrayBuffer,
6168
isTypedArray,
6269
isFloat16Array,
6370
isFloat32Array,
@@ -69,6 +76,35 @@ const { FastBuffer } = require('internal/buffer');
6976
const kMaxInt32 = 2 ** 31 - 1;
7077
const kMaxPossibleLength = MathMin(kMaxLength, kMaxInt32);
7178

79+
function getByteLength(buf) {
80+
if (isArrayBufferView(buf)) {
81+
return isDataView(buf) ?
82+
DataViewPrototypeGetByteLength(buf) : TypedArrayPrototypeGetByteLength(buf);
83+
}
84+
if (isSharedArrayBuffer(buf))
85+
return TypedArrayPrototypeGetByteLength(new Uint8Array(buf));
86+
return ArrayBufferPrototypeGetByteLength(buf);
87+
}
88+
89+
function getElementSize(buf) {
90+
switch (TypedArrayPrototypeGetSymbolToStringTag(buf)) {
91+
case 'Int16Array':
92+
case 'Uint16Array':
93+
case 'Float16Array':
94+
return 2;
95+
case 'Int32Array':
96+
case 'Uint32Array':
97+
case 'Float32Array':
98+
return 4;
99+
case 'Float64Array':
100+
case 'BigInt64Array':
101+
case 'BigUint64Array':
102+
return 8;
103+
default:
104+
return 1;
105+
}
106+
}
107+
72108
function assertOffset(offset, elementSize, length) {
73109
validateNumber(offset, 'offset');
74110
offset *= elementSize;
@@ -125,14 +161,15 @@ function randomFillSync(buf, offset = 0, size) {
125161
buf);
126162
}
127163

128-
const elementSize = buf.BYTES_PER_ELEMENT || 1;
164+
const elementSize = getElementSize(buf);
165+
const byteLength = getByteLength(buf);
129166

130-
offset = assertOffset(offset, elementSize, buf.byteLength);
167+
offset = assertOffset(offset, elementSize, byteLength);
131168

132169
if (size === undefined) {
133-
size = buf.byteLength - offset;
170+
size = byteLength - offset;
134171
} else {
135-
size = assertSize(size, elementSize, offset, buf.byteLength);
172+
size = assertSize(size, elementSize, offset, byteLength);
136173
}
137174

138175
if (size === 0)
@@ -159,26 +196,27 @@ function randomFill(buf, offset, size, callback) {
159196
buf);
160197
}
161198

162-
const elementSize = buf.BYTES_PER_ELEMENT || 1;
199+
const elementSize = getElementSize(buf);
163200

164201
if (typeof offset === 'function') {
165202
callback = offset;
166203
offset = 0;
167204
// Size is a length here, assertSize() call turns it into a number of bytes
168-
size = buf.length;
205+
size = isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : undefined;
169206
} else if (typeof size === 'function') {
170207
callback = size;
171-
size = (buf.length ?? buf.byteLength) - offset;
208+
size = (isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : getByteLength(buf)) - offset;
172209
} else {
173210
validateFunction(callback, 'callback');
174211
}
175212

176-
offset = assertOffset(offset, elementSize, buf.byteLength);
213+
const byteLength = getByteLength(buf);
214+
offset = assertOffset(offset, elementSize, byteLength);
177215

178216
if (size === undefined) {
179-
size = buf.byteLength - offset;
217+
size = byteLength - offset;
180218
} else {
181-
size = assertSize(size, elementSize, offset, buf.byteLength);
219+
size = assertSize(size, elementSize, offset, byteLength);
182220
}
183221

184222
if (size === 0) {
@@ -328,7 +366,8 @@ function getRandomValues(data) {
328366
'The data argument must be an integer-type TypedArray',
329367
'TypeMismatchError');
330368
}
331-
if (data.byteLength > 65536) {
369+
const byteLength = TypedArrayPrototypeGetByteLength(data);
370+
if (byteLength > 65536) {
332371
const { QuotaExceededError } = internalBinding('messaging');
333372
throw new QuotaExceededError(
334373
'The requested length exceeds 65,536 bytes');
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const { randomFill, randomFillSync } = require('crypto');
9+
10+
const typedArrays = [
11+
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
12+
Int32Array, Uint32Array, Float32Array, Float64Array,
13+
BigInt64Array, BigUint64Array,
14+
];
15+
if (globalThis.Float16Array !== undefined)
16+
typedArrays.push(globalThis.Float16Array);
17+
18+
const factories = [];
19+
for (const Backing of [ArrayBuffer, SharedArrayBuffer]) {
20+
for (const Type of [...typedArrays, Buffer, DataView]) {
21+
const elementSize = Type.BYTES_PER_ELEMENT || 1;
22+
factories.push({
23+
name: `${Type.name} on ${Backing.name}`,
24+
elementSize,
25+
make(length = 64) {
26+
const backing = new Backing(length + 64);
27+
const input = Type === Buffer ? Buffer.from(backing, 32, length) :
28+
new Type(backing, 32, length / elementSize);
29+
return { input, backing, bytes: new Uint8Array(backing), start: 32, length };
30+
},
31+
});
32+
}
33+
factories.push({
34+
name: Backing.name,
35+
elementSize: 1,
36+
make(length = 64) {
37+
const input = new Backing(length);
38+
return { input, backing: input, bytes: new Uint8Array(input), start: 0, length };
39+
},
40+
});
41+
}
42+
43+
function shadowMetadata(target, mode) {
44+
const values = {
45+
byteLength: mode === 'zero' ? 0 : 4096,
46+
length: mode === 'zero' ? 0 : 4096,
47+
BYTES_PER_ELEMENT: mode === 'zero' ? 0 : 4096,
48+
buffer: new ArrayBuffer(0),
49+
byteOffset: mode === 'zero' ? 0 : 4096,
50+
[Symbol.toStringTag]: 'NotAnArrayBufferView',
51+
};
52+
for (const key of Reflect.ownKeys(values)) {
53+
Object.defineProperty(target, key, mode === 'getters' ?
54+
{ get: common.mustNotCall(`Unexpected ${String(key)} getter`) } :
55+
{ value: values[key] });
56+
}
57+
}
58+
59+
function checkFilled({ input, bytes, start }, result, offset, size, label) {
60+
assert.strictEqual(result, input, label);
61+
assert(bytes.subarray(0, start + offset).every((byte) => byte === 0), label);
62+
assert(bytes.subarray(start + offset + size).every((byte) => byte === 0), label);
63+
// Every nonempty test range contains at least 32 random bytes. Detect a
64+
// no-op without assuming that any particular generated byte is nonzero.
65+
if (size !== 0)
66+
assert(bytes.subarray(start + offset, start + offset + size).some((byte) => byte !== 0), label);
67+
}
68+
69+
for (const factory of factories) {
70+
const { elementSize } = factory;
71+
const ranges = [
72+
{ args: [], offset: 0, size: 64 },
73+
{ args: [16 / elementSize], offset: 16, size: 48 },
74+
{ args: [16 / elementSize, 32 / elementSize], offset: 16, size: 32 },
75+
{ args: [16 / elementSize, undefined], offset: 16, size: 48 },
76+
{ args: [64 / elementSize], offset: 64, size: 0 },
77+
];
78+
for (const mode of ['getters', 'zero', 'inflated']) {
79+
for (const { args, offset, size } of ranges) {
80+
for (const async of [false, true]) {
81+
const value = factory.make();
82+
shadowMetadata(value.input, mode);
83+
if (value.backing !== value.input)
84+
shadowMetadata(value.backing, mode);
85+
const label = `${factory.name}, ${mode}, ${async ? 'async' : 'sync'}, ${args}`;
86+
if (async) {
87+
assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => {
88+
checkFilled(value, result, offset, size, label);
89+
})), undefined);
90+
} else {
91+
checkFilled(value, randomFillSync(value.input, ...args), offset, size, label);
92+
}
93+
}
94+
}
95+
}
96+
97+
// Inflated public bounds must not let an invalid range reach the native job.
98+
// Keep the real element width here so each range exceeds the actual buffer.
99+
const units = 64 / elementSize;
100+
for (const args of [[units + 1], [0, units + 1], [units, 1]]) {
101+
const { input } = factory.make();
102+
Object.defineProperties(input, {
103+
byteLength: { value: 4096 },
104+
length: { value: 4096 },
105+
});
106+
assert.throws(() => randomFillSync(input, ...args), { code: 'ERR_OUT_OF_RANGE' });
107+
assert.throws(() => randomFill(input, ...args, common.mustNotCall()), {
108+
code: 'ERR_OUT_OF_RANGE',
109+
});
110+
}
111+
112+
for (const args of [[], [0], [0, 0], [0, undefined]]) {
113+
const value = factory.make(0);
114+
shadowMetadata(value.input, 'inflated');
115+
checkFilled(value, randomFillSync(value.input, ...args), 0, 0, factory.name);
116+
assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => {
117+
checkFilled(value, result, 0, 0, factory.name);
118+
})), undefined);
119+
}
120+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const webcrypto = globalThis.crypto;
9+
10+
for (const Type of [Uint8Array, Uint16Array, Uint32Array, BigUint64Array]) {
11+
const storage = new Uint8Array(128);
12+
const view = new Type(storage.buffer, 32, 32 / Type.BYTES_PER_ELEMENT);
13+
for (const name of ['buffer', 'byteOffset', 'byteLength', 'BYTES_PER_ELEMENT']) {
14+
Object.defineProperty(view, name, { get: common.mustNotCall() });
15+
Object.defineProperty(storage.buffer, name, { get: common.mustNotCall() });
16+
}
17+
assert.strictEqual(webcrypto.getRandomValues(view), view);
18+
assert(storage.subarray(32, 64).some((byte) => byte !== 0));
19+
assert(storage.subarray(0, 32).every((byte) => byte === 0));
20+
assert(storage.subarray(64).every((byte) => byte === 0));
21+
}
22+
23+
const oversized = new Uint8Array(65537);
24+
Object.defineProperty(oversized, 'byteLength', { value: 0 });
25+
assert.throws(() => webcrypto.getRandomValues(oversized), {
26+
name: 'QuotaExceededError',
27+
});
28+
29+
const empty = new Uint8Array(0);
30+
Object.defineProperty(empty, 'byteLength', { value: 65537 });
31+
assert.strictEqual(webcrypto.getRandomValues(empty), empty);

0 commit comments

Comments
 (0)