]> git.kaiwu.me - njs.git/log
njs.git
25 hours agoModules: clear QuickJS hooks before a context is reused master
Dmitry Volyntsev [Thu, 12 Nov 2026 01:37:06 +0000 (17:37 -0800)]
Modules: clear QuickJS hooks before a context is reused

Previously, an exit hook installed during context teardown, for example
by a promise job queued by the exit hook, was kept in the context.  When
the context was reused, the hook was called during the teardown of a
later request.

Now hooks are cleared together with the pending events, after the last
user code of the request has run.

The issue was introduced in 04f6dfb9 (0.9.2).

25 hours agoQuickJS: find the exit hook without the "njs" global property
Dmitry Volyntsev [Thu, 12 Nov 2026 01:36:24 +0000 (17:36 -0800)]
QuickJS: find the exit hook without the "njs" global property

Previously, the exit hook was found through the "njs" global property.
If a script replaced or deleted globalThis.njs, the hook installed with
njs.on() was not called, unlike with the njs engine, where hooks are
kept in the VM.  A getter on the property was also run as user code
during context teardown.

Now the njs object is kept in a class prototype slot of the context,
the same way as the Uint8Array constructor in the Buffer module, and
the exit hook is found there.

25 hours agoQuickJS: fix njs.on() replacing the exit hook during its call
Dmitry Volyntsev [Thu, 12 Nov 2026 01:34:31 +0000 (17:34 -0800)]
QuickJS: fix njs.on() replacing the exit hook during its call

Previously, the exit hook was released after the call while the caller
kept no reference to it.  If the hook replaced or reset itself with
njs.on(), the function was freed while it was still running, which
resulted in use-after-free.

Now the hook is detached from the njs object before the call, and the
call owns the reference.

The issue was introduced in 04f6dfb9 (0.9.2).

25 hours agoQuickJS: fix njs.on() with a foreign receiver
Dmitry Volyntsev [Thu, 12 Nov 2026 01:33:52 +0000 (17:33 -0800)]
QuickJS: fix njs.on() with a foreign receiver

Previously, njs.on() allocated the hooks state lazily and attached it to
its receiver with JS_SetOpaque() if the receiver was not an njs object.
Called with another receiver, for example njs.on.call(f, ...), it
overwrote the internal opaque pointer of an arbitrary object, which
resulted in memory corruption.

Now the hooks state is allocated with the njs object, and njs.on()
called with another receiver throws TypeError.

The issue was introduced in 04f6dfb9 (0.9.2).

25 hours agoStream: check optional arguments in QuickJS direct send methods
Dmitry Volyntsev [Wed, 11 Nov 2026 22:24:35 +0000 (14:24 -0800)]
Stream: check optional arguments in QuickJS direct send methods

sendUpstream() and sendDownstream() have a declared length of one,
but their shared handler accessed the second argument unconditionally.
QuickJS pads missing arguments only up to the declared length, so calls
without options could read beyond the argument array and crash.

Check the argument count before accessing the optional options object,
without changing the methods length.  The issue was introduced in
201b1276 (Modules: introduced QuickJS engine).

25 hours agoStream: do not use the filter output chain after the filter returns
Dmitry Volyntsev [Wed, 11 Nov 2026 04:04:32 +0000 (20:04 -0800)]
Stream: do not use the filter output chain after the filter returns

Asynchronous s.send() without a direction appended to the output chain
of the stream filter call, which was a local variable.  The pointer to
it was kept in the context after the filter returned.  Only calls that
passed an options object were rejected, so a plain s.send(data) from a
timer or a promise wrote to a returned stack frame with both engines.

Reset the pointer when the filter returns and reject asynchronous sends
that have no direction regardless of the options argument.

Changed behaviour:
- Asynchronous s.send(data, options) without a direction in the options
  throws TypeError, as s.send(data) already did for the options-less
  call, instead of writing to a returned stack frame.

25 hours agoModules: release work started while logging unhandled rejections
Dmitry Volyntsev [Wed, 11 Nov 2026 01:56:57 +0000 (17:56 -0800)]
Modules: release work started while logging unhandled rejections

