]> git.kaiwu.me - njs.git/log
njs.git
3 weeks agoModules: limit waiting for shared dict state save on exit
Dmitry Volyntsev [Wed, 14 Oct 2026 23:39:07 +0000 (16:39 -0700)]
Modules: limit waiting for shared dict state save on exit

If a worker dies while saving a shared dict zone state, the writing
flag remains set in shared memory and survives reloads.  Workers
with unsaved changes then receive NGX_AGAIN on every attempt and
keep rearming the save timer, so they never finish graceful shutdown.

Limit how long an exiting worker waits for an outstanding save to
60 seconds since the save started.  The shared writing flag is not
cleared and the writer is not interrupted, so a slow save is not
disturbed.  Running workers keep waiting as before.

3 weeks agoModules: track shared dict state changes per worker
Dmitry Volyntsev [Sat, 10 Oct 2026 01:32:25 +0000 (18:32 -0700)]
Modules: track shared dict state changes per worker

With frequent changes to a shared dict zone using state=, old workers
can remain in shutting down after a configuration reload and continue
consuming resources even though their changes have already been included
in another worker's snapshot.  The shared dirty flag cannot distinguish
those changes from subsequent changes made by new workers, so old
workers can keep rearming their save timers while competing for writes.

Replace the shared dirty flag with a write_id counter and record its
value locally in each worker on mutation.  Advance the shared counter
after preparing a snapshot so workers whose changes are covered can
stop retrying regardless of subsequent mutations.  On I/O failure, the
writer records the advanced counter locally to retain responsibility
for retrying the save.

3 weeks agoModules: limit shared dict state save frequency
Dmitry Volyntsev [Sat, 10 Oct 2026 01:16:25 +0000 (18:16 -0700)]
Modules: limit shared dict state save frequency

Saving shared dict zone content to file is scheduled with a maximum
rate of one write per second in each worker.  The total rate can be
larger by a factor of the number of running workers.

Postpone a zone save if the previous save was less than one second
ago, using a shared timestamp to limit the rate across all workers.

3 weeks agoModules: fix shared dict state save error ordering
Dmitry Volyntsev [Sat, 10 Oct 2026 01:03:47 +0000 (18:03 -0700)]
Modules: fix shared dict state save error ordering

On save failure, writing is cleared before dirty is restored.  Another
worker can observe a clean, idle zone and stop retrying.  If the failed
writer is exiting, it also stops retrying and the changes remain unsaved.

Set dirty first and add a memory barrier before clearing writing to
publish the need for another save before releasing the writer.

3 weeks agoModules: fixed shared dict state save stuck forever
vitalyn [Fri, 2 Oct 2026 21:50:43 +0000 (23:50 +0200)]
Modules: fixed shared dict state save stuck forever

The "dirty" and "writing" flags of a shared dict were bit fields of
the same word.  ngx_js_dict_save() clears "writing" without holding
the lock, while set(), incr(), delete() and clear() set "dirty" under
the write lock in other workers.  The two read-modify-write operations
could interleave and leave "writing" set for good.

After that, every save attempt returned NGX_AGAIN: the state file was
never written again, and the save timer, re-armed every second and not
cancelable, prevented any worker of the zone from finishing a graceful
shutdown, regardless of worker_shutdown_timeout.  The flag lives in
shared memory, so it survived reloads.

The fix is to store the flags in separate words.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This fixes #1136.

3 weeks agoModules: abort shared dict state save on close failure
Dmitry Volyntsev [Fri, 9 Oct 2026 23:03:38 +0000 (16:03 -0700)]
Modules: abort shared dict state save on close failure

A close failure may report a delayed write error.  Do not rename the
temporary file over the previous state file in this case.  Invalidate
the descriptor before cleanup to avoid closing it twice.

3 weeks agoModules: initialize shared dict state file before allocation
Dmitry Volyntsev [Fri, 9 Oct 2026 22:59:52 +0000 (15:59 -0700)]
Modules: initialize shared dict state file before allocation

Initialize the state file descriptor before allocating chain links and
buffers.  Allocation failures otherwise reach cleanup with an
uninitialized descriptor and may close an unrelated file.

3 weeks agoModules: preserve shared dict entries on failed updates
Dmitry Volyntsev [Tue, 11 Aug 2026 21:29:33 +0000 (14:29 -0700)]
Modules: preserve shared dict entries on failed updates

Shared dictionary updates temporarily remove timeout entries from the
expiration rbtree to prevent eviction while allocating a replacement.
ngx_rbtree_delete() clears the removed node key, so the failure path
reinserted the entry with a zero expiration and made its old value appear
expired.

Factor the shared-memory update into an engine-independent helper and
restore the previous expiration when allocation fails.  Keep value
conversion in the njs and QuickJS wrappers.

3 weeks agoVersion bump
Dmitry Volyntsev [Wed, 14 Oct 2026 15:24:44 +0000 (08:24 -0700)]
Version bump

5 weeks agoVersion 1.0.2 1.0.2
Dmitry Volyntsev [Wed, 30 Sep 2026 04:51:00 +0000 (21:51 -0700)]
Version 1.0.2

5 weeks agoXML: return complete attribute values
Dmitry Volyntsev [Wed, 16 Sep 2026 23:17:58 +0000 (16:17 -0700)]
XML: return complete attribute values

Previously, $attr$ returned undefined for attributes containing entity
references, while $attrs returned only the first text fragment. libxml2
canonicalization expands the complete attribute value, creating an
interpretation difference for signed XML consumers.

Use xmlNodeGetContent() for both getters in the njs and QuickJS backends.
This preserves the existing explicit-attribute lookup and makes the
JavaScript value agree with canonicalization.

5 weeks agoXML: release parser context after parsing
Dmitry Volyntsev [Wed, 16 Sep 2026 22:03:40 +0000 (15:03 -0700)]
XML: release parser context after parsing

Previously, XML parser contexts were retained with their parsed documents.
When parsing failed in the njs engine, no cleanup was registered, leaking
the context and allowing repeated malformed XML input to grow memory usage
in long-lived NGINX worker processes.

The fix is to limit parser contexts to the parse operation and release
them immediately after parsing or error reporting. The VM cleanup now owns
only the parsed document.

5 weeks agoVM: report import of a module removed after failed compilation
Dmitry Volyntsev [Wed, 30 Sep 2026 06:12:30 +0000 (23:12 -0700)]
VM: report import of a module removed after failed compilation

