Skip to content

src: use V8 entropy source if RAND_bytes() != 1 - #44493

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
tniessen:src-use-v8-entropy-source-if-openssl-does-not-work
Sep 4, 2022
Merged

src: use V8 entropy source if RAND_bytes() != 1#44493
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
tniessen:src-use-v8-entropy-source-if-openssl-does-not-work

Conversation

@tniessen

@tniessen tniessen commented Sep 2, 2022

Copy link
Copy Markdown
Member

RAND_bytes() may return 0 to indicate an error, in which case the buffer might not have been filled with random data at all. Instead of ignoring this case, let V8 use its own entropy source. Historically, this used to be a weak source of entropy, but V8 now implements a proper source even on Windows.

And even if V8's own entropy source turns out to be weak, it does not matter much: V8's PRNG itself is not cryptographically secure, so even if it is seeded from a cryptographically secure entropy source, it does not produce cryptographically secure random numbers.

RAND_bytes() may return 0 to indicate an error, in which case the buffer
might not have been filled with random data at all. Instead of ignoring
this case, let V8 use its own entropy source. Historically, this used to
be a weak source of entropy, but V8 now implements a proper source even
on Windows. And even if V8's own entropy source turns out to be weak, it
does not matter much: V8's PRNG itself is not cryptographically secure,
so even if it is seeded from a cryptographically secure entropy source,
it does not produce cryptographically secure random numbers.
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/startup

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Sep 2, 2022
@tniessen tniessen added openssl Issues and PRs related to the OpenSSL dependency. v8 engine Issues and PRs related to the V8 dependency. and removed lib / src Issues and PRs involving general changes in the lib/ or src/ directories. labels Sep 2, 2022
@tniessen tniessen added author ready PRs with CI started, the required approvals, and no outstanding review comments. request-ci Add this label to start a Jenkins CI on a PR. labels Sep 2, 2022
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Sep 2, 2022
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@tniessen tniessen added the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 4, 2022
@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 4, 2022
@nodejs-github-bot
nodejs-github-bot merged commit 7371d33 into nodejs:main Sep 4, 2022
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 7371d33

@RafaelGSS

Copy link
Copy Markdown
Member

Hi @tniessen, this PR didn't land clearly on v18.x-staging. Would you mind manually backporting it? I know that's little changes, but the cherry-pick was applying other changes when I tried to fix the conflict (https://gist.github.com/RafaelGSS/bde210dddc7a3019e867ba52db037ade) making the make test fail.

@juanarbol

Copy link
Copy Markdown
Member

This seems to depend on a crypto (native) method introduced in #35093 and marked as "semver-major"; I will proceed to mark this as a "dont-land-on-v16.x"

colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
activates the default provider and constructs its algorithm and name
tables (ossl_method_construct, ossl_namemap_stored): 3.7% of the
samples of `node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). Run the eager seeding check only
when a user-supplied OpenSSL configuration or FIPS could have left
OpenSSL without a DRBG, where an early abort beats a hang at the first
crypto call; Node's own configuration always activates the default
provider, so nothing changes for it except when the first fetch
happens. Every crypto consumer stays on OpenSSL, and a system without
a usable CSPRNG still aborts at startup, now from uv_random() failing.

Measured min of 100 runs, paired against an unpatched build on the
same machine: `node -e 0` 18.35 -> 17.73 ms on Linux x64, nodeStart to
v8Start 2.37 -> 1.62 ms on Linux and 2.48 -> 1.90 ms on macOS arm64.
The startup profile no longer shows RAND_status or the provider
construction; the first crypto.randomBytes() instantiates the DRBG in
0.19 ms. The `parallel`, `sequential`, `message` and `es-module`
suites show no failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on: its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The `parallel`,
`sequential`, `message`, `es-module` and `addons` suites show no
failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on: its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The `parallel`,
`sequential`, `message`, `es-module` and `addons` suites show no
failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on. Its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The `parallel`,
`sequential`, `message`, `es-module` and `addons` suites show no
failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on. Its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Two other behaviors change. A configuration whose [random] section
names a DRBG that cannot be fetched used to abort at startup; it now
starts and the first crypto call fails on the fetch. With --secure-heap
the process DRBGs are instantiated after the secure heap exists, so
they are allocated from it, and a Worker whose per-thread DRBG cannot
be allocated no longer aborts the process from the entropy callback.
Tests cover both, and the default provider staying active under
--openssl-legacy-provider.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The `parallel`,
`sequential`, `message`, `es-module` and `addons` suites show no
failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on. Its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Two other behaviors change. A configuration whose [random] section
names a DRBG that cannot be fetched used to abort at startup; it now
starts and the first crypto call fails on the fetch. With --secure-heap
the process DRBGs are instantiated after the secure heap exists, so
they are allocated from it, and a Worker whose per-thread DRBG cannot
be allocated no longer aborts the process from the entropy callback.
Tests cover both, and the default provider staying active under
--openssl-legacy-provider.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The
`parallel`, `sequential`, `message`, `es-module` and `addons` suites
show no failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
colinhacks added a commit to colinhacks/node that referenced this pull request Sep 4, 2026
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on. Its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Two other behaviors change. A configuration whose [random] section
names a DRBG that cannot be fetched used to abort at startup; it now
starts and the first crypto call fails on the fetch. With --secure-heap
the process DRBGs are instantiated after the secure heap exists, so
they are allocated from it, and a Worker whose per-thread DRBG cannot
be allocated no longer aborts the process from the entropy callback.
Tests cover both, and the default provider staying active under
--openssl-legacy-provider.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The
`parallel`, `sequential`, `message`, `es-module` and `addons` suites
show no failure the unpatched build does not have.

Refs: nodejs@5cc36c39d2
Refs: nodejs#44493
Refs: nodejs#46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. openssl Issues and PRs related to the OpenSSL dependency. v8 engine Issues and PRs related to the V8 dependency.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants