Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/node.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,6 +49,9 @@

#if HAVE_OPENSSL
#include "ncrypto.h"
#if OPENSSL_VERSION_MAJOR >= 3
#include <openssl/provider.h>
#endif
#include "node_crypto.h"
#if OPENSSL_VERSION_MAJOR >= 3 && !defined(CONF_MFLAGS_IGNORE_MISSING_FILE)
// OpenSSL hides this deprecated macro under OPENSSL_NO_DEPRECATED, but the
Expand DownExpand Up@@ -1259,15 +1262,36 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
}
crypto::InstallFipsIndicatorCallback();

// Ensure CSPRNG is properly seeded.
CHECK(ncrypto::CSPRNG(nullptr, 0));
// Activating the default provider here keeps --openssl-legacy-provider
// working. Its explicit load disables OpenSSL's fallback, and the eager
// CSPRNG check used to activate the provider as a side effect. Only
// check the seeding when that provider is missing or FIPS is on, so a
// configuration without a DRBG still aborts at startup instead of
// hanging at the first crypto call. Otherwise the DRBG is instantiated
// on first use.
#if OPENSSL_VERSION_MAJOR >= 3
const bool check_csprng = ncrypto::isFipsEnabled() ||
!OSSL_PROVIDER_available(nullptr, "default");
#else
const bool check_csprng = true;
#endif
if (check_csprng) {
CHECK(ncrypto::CSPRNG(nullptr, 0));
}

// V8 uses the entropy for hash seeds, ASLR and Math.random(), none of
// it cryptographic. Going through OpenSSL would instantiate the DRBG
// and build the default provider's algorithm tables on every startup.
// V8 falls back to very weak entropy when the source fails, so abort
// instead.
V8::SetEntropySource([](unsigned char* buffer, size_t length) {
// V8 falls back to very weak entropy when this function fails
// and /dev/urandom isn't available. That wouldn't be so bad if
// the entropy was only used for Math.random() but it's also used for
// hash table and address space layout randomization. Better to abort.
#ifdef _AIX
// uv_random() reads /dev/random on AIX, which blocks. OpenSSL seeds
// from /dev/urandom there.
CHECK(ncrypto::CSPRNG(buffer, length));
#else
CHECK_EQ(uv_random(nullptr, nullptr, buffer, length, 0, nullptr), 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test this on AIX? It looks like uv_random() uses /dev/random there, which I think can block.

@colinhackscolinhacksSep 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested on AIX. uv_random() does read /dev/random there and that blocks when the pool is empty, so AIX now keeps OpenSSL's CSPRNG as V8's source. OpenSSL seeds from /dev/urandom first.

#endif
return true;
});
#endif // !defined(OPENSSL_IS_BORINGSSL)
Expand Down
3 changes: 3 additions & 0 deletions test/addons/openssl-providers/test-legacy-provider-option.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,3 +22,6 @@ if (getFips()) {
common.skip('this test cannot be run in FIPS mode');
}
providers.testProviderPresent('legacy');
// The explicit legacy load disables OpenSSL's provider fallback, so the
// default provider has to be active before it runs.
providers.testProviderPresent('default');
7 changes: 7 additions & 0 deletions test/fixtures/openssl3-conf/random_unavailable.cnf
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
nodejs_conf = nodejs_init

[nodejs_init]
random = random_sect

[random_sect]
random = NO-SUCH-DRBG
16 changes: 16 additions & 0 deletions test/parallel/test-crypto-no-algorithm.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,3 +57,19 @@ if (isMainThread) {
assert(common.nodeProcessAborted(cp.status, cp.signal),
`process did not abort, code:${cp.status} signal:${cp.signal}`);
}

{
// A configuration whose random section names a DRBG that cannot be
// fetched starts normally; the first crypto call fails, without a hang.
const fixtures = require('../common/fixtures');
const { spawnSync } = require('node:child_process');
const randomConf = fixtures.path('openssl3-conf', 'random_unavailable.cnf');
const cp = spawnSync(process.execPath,
[ `--openssl-config=${randomConf}`, '-e',
'require("node:crypto").randomBytes(8)' ],
{ encoding: 'utf8' });
assert(!common.nodeProcessAborted(cp.status, cp.signal),
`process aborted, code:${cp.status} signal:${cp.signal}`);
assert.strictEqual(cp.status, 1);
assert.match(cp.stderr, /unable to fetch drbg/);
}
22 changes: 22 additions & 0 deletions test/parallel/test-crypto-secure-heap.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,17 @@ if (process.argv[2] === 'child') {
return;
}

if (process.argv[2] === 'workers') {
// A Worker's crypto calls may run out of secure heap; that surfaces as an
// error in the Worker, never as an abort of the process.
const { Worker } = require('worker_threads');
for (let i = 0; i < 8; i++) {
new Worker('try { require("crypto").randomBytes(4); } catch {}',
{ eval: true });
}
return;
}

const child = fork(
process.argv[1],
['child'],
Expand All@@ -70,6 +81,17 @@ child.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
}));

{
const child = fork(
process.argv[1],
['workers'],
{ execArgv: ['--secure-heap=1024', '--secure-heap-min=4'] });
child.on('exit', common.mustCall((code, signal) => {
assert.strictEqual(signal, null);
assert.strictEqual(code, 0);
}));
}

{
const child = fork(fixtures.path('a.js'), {
execArgv: ['--secure-heap=3', '--secure-heap-min=3'],
Expand Down
Loading