fs.cpSync, fs.cp and fs/promises cp compile with every option:
dereference, errorOnExist, filter, force, mode, preserveTimestamps,
recursive and verbatimSymlinks. They follow Node's two implementations.
cpSync follows cp-sync.js over the fs binding: copy errors are plain
Errors with a code, std::error_code failures throw an ErrnoException,
and links inside a copied directory point at their canonical targets.
fs.cp and fs/promises cp follow cp.js, whose copy errors are
SystemErrors with code, info, errno, syscall and path.
Options and paths validate in Node's order with Node's errors. filter
runs for the top-level paths and each entry in Node's order; an async
filter must return a promise that is already settled, and a pending one
throws SC2020. The callback and promise forms complete the copy before
the callback runs or the promise settles. Windows builds throw SC2020.
The copy lives in the new scr_fs_cp.c. Directory walks keep their frames
on the heap, so deep trees also copy on a fiber's stack. File contents
copy with sendfile on Linux, and COPYFILE_FICLONE uses FICLONE
where it exists. ScrError gains the SystemError context and the
ErrnoException key order, with matching fields in the compiler's Error
layout. The stored fs.cp callback uses the same implementation.
Corpus 5567-5571 cover the sync, callback and promise forms, errors,
symlinks and 1500-level trees.
On x86-64 CPUs with the SHA extensions (and SSSE3 and SSE4.1), SHA-1 and
SHA-256 process their blocks with SHA1RNDS4/SHA256RNDS2 and the
message-schedule instructions, several blocks per call. The CPU check
runs once and is cached; other CPUs, other architectures and wasm32 keep
the portable blocks, and both give the same digests. createHash,
crypto.hash, createHmac, pbkdf2, hkdf and every other caller of the two
digests use the new path.
sha-kernels.test.ts runs both block implementations over known vectors
and every length from 0 to 1100 bytes and 1 MiB, whole and in 1-, 7-,
64- and 100-byte updates. Corpus 5763 compares the digests, HMAC, PBKDF2
and Hash.copy() with Node across block-boundary lengths and update
sizes.
A Map or Set header held six adapter pointers, the union key table and
size_t counts: 168 bytes, 176 in its size class. The adapters and union
key kinds now live in an immutable per-type ScrMapOps descriptor that the
compiler emits once per collection type, a traced byte replaces the two
trace-pointer tests, and the counts and capacities are 32-bit. The header
is 88 bytes.
The entries array starts with one slot and grows to four, then doubles,
and maps of up to 8 entries scan their cached hashes instead of keeping a
bucket table. Number-key maps still move to the direct index from four
entries.
One million maps held at once (RSS per map): empty 188 -> 100 bytes,
1 entry 286 -> 125, 4 entries 286 -> 198, 8 entries 450 -> 295.
Every release of a LifetimeRegion, and every retry at region creation, ran
scr_collect_cycles(): full cycle-collector passes over the thread's heap, to
a fixpoint, before checking any region, in case dead cycles of ordinary
objects held counted references into it. Freeing a region ran them again
whenever the owned ordinary objects left counts behind after the region
released its fields. Next to a large heap of candidates (a type checker
beside per-file emit regions) those passes cost far more than the regions.
A retry now checks the due regions first. A region whose references are all
explained frees without a pass; only when one would be kept does the heap
get the full pass, after which every due region is judged as before. The
first check leaves a region it cannot free as it was, so the backoff, the
kept report and SCRIPTC_REGION_DEBUG see only the check after the pass. It
keeps what its walk found: a region whose counted references the pass did not
change is judged from that, without walking it again. A pass that freed a dead
cycle referring only to an owned object leaves the count unchanged; such a
region waits for its next attempt.
After a region releases its instances' fields, what can still hold counts
is the owned ordinary objects' cycles. Each has a member that lost a
reference in those releases and is buffered as a candidate, and the release
walk entered every cycle member. The cycle collector gains a pass limited to
a set of candidates (scr_cyc_collect_set): it takes the set's members out of
the buffers up to a given generation, reading only the buffers' pointers, so
the candidates it leaves are not touched, and it walks at full depth without
moving the schedule's baselines. A dead object such a pass reaches through a
candidate it did not take leaves its buffer before it is freed. Freeing a
region runs these passes over the walk's objects from the youngest buffer
out, and the whole heap only if counts remain.
SCRIPTC_REGION_STATS also prints, per thread at exit, how many full passes
over the heap regions needed.
A collection pass copied every buffered candidate from the generation
root buffers into its candidate list, so it held two buffers of every
candidate, and each work vector kept its largest size for the rest of
the program.
A pass now takes the fullest root buffer as its candidate list and
appends the others; that generation gets the previous candidate buffer,
empty. The vectors a pass fills and leaves empty (pending, white,
promote, restore and cross-generation lists) record how many entries the
pass held at most. When a pass ends, any of them that holds more than
65536 entries is freed if the pass used less than a quarter of it, and so
is the candidate list's buffer when this pass's candidates filled less
than a quarter of it. Passes of a steady size keep reusing their
buffers; a spike in one pass no longer keeps its buffers for the rest of
the program.
When an older generation's buffer is the fullest, candidates are
visited, and a pass's garbage is freed, in a different order; the set
of objects a pass frees does not change.
Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and
Buffer.concat now return views into a per-thread pool slab of
Buffer.poolSize bytes (64 KiB by default) when the size is below
Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and
fromArrayLike: 8-byte aligned offsets, a new slab when a request does not
fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize
can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer.
A slab lives while any of its views does. Its ArrayBuffer cannot be
transferred, and structuredClone of a typed array view copies the whole
ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and
allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error.
The first pooled allocation finds the startup slab, which has the
default 64 KiB size. A request it cannot hold gets a slab of the current
Buffer.poolSize, as in Node, and the room is checked after every new
slab, so a Buffer.poolSize raised before the first pooled allocation
cannot place a reservation past the end of a 64 KiB slab. A reservation
larger than a fresh slab traps instead of writing past it.
Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the
first pooled allocation and allocate just under the new threshold
through each pooled path, each case in a fresh child process, and also
shrink Buffer.poolSize and change it between allocations. The
node-types fenced fixture no longer lists Buffer.poolSize, which now
compiles.
publish() used to remove each published object from this thread's
cycle-collector candidate buffers after the walk, which loaded every
object's collector header a second time. On graphs larger than the
cache that second pass took about a third of the publish.
scr_pub_mark now takes a buffered object out of its buffer when it marks
it, while the header is in cache: the slot is cleared and its index
recorded. When the publish ends, scr_cyc_pub_fill closes the empty slots
with entries from the ends of the buffers. A refused publish puts each
taken object back in its slot first. After the walk, the log is read only
for dyn containers, the RC audit and LeakSanitizer.
The emitted walkers test null and immortal references (parent links,
values published before) before calling scr_pub_push, and the string
walker checks for ASCII 32 bytes at a time.
Adds the published-graphs runtime benchmark, a cycle collector test for
the take, restore and fill hooks, and corpus program 5702.
Exact constructions (literals, slice, concat results) round their request
to 16 bytes before the size class, so arrays of 2, 4 and 6 slots get one
spare slot and an append right after a copy no longer reallocates. Arrays
of 8-byte slots with their own storage and no sparse entries grow below
the dense cutoff on a path with a constant slot size, and dense copies of
8-byte slots use a constant size.
createCipheriv, createDecipheriv, update(), setAAD() and setAuthTag()
now read the value a typed-ref box holds when it is a string, view,
ArrayBuffer or KeyObject. ERR_INVALID_ARG_TYPE names the class of a
class instance that arrives through an untyped value, as Node does,
instead of Object. An unknown output encoding that is not a string is
rendered as Node's %s formats it (util.inspect at depth 0, or the
object's own toString); a function, or an instance of a class that
defines toString, throws a coded SC2020 error. Cipher programs link the
inspect unit for that rendering.
5250 passes each argument through `any`, including class instances,
records and arrays that Node rejects, and 5254 covers output encodings
that are not strings from JavaScript.
A candidate release now also runs a pass once the live cycle-headered
count has grown by 4096 objects since the last pass. A dead graph is
usually reachable from one candidate (its root), so the candidate count
alone let a few hundred dead graphs pile up before a pass, which then
walked them after they had left the cache.
markGray records the objects it grays and counts the ones still
externally referenced after the trial decrements. When none is, every
gray object is garbage, so the pass whitens the recorded set and skips
scan. markGray also records each edge it skips into an older generation,
so a restricted pass pins those targets without tracing the white set a
second time.
Adds the cyclic-graphs runtime benchmark and corpus programs 5700 and
5701.
The three glob entry points run a port of Node 24's
lib/internal/fs/glob.js walk in scr_glob.c: the same LIFO queue of
(path, patterns) items over the sorted directory listings, the same
lstat, stat, readdir, realpath and seen-pattern caches, the same result
set in insertion order, and the same exclusion checks, so matches come
out in Node's order. Patterns compile with the existing minimatch port.
glob_match is split into a compile step and a test step so the walk can
reuse each compiled pattern and test it segment by segment.
nocaseMagicOnly is now an option, because exclude patterns turn it off.
Patterns (a string or string array) and options (cwd as a path or
file: URL, exclude as patterns or a function, followSymlinks,
withFileTypes) are validated in Node's order with Node's errors,
including the internal assertion Node hits for a non-string cwd with
exclude patterns. withFileTypes results are Dirent rows: name,
parentPath and the type probes. parentPath follows Node's rule for each
form: the joined path for globSync, the resolved path for fs/promises
glob.
fs.glob checks the callback and validates synchronously, then delivers
string[] or the walk's error through the filesystem callback queue.
fs/promises glob lowers as the source of a for await loop. The walk runs
when the loop starts, and the matches found before an error bind before
the error is thrown, as Node's async iterator yields them first. The
older glob(3)-based fs.glob branch is removed.
Corpus 5448-5452 and a node-types fixture check the behavior against
Node.
readFileSync(path | fd, "utf8") (any ASCII-case utf8/utf-8 spelling and
the empty default) used to read into a Buffer, scan it for UTF-8
validity, then copy it into a new string while scanning it again for
ASCII. It now reads directly into the final string allocation, scans
each read chunk for ASCII while it is still in cache, and validates the
remainder in place (scr_str_adopt_utf8_lossy). Only malformed input is
repaired into a new string, with the same WHATWG maximal-subpart U+FFFD
replacement as Buffer#toString; a BOM is kept.
The raw-string readers behind fs.promises.readFile(path, "utf8") and
readFileSync(fd, "utf8") now decode the same way, so malformed bytes and
encoded surrogates become U+FFFD as in Node instead of passing through.
The ASCII prefix scan tests 32 bytes per step (scr_bytes_ascii_span),
which also speeds up Buffer and TextDecoder UTF-8 decoding of long ASCII
runs.
The corpus program covers empty files, BOMs, every malformed-subpart
class, truncation at EOF, chunk-boundary offsets, large ASCII and
multibyte files, /proc pseudo-files, fd and promise reads; the runtime
unit test checks the in-place decode under ASan and the RC audit.
scr_runtime.h now includes 48 module headers (scr_core.h, scr_rc.h,
scr_string.h, scr_dyn.h, scr_http.h, ...) in dependency order and holds no
declarations. Each module header holds whole sections of the former file,
includes the headers whose names it uses, and compiles on its own.
Declarations, macros, struct layouts and inline bodies are unchanged. Five
blocks move to join their module: the class-hierarchy vtable header (into
scr_error.h, after the string core), the node:timers/promises pair (into
scr_async.h), the JSON.parse entry points and the JSON output buffer (into
scr_json.h, after the dyn declarations) and the EventTarget block (after
scr_json.h). No moved block uses or declares a name that a block it moves
past uses or declares. The string methods keep their position after the
class objects in scr_string_methods.h, because their struct ScrDyn
parameters rely on the file-scope struct ScrDyn declaration there.
The runtime ABI scan reads scr_runtime.h with its module headers expanded
in include order. A runtime test compiles every module header on its own.
scripts/runtime-header-hunks.mjs retargets a diff of the single-file
header onto the module headers.
scr_exc_alert, the process-wide exception alert of worker executables on
targets without the ELF per-thread pending word, was a tentative
definition. That branch compiles only for Mach-O, COFF and WASI, so the
macOS runtime build emitted it as a COMMON symbol, which library
localization keeps global, and the COMMON-symbol test failed on macOS
while passing on Linux.
Initialize it explicitly, and have the test also compile every runtime
source for aarch64-macos through zig cc when zig is on PATH and the host
is not macOS, so declarations in __APPLE__ branches are checked on Linux
too.
scr_lib.c calls the digest functions in scr_hash.c since #1006, and sort.test.ts copied array.test.ts's source list from before that change, so the test binary failed to link (undefined scr_ripemd160_init and others). The list now matches array.test.ts's.
- Every mbedTLS call in the cipher runtime is checked. A failed update(),
final() or setAAD() returns no output and retires the context, so later
calls throw and no tag is returned or accepted. An update past the
AES-GCM limit of 2^36 - 32 bytes per message throws Node's "Trying to
add data in unsupported state" error; it used to return zeroed output
and leave the tag covering only the earlier data.
- chacha20-poly1305 decryption without setAuthTag() fails in final()
again. Node 24.22 skips the tag check when authTagLength is not passed
either and returns unverified plaintext; limitations.mdx records the
difference.
- A runtime unit test moves contexts next to the GCM message and AAD
limits and checks that the failure is permanent; errors.test.ts covers
decryption without a tag.
The gallop searches in the comparator sort held their search bounds in
long, which is 32 bits on Windows x64, so a run of more than 2^31 slots
gave a truncated bound and the merges could copy outside the array. The
bounds are now ptrdiff_t; every other index, length, offset, hint, run
length and run-stack entry in the sort was already size_t. Each gallop
result is checked against its run length (0 <= k <= length) before any
merge uses it, and a result outside the run is an internal error rather
than a copy.
A new runtime unit test calls gallop and the merge step, through test-only
entry points built with SCR_SORT_TEST, at indices past 2^32 and over runs
of more than 2^32 slots in a sparse mapping, and checks that an
out-of-range gallop result traps.
createHash, createHmac, crypto.hash, pbkdf2, pbkdf2Sync, hkdf and hkdfSync
accept every digest crypto.getHashes() lists in Node 24: SHA-512/224,
SHA-512/256, SHA-256/192, SHA3-224 to SHA3-512, Keccak-224 to Keccak-512,
SHAKE128, SHAKE256, KECCAK-KMAC-128, KECCAK-KMAC-256, RIPEMD-160,
BLAKE2b-512, BLAKE2s-256, SM3 and MD5-SHA1. The new runtime unit
scr_hash.c implements Keccak, RIPEMD-160, BLAKE2b, BLAKE2s and SM3, and
is registered in both runtime pack matrices and the C-compile path.
Names resolve as Node 24.22 resolves them: case-insensitively, with
OpenSSL's provider aliases and object identifiers, ending at the first
NUL, and with trailing spaces allowed except on the RSA signature
aliases. "dss1" and "DSS1" are SHA-1, and createHash and crypto.hash
accept "dss1" in any case. The compiler no longer refuses literal digest
names outside SHA-1/SHA-2 and MD5; unknown names throw Node's errors at
run time.
SHAKE and KMAC digests use their default output lengths. createHmac
rejects them with Node's OpenSSL error, pbkdf2 with "xof digests not
allowed", and hkdf with "Deriving bits failed". pbkdf2 with a key length
of 0 now fails with "Deriving bits failed" as in Node 24.22, and the
callback forms pass these failures to the callback.
PBKDF2 computes the keyed HMAC states once instead of on every
iteration, and digest contexts are cleared with memset and a compiler
barrier instead of a byte loop. The Keccak permutation, the RIPEMD-160
and BLAKE2 compression functions and the SM3 rounds are unrolled with
constant indices.
Array.prototype.sort and toSorted with a comparator now run V8's
TimSort (array-sort.tq) step for step in the runtime, in the generated
sort for booleans and unions with undefined, and in the checked-value
sort. The comparator is called as f(x, y) < 0 in Node's argument order,
so a comparator sees Node's calls in Node's order and an inconsistent
comparator yields Node's permutation. Undefined values, including a
union's undefined arm, are set aside before sorting and never reach the
comparator, and a checked comparator's result converts with ToNumber.
Structured inputs take fewer comparator calls than the previous merge
sort. Each thread reuses one sort buffer of up to 1 MiB.
For radixes other than 2, 4, 8, 10, 16 and 32, V8 folds each 32-bit digit
chunk with `value * multiplier + part`. Node's arm64 builds fuse that
multiply-add into one rounding and its x64 builds round twice, so long
inputs such as parseInt("9007199254740993", 36) answer
1.9896986116031812e+24 on arm64 and 1.9896986116031807e+24 on x64. The
runtime followed the same split only through the C compiler's default
contraction, and the committed string oracle held one architecture's
answers: regenerating it on x64 replaced the two arm64 values, failing the
string oracle tests on macOS arm64.
scr_radix_fold now writes out each form: fma() on aarch64, and an unfused
multiply and add with contraction disabled elsewhere, so the result matches
the same architecture's Node under any contraction setting. The generator
records the architecture-dependent cases once per architecture
("parseInt.arm64" / "parseInt.x64") from a listed table and checks the
current machine's answer against Node, so the committed file is the same
whichever machine generates it. The C harness runs the cases for the
architecture it was built for.
scr_cyc_types, scr_str_scan and scr_wtf8_lone were tentative definitions.
The macOS runtime build emits tentative definitions as COMMON symbols, and
Mach-O `ld -r` writes COMMON symbols back out as commons, which
-exported_symbols_list does not demote. Every runtime-localized library
archive therefore defined them next to its declared API, failing the
library-multi M1 and M7 symbol checks on macOS. Linux clang defaults to
-fno-common, so the same declarations were ordinary definitions there and
the checks passed.
Initialize the three globals explicitly, as scr_sa and
scr_stdio_write_hook already are, and add a runtime test that compiles
every runtime source with -fcommon in the plain and worker configurations
and expects no COMMON symbols, so a new tentative definition fails on
Linux too.
The emitted JSON writers read each array element through scr_arr_state
and scr_arr_get_ref, retaining and releasing it, and appended every
bracket, comma and key through a runtime call. Arrays whose indices below
the length all hold values now serialize from their slots
(scr_arr_dense_slots) with borrowed elements; other arrays keep the
element walk. Fixed text is stored inline when it fits the buffer's spare
capacity, booleans write their literal inline, and numbers are formatted
directly into the buffer.
Pin Node v24.22.0 (commit f14cd3aa7bf9, Undici 7.29.1, OpenSSL 3.5.8) in
.node-version, Dockerfile.sandbox, the docs matrix fallback, the
compatibility pin, the fetch profile, and process.version. A node-pin test
checks that these pins agree. The Node compatibility snapshot is
regenerated from the v24.22.0 API docs; the new stream_iter chapter maps
to node:stream/iter, and the SuiteContext rows no longer match the
TestContext evidence.
Runtime and compiler changes follow Node 24.22 behavior:
- fs.read, fs.readSync, and FileHandle.read validate the position before
the zero-length return.
- fs.statfs results include frsize; Windows reads
FileFsFullSizeInformation and reports bfree and bavail separately.
- fs.utimes, futimes, and lutimes keep nanosecond precision.
- ServerResponse sets writableFinished where 'finish' is emitted and runs
end() callbacks before the 'finish' listeners while the socket is still
attached; a request read to 'end' closes before its response.
- socket.write(string, 'buffer') throws ERR_UNKNOWN_ENCODING.
- URL protocol-setter rejections keep the current URL, the host setter
no longer leaves a "/." escape when it adds a port to a "//" path, and
new URLSearchParams(null) parses "null".
- util.stripVTControlCharacters uses Node 24.22's ANSI pattern,
util.styleText accepts #RGB and #RRGGBB colors, and
util.types.isDataView has its JS name and length.
- structuredClone and AbortSignal.any use the "cannot be converted to
sequence" message.
- crypto.getCiphers, getHashes, and constants match Node 24.22.
- dns.lookup, net.connect, and tls.connect throw ERR_INVALID_ARG_VALUE for
hosts with null bytes.
- fs.readFile validates options.buffer and refuses its valid forms;
httpValidation and certificateCompression refuse by name.
- url.domainToASCII, url.domainToUnicode, and url.parse hostnames follow
ada 4.0: ASCII domains are lowercased without checking their xn--
labels, the RFC 5893 rule checks how LTR labels end and how RTL labels
start, a Punycode label whose decoded form begins with "xn--" fails, and
ToUnicode decodes only labels that pass the ToASCII checks. The url
legacy test cases are regenerated with Node 24.22.
Corpus programs 5275-5279 cover the new behavior. 1640, 2283, 2599, and
3136 are updated where they pinned Node 24.15 output.