A module whose compilation fails is removed from the cache, but another
module compiled earlier in the same cycle may still import it.  Executing
such an import returned an error without setting an exception, so the
shell reported an unrelated TypeError.

Throw "Cannot load module" instead, and add a shell test for a cyclic
dependency of a module that fails to compile.

5 weeks agoVM: preserve dependency code after failed module compilation
Dmitry Volyntsev [Wed, 30 Sep 2026 06:06:42 +0000 (23:06 -0700)]
VM: preserve dependency code after failed module compilation

Commit fd54d52d freed all bytecode added after the parent module's
compilation checkpoint, including successfully compiled dependencies.
Those dependencies remain cached, so a later import in the same VM
executes freed bytecode.  This is reachable in the interactive shell
and in embedders that continue after a compilation error.

The fix is to remove eager bytecode cleanup while retaining removal of
the failed module from the cache so subsequent loads can retry.  Keep
the removed module and its name in the VM pool because cyclic
dependencies and code metadata can still reference them.  These
allocations are released when the VM is destroyed.

Add a shell regression test that retries a failed parent import and then
executes its cached dependency.

6 weeks agoXML: preserve node methods with matching children
Dmitry Volyntsev [Tue, 29 Sep 2026 05:23:45 +0000 (22:23 -0700)]
XML: preserve node methods with matching children

QuickJS exposes child elements as own properties, so a child named
like an XMLNode method hides the method on its prototype. Let methods
take precedence over bare child names, as on the njs engine. Keep the
child accessible through $tag$name and avoid deleting it when deleting
the method name.

Cover collisions for all XMLNode methods and prototype changes.

6 weeks agoXML: add namespace-aware node accessors
Dmitry Volyntsev [Fri, 25 Sep 2026 01:34:22 +0000 (18:34 -0700)]
XML: add namespace-aware node accessors

The existing $attr$name and $tag$name accessors compare local names
only, so x:ID and ID, or x:Issuer and Issuer, are indistinguishable.

The new XMLNode methods getAttributeNS(namespaceURI, localName) and
getChildNS(namespaceURI, localName) match both the namespace URI and
the local name.

6 weeks agoQuickJS: fix XML value leaks in "in" checks
Dmitry Volyntsev [Fri, 25 Sep 2026 01:33:48 +0000 (18:33 -0700)]
QuickJS: fix XML value leaks in "in" checks

Previously, property existence checks such as "'$attr$a' in node",
"'to' in node", or "'$tags$to' in node" created the property value and
never released it when no property descriptor was requested.  A leaked
XMLNode also held a reference to its document, so the whole document
was never freed.

Also, qjs_xml_node_make() and qjs_xml_attr_make() incremented the
document reference count before creating the object, so a failed
JS_NewObjectClass() left the document permanently referenced.

The fix is to free the value when no descriptor is requested and to
take the document reference only after the object is created.

6 weeks agoTests: tighten shell test checks
Dmitry Volyntsev [Tue, 29 Sep 2026 01:31:09 +0000 (18:31 -0700)]
Tests: tighten shell test checks

Previously, Expect treated timeouts as successful matches, and EOF
passed even if the REPL exited abnormally.  Some checks could match
the input echo alone, while completion checks required a bell that
varies between libedit versions.

The fix is to fail on timeouts, premature EOF, and abnormal exit;
check the result and prompt for Unicode input and guard against a
repeated promise rejection.  Send Unicode as ASCII escapes to work
around libedit 3.1.snap20150325-2.12 on SLES 15 SP7, which reports
a character count where the readline callback expects a byte count.

6 weeks agoQuickJS: stop tracking rejections during final report
Dmitry Volyntsev [Mon, 28 Sep 2026 22:42:40 +0000 (15:42 -0700)]
QuickJS: stop tracking rejections during final report

Converting the rejection reason to a log string may reject another
promise.  Disable the tracker before reporting so new rejections
cannot leave references in the request context after its list has
been drained.

6 weeks agoShell: fix unhandled rejection report in QuickJS
Dmitry Volyntsev [Fri, 25 Sep 2026 05:07:14 +0000 (22:07 -0700)]
Shell: fix unhandled rejection report in QuickJS

Previously, njs_engine_qjs_unhandled_rejection() kept a pointer to
the rejected promise list while converting the first rejection reason
to a string.  The conversion may call user JavaScript code, which can
reject more promises and cause the list to be reallocated.  After that,
the old pointer was used together with the new item count to free the
promise and reason values, which caused out-of-bounds reads and invalid
frees.

The fix is to take a reference to the first reason and free the list
before conversion, so that rejections created during conversion are
tracked in a new list.

The test/js/promise_rejection_tracker_recursive.t.js test is no longer
skipped for the QuickJS engine.

6 weeks agoPromise: do not track handled rejections with no list
Dmitry Volyntsev [Mon, 28 Sep 2026 22:10:03 +0000 (15:10 -0700)]
Promise: do not track handled rejections with no list

Previously, when a handled promise was reported and the rejected promise
list was NULL, the trackers fell through and added it as unhandled.  In
the interactive shell, handling a promise after its rejection had been
reported caused another, false unhandled rejection.  The QuickJS nginx
module could do the same while formatting a rejection reason.

The fix is to return for every handled notification, removing the promise
from the list only when the list exists.  Apply this to both engines in the
shell and nginx module.

6 weeks agoZlib: copy QJS dictionaries before conversion
Dmitry Volyntsev [Tue, 22 Sep 2026 04:13:22 +0000 (21:13 -0700)]
Zlib: copy QJS dictionaries before conversion

Copy dictionary bytes before converting other JavaScript arguments so their
backing storage cannot be invalidated during zlib setup.

6 weeks agoZlib: validate NJS chunk sizes
Dmitry Volyntsev [Tue, 22 Sep 2026 04:03:53 +0000 (21:03 -0700)]
Zlib: validate NJS chunk sizes

6 weeks agoTypes: declare Buffer swap64 correctly
Dmitry Volyntsev [Tue, 22 Sep 2026 04:03:48 +0000 (21:03 -0700)]
Types: declare Buffer swap64 correctly

6 weeks agoXML: guard against content allocation failure
Dmitry Volyntsev [Tue, 22 Sep 2026 04:03:48 +0000 (21:03 -0700)]
XML: guard against content allocation failure

Report a memory error when libxml cannot allocate node content instead of
passing a NULL pointer to the string conversion path.