Context teardown cleared pending events before logging an unhandled
rejection.  Converting the rejection reason to a string runs user code,
which could start a timer or a fetch() after the cleanup.  The timer or
connection then outlived the request pool, resulting in use-after-free
with both engines.

Log unhandled rejections before clearing events.

25 hours agoModules: initialize the JS context of a completed subrequest
Dmitry Volyntsev [Wed, 11 Nov 2026 01:46:20 +0000 (17:46 -0800)]
Modules: initialize the JS context of a completed subrequest

When a subrequest completed, its JS context was allocated by the
subrequest completion handler without ngx_js_ctx_init(), so the log and
the waiting events tree were not initialized.  If the subrequest had no
JS context of its own, a reply received by the parent could then use
it:

- readRequest*() on the reply inserted a caller event into the
  uninitialized tree;
- reading a js_set variable through reply.variables created an engine
  for the uninitialized context.

Both crashed with both engines.  The waiting events tree is missing
since 9de30135 (0.8.3), the log since 9b674412 (0.8.6).

Now the context is initialized as in ngx_http_js_init_vm().  Also,
deferred body reads are allowed only for the executing request, because
the body read state of a reply belongs to the subrequest.

Changed behaviour:
- readRequestText() and the other readRequest*() methods called on a
  subrequest reply throw TypeError.  They used to resolve with the body
  of the parent request, or to crash when the body had to be read.

25 hours agoVM: fix call stack sharing in cloned VMs
Dmitry Volyntsev [Thu, 12 Nov 2026 04:04:23 +0000 (20:04 -0800)]
VM: fix call stack sharing in cloned VMs

Previously, njs_vm_clone() copied the call stack pointers of the VM
being cloned.  A VM created with the "init" option keeps its initial
frame, so every clone allocated frames in the spare space of that
frame instead of its own memory.  When two clones of the same VM ran at
the same time, they overwrote each other's frames.  In nginx, this
happened when a parent handler evaluated a js_set variable of a
subrequest: the subrequest clone overwrote the frames of the running
parent clone, which crashed after returning from the native call.

Now a cloned VM starts with an empty call stack and allocates its own
initial frame.

The issue was introduced in 16f42c87 (0.8.8).

25 hours agoModules: keep HTTP request JS state in its host context
Dmitry Volyntsev [Wed, 11 Nov 2026 01:44:27 +0000 (17:44 -0800)]
Modules: keep HTTP request JS state in its host context

A subrequest handler could receive the parent request JS object from
another context.  Changing that object used foreign QuickJS shapes, and
setting a return value through a reply or parent stored a value from one
context in another context result slot.

Expose r.parent only on a reply whose parent is the executing request,
which matches the documented API.  Allow setReturnValue() only for the
executing request.  Check both engines so mixed njs and QuickJS
locations never interpret each other values.

The QuickJS parent getter dates back to 201b1276.

Changed behaviour:
- r.parent is undefined in a subrequest handler, as the API reference
  says.  It used to be the parent request object of another context.
- setReturnValue() called through a reply or a parent throws TypeError
  instead of storing the value in another request.

26 hours agoFix String.prototype.match() returning an empty array on no match
Dmitry Volyntsev [Thu, 12 Nov 2026 02:21:53 +0000 (18:21 -0800)]
Fix String.prototype.match() returning an empty array on no match

Previously, global matching replaced the initial null result with an
array even when no matches were collected.  This differs from the
ECMA-262 RegExp.prototype [ @@match ] algorithm, which returns null
when the first execution finds no match.

The issue was introduced in 83645861 (0.3.2), which moved
array allocation outside the matching loop.

Return the array only when it contains matches.  Empty-string matches
still count as matches, and lastIndex continues to be reset to zero.

This closes #1152 issue on GitHub.

26 hours agoFix initialization checks for identifier expression statements
Dmitry Volyntsev [Thu, 12 Nov 2026 02:20:38 +0000 (18:20 -0800)]
Fix initialization checks for identifier expression statements

Previously, a bare identifier statement emitted its initialization
check before the preceding statements.  An initialized let or const
binding could therefore throw ReferenceError, and side effects before
a real TDZ error could be skipped.

The issue was introduced in 158f54e6 (0.6.0), which added let.

