crypto: use internal random buffer bounds · nodejs/node@ab9a3f2 · GitHub
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
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

0 commit comments

Comments
 (0)