Flatten dictionary members at construction while preserving inheritance
order. Skip member scans for nullish inputs without defaults or required
members, and define own descriptor properties to avoid inherited
setters.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65857
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This adds support for transferring net.BoundSocket instances to other
threads via the worker_threads postMessage() transfer list, and for
sending them to child processes as the sendHandle argument of
subprocess.send(), following on from the BoundSocket introduction.
A BoundSocket reserves a port synchronously at construction time.
Making it transferable means a port can be reserved on one thread or
process and the bound (but not yet listening or connected) TCP handle
handed off to another to listen or connect on, without racing on the
bind.
For threads, BoundSocket implements kTransfer/kTransferList/
kDeserialize, moving the underlying TCP handle with the same mechanism
used for net.Socket and net.Server transfer. For child processes, the
handleConversion entry reuses the same transfer protocol on the
sending side and the same _TransferredBoundSocket deserialization path
on the receiving side; the underlying transport is that of cluster's
shared-handle scheduling: SCM_RIGHTS on Unix and WSADuplicateSocket on
Windows, both of which carry bind state.
In both cases the source instance is left in the adopted state:
address(), fd() and close() throw ERR_SOCKET_HANDLE_ADOPTED. Transfer
requires an un-adopted, open TCP handle, otherwise
ERR_WORKER_HANDLE_NOT_TRANSFERABLE is thrown; pipe (path) binds are
not transferable and throw ERR_INVALID_HANDLE_TYPE when sent over IPC.
On the receiving side the local address is re-derived from the handle
rather than trusted from serialized state.
Signed-off-by: Guy Bedford <guybedford@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64725
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
A file descriptor obtained on a mounted path answers several `node:fs`
calls differently from one on a real file, in both providers:
* `writeFileSync(path, data, { flag: 'r+' })` replaces the whole file
instead of overwriting bytes from offset 0 and keeping the tail.
* Numeric open flags are mapped by treating any write-ish bit as "w":
`O_WRONLY` alone truncates, and `O_RDONLY | O_CREAT` opens the file
write-only and truncates it.
* A handle opened with "a+" starts its read offset at the end of the
file, so the first read returns nothing; O_APPEND only affects writes.
The ZipProvider handle additionally:
* throws EISDIR instead of EBADF when reading a write-only handle or
writing a read-only one;
* leaves stale bytes in place when `ftruncate` grows a file that was
previously shrunk, where real files read back as zeros;
* rejects a BigInt `position` with a TypeError from mixing number and
BigInt arithmetic.
This adds a test that runs the same sequence of calls against a memory
mount and a ZIP mount and expects the real-fs result, so every
divergence shows up as its own failing case.
Proposed solution: decode numeric flags bit by bit (O_TRUNC decides
truncation, O_CREAT decides creation, O_WRONLY/O_RDWR decide access)
instead of collapsing them to a flag string; keep the read offset at 0
for append handles and only force writes to the end; make the handle
`writeFile` for non-truncating flags write at offset 0 without
shrinking; in the ZIP handle use EBADF for access-mode violations,
zero-fill on growth in `#doTruncate`, and coerce `position` with
`Number()` as the memory handle does.
Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
PR-URL: https://github.com/nodejs/node/pull/65854
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Use explicit brand predicates for interface conversion instead of
prototype ancestry. Update CryptoKey and AbortSignal together with the
shared converter contract.
Read internal AbortSignal state during composition. Preserve genuine
signals after prototype changes without invoking shadowed getters.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: GitHub Copilot
PR-URL: https://github.com/nodejs/node/pull/65846
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
On Windows, libc++ std::filesystem::remove and remove_all do not
automatically clear the read-only attribute before deleting a file
(unlike MSVC STL). This causes fs.rmSync to fail with EPERM when
trying to remove read-only files in environments where Node.js is
built using clang libc++ (such as Electron).
This commit introduces a Windows-specific helper
ClearReadOnlyAttributeW which clears the FILE_ATTRIBUTE_READONLY
attribute recursively (or for a single file) when
operation_not_permitted is returned, allowing rmSync to
successfully delete read-only files/folders.
Fixes: https://github.com/nodejs/node/issues/64374
Signed-off-by: SparshGarg999 <sparshgarg999@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64453
Fixes: https://github.com/nodejs/node/issues/64374
Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com>
The embedder docs say each Environment has exactly one `uv_loop_t` and
that an `IsolateData` can be shared between Environments, and
`CreateIsolateData()` takes the loop, so several same-thread
Environments naturally end up on one loop. Nothing mentions that
`FreeEnvironment()` then runs that loop, with JavaScript disallowed on
the isolate, until the freed Environment's handles are gone, so the
other Environments' callbacks can fire inside it. Document that, and
point at it from `FreeEnvironment()` in node.h.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65691
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
SocketAddressLRU::Upsert always inserts an entry before evicting down
to max_size_. With max_size_ == 0, it evicts the entry it just
inserted and then accesses the now-missing key via
map_[address]->second. operator[] recreates the key with a
default-constructed std::list iterator, which is then dereferenced.
This is undefined behavior, observed as a SIGSEGV in Endpoint::Receive
on the first UDP packet accepted by a QuicEndpoint constructed with
{ addressLRUSize: 0 }.
SocketAddressLRU has no useful semantics for a zero-capacity cache,
and Upsert's callers rely on it returning a valid pointer. Reject 0
(and 0n) at the options-parsing boundary instead of changing Upsert's
contract.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65827
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Without a startup snapshot (`--no-node-snapshot`, a
`--without-node-snapshot` build, or an embedder Environment that was
bootstrapped from scratch) starting a Worker made a member call through
a null `SnapshotData*`, and so did `NodeMainInstance` while setting
itself up. It only worked because the function called does not touch
`this`; UBSan reports it for every such Worker. The call existed
because `IsolateData::CreateIsolateData()` took an
`EmbedderSnapshotData*` and unwrapped it straight away, so the two
internal callers wrapped their possibly-null `SnapshotData*` with
`AsEmbedderWrapper()` only for it to be unwrapped again.
Let the internal function take the `SnapshotData*` itself, unwrap in
the public `CreateIsolateData()` only, and drop `AsEmbedderWrapper()`,
which has no other users.
Refs: https://github.com/nodejs/node/pull/47731
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65820
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
An embedder that creates its IsolateData without a MultiIsolatePlatform
(allowed; node.h says only Workers need one) and keeps the inspector
segfaulted on the first `console.log()`, `console.time()` or profiler
use after a `node:inspector` session was connected: V8 calls the
inspector client's `currentTimeMS()` there, and
`NodeInspectorClient::currentTimeMS()` dereferenced
`isolate_data()->platform()` unconditionally.
Fall back to the wall clock when there is no platform, which is what
`NodePlatform::CurrentClockTimeMillis()` returns anyway.
Refs: https://github.com/nodejs/node/pull/21917
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65818
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Every stream write created a WriteWrap JS object up front, even though
most writes complete synchronously via uv_try_write() and never use it.
Let stream_base_commons pass null instead of a request object.
StreamBase::Write() already creates the wrap object only when the write
does not complete synchronously; return that object to JS (which
attaches oncomplete/callback to it) and a plain error code otherwise.
Writes that complete synchronously now cross the JS/C++ boundary once
and allocate nothing. Callers that pass in a request object
(child_process IPC, webstreams adapters) behave as before.
Since Http2Stream::DoWrite() can invoke the completion callback
synchronously - before JS has attached oncomplete - such completions
are now recorded on the request object's writeStatus field and replayed
by stream_base_commons after dispatch. This also replaces a Has() plus
name-based MakeCallback() pair with a single Get().
Also pre-create the JS fields of WriteWrap instances in the object
template, as was already done for ShutdownWrap, so that they are
in-object properties.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64455
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Read buffers for streams that emit their data to JS were allocated per
read: a 64KB backing store, tracked in a map, and then - since reads
rarely fill the whole buffer - reallocated to the right size and copied.
Allocate read buffers from a 64KB slab instead. Reads reserve the
suggested size from the slab and JS receives a view over the slab's
ArrayBuffer at the read's offset, using the offset mechanism that
onStreamRead already supports. Unused reservation space is rewound when
a read returns less than was reserved, so small reads (e.g. TLS records)
share a slab.
This removes the per-read allocations, the map bookkeeping and the
resize copy.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64455
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
The module loader manufactures paths under the reserved VFS root that
no layer owns: resolving a mount point as a directory first probes the
sibling names `<mount>.js`, `<mount>.json` and `<mount>.node`, and a
package.json walk-up passes the parents of the mount point. The lookup
declined those because their layer segment is not a plain id, so they
fell through to the native loader and the real file system.
On POSIX that is harmless (ENOTDIR under /dev/null), but on Windows
the root sits under `\\.\nul`, and `\\.\nul\<anything>` opens the NUL
device: libuv reports it as a character device and a read returns
nothing. The loader therefore picked `\\.\nul\vfs\<id>.js` as an
existing file, and the native walk-up above it then read the device as
an empty package.json and failed with ERR_INVALID_PACKAGE_CONFIG for
`\\.\nul\package.json`. Any require() of a mount point hits this on
Windows.
Distinguish "under the root but unowned" from "outside the root" in the
lookup and have every loader override report the former as not found:
stat gives ENOENT, reads and realpath throw ENOENT, the package.json
lookups return their "no package.json" results, and upward walks stop
at the reserved root. Paths outside the root still go to the native
loader as before.
Refs: https://github.com/nodejs/node/pull/65748
Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
PR-URL: https://github.com/nodejs/node/pull/65814
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Original commit message:
[stack traces] Fall back to full walk when receiver is unboxed
The lightweight OptimizedJSFrame::Summarize path added in
crrev.com/c/7722138 assumed that closure and receiver are always
encoded as LITERAL or TAGGED_STACK_SLOT in the deopt translation.
This holds for the closure (a JSFunction reference is always tagged)
but not for the receiver: the "receiver" slot in an InterpretedFrame
translation is just parameter 0 of the (possibly inlined) frame and
is emitted via BuildDeoptFrameSingleValue, which encodes it according
to the value's representation. An inlined frame whose `this` was
typed as Float64 ends up as DOUBLE_STACK_SLOT, hitting UNREACHABLE in
ResolveTaggedValue.
Rename ResolveTaggedValue to TryResolveTaggedValue, return
std::optional, and fall back to SummarizeFull when the receiver isn't
a directly-resolvable tagged value.
Fixed: 499260582
Change-Id: I1f9cdd28e4b6b76a253a46e43b248d9239a4ecd8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7748309
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Jakob Linke <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106423}
Refs: https://github.com/v8/v8/commit/1a0089053443ae4001fd15b84f1b46f330a0b3fa
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65764
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Original commit message:
[stack traces] Reduce stack frame summarization costs
During stack trace capture, Summarize() is the most expensive step — it
creates a full TranslatedState for every optimized frame even though
most frames are never inspected. This CL reduces that cost in two ways:
1. Lightweight Summarize() for optimized frames: instead of building a
full TranslatedState, walk only the deopt translation frame headers
and resolve function/receiver via ResolveTaggedValue(), falling back
to the full TranslatedState path for wasm-inlined or unresolvable
closures.
2. Deferred baseline frames: during CaptureSimpleStackTrace, baseline
frames store the raw Code + PC offset and defer bytecode offset
resolution to ExpandDeferredFrames(), which runs lazily before the
stack trace is formatted or inspected.
A new Torque bitfield flag (is_deferred_baseline_frame) marks entries in
the raw capture array that still need resolution. All consumers
(GetSimpleStackTrace, GetDetailedStackTraceFromCallSiteInfos,
GetFormattedStack, PrintCurrentStackTrace) call ExpandDeferredFrames()
before processing the array.
Change-Id: I1fe8cce918ba129d655d66f608ac6aa0ed160920
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7722138
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106237}
Refs: https://github.com/v8/v8/commit/74e153d17187f07535c1812f10937f60e0ae0974
Refs: https://github.com/v8/v8/commit/c9c0abfa51f06ea3c87c4ee9bb519fa8b01fcbbf
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65764
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
`CommonEnvironmentSetup` created a `CppHeap` for its `CreateParams`
before deciding how to create the isolate, but `NewIsolate()` ignores
`params->cpp_heap` and attaches a heap of its own (or the one from
`IsolateSettings`). The first heap was never attached or destroyed, so
every non-snapshotting setup leaked one `CppHeap`; cppgc's heap
registry keeps it reachable, which is why LSAN stays quiet about it.
Only create the heap on the snapshotting path, where the params go to
the `SnapshotCreator` directly.
Refs: https://github.com/nodejs/node/pull/55337
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65792
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
The GN build always compiles the bundled copies of brotli, c-ares,
HdrHistogram, llhttp, libuv, nghttp2, SQLite and zstd, so anyone
packaging a GN-built Node.js for a Linux distribution has no
counterpart to configure's --shared-* options.
Add node_shared_* args named after the GYP variables. When one is set,
that dependency's GN template defines its target as a group carrying a
pkg-config config instead of the bundled sources, so every existing
"deps/<name>" reference picks up the system library without further
changes, and config.gypi reports the choice like a GYP build does.
Refs: https://github.com/nodejs/node/pull/55903
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65797
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
fs.cp() and fs.promises.cp() walked the tree in JavaScript with several
thread pool round trips per entry (opendir batches, two stat()s, the
copyFile(), a chmod()), all awaited in sequence: a 2 100-file tree took
~215 ms with ~110 ms of that on the main thread, against ~36 ms for
fs.cpSync(), which copies the tree in C++ when no filter is given.
Factor that C++ walk into CopyDirRecursive(), which records the error
instead of throwing so that it can run on any thread, and run it as one
ThreadPoolWork request (CpDirJob) for fs.cp()/fs.promises.cp() when
the destination directory does not exist yet and nothing has to run per
entry (no filter, no dereference, permission model off). Copying into
an existing tree keeps the JavaScript walk and its rules for what may
already be there. The same tree now takes ~30 ms with under 1 ms on the
main thread.
For that job the walk follows the JavaScript walk's rules rather than
cpSync's: it creates every directory with mkdir() and every file with
an exclusive uv_fs_copyfile() (honouring the copyFile() mode flags) and
fails with EEXIST if anything has appeared in their place since the
JavaScript check, so it never opens or follows something it did not
create; sockets, FIFOs and unknown entries are reported back to
JavaScript, which rejects them with the same SystemErrors as before;
relative link targets are made absolute lexically as path.resolve()
does. cpSync keeps merging into existing directories, skipping special
files and canonicalizing link targets.
The walk now uses the error_code overloads of std::filesystem throughout
(directory iteration included), so an unreadable directory inside the
tree is reported as EACCES by both cp() and cpSync() instead of
terminating the process, which cpSync() has done since the walk moved
to C++. Filesystem errors raised inside the walk keep their codes, with
'cp' or 'copyfile'/'mkdir' as the syscall. With preserveTimestamps the
walk stamps each directory it filled, root included, so cp() keeps
preserving directory times and cpSync() without a filter now does too.
Both file copies set the destination's mode before writing the data,
which clears setuid and setgid, so the walk puts the source mode back
after each file it copied; the JavaScript walk already chmod()s there.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65488
Refs: https://github.com/nodejs/node/pull/58461
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
The C++ fast path that fs.cpSync() takes when no filter is given created
the destination directories with default permissions, so a 0700
directory came out of the copy as 0755 (with the default umask). The
JavaScript implementation, which fs.cp(), fs.promises.cp() and
fs.cpSync() with a filter still use, chmod()s every directory it
creates to the mode of its source, and so did cpSync before the port.
Set the source directory's permissions on each directory the copy
creates (the destination root included); directories that already
exist keep theirs, as before.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65488
Refs: https://github.com/nodejs/node/pull/58461
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
V8 defaults `v8_enable_sandbox` to on whenever the shared pointer
compression cage and the external code space are enabled, and that is
the configuration embedders that use the sandbox build with. Now that
the sandbox builds and passes the tests, follow that default for
`--experimental-pointer-compression-shared-cage` so the configuration
is reachable from `configure`. Multi-cage pointer compression builds
stay without it: there every IsolateGroup gets its own sandbox, and
`NodeArrayBufferAllocator` always allocates from the default one.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62237
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
With V8_ENABLE_SANDBOX every ArrayBuffer backing store has to be
allocated inside the sandbox, so memory that Node.js or a library
allocated itself cannot be wrapped and has to be copied in. Several
places already special-cased this, each with its own #ifdef, but a
build with the sandbox enabled still failed to compile
(`kMaxSafeBufferSizeForSandbox`), aborted in `crypto.randomUUID()`
(`SecureBuffer` was UNREACHABLE) and threw from every caller of the
malloc-owning `Buffer::New()` (v8.serialize, string transcoding, IPC,
the public `Buffer::New(isolate, data, length)`), and trace_events,
SEA assets and FFI still wrapped outside memory unconditionally.
Add `AdoptIntoBackingStore()`, which wraps the memory as before in
regular builds and copies it into an isolate-allocated backing store,
running the deleter right away, when the sandbox is enabled, and use it
at all of those sites so the #ifdef lives in one place. The trace
category flag cannot be copied since JS polls it, so under the sandbox
`getCategoryEnabledBuffer()` returns nothing and lib falls back to
`isTraceCategoryEnabled()`; FFI zero-copy views throw
ERR_OPERATION_FAILED there for the same reason.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62237
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
ffi.getInt8() through ffi.getFloat64(), ffi.setInt8() through
ffi.setFloat64(), ffi.toBuffer() and ffi.toArrayBuffer() return
undefined instead of throwing when a required argument is omitted, so a
call that read or wrote nothing cannot be told apart from one that read
a zero byte. All 22 helpers behave this way.
GetValidatedPointerAddress() and GetValidatedSize() already reject the
same argument when it is passed explicitly as undefined. The
args.Length() test in front of them short-circuits the call and returns
Nothing without scheduling an exception. These six are the only tests in
src/ where args.Length() can skip a call that throws; the only other
Length() tests that guard a call at all guard Buffer::HasInstance(),
which cannot throw. The remaining tests in this file guard an inline
predicate and throw in the branch, which is why setUint8(ptr) reports
"Expected an offset argument" while setUint8() reports nothing at all.
Drop those tests. FunctionCallbackInfo::operator[] returns Undefined for
an out-of-range index, which is exactly the value these validators
reject, so each missing argument now produces the error its explicit
undefined counterpart produces. The documentation already describes this
behavior: the signatures are ffi.getInt8(pointer[, offset]),
ffi.setInt8(pointer, offset, value) and
ffi.toBuffer(pointer, length[, copy]), and the getters are documented to
return a number or a bigint.
ExportBytes() carried the same two tests. They are unreachable through
the public API because exportBytes is not exported and its three callers
all validate len in JavaScript first, but they are the same shape.
Signed-off-by: Soul Lee <alus20x@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65500
Fixes: https://github.com/nodejs/node/issues/65499
Refs: https://github.com/nodejs/node/pull/62072
Refs: https://github.com/nodejs/node/pull/62858
Refs: https://github.com/nodejs/node/pull/65842
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>