Defer identifier resolution and its initialization check until the
preceding statements have been generated.  This preserves the existing
unused-value optimization and runtime checks for captured bindings,
while evaluating statements in source order as required by ECMA-262.

This closes #1151 issue on GitHub.

26 hours agoFix Array.prototype.toString() forwarding arguments to join()
Dmitry Volyntsev [Thu, 12 Nov 2026 02:19:22 +0000 (18:19 -0800)]
Fix Array.prototype.toString() forwarding arguments to join()

Previously, arguments passed to toString() were forwarded to join(),
changing the separator and the arguments seen by a custom join method.
The same issue affected typed arrays using the shared implementation.
This behavior dates back to the initial import in bffbe5e4 (0.1.0).

Call join() with only the receiver, as required by ECMA-262 section
23.1.3.36.  Extra arguments must neither reach join() nor be converted
to strings.

This closes #1150 issue on GitHub.

2 days agoTests: honor injected engines in QuickJS lifetime tests
Dmitry Volyntsev [Wed, 11 Nov 2026 00:48:58 +0000 (16:48 -0800)]
Tests: honor injected engines in QuickJS lifetime tests

Hardcoded QuickJS directives run these tests in the default njs pass
and conflict with CI engine injection.  Probe the selected engine and
skip the QuickJS-specific checks when njs is active.

Check both HTTP and stream engines for the GC test.  Use an isolated
probe module for the redirect test so probing does not change the
source and destination visit counters.

2 days agoModules: bind QuickJS native views to their context lifetime
Dmitry Volyntsev [Tue, 10 Nov 2026 20:17:46 +0000 (12:17 -0800)]
Modules: bind QuickJS native views to their context lifetime

HTTP and stream views can outlive their request or connection pool
in a reused runtime.  Detaching only the root object leaves variables,
headers, forms and replies with dangling native pointers.  Direct
session accessors and cold body getters can dereference detached roots,
while populated request caches hide their expiry.

Share a runtime-owned revocable token across each binding and its
views.  Reject expired access before native dereferences, including
cache access, without retaining pools or copying native data.
Keep reply tokens tied to the originating subrequest operation so
completed replies remain usable until the parent binding is detached.

The header and variable lifetime gap dates back to 201b1276, which
introduces QuickJS integration.  RequestForm uses the same unguarded
model since 0ffc96df.  Extracted JS values and stream callback flags
remain independent; RequestForm remains a guarded request-bound view.

2 days agoModules: embed the common opaque prefix in QuickJS wrappers
Dmitry Volyntsev [Tue, 10 Nov 2026 02:47:44 +0000 (18:47 -0800)]
Modules: embed the common opaque prefix in QuickJS wrappers

Context detach accesses the native pointer through a shared opaque
layout.  Embed that layout explicitly in HTTP and stream wrappers
instead of relying on unrelated first pointer fields.

This prepares shared lifetime invalidation without changing behavior.

2 days agoQuickJS: make Fetch storage independent of request pools
Dmitry Volyntsev [Thu, 5 Nov 2026 02:58:17 +0000 (18:58 -0800)]
QuickJS: make Fetch storage independent of request pools

Fetch objects could remain reachable in a reused runtime after their
native request-pool data was freed.  Headers views also pointed into
parent records, and unread Response bodies had no object owner.

Give Headers, Request, and Response runtime-owned storage.  Keep
associated Headers and Request body snapshots alive through marked
JSValue references.
Header getters reuse the associated object; independent construction
copies mutable header lists without borrowing another object's data.

The Fetch operation holds its Request and Headers during transport and
moves runtime-allocated body chains into the published Response.  Only
transient metadata need copying at publication.  Finalizers and failure
paths release owned data, including unread bodies; consumed chains are
reset so finalization cannot free their nodes twice.

The njs engine retains its pool-backed implementation.

2 days agoQuickJS: distinguish buffer copies and storage ownership
Dmitry Volyntsev [Tue, 10 Nov 2026 01:45:32 +0000 (17:45 -0800)]
QuickJS: distinguish buffer copies and storage ownership

The creation helpers do not express whether they copy bytes or take
responsibility for an existing allocation.  Use copy names for Buffer
snapshots and adopt for owned ArrayBuffer storage.

