The epoll poller gave every armed timer its own timerfd. A keep-alive
HTTP server re-arms and cancels the socket idle timer on every request,
which cost six system calls per request (timerfd_create,
timerfd_settime twice, epoll_ctl twice, close) on top of the read and
the send; Node makes two.
Armed keys now live in a deadline min-heap with a pointer hash, and one
timerfd wakes the loop at the earliest deadline. Arming a later
deadline, re-arming and cancelling make no system call; the timerfd is
reprogrammed only when an earlier deadline arrives or when it fires.
The fd table is indexed by fd number instead of scanned. Delivery is
unchanged: each expired timer reports once (one-shot), now in deadline
order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With while-loop versions, `while (pos < end && s.charCodeAt(pos) === 32)
pos++` took a version whose guard could never hold. The counter scan
records only typed-array accesses, so the string read fell back to the
statically ranged check. For a counter held in an int32 slot, that check
asks for a length above 2^31 - 2. The loop paid for the guard and ran its
checked copy.
The counter scan now records `s.charCodeAt(i)` and `s.charAt(i)` of a
string like typed-array reads. Their guard compares the counter's bound
with the string's length, as for a typed array. Corpus 7302 adds while
loops over counters below a string's length.
An object literal that spreads an `any` value into a typed slot
(service.list({ ...c.req.valid("query"), offset: 0 }) where the validated
value is untyped) was refused, because only known record shapes spread.
TypeScript types such a literal `any` and the copy holds whatever own
properties the source has at runtime, so it now builds as a
checked-dynamic object and checks into the expected record like any other
`any` value assigned to that slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A keyed read of a record whose declared fields have different types, with
a key the checker types as keyof the record (row[key] in a loop over
Object.keys(patch) as (keyof Row)[]), was refused: the read needed one
common field type. The checker already types such a read as the union of
the field types; when every declared field surfaces in that union, the
existing per-field dispatch wraps each hit into its arm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import-cycle analysis treated every deferred reference (a function
body, parameter default or instance field) to a cycle binding as early
once any user code could run before the declaration completed. With a
static evaluation order it now walks the cycle's top-level code in
evaluation order, following stored callables (function and class
declarations, functions and methods stored by initializers) across the
cycle's modules through import aliases, and counts a deferred reference
as early only when that walk reaches it before the declaration ran.
Top-level reads, unknown orders, top-level await and default-exported
declarations keep the previous rule.
This removes the SC1016 refusal on zod 4's `fn[CONSTANT_CATCH] = true`
(a computed key folded from a const's literal type inside a function
nothing calls during the cycle's evaluation), and drops initialization
flags and checks that can never fire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getCipherInfo(nameOrNid[, options]) runs in a new runtime unit,
scr_cipher_info.c, over a table that scripts/gen-cipher-info.mjs generates
from Node 24.22's getCipherInfo. The table covers every cipher OpenSSL
names, including the ones without a native implementation. Lookups match
names case-insensitively and accept OpenSSL's nids. The unit applies
cipher.js's argument checks and GetCipherInfo's keyLength and ivLength
rules, and returns the info object in Node's key order. CipherInfo maps
to a checked-dynamic value, so TypeScript and JavaScript code receive
Node's object. Reading the nid Node leaves out for the SIV and SM4 GCM,
CCM and XTS ciphers, where the declared type gives number, throws a
TypeError.
createCipheriv and createDecipheriv support aes-128/192/256-ccm (and the
id-aesN-ccm names) over OpenSSL's ccm128 steps. They follow Node's tag and
IV lengths, the plaintextLength requirement, the message-length caps and
the one-update rule, and report a failed authentication from final().
They also support the RFC 3394 and RFC 5649 key wrap ciphers (aes-N-wrap,
aesN-wrap, id-aesN-wrap, the -pad forms and the -inv ciphers) over
wrap128.c's routines, with Node's lengths, integrity failures and
one-update rule.
The new names follow the existing ones in the cipher table, so the lookup
cost of the existing ciphers is unchanged.
An array or typed-array index that is an exact integer or NaN, such as an
element read that may miss (`t[i]!`), a `charCodeAt`, or a sum of those, is
a valid index when it is ordered and below the length. The index check now
tests that with one ordered compare and the integer, taken from the value's
32-bit view or from one conversion, instead of converting the index back to a
double and comparing it.
Number locals that are declared once with an initializer, are not captured
or boxed, and are only written such values carry the same fact, so
`const c = t[i]!; table[c]` and `(h * 31 + c) | 0` take the integer paths
that the inline read already takes.
Stream events and callbacks ran with no store. Node's streams run each
step in the context of the code that triggers it: a step deferred with
process.nextTick (resume, emitReadable, endReadable, afterWrite, the
destroy and finish steps) in the context that scheduled it, and other
events in the context of the push(), write(), end(), destroy() or pipe()
call that emits them. Native-backed streams run their events in the
context of their handle.
The runtime now captures the active context where it defers a stream
step and enters it around the step's dispatch:
- the stream tick queue keeps each tick's scheduling context;
- the runtime's own process.nextTick jobs (zlib completions, pipeline and
finished steps, readline) run in the context they were queued in, as
user ticks already did;
- fs read and write stream requests keep the context of the call that
started them, so a stream's open, reads, writes and close follow its
creation;
- zlib streams keep their creation context and run each engine step's
completion in it.
In programs that use AsyncLocalStorage, stream.finished and
stream.pipeline callbacks are bound to the context of their call, like
the other completion callbacks. Programs that do not use
AsyncLocalStorage compile to the same IR.
child_process and http2 events are unchanged.
Corpus 7383 and 7384.
ToInt32 and ToUint32 of a double that no range proves an integer took the
i64 conversion only up to 2^53 in magnitude, and decoded the double's bits
above that. Every double between 2^53 and 2^63 is an integer, so the i64
conversion is exact there too, and its low 32 bits are the result. The bit
decoding now handles only NaN, the infinities and magnitudes from 2^63.
Multiplicative hashes such as `((h ^ b) * 16777619) >>> 0` and
`(h * 1000003 + b) | 0`, whose products usually pass 2^53, convert on the
fast path.
`const kinds = [Dog, Cat, Animal]; new kinds[0]()` now works as in Node:
- TypeScript: the checker types the element as the common base
(`typeof Animal`), and an element read keeps the `undefined` an index
past the end reads. The construction now narrows the class value out of
that union, and an index past the end throws Node's TypeError
("kinds[5] is not a constructor"). Before, it refused with SC2001.
- JavaScript: such a literal is a checked-dynamic array stored in a slot
of the base's class values, and the check accepted only the base's own
class value, so storing it threw "expected class constructor".
A class value of a subclass now checks into its base class's slot, as a
subclass instance already checks into a base-class instance slot.
Classes without subclasses keep the exact check.
request.end(body) on a request whose head was not sent yet wrote the
head and then the body. On a socket that was already connected (a
reused keep-alive connection), each write is sent at once, so the two
went out as separate segments, and the server could handle the head and
answer before the body arrived. Node sends the head together with the
body. When the client and the server run in one program, the server's
'end' and 'close' for the request and the client's reading of the
response then depended on which socket the poller reported first: corpus
5409-http-held-request-body printed the client's line before the
request's 'end' and 'close' in a macOS dev build.
The head and a body known at end() now go out as one write, as Node
sends them (the server's response already did).
The backend gives every function that returns a complete record literal a
result-slot entry, `%into.<fn>(...params, dst)`, which stores the literal's
fields into caller frame storage. The cycle analysis counted those stores
as writes after construction, so a record type that refers to itself and
is never written after construction carried the cycle collector's header,
its live count and its candidate checks in release builds, even when
nothing called the entry. Dev builds, which add no entries, left it
acyclic.
The destination is frame storage of a caller that only projects the value
(stack-objects.ts): no object can point to it, so its fields cannot lie on
a cycle. computeCycleMutability now skips those stores, under the same
condition emit-statements.ts already uses to emit them as initializations
(resultSlotStore in the new result-slots.ts). Every other store still
makes its field mutable. The same facts mark such fields as never
replaced, so reads of them borrow instead of retaining.
A program that rebuilds a tree of 2^21 - 1 object literals 12 times runs
in 1.19 s instead of 1.35 s and peaks at 68 MB instead of 84 MB: each node
is 32 bytes instead of 40.
g01 (twenty fs.stat round trips against a 2 ms timer), p10 (two 0 ms
timers armed on either side of an island call) and s06 (fs callbacks and
timers pending when Node loads) each failed one CI run in a different
lane: their order depends on how long the island call or the I/O takes,
which differs between the sides and grows under ASan. Their native runs
now have to produce the lines of one of Node's outputs, like the other
race programs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>