6 weeks agoCore: avoid float padding pointer wraparound
Dmitry Volyntsev [Tue, 22 Sep 2026 03:53:54 +0000 (20:53 -0700)]
Core: avoid float padding pointer wraparound

Skip padding when the formatted float is wider than its requested field.
This prevents unsigned width subtraction from forming an invalid pointer.

6 weeks agoFS: convert QJS numeric modes safely
Dmitry Volyntsev [Tue, 22 Sep 2026 03:37:09 +0000 (20:37 -0700)]
FS: convert QJS numeric modes safely

6 weeks agoCHB: avoid overflow when enforcing the chain limit
Dmitry Volyntsev [Tue, 22 Sep 2026 03:28:29 +0000 (20:28 -0700)]
CHB: avoid overflow when enforcing the chain limit

Check the requested size against the remaining chain capacity instead of
adding it to the current total.  This prevents the size check from wrapping
and bypassing the configured chain limit.

6 weeks agoBuffer: free QJS buffers on conversion failures
Dmitry Volyntsev [Thu, 24 Sep 2026 22:53:56 +0000 (15:53 -0700)]
Buffer: free QJS buffers on conversion failures

qjs_buffer_from_string() and qjs_buffer_create() leaked the newly
allocated buffer when qjs_typed_array_data() or string decoding failed.

Also check the JS_ToCStringLen() result in qjs_buffer_from_string(), so
an allocation failure is not followed by a NULL pointer dereference.

6 weeks agoBuffer: reject oversized QJS alloc() and concat() sizes
Dmitry Volyntsev [Thu, 24 Sep 2026 22:53:30 +0000 (15:53 -0700)]
Buffer: reject oversized QJS alloc() and concat() sizes

Buffer.alloc() and Buffer.concat() converted sizes with JS_ToUint32(),
which wraps values modulo 2^32: Buffer.alloc(2 ** 32 + 16) returned a
16-byte buffer.  Use JS_ToIndex() instead, so these sizes throw a
RangeError.

In Buffer.concat(), the total length of the list could also overflow the
32-bit accumulator.  Keep it in 64 bits and check it against
QJS_BUFFER_MAX_LENGTH.  The overflow now throws a RangeError instead of a
TypeError, as in Node.js.

6 weeks agoBuffer: convert QJS array-like lengths
Dmitry Volyntsev [Tue, 22 Sep 2026 03:28:29 +0000 (20:28 -0700)]
Buffer: convert QJS array-like lengths

Convert array-like lengths through checked floating-point conversion before
applying ToLength semantics.  This avoids reading float-tagged QuickJS
numbers through the integer accessor.

The length is kept as a 64-bit value and passed to qjs_buffer_alloc()
unchanged, so an oversized length is rejected before any allocation
instead of being truncated.

6 weeks agoBuffer: check QJS allocation sizes before narrowing
Dmitry Volyntsev [Thu, 24 Sep 2026 22:48:09 +0000 (15:48 -0700)]
Buffer: check QJS allocation sizes before narrowing

Take a 64-bit size in qjs_buffer_alloc() and reject sizes above
QJS_BUFFER_MAX_LENGTH before converting them.  Previously, a caller
holding a wider size had to narrow it to size_t first, which truncated
it silently on 32-bit platforms.

6 weeks agoTests: improve compatibility with Node.js
Dmitry Volyntsev [Tue, 22 Sep 2026 03:16:38 +0000 (20:16 -0700)]
Tests: improve compatibility with Node.js

Accept RangeError from Buffer.concat() revalidation.
Remove oversized Buffer.from() cases that make Node.js allocate 4 GiB.
Expect empty Buffer.fill() values to fail.
Use fs.rm() for recursive removal under Node.js.
Check non-enumerable fs.Stats date properties directly.
Use distinct readlink fixtures for concurrently run test cases.

Known failures for js_test --binary=node are limited to module
resolution in import and XML module tests for v25.2.1 on linux.

6 weeks agoFS: fix typo during BytesRead prototype instantiation
Dmitry Volyntsev [Tue, 22 Sep 2026 02:05:31 +0000 (19:05 -0700)]
FS: fix typo during BytesRead prototype instantiation

6 weeks agoPromise: reject empty any inputs
Dmitry Volyntsev [Tue, 22 Sep 2026 04:56:00 +0000 (21:56 -0700)]
Promise: reject empty any inputs

Reject Promise.any() with an AggregateError when the input contains no
promises, as required by the promise combinator semantics.

6 weeks agoTests: update Test262 and allow unhandled rejections
Dmitry Volyntsev [Tue, 22 Sep 2026 04:55:31 +0000 (21:55 -0700)]
Tests: update Test262 and allow unhandled rejections

Track the current Test262 revision and let the Test262 runner follow the
suite's handling of rejected promises instead of imposing a separate policy.

6 weeks agoTests: rename local JavaScript test target
Dmitry Volyntsev [Tue, 22 Sep 2026 00:39:35 +0000 (17:39 -0700)]
Tests: rename local JavaScript test target

Previously, the local JavaScript tests, including WebCrypto tests, were
run by the test262 target, while the official TC39 Test262 suite was
run by njs_test262_test.

The fix is to rename the local test target to js_test and make test262
run the official suite.  The engine-specific local targets are renamed
to js_test_njs and js_test_quickjs.

6 weeks agoWebcrypto: clean up ECDSA conversion failures
Dmitry Volyntsev [Tue, 22 Sep 2026 00:04:12 +0000 (17:04 -0700)]
Webcrypto: clean up ECDSA conversion failures

While here, also report ECDSA signature encoding failures.

6 weeks agoWebcrypto: clean up AES-CTR allocation failures
Dmitry Volyntsev [Mon, 21 Sep 2026 23:23:09 +0000 (16:23 -0700)]
Webcrypto: clean up AES-CTR allocation failures

6 weeks agoWebcrypto: use constant-time HMAC verification
Dmitry Volyntsev [Mon, 21 Sep 2026 23:22:45 +0000 (16:22 -0700)]
Webcrypto: use constant-time HMAC verification

6 weeks agoWebcrypto: validate EC JWK curve type
Dmitry Volyntsev [Mon, 21 Sep 2026 23:22:24 +0000 (16:22 -0700)]
Webcrypto: validate EC JWK curve type

7 weeks agoMemory pool: cached slot index in a page
Dmitry Volyntsev [Tue, 8 Sep 2026 22:50:16 +0000 (15:50 -0700)]
Memory pool: cached slot index in a page