Create the external SharedArrayBuffer directly in the sole shell
caller instead of exposing a non-owning wrapper as a common helper.

Preserve the existing ownership and allocation-failure contracts.

2 days agoQuickJS: add runtime-owned byte storage helpers
Dmitry Volyntsev [Tue, 10 Nov 2026 01:42:56 +0000 (17:42 -0800)]
QuickJS: add runtime-owned byte storage helpers

Fetch ownership needs native copies and chain allocations that do not
depend on request pools or a surviving JSContext.  Provide common
helpers for these operations while preserving nginx value conversion
semantics.

2 days agoFetch: separate header storage from request and response records
Dmitry Volyntsev [Thu, 5 Nov 2026 02:14:02 +0000 (18:14 -0800)]
Fetch: separate header storage from request and response records

Headers are embedded in Request and Response records, so a Headers view
cannot retain its storage independently of the parent record.

Use explicit header-storage pointers while keeping the existing pool
allocation and lifetime policy in both engines.  This prepares the
shared transport for independently owned QuickJS Headers without adding
allocator modes to the header helpers.

2 days agoModules: guard debug engine id access during initialization
Dmitry Volyntsev [Thu, 5 Nov 2026 00:49:38 +0000 (16:49 -0800)]
Modules: guard debug engine id access during initialization

The request engine is not published while global code runs.  Reading
ngx.engine_id at that point dereferenced NULL instead of reporting that
the debug property is unavailable.

Reject the access in both engines without publishing a partial clone.

2 days agoModules: reject asynchronous work during module initialization
Dmitry Volyntsev [Thu, 5 Nov 2026 00:49:18 +0000 (16:49 -0800)]
Modules: reject asynchronous work during module initialization

Global code is intended for synchronous initialization, but Fetch and
timers could start request-bound work before clone publication.  QuickJS
Fetch dereferenced the unpublished engine, and module-level timers could
escape initialization.

Reject Fetch, setTimeout(), and setImmediate() before argument conversion
or resource allocation in both engines.  This does not prohibit ordinary
promise jobs or synchronous module initialization.

2 days agoModules: decouple QuickJS dump buffers from the request engine pool
Dmitry Volyntsev [Thu, 5 Nov 2026 00:44:35 +0000 (16:44 -0800)]
Modules: decouple QuickJS dump buffers from the request engine pool

Module-level logging dereferenced ctx->engine before clone publication.
Formatting only needs the current JSContext, not a published request
engine.  Pool-backed js_set results also coupled nginx variable storage
to the engine's lifetime.

Return temporary runtime-owned dump buffers and release them immediately
after logging.  Copy js_set results to the request or session pool and
release the temporary buffer, including on allocation failure.  Keep the
njs conversion path unchanged.

2 days agoModules: use the caller context for QuickJS string conversion
Dmitry Volyntsev [Thu, 5 Nov 2026 00:44:13 +0000 (16:44 -0800)]
Modules: use the caller context for QuickJS string conversion

String conversion recovered the context through ctx->engine even though
both the JSContext and destination pool were already supplied.  During
module evaluation the engine is not published yet, so synchronous
Request and Headers construction could dereference NULL.

Use the supplied context without changing allocation or copy behavior.
The dependency was introduced in 201b1276.

11 days agoModules: fix clearTimeout() of a timer from its own callback
Dmitry Volyntsev [Fri, 30 Oct 2026 05:30:31 +0000 (22:30 -0700)]
Modules: fix clearTimeout() of a timer from its own callback

Previously, the timer event was removed from the pending events after
its callback was called.  When the callback called clearTimeout() for
its own timer, the event was removed twice, which corrupted the
events tree.  In QuickJS, the timer function and arguments were also
freed while the function was still running, and then freed again.

Now, the timer event is removed from the pending events before the
callback is called, and its data is freed after the callback returns.

The issue was introduced in 9de30135 (0.8.3).

11 days agoModules: ignore unknown and non-timer ids in clearTimeout()
Dmitry Volyntsev [Fri, 30 Oct 2026 05:29:15 +0000 (22:29 -0700)]
Modules: ignore unknown and non-timer ids in clearTimeout()

