crypto: reset random caches across snapshots · nodejs/node@dd6b475 · GitHub
Skip to content

Commit dd6b475

Browse files
panvaaduh95
authored andcommitted
crypto: reset random caches across snapshots
Startup snapshots retain unused randomInt() and UUID cache entries, causing deserialized processes to replay the same random values. Clear the caches after user serialization callbacks and before user deserialization callbacks. The latter also handles caches refilled by microtasks, next ticks, or exit handlers after serialization callbacks. Refs: https://hackerone.com/reports/4059814 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex Backport-PR-URL: nodejs-private/node-private#1005 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: nodejs-private/node-private#987 CVE-ID: CVE-2026-106074
1 parent d766356 commit dd6b475

4 files changed

Lines changed: 161 additions & 1 deletion

File tree

‎lib/internal/crypto/random.js‎

Lines changed: 30 additions & 0 deletions

‎lib/internal/v8/startup_snapshot.js‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,28 @@ function throwIfBuildingSnapshot(reason) {
3535
}
3636

3737
const deserializeCallbacks = [];
38+
const beforeUserDeserializeCallbacks = [];
3839
let deserializeCallbackIsSet = false;
3940
function runDeserializeCallbacks() {
41+
while (beforeUserDeserializeCallbacks.length > 0) {
42+
const { 0: callback, 1: data } = beforeUserDeserializeCallbacks.shift();
43+
callback(data);
44+
}
4045
while (deserializeCallbacks.length > 0) {
4146
const { 0: callback, 1: data } = deserializeCallbacks.shift();
4247
callback(data);
4348
}
4449
}
4550

4651
function addDeserializeCallback(callback, data) {
52+
addDeserializeCallbackImpl(deserializeCallbacks, callback, data);
53+
}
54+
55+
function addBeforeUserDeserializeCallback(callback, data) {
56+
addDeserializeCallbackImpl(beforeUserDeserializeCallbacks, callback, data);
57+
}
58+
59+
function addDeserializeCallbackImpl(callbacks, callback, data) {
4760
throwIfNotBuildingSnapshot();
4861
validateFunction(callback, 'callback');
4962
if (!deserializeCallbackIsSet) {
@@ -54,7 +67,7 @@ function addDeserializeCallback(callback, data) {
5467
setDeserializeCallback(runDeserializeCallbacks);
5568
deserializeCallbackIsSet = true;
5669
}
57-
deserializeCallbacks.push([callback, data]);
70+
callbacks.push([callback, data]);
5871
}
5972

6073
const serializeCallbacks = [];
@@ -127,4 +140,5 @@ module.exports = {
127140
isBuildingSnapshot,
128141
},
129142
addAfterUserSerializeCallback,
143+
addBeforeUserDeserializeCallback,
130144
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const assert = require('node:assert');
4+
const {
5+
addDeserializeCallback,
6+
addSerializeCallback,
7+
setDeserializeMainFunction,
8+
} = require('node:v8').startupSnapshot;
9+
10+
// Register before loading crypto to ensure its cleanup takes precedence even
11+
// over user callbacks that were registered earlier.
12+
let deserialized;
13+
addDeserializeCallback(() => {
14+
deserialized = sample();
15+
});
16+
17+
const {
18+
randomInt,
19+
randomUUID,
20+
} = require('node:crypto');
21+
22+
const buildValues = [];
23+
const max = 2 ** 48 - 1;
24+
25+
function primeCaches() {
26+
buildValues.push(
27+
randomUUID(),
28+
randomInt(max),
29+
randomUUID({ disableEntropyCache: true }),
30+
);
31+
}
32+
33+
switch (process.env.NODE_TEST_CRYPTO_RANDOM_MODE) {
34+
case 'uuid':
35+
primeCaches();
36+
break;
37+
case 'object-url':
38+
URL.revokeObjectURL(URL.createObjectURL(new Blob(['snapshot'])));
39+
break;
40+
case 'async':
41+
randomInt(max, (err, value) => {
42+
assert.ifError(err);
43+
buildValues.push(value);
44+
});
45+
break;
46+
case 'serialize':
47+
primeCaches();
48+
// A user callback can refill the caches after an earlier cleanup callback.
49+
addSerializeCallback(primeCaches);
50+
break;
51+
case 'microtask':
52+
primeCaches();
53+
addSerializeCallback(() => queueMicrotask(primeCaches));
54+
break;
55+
case 'next-tick':
56+
primeCaches();
57+
addSerializeCallback(() => process.nextTick(primeCaches));
58+
break;
59+
case 'exit':
60+
primeCaches();
61+
process.once('exit', primeCaches);
62+
break;
63+
case 'unprimed':
64+
break;
65+
default:
66+
assert.fail('Unknown cache priming mode');
67+
}
68+
69+
function sample() {
70+
return {
71+
uuid: Array.from({ length: 4 }, () => randomUUID()),
72+
webcrypto: Array.from({ length: 4 }, () => globalThis.crypto.randomUUID()),
73+
int: Array.from({ length: 4 }, () => randomInt(max)),
74+
};
75+
}
76+
77+
setDeserializeMainFunction(async () => {
78+
const main = sample();
79+
main.asyncInt = await Promise.all(Array.from({ length: 4 }, () => new Promise((resolve, reject) => {
80+
randomInt(max, (err, value) => {
81+
if (err) reject(err);
82+
else resolve(value);
83+
});
84+
})));
85+
console.log(JSON.stringify({ buildValues, deserialized, main }));
86+
});
Lines changed: 30 additions & 0 deletions

0 commit comments

Comments
 (0)