Previously, freeing a chunk searched the pool slots linearly to map the
chunk size back to its slot.

The fix is to keep the slot index in the page descriptor, in a byte
that was unused.

7 weeks agoMemory pool: free page counter in a cluster
Dmitry Volyntsev [Tue, 8 Sep 2026 22:48:47 +0000 (15:48 -0700)]
Memory pool: free page counter in a cluster

Previously, releasing a page walked all pages of its cluster to test
whether the whole cluster had become free.

The fix is to keep the number of free pages in the cluster block.

7 weeks agoMemory pool: junk filling only in debug builds
Dmitry Volyntsev [Tue, 8 Sep 2026 22:46:52 +0000 (15:46 -0700)]
Memory pool: junk filling only in debug builds

Previously, every freed chunk was overwritten with 0x5A in all builds,
adding a memset to each njs_mp_free() call.

The fix is to keep the junk filling under NJS_DEBUG.  Release builds no
longer poison freed chunks; nothing ever inspected the pattern.

7 weeks agoMemory pool: compare block addresses without overflow
Dmitry Volyntsev [Thu, 17 Sep 2026 23:47:35 +0000 (16:47 -0700)]
Memory pool: compare block addresses without overflow

Previously, the rbtree comparator converted the difference between two
addresses to intptr_t.  On 32-bit systems, addresses that differ by at
least 2 GiB could produce an incorrect ordering and prevent a later block
lookup from finding its cluster.

The fix is to compare the addresses directly and return -1, 0, or 1.
This fixes a pool that remained non-empty after all allocations were freed
in the x86 memory pool test.

7 weeks agoTests: memory pool unit test
Dmitry Volyntsev [Fri, 18 Sep 2026 00:11:36 +0000 (17:11 -0700)]
Tests: memory pool unit test

Previously, the memory pool was covered only indirectly by a single
flathsh configuration.

The fix is to add direct coverage for allocation, reuse, alignment, and
mixed allocation sizes across valid pool configurations.  The test uses a
smaller workload in the regular matrix and a full stress workload in the
ASan job.

Library tests are included in the standard test target.  The added ASan
configuration omits debug memory so that the complete test target and the
pool page and cluster paths are exercised.  The library-test dependency
rule is also corrected for the new test executable.

7 weeks agoAvoid undefined shifts in MurmurHash2
Dmitry Volyntsev [Thu, 17 Sep 2026 21:51:00 +0000 (14:51 -0700)]
Avoid undefined shifts in MurmurHash2

Previously, input bytes were promoted to signed int before being shifted
into a hash word.  A byte with its high bit set could make the 24-bit
shift undefined.

The fix is to cast input bytes to uint32_t before shifting.  This
preserves the hash result and avoids undefined behavior under UBSan.

7 weeks agoFetch: limit decoded chunked response bodies
Dmitry Volyntsev [Wed, 16 Sep 2026 00:45:14 +0000 (17:45 -0700)]
Fetch: limit decoded chunked response bodies

Previously, fetch allowed decoded chunked bodies to reach ten times
max_response_body_size.

The fix is to apply max_response_body_size to decoded chunks.

7 weeks agoFetch: avoid reusing framed response connections
Dmitry Volyntsev [Wed, 16 Sep 2026 00:42:55 +0000 (17:42 -0700)]
Fetch: avoid reusing framed response connections

Previously, fetch cached fixed-length responses with bytes after their
declared body. Bodyless and chunked responses with such bytes did not
complete correctly.

The fix is to close a framed response connection with unconsumed bytes
instead of adding it to the keepalive cache.

7 weeks agoFetch: honor response body framing
Dmitry Volyntsev [Wed, 16 Sep 2026 00:35:30 +0000 (17:35 -0700)]
Fetch: honor response body framing

Previously, fetch inferred body completion from Content-Length alone. This
completed close-delimited responses before EOF and conflated framing modes.

The fix is to track body framing explicitly, reject unsupported 1xx
responses, and exempt 204 and 304 representation lengths from body limits.

7 weeks agoFetch: validate response framing
Dmitry Volyntsev [Tue, 15 Sep 2026 22:46:22 +0000 (15:46 -0700)]
Fetch: validate response framing

Previously, fetch accepted conflicting Content-Length fields and a response
with both Transfer-Encoding and Content-Length.

The fix is to reject ambiguous framing to avoid interpreting the response
body inconsistently.

7 weeks agoFetch: validate response body size
Dmitry Volyntsev [Tue, 15 Sep 2026 22:45:50 +0000 (15:45 -0700)]
Fetch: validate response body size

Previously, a negative max_response_body_size could make a close-delimited
response appear as a successful empty body.

The fix is to reject the invalid option while parsing fetch arguments.

7 weeks agoModules: fix js_periodic worker affinity parsing
Dmitry Volyntsev [Tue, 15 Sep 2026 22:24:06 +0000 (15:24 -0700)]
Modules: fix js_periodic worker affinity parsing

Previously, worker_affinity parsing reused the directive option index. This
skipped options after worker_affinity in HTTP and stream configurations.

The fix is to use a separate index while parsing the affinity mask.

7 weeks agoFetch: fix QJS numeric conversion
Dmitry Volyntsev [Tue, 15 Sep 2026 22:24:06 +0000 (15:24 -0700)]
Fetch: fix QJS numeric conversion

Previously, QuickJS wrote an int64_t through ngx_int_t fetch fields.

The fix is to use the checked native conversion helper so ILP32 builds
cannot overwrite adjacent state.

8 weeks agoTypedArray: coerce length argument once on buffer construction
basavaraj-sm05 [Wed, 9 Sep 2026 06:56:41 +0000 (12:26 +0530)]
TypedArray: coerce length argument once on buffer construction

Previously, njs_typed_array_alloc() passed the length argument through
njs_value_to_index() twice, before and after the detached-buffer check,
and kept only the second result.  The argument's valueOf() was invoked
twice, which is observable.

The fix is to remove the second conversion.

8 weeks agoTest262: add a native runner for the official suite
Dmitry Volyntsev [Wed, 9 Sep 2026 01:44:42 +0000 (18:44 -0700)]
Test262: add a native runner for the official suite

8 weeks agoCI: test njs with UBSan
Dmitry Volyntsev [Sat, 12 Sep 2026 05:39:30 +0000 (22:39 -0700)]
CI: test njs with UBSan