Previously, clearTimeout() looked up the id among all pending native
events of the context, so a timer id coinciding with the id of
another event, such as a pending ngx.fetch() or a subrequest, removed
that event and freed its data.  In addition, an exception was thrown
for an unknown id.

Now, only timer events are removed, and unknown ids are ignored, as
required by the specification.

The issue was introduced in 9de30135 (0.8.3).

11 days agoWebCrypto: fix crypto.getRandomValues() with non-buffer arguments
Dmitry Volyntsev [Fri, 30 Oct 2026 02:10:44 +0000 (19:10 -0700)]
WebCrypto: fix crypto.getRandomValues() with non-buffer arguments

Previously, crypto.getRandomValues() obtained the destination bytes
with njs_vm_value_to_bytes(), which also accepts strings and does not
make a private copy of shared data.  As a result:

  - for a string argument the random bytes were written into the
    string data, which may be shared with other strings, including
    literals in the script;
  - for r.requestBuffer in nginx the request body seen by subsequent
    reads was modified.

Now, only typed arrays and DataView objects are accepted, and the
buffer is made writable before the write.

The issue was introduced in 7b2b7612 (0.7.0).

11 days agoTextEncoder: fix copy-on-write in encodeInto()
Dmitry Volyntsev [Fri, 30 Oct 2026 01:57:39 +0000 (18:57 -0700)]
TextEncoder: fix copy-on-write in encodeInto()

Previously, TextEncoder.prototype.encodeInto() wrote directly into the
destination buffer without making a private copy of shared data.  As a
result, encoding into r.requestBuffer in nginx modified the request
body seen by subsequent reads.

Now, the buffer is made writable before the write, as is done for
typed arrays.

The issue was introduced in 409004cb (0.4.4).

11 days agoArrayBuffer: fix copy-on-write for external data
Dmitry Volyntsev [Fri, 30 Oct 2026 01:53:23 +0000 (18:53 -0700)]
ArrayBuffer: fix copy-on-write for external data

Previously, writes to ArrayBuffers returned by nginx body accessors
or raw exportKey() could modify the request body or key material.

Now, njs_vm_value_array_buffer_set() takes a shared argument, like
other creation APIs.  Shared data uses copy-on-write; private
WebCrypto results and joined fetch response bodies remain writable
without copying.

The issue was introduced in f6485009 (0.5.1).

11 days agoDataView: fix copy-on-write in set methods
Dmitry Volyntsev [Fri, 30 Oct 2026 01:50:51 +0000 (18:50 -0700)]
DataView: fix copy-on-write in set methods

Previously, DataView.prototype.setXXX() wrote directly into the
underlying ArrayBuffer without making a private copy of shared data.
As a result, writing through a DataView over r.requestBuffer in nginx
modified the request body seen by subsequent reads.

Now, the buffer is made writable before the write, as is done for
typed arrays.

The issue was introduced in 409004cb (0.4.4).

11 days agoModules: clarify context reuse memory check comment
Dmitry Volyntsev [Fri, 30 Oct 2026 01:48:58 +0000 (18:48 -0700)]
Modules: clarify context reuse memory check comment

The comment claimed that the memory usage of a reused QuickJS
runtime could only remain high if the global scope was modified.
Values retained by engine internals and uncollected garbage are also
counted, while memory outside of the runtime is not.

11 days agoModules: fix top-level await handling in QuickJS
Dmitry Volyntsev [Fri, 30 Oct 2026 01:46:09 +0000 (18:46 -0700)]
Modules: fix top-level await handling in QuickJS

Previously, the result of module evaluation was awaited, but the
outcome was not checked:

  - a rejected top-level await was silently ignored;
  - a top-level await on a promise settled only by a timer or another
    asynchronous event remained pending.

In both cases the module was considered loaded without its exports
and each request failed later with a misleading "js function not
found" error.

Now, both cases fail the context creation with an explicit error.

The issue was introduced in 9a5430e6 (0.9.5).

11 days agoQuickJS: copy body bytes into ArrayBuffer
Dmitry Volyntsev [Fri, 30 Oct 2026 01:44:24 +0000 (18:44 -0700)]
QuickJS: copy body bytes into ArrayBuffer