Build and run the base njs test suite with UndefinedBehaviorSanitizer.

8 weeks agoAlign opaque values
Dmitry Volyntsev [Sun, 13 Sep 2026 19:53:30 +0000 (12:53 -0700)]
Align opaque values

Opaque values are cast to njs_value_t pointers by the public API.  Match
njs_value_t alignment while retaining the four uint32_t filler fields.

UndefinedBehaviorSanitizer reports unaligned njs_value_t accesses.

8 weeks agoCHB: avoid incompatible function pointer calls
Dmitry Volyntsev [Sat, 12 Sep 2026 05:25:53 +0000 (22:25 -0700)]
CHB: avoid incompatible function pointer calls

Use adapters for memory pool allocators instead of casting their function
pointers to the generic chain buffer callback types.

Casting does not make an indirect call through an incompatible function
pointer type valid.  UndefinedBehaviorSanitizer reports these calls.

8 weeks agoDate: avoid out-of-range conversion in timezone offset
Dmitry Volyntsev [Sat, 12 Sep 2026 05:29:38 +0000 (22:29 -0700)]
Date: avoid out-of-range conversion in timezone offset

The timezone offset helper converted a double to int64_t before checking
whether it was representable.  Check the range first.

UndefinedBehaviorSanitizer reports the out-of-range conversion.

8 weeks agoDate: fix UTC setters
Dmitry Volyntsev [Sat, 12 Sep 2026 03:33:48 +0000 (20:33 -0700)]
Date: fix UTC setters

The Date setters always converted their result as local time.  Use the
setter's UTC flag instead.

Remove the intermediate range check in njs_make_date().  TimeClip already
checks the result after local-to-UTC conversion, which can move a value
across the valid time range boundary.

8 weeks agoFix the conversion of a property key to an array index
Dmitry Volyntsev [Wed, 9 Sep 2026 01:44:05 +0000 (18:44 -0700)]
Fix the conversion of a property key to an array index

Previously, a double was converted to uint32_t before its range was tested,
so a property key that is NaN, negative or larger than 2^31 made the
conversion undefined.  UndefinedBehaviorSanitizer reported it for every VM
creation, where an environment variable name is atomized.

The fix is to test the range first and to share one njs_number_is_atom_index()
helper between the four places that repeated the pattern.

8 weeks agoVM: remove dead code introduced in fd54d52de557
Dmitry Volyntsev [Sat, 12 Sep 2026 01:45:23 +0000 (18:45 -0700)]
VM: remove dead code introduced in fd54d52de557

The lambda structure is allocated at the end of module compilation,
and if the allocation fails, lambda is NULL.  Since nothing fails after
a successful allocation, lambda is always NULL on the failed path.

Found by Coverity (CID 1701143).

2 months agoTypes: align XML declarations with API
Dmitry Volyntsev [Thu, 10 Sep 2026 22:42:06 +0000 (15:42 -0700)]
Types: align XML declarations with API

Previously, XML declarations did not accurately model XML node property
values, mutation deletion semantics, or canonicalization aliases.

The fix is to align XMLDoc and XMLNode types with the current API and
add TypeScript coverage for the supported operations.

2 months agoQuickJS: align XML tag assignment with njs
Dmitry Volyntsev [Thu, 10 Sep 2026 23:02:35 +0000 (16:02 -0700)]
QuickJS: align XML tag assignment with njs

Previously, assigning null or undefined to node.$tag$name removed
matching children in QuickJS, while the njs engine rejected every
assignment.

The fix is to distinguish property assignment from deletion in the
QuickJS exotic property handler. Assignments now throw TypeError in both
engines, while delete node.$tag$name remains supported.

2 months agoXML: retain QuickJS tag sources during copy
Dmitry Volyntsev [Thu, 10 Sep 2026 03:26:26 +0000 (20:26 -0700)]
XML: retain QuickJS tag sources during copy

2 months agoXML: mutate XML nodes in place
Dmitry Volyntsev [Thu, 10 Sep 2026 03:26:26 +0000 (20:26 -0700)]
XML: mutate XML nodes in place

Previously, text and structural mutators copied and replaced the receiver's
libxml2 node.  Existing XMLNode wrappers continued to point at the detached
original, while subsequent lookups returned the replacement.  Wrappers for
children preserved by the operation likewise pointed into the detached old
subtree.  In QuickJS, mutating the detached receiver again could dereference
the NULL result of xmlReplaceNode().

The fix is to modify the existing receiver's child list directly.  Existing
wrappers and subsequent lookups therefore continue to refer to the same
libxml2 node.  Removed child lists remain readable until the QuickJS
document or njs VM is destroyed.  A node is considered live only when
following its parent links reaches its xmlDoc.  Replacement children and
cleanup records are prepared before the live tree is modified.

Changed behaviour:
- A removed child has an undefined $parent, while its descendants retain
  their internal parent links.
- Mutating or canonicalizing a removed XMLNode throws TypeError.
- Text and attribute values must be primitive strings; null and undefined
  retain their deletion semantics.
- C14N rejects an excluding node from another document or a detached list.
- addChild() and $tags reject namespace-bearing or unsupported source trees
  before mutation.
- Named $tags assignments are consistently rejected by both engines.

While here, use an unsigned QuickJS $tags array length, support assigning
$tags on an empty njs XMLNode, and make deleting $tags from an empty node
a no-op.

2 months agoXML: create text nodes before mutation
Dmitry Volyntsev [Thu, 10 Sep 2026 00:04:47 +0000 (17:04 -0700)]
XML: create text nodes before mutation

Previously, text updates encoded content manually and then passed it to
libxml2. This duplicated libxml2 escaping rules and allocated the
encoded buffer before creating the text node.

The fix is to create a libxml2 text node from the unencoded value before
replacing the copied node's children. libxml2 now owns escaping, and a
failure leaves the original XML tree unchanged.

2 months agoGenerator: bound value preservation analysis
Dmitry Volyntsev [Fri, 4 Sep 2026 01:09:17 +0000 (18:09 -0700)]
Generator: bound value preservation analysis

Value-preservation analysis recursively examines the upcoming expression before
the generator decides whether mutable-slot operands need temporary copies.
Deeply nested binary expressions and arrays could therefore exhaust the native
stack.

Move the generator-specific predicate out of the parser API and limit its
recursive walk to 128 levels.  At the limit, conservatively require
preservation: an extra move is safe, whereas missing a deeper side effect can
change evaluation semantics.