Previously, r.readRequestArrayBuffer() and the arrayBuffer() methods
of fetch Request and Response objects returned an ArrayBuffer over
memory owned by a pool: the request pool or the engine pool.  The
buffer was created without a free function, which caused the
following problems:

  - with js_context_reuse, a buffer retained in the module scope
    outlived the pool and pointed to freed memory in the next
    request;
  - ArrayBuffer.prototype.transfer() called the NULL free function;
  - writes through the buffer modified the request body seen by
    subsequent r.requestText and r.readRequestText() calls, for a
    single buffer body directly in the nginx request buffers.

Now, the bytes are copied into memory owned by the ArrayBuffer.  For
Response.arrayBuffer() the body chain is joined directly into the
runtime memory, so the number of copies is unchanged.

The issue was introduced in b7f76b71 (0.9.1) and 7684d702 (0.9.9).

Based on a patch by Vadim Zhestikov.

11 days agoFetch: free QuickJS response body chain after reading
Dmitry Volyntsev [Fri, 30 Oct 2026 03:16:10 +0000 (20:16 -0700)]
Fetch: free QuickJS response body chain after reading

Previously, Response.text(), json() and arrayBuffer() joined the body
chain into a new block of the engine pool and left both the chain
nodes and the joined copy in the pool.  The memory was released only
with the engine, so with js_context_reuse every response body read
accumulated in the pool, unaccounted by the context reuse memory
check.

Now, the chain is destroyed after the body is read.  For text() the
string is created with qjs_string_create_chb(), and for json() the
joined copy is freed after parsing.

The issue was introduced in b7f76b71 (0.9.1).

11 days agoQuickJS: free joined string with chain pool in qjs_string_create_chb()
Dmitry Volyntsev [Fri, 30 Oct 2026 03:14:34 +0000 (20:14 -0700)]
QuickJS: free joined string with chain pool in qjs_string_create_chb()

Previously, the joined string was freed with the JSContext passed as
the pool argument of the chain free function.  This only worked for
chains initialized with NJS_CHB_CTX_INIT(), where the pool is the
context itself.

Now, the chain pool is used, so the function works with chains using
any allocator.  No functional changes for existing callers.

11 days agoQuickJS: fix fs callback return value leak
Dmitry Volyntsev [Fri, 30 Oct 2026 01:40:09 +0000 (18:40 -0700)]
QuickJS: fix fs callback return value leak

Previously, the value returned by a callback passed to the callback
flavor of fs functions was never freed.  The value stayed referenced
until the runtime was freed, triggering the
"list_empty(&rt->gc_obj_list)" assertion in JS_FreeRuntime() in debug
builds and accumulating in the runtime with js_context_reuse.

The issue was introduced in 1f8f9992 (0.8.9).

11 days agoModules: fix subrequest response body leak in QuickJS
Dmitry Volyntsev [Fri, 30 Oct 2026 01:39:31 +0000 (18:39 -0700)]
Modules: fix subrequest response body leak in QuickJS

Previously, alternating r.responseText and r.responseBuffer on a
subrequest reply overwrote the cached req->response_body value
without freeing it.  The value stayed referenced until the runtime
was freed, triggering the "list_empty(&rt->gc_obj_list)" assertion
in JS_FreeRuntime() in debug builds and accumulating in the runtime
with js_context_reuse.

The fix is to free the cached value before replacing it, as is
already done for r.requestText and r.requestBuffer.

The issue was introduced in 201b1276 (0.8.6).

2 weeks agoWebCrypto: fix AES-CTR out-of-bounds write on empty input
Dmitry Volyntsev [Thu, 29 Oct 2026 16:17:33 +0000 (09:17 -0700)]
WebCrypto: fix AES-CTR out-of-bounds write on empty input

Previously, the AES-CTR block count helper computed the number of
blocks as "1 + (dend - 1) / dsor".  This underflows for a zero-length
input, so njs_ceil_div(0, 16) and qjs_ceiling_div(0, 16) returned
2^28 instead of 0.

The fix is to use the zero-safe "(dend + dsor - 1) / dsor" form.

The njs engine code was introduced in 7b2b7612 (0.7.0) and the
QuickJS counterpart in 75ca26f8 (0.8.10).