2 months agoVM: clean up failed module compilation
Dmitry Volyntsev [Thu, 3 Sep 2026 15:53:25 +0000 (08:53 -0700)]
VM: clean up failed module compilation

Module compilation publishes a provisional module before parsing to support
recursive loading.  Remove that module on failure and release code records,
line maps, and code buffers produced after the compilation checkpoint so a
failed load does not poison subsequent lookups.

2 months agoVM: clean up failed AST serialization
Dmitry Volyntsev [Thu, 3 Sep 2026 15:53:25 +0000 (08:53 -0700)]
VM: clean up failed AST serialization

2 months agoParser: release non-global semantic state
Dmitry Volyntsev [Thu, 3 Sep 2026 15:53:14 +0000 (08:53 -0700)]
Parser: release non-global semantic state

Allocate non-global scopes, local variables, variable tree nodes, references,
and declaration arrays from the compilation pool.  The global scope and its
symbol table remain persistent for runtime lookup and later compilation.

Build closure-index arrays temporarily, then copy the finalized indexes into
VM-owned storage referenced by each lambda.  Preserve declaration-origin type
without retaining pointers to released block scopes.

2 months agoGenerator: release temporary compilation memory
Dmitry Volyntsev [Thu, 3 Sep 2026 15:53:14 +0000 (08:53 -0700)]
Generator: release temporary compilation memory

Use the parser invocation pool for generator stack entries, copied contexts,
blocks, jump patches, index caches, and traversal scratch.  Nested function
generators share the same temporary pool, while generated code and runtime
metadata remain in the VM pool.

2 months agoParser: release temporary compilation memory
Dmitry Volyntsev [Thu, 3 Sep 2026 15:53:03 +0000 (08:53 -0700)]
Parser: release temporary compilation memory

Create a pool for each parser invocation and allocate parser nodes, array-item
storage, continuations, preread tokens, and active labels from it.  Destroy the
pool after generation and on parser or generator failures.

Keep atoms, lambdas, regexp patterns, scopes, variables, bytecode, and runtime
constants in the VM pool.  Clear persistent scope AST roots before releasing
the temporary pool.

2 months agoParser: shrink variable flags
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Parser: shrink variable flags

2 months agoParser: store statement labels as atom identifiers
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Parser: store statement labels as atom identifiers

2 months agoParser: use compact active labels
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Parser: use compact active labels

2 months agoParser: reduce variable reference payload
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Parser: reduce variable reference payload

Store the atom identifier and resolved variable directly in parser nodes and
move the undefined-reference flag into the common node flags.  This removes the
separately allocated reference object and helps reduce parser nodes to the
64-byte memory-pool class.

2 months agoParser: compact array literal elements
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Parser: compact array literal elements

The generic object-property lowering used six parser nodes and several
generator continuations for each array element.  Store array values and indexes
in compact records and emit a dedicated immediate-index initializer instead.

This also avoids interning numeric element indexes and generates elements in
source order with constant continuation depth.  Keep template backing arrays
flat because template concatenation requires contiguous storage.

2 months agoFix large array literal initialization
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Fix large array literal initialization

Large literals switch to slow arrays above the fast-array capacity limit, but
the previous initialization path left their indexed values unset.  Initialize
slow arrays directly so literals above 32760 elements preserve their contents.

2 months agoTests: add parser stress benchmarks
Dmitry Volyntsev [Thu, 3 Sep 2026 00:39:41 +0000 (17:39 -0700)]
Tests: add parser stress benchmarks

Add generated workloads for large array literals, declarations, references,
closures, and labels.  These tests are intended to expose catastrophic parser
scaling and compare compilation changes; source construction and Function()
compilation are both included in the measured interval.

2 months agoFix Array.prototype.sort() with huge sparse arrays
Dmitry Volyntsev [Fri, 4 Sep 2026 01:48:39 +0000 (18:48 -0700)]
Fix Array.prototype.sort() with huge sparse arrays

Previously, sort deleted every index from the compacted output position to
the original length.  A sparse array with length near 2^32 therefore caused
billions of property-delete operations and unbounded memory growth.

The fix is to retain the direct cleanup loop for fast arrays and, for slow
arrays, enumerate and delete only existing indexed properties in the range.

The issue was introduced in 1a2a65f7 (0.8.0).

This closes #1095 issue on GitHub.

2 months agoFix reentrant object-to-primitive conversion
Dmitry Volyntsev [Fri, 4 Sep 2026 01:48:39 +0000 (18:48 -0700)]
Fix reentrant object-to-primitive conversion

Previously, njs_value_to_primitive() retained a pointer to the caller's value
slot while valueOf(), toString(), or property getters executed user code.
Reentrant code could overwrite that slot, after which the conversion treated
the replacement primitive as the original object.

The fix is to snapshot the input value before the first observable property
lookup and use the stable copy throughout conversion.

The issue was introduced in 64dbf104 (0.3.4).

This closes #1094 issue on GitHub.

2 months agoTypedArray: fix numeric property definitions
Dmitry Volyntsev [Fri, 4 Sep 2026 01:48:39 +0000 (18:48 -0700)]
TypedArray: fix numeric property definitions

Previously, integer-indexed exotic validation was restricted to keys marked
as strings.  Numeric atoms and the canonical string "NaN" could bypass the
check and be installed as ordinary typed-array properties.

The fix is to validate numeric atoms and canonical numeric strings after key
conversion, and to remove the obsolete source-type flag.

The numeric-key bypass was introduced in b28e50b1 (0.9.0); the incomplete
canonical-index handling dates to bf208048 (0.3.8).

This closes #1098 issue on GitHub.

2 months agoRegex: fix decoding of named capture indexes
Dmitry Volyntsev [Fri, 4 Sep 2026 01:48:11 +0000 (18:48 -0700)]
Regex: fix decoding of named capture indexes

Previously, PCRE name-table capture indexes were decoded through plain char.
On signed-char targets, bytes with the high bit set were sign-extended and
produced a wild or incorrect capture index used to read a match array.

The fix is to decode unsigned bytes through a checked output parameter and to
validate every named capture index against the compiled capture count.

The issue was introduced in 9496944a (0.3.2).

This closes #1088 issue on GitHub.

2 months agoFix Object.defineProperty() with reentrant descriptors
Dmitry Volyntsev [Fri, 4 Sep 2026 01:48:11 +0000 (18:48 -0700)]
Fix Object.defineProperty() with reentrant descriptors

Previously, njs_object_prop_define() retained a pointer to a target hash
property while descriptor getters executed user code.  Reentrant mutation of
the target could resize the flat hash and leave the retained pointer dangling.
Descriptor getters were also evaluated again after fast-array conversion.

The fix is to convert the descriptor before querying the target and to place
the retry point after descriptor conversion.

The issue was introduced in 2f288a9c (0.9.1).

This closes #1089 issue on GitHub.

2 months agoFix Array.prototype.concat() with sparse arrays
Dmitry Volyntsev [Fri, 4 Sep 2026 01:47:58 +0000 (18:47 -0700)]
Fix Array.prototype.concat() with sparse arrays

Previously, the fast concat path represented a missing source property with
njs_value_invalid and still created the corresponding destination property.
For a slow sparse destination this materialized an internal hole sentinel as
a normal property, which later property access could interpret as an invalid
accessor.

The fix is to advance the destination index without creating a property when
the source lookup reports a hole.

The issue was introduced in 232fb594 (0.7.2).

This closes #1095 issue on GitHub.

2 months agoFix indexed fast paths for exotic objects
Dmitry Volyntsev [Fri, 4 Sep 2026 01:47:58 +0000 (18:47 -0700)]
Fix indexed fast paths for exotic objects

Previously, Object.values() and Object.entries() erased the concrete type of
an exotic receiver, while indexed property fast paths accepted any object
with the fast_array bit.  Typed arrays could consequently be interpreted as
njs_array_t objects, causing invalid memory reads and writes.

The fix is to preserve the receiver type during enumeration and to use
njs_is_fast_array() for both indexed get and set fast paths.

The issue was introduced in 69072164 (0.8.6); the overbroad fast path dates
to 912ab387 (0.3.8).

This closes #1098 issue on GitHub.

2 months agoRegExp: fix cleanup of custom exec results
Dmitry Volyntsev [Fri, 4 Sep 2026 01:47:58 +0000 (18:47 -0700)]
RegExp: fix cleanup of custom exec results

Previously, RegExp.prototype[Symbol.replace]() and Symbol.split() used
non-NULL exotic slots as proof that an exec() result was an internally
allocated match array.  A custom exec() could return any exotic object, which
was then cast to an array and freed, corrupting unrelated VM state.

The fix is to record whether RegExpBuiltinExec produced each result and to
release only fresh builtin results that could not escape through user code.

The replace issue was introduced in f7813172 (0.8.2), and the split issue was
introduced in aa697e3e (0.8.2).

This closes #1090, #1091, #1092, #1093, #1096, and #1097 issues on GitHub.

2 months agoFix the runtime model in the agent developer guide
Dmitry Volyntsev [Thu, 27 Aug 2026 22:32:56 +0000 (15:32 -0700)]
Fix the runtime model in the agent developer guide

The guide claimed that module scope survives between requests on the same
worker and that module imports load once per worker.  Both are wrong.
js_import only compiles at configuration time; imported module bodies run
when a context is created.

While there, corrected the integration points:

*) js_periodic receives a periodic session rather than nothing, and lives
   in server { } for stream;

*) js_filter is installed in the stream filter chain rather than as a
   phase handler, and cannot call s.done() once filtering started;

*) stream js_access and js_preread may leave pending events, while stream
   js_set may not;

*) "no async support" means no pending nginx events rather than no async
   keyword;

*) HTTP has no js_filter directive, stream js_set receives s, and
   ngx.shared is cross-worker rather than process-wide and holds only
   strings and numbers.

2 months agoGenerator: select a for-in route by the target's own token type
Dmitry Volyntsev [Tue, 1 Sep 2026 14:42:49 +0000 (07:42 -0700)]
Generator: select a for-in route by the target's own token type

Previously, njs_generate_for_in_statement() worked out what a for-in header
declared from the shape of the tree: a declaration was recognised as a
STATEMENT wrapper whose right child held the kind, and the kind and the name
were reached through that wrapper.  The wrapper was navigated again in the
object and body states, and njs_generate_for_let_update() walked it to find
lexical bindings.  Nothing named the contract, and the property route used
the same "foreach->left->right" to mean the property key.

The fix is to let the parser put the declaration node under the "in" node
directly, so the target's token type says what it is: VAR, LET or CONST for a
declaration, PROPERTY or PROPERTY_REF for a member target, and NAME for a
plain one.  An unrecognised target is now an internal error instead of
falling into the plain-name route.  njs_generate_let_update() takes the
single declaration a for-in header has, and njs_generate_for_let_update()
keeps the list walk the classic "for" needs.

The instruction stream is unchanged.  The code map for PROPERTY_NEXT on the
declaration route now carries the line of the declaration instead of the zero
the removed wrapper held; backtraces are unaffected.

2 months agoParser: classify a "for" declaration header like an expression one
Dmitry Volyntsev [Tue, 1 Sep 2026 14:26:12 +0000 (07:26 -0700)]
Parser: classify a "for" declaration header like an expression one

Previously, njs_parser_for_var_binding_or_var_list() peeked one token past a
declaration binding and, on "in", built the for-in nodes itself, consumed the
tokens and used its own right-hand-side and body states.  A declaration
header therefore never reached the classifier the expression header uses, and
the two carried separate copies of the same completion.

The fix is to parse an identifier binding once under [~In] and let
njs_parser_for_var_in_of_expression() classify the token it stopped at.  The
"in" case now accepts a declaration result, checks there that a for-in
declaration has one binding and no initializer, and continues through the
same right-hand-side, body and NJS_TOKEN_FOR_IN construction as an
expression target.  njs_parser_for_var_in_statement() and its completion go
with the duplication, and the restricted-identifier check is left to the
ordinary declaration parser, which reports it identically.

A binding pattern still leaves this route before the classifier, into the
handlers that report it as unsupported.

The tree shape is unchanged, so the generator still selects a for-in route
the way it did, and the emitted code is identical.  The declaration node now
takes its source line from the ordinary declaration parser, so a header split
across lines reports the line of "in" rather than of the binding; this shows
only in the AST dump, not in generated code or backtraces.

2 months agoGenerator: stop parking a for-in index in the AST
Dmitry Volyntsev [Tue, 1 Sep 2026 14:01:44 +0000 (07:01 -0700)]
Generator: stop parking a for-in index in the AST