3 weeks agoParser: fix reference atom IDs on big-endian systems
Dmitry Volyntsev [Thu, 22 Oct 2026 04:49:20 +0000 (21:49 -0700)]
Parser: fix reference atom IDs on big-endian systems

Previously, references to local variables and function arguments
failed with ReferenceError on 64-bit big-endian systems.  The parser
overwrote a uintptr_t reference atom ID through the overlapping
uint32_t value member, replacing the high half of the ID.

The issue was introduced in 9ff881e3 (1.0.2), which moved the
reference atom ID to the start of the parser node union.

Now, reference and value payloads share a uint32_t atom ID at the same
offset, initialized in the corresponding parser paths.
This removes the redundant value-member write and makes atom ID access
independent of byte order without increasing the parser node size.

This fixes #1143.

3 weeks agoCI: split CLI and nginx checks into parallel jobs
Dmitry Volyntsev [Thu, 22 Oct 2026 23:27:06 +0000 (16:27 -0700)]
CI: split CLI and nginx checks into parallel jobs

Previously, check-pr runs all build configurations sequentially, so
an early failure prevents later configurations from being tested.

Split CLI checks into matrix configurations and nginx checks
into separate njs, QuickJS, and QuickJS-NG matrix jobs.  Disable
fail-fast and use fresh build directories without make clean.

Preserve the nginx module and SSL configure flags, sanitizer and
debug-memory settings, QuickJS-NG version, checkout action revision,
test commands, and failed-test retries.

Make engine selection explicit: njs jobs no longer auto-detect
QuickJS-NG, and the QuickJS CLI build uses an explicit library.

3 weeks agoCI: add big-endian build and tests
Dmitry Volyntsev [Thu, 22 Oct 2026 22:41:10 +0000 (15:41 -0700)]
CI: add big-endian build and tests

Previously, GitHub Actions did not test the built-in engine on
big-endian systems, leaving byte-order regressions undetected.

3 weeks agoModules: fix incremental engine build dependencies
Dmitry Volyntsev [Thu, 22 Oct 2026 00:38:04 +0000 (17:38 -0700)]
Modules: fix incremental engine build dependencies

Existing archives prevent the nested make from checking engine source
changes.  QuickJS builds also omit libnjs.a and engine headers from
the module dependencies.

Changes to engine sources are now picked up by nginx make without
rerunning configure or removing the existing libraries.

3 weeks agoModules: isolate engine builds from the standalone CLI
Dmitry Volyntsev [Thu, 22 Oct 2026 00:37:10 +0000 (17:37 -0700)]
Modules: isolate engine builds from the standalone CLI

nginx and the CLI share build artifacts and configuration.  Building
nginx replaces the CLI Makefile, while rebuilding the CLI can leave
nginx linking libraries compiled with incompatible flags.

Building nginx no longer changes the CLI configuration or libraries.
Rebuilding the CLI no longer breaks subsequent nginx builds.

3 weeks agoConfigure: fix pkg-config paths for absolute build directories
Dmitry Volyntsev [Thu, 22 Oct 2026 14:14:14 +0000 (07:14 -0700)]
Configure: fix pkg-config paths for absolute build directories

The generated njs.pc prepends the source directory to an absolute
build directory, producing invalid include and library paths.

Preserve absolute build paths and resolve only relative ones against
the source directory.  This also supports engine builds embedded in
nginx without changing the standalone CLI configuration.

3 weeks agoConfigure: allow explicit QuickJS library selection
Dmitry Volyntsev [Thu, 22 Oct 2026 00:36:28 +0000 (17:36 -0700)]
Configure: allow explicit QuickJS library selection

Independent configure runs can select different QuickJS libraries.
Allow callers to pass the selected headers and linker options without
falling back to another library.

3 weeks agoConfigure: add configure mode without a source-root Makefile
Dmitry Volyntsev [Thu, 22 Oct 2026 00:35:52 +0000 (17:35 -0700)]
Configure: add configure mode without a source-root Makefile

Configuring a separate build directory still replaces the source-root
Makefile and redirects subsequent CLI builds.  Allow embedded builds
to leave the standalone build entry point unchanged.