Previously, the declaration route of a for-in loop stored the resolved
variable index in the AST wrapper njs_parser_node_t, and read it back one
generator state later.  The other two routes keep the same value in
ctx->index_next_value, a field njs_generator_loop_ctx_t already has for it.
The inconsistency was the only difference between
njs_generate_for_in_body_wo_decl() and njs_generate_for_in_body(): the rest
was duplicated verbatim, including a guard that is dead in the copy.

The fix is to use the context on the declaration route as well and delete the
duplicate.  One AST field is no longer written by the generator, and one of
the three shape-selected for-in paths is gone.  The emitted code is unchanged.

2 months agoParser: carry the declaration kind across nested parsing
Dmitry Volyntsev [Tue, 1 Sep 2026 13:46:16 +0000 (06:46 -0700)]
Parser: carry the declaration kind across nested parsing

Previously, the kind of a declaration being parsed lived in a mutable
parser->var_type, written once when "var", "let" or "const" was seen and read
for every declarator in the list.  A declaration nested in a declarator
initializer overwrote it, so the declarators after it were recorded with the
wrong kind and lexical ones lost their per-iteration binding:

    let out = [];
    for (let a = function() { var z; }, b = 0; b < 2; b++) out.push(() => b);
    out[0]() + ',' + out[1]()

gave "2,2" instead of "0,1", because "b" was recorded as a var.

The fix is to carry the kind in njs_parser_stack_entry_t, as the [In]
parameter already is, so a continuation restores the kind in effect when it
was pushed.  njs_parser_stack_entry_t does not grow.

2 months agoParser: remove the dead cover parenthesized production
Dmitry Volyntsev [Tue, 1 Sep 2026 04:49:27 +0000 (21:49 -0700)]
Parser: remove the dead cover parenthesized production

Previously, njs_parser_cover_parenthesized_expression(), its two
continuations and njs_parser_binding_identifier_pattern() formed a closed
island: every reference to them came from inside it, and the only thing
linking it to the parser was a "(void)" cast left with a TODO in 2020.  It
honours neither contract this series introduces.  Two of its states are
reject landing states that never inspected parser->ret, so with a ")" or a
"," in hand they would consume the token and pop the stack after a reject.
Its parenthesized expression opens no [+In] region.

The fix is to delete the island rather than repair it.  Arrow parameters are
not going to be completed through this production, and the binding pattern
entry points it dispatched to are stubs reachable on their own.

2 months agoParser: remove the use_lhs parser flag
Dmitry Volyntsev [Tue, 1 Sep 2026 01:32:50 +0000 (18:32 -0700)]
Parser: remove the use_lhs parser flag

Previously, use_lhs told njs_parser_exponentiation_expression() to skip the
unary part and njs_parser_assignment_expression() to skip the arrow check, so
that an expression could resume from an already parsed left-hand side.

The fix is to remove it: its only writer went with the speculative "for"
initializer parse.

2 months agoParser: parse the "for" initializer once
Dmitry Volyntsev [Tue, 1 Sep 2026 04:07:06 +0000 (21:07 -0700)]
Parser: parse the "for" initializer once

Previously, the initializer was parsed twice: speculatively as a
LeftHandSideExpression so that the token after it could be inspected, and
then the rest of the expression was grafted onto that left-hand side by four
continuation states.  A dispatch list and an arrow pre-check existed only to
keep inputs away from the speculative parse.

The fix is to let [~In] end the initializer: the "in" operator is no longer
part of the expression, so the header is a single <Expression> and
njs_parser_for_var_in_of_expression(), which already classified the other
routes, classifies this one too.  Its post-hoc for-in detection goes with it.

A declaration header stopping at "in" now reports an invalid target instead
of an unexpected token:

    for (var a, b in []);

The first token's text was carried through the header only to be quoted in
the removed diagnostic, so the allocation goes with it.

2 months agoParser: report an invalid "for-in" target as a SyntaxError
Dmitry Volyntsev [Tue, 1 Sep 2026 01:31:20 +0000 (18:31 -0700)]
Parser: report an invalid "for-in" target as a SyntaxError

Previously, an invalid for-in target was a ReferenceError when the
initializer happened to parse as a LeftHandSideExpression and a SyntaxError
otherwise, so the same target changed class once parenthesized:

    for (a = 1 in {});     SyntaxError
    for ((a = 1) in {});   ReferenceError

The fix is to report a SyntaxError for both, as the specification and V8 do.
The type is observable through Function(), so this is a compatibility change.
njs keeps ReferenceError for invalid assignment targets elsewhere.

2 months agoParser: own the [In] grammar parameter
Dmitry Volyntsev [Tue, 1 Sep 2026 01:30:37 +0000 (18:30 -0700)]
Parser: own the [In] grammar parameter

Previously, the [In] parameter lived in the lexer, in a byte array indexed by
bracket nesting depth.  The lexer advanced the depth while prereading, the
parser wrote at the depth it had consumed to, so a delimiter in a conditional
consequent left the write in the wrong slot and the restriction was lost:

    for (a ? (b) : c in d; false;);

The fix is to carry the parameter in njs_parser_stack_entry_t: a frame
records the value in effect when it was pushed and restores it when popped,
including on the njs_parser_reject() landing frame.  An arrow initializer now
gets [~In] as well, which it escaped before.

This also allows to remove the [In] support from the lexer. The parser
owns the parameter now. njs_lexer_t drops from 104 to 80 bytes and no longer
allocates.

2 months agoParser: fix "for" initializer parsing after a failed speculative parse
Dmitry Volyntsev [Tue, 1 Sep 2026 01:29:41 +0000 (18:29 -0700)]
Parser: fix "for" initializer parsing after a failed speculative parse

Previously, the "for" initializer was parsed speculatively as a
LeftHandSideExpression, and njs_parser_for_expression_map_reparse() resumed
parsing whenever that parse produced no node.  njs_parser_reject() rewinds
the state stack only, so the resume continued from a later token with an
abandoned function scope still on parser->scope, and generated bytecode with
a scope that is never instantiated at run time.

The fix is to remove that landing state together with its frame, so a
rejected speculative parse reports a SyntaxError, and to dispatch the
initializer starts that cannot begin a LeftHandSideExpression to <Expression>
directly.  Routing "await" through that dispatch also applies [~In] to it, so
a bare "in" in an await initializer is now a SyntaxError.

Found by OSS-Fuzz.