4 weeks agoWebCrypto: return false for wrong-length ECDSA signatures
Dmitry Volyntsev [Thu, 15 Oct 2026 06:06:08 +0000 (23:06 -0700)]
WebCrypto: return false for wrong-length ECDSA signatures

ECDSA signatures of an invalid length cause verify() to throw instead
of returning false.  njs passes an empty DER signature to
EVP_PKEY_verify(), while QuickJS rejects the length during conversion.

The njs behavior dates back to 86c202c7 (0.7.7); QuickJS rejects these
signatures since 75ca26f8 (0.8.10).

Treat a length mismatch as an invalid signature, as required by
WebCrypto ECDSA Verify, while preserving diagnostics for conversion
failures.  Test all three supported curves with invalid lengths and
a valid signature.

4 weeks agoWebCrypto: clear OpenSSL errors after a signature mismatch
Dmitry Volyntsev [Wed, 14 Oct 2026 05:18:37 +0000 (22:18 -0700)]
WebCrypto: clear OpenSSL errors after a signature mismatch

Previously, crypto.subtle.verify(false) could leave errors for an
unrelated TLS handshake or WebCrypto operation.

The fix is to clear the queue when returning false, while retaining
diagnostics for operation failures.

This fixes #1141.

4 weeks agoWebCrypto: fix algorithm availability in nginx modules
Dmitry Volyntsev [Tue, 13 Oct 2026 05:37:30 +0000 (22:37 -0700)]
WebCrypto: fix algorithm availability in nginx modules

Previously, configure probes disabled Ed25519, X25519, and AES-KW in
nginx modules because the engine is configured with --no-openssl,
although WebCrypto uses nginx's OpenSSL and these algorithms were
available.

The fix is to detect 25519 APIs from OpenSSL headers and look up AES-KW
ciphers at runtime, removing the dependency on engine configuration
and direct references to wrap functions absent in some OpenSSL flavors.

This fixes #1139.

4 weeks agoModules: treat expired shared dict entries as absent
Dmitry Volyntsev [Sat, 10 Oct 2026 01:54:41 +0000 (18:54 -0700)]
Modules: treat expired shared dict entries as absent

Expired entries are hidden by read APIs but can still be found by
write operations.  replace() can revive an expired entry, delete()
can report that it existed, and incr() can use its old value instead
of init.  Without an explicit timeout, incr() can return a value that
a subsequent get() cannot retrieve.

Use a common write-side lookup to remove expired entries and handle
them as missing in both engines.  incr() then creates a new entry
with init plus delta and the directive or explicit timeout.

Also return false instead of undefined when QuickJS delete() is
called for a missing key.

4 weeks agoModules: fix shared dict state JSON separators
Dmitry Volyntsev [Sat, 10 Oct 2026 01:40:28 +0000 (18:40 -0700)]
Modules: fix shared dict state JSON separators

When saving a shared dict zone with timeout=, expired entries are
skipped.  A comma is appended whenever another tree node follows a live
entry, even if all remaining nodes are expired.  This can leave a
trailing comma in the state file, making it invalid JSON.

Insert commas before live entries only when a previous entry has been
rendered, so skipped entries do not affect the separators.

4 weeks agoModules: initialize shared dict state entry expiration
Dmitry Volyntsev [Sat, 10 Oct 2026 01:36:20 +0000 (18:36 -0700)]
Modules: initialize shared dict state entry expiration

When a shared dict state entry omits the optional expire field, its
expiration remains uninitialized.  In a zone with timeout=, the entry
can be discarded as expired or loaded with an arbitrary expiration
instead of receiving the zone timeout.

Initialize expiration to zero before parsing the entry so an omitted
expire field is treated like an explicit zero.  The entry then receives
the zone timeout and the state file is scheduled for saving.

4 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.

4 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.

4 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.

4 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.

4 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.

4 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.

4 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.

4 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.

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

6 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

6 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.

6 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.

6 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.

6 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.

7 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.

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

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

7 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.

7 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.

7 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

7 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.

7 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.

7 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.

7 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.

7 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.

7 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.

7 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

7 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.

7 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.

7 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.

7 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.

7 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

7 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

7 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

8 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.

8 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.

8 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.

8 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.

8 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.

8 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.

8 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.