WebCrypto: fixed RSA JWK test data for Node.js compatibility.
Removed the incorrect "alg":"RS256" field from RSA JWK test data
files. The field is optional per RFC 7517, and the value RS256
(RSASSA-PKCS1-v1_5) was wrong for tests that use RSA-OAEP and
RSA-PSS algorithms. Node.js correctly rejects JWK imports when
the "alg" field does not match the requested algorithm.
The hash mismatch negative test was updated to use an inline JWK
with an explicit "alg" field instead of the shared file.
This ensures: `test/test262 --binary=node test/webcrypto` pass.
Dmitry Volyntsev [Tue, 24 Mar 2026 01:46:02 +0000 (18:46 -0700)]
Crypto: switched to OpenSSL EVP for hashing.
Previously, the crypto module used built-in software implementations
for a limited set of hash algorithms (md5, sha1, sha256). This
prevented users from using algorithms like sha384, sha512, and sha3
family, even when the underlying OpenSSL library supported them.
The change replaces built-in hash implementations with OpenSSL
EVP_MD_CTX for createHash() and HMAC_CTX for createHmac(), following
the webcrypto module. Algorithm lookup now uses EVP_get_digestbyname(),
making any digest supported by the linked OpenSSL available to
JavaScript code.
The module now requires OpenSSL and is conditionally compiled, same as
the webcrypto module. Builds without OpenSSL (--no-openssl) will no
longer have the crypto module available.
Tested with OpenSSL 3.0, OpenSSL 1.1.1w, LibreSSL 3.9.2, and
BoringSSL. SHA-3 tests are skipped when the SSL library does not
support them (e.g. BoringSSL).
Dmitry Volyntsev [Fri, 27 Mar 2026 01:03:41 +0000 (18:03 -0700)]
Modules: removed shared dict expiration from read-locked paths.
Previously, keys(), items(), and size() called ngx_js_dict_expire()
under a read lock. Since ngx_js_dict_expire() deletes nodes from
both rbtrees and frees slab memory, concurrent readers on different
worker processes could corrupt shared memory by freeing the same
expired nodes simultaneously.
The fix removes ngx_js_dict_expire() calls from all read-locked
paths and instead skips expired entries during iteration, consistent
with how get() and has() already handle expiry. Actual cleanup of
expired entries is deferred to write-side operations (set, add,
delete, clear).
Dmitry Volyntsev [Fri, 27 Mar 2026 00:16:25 +0000 (17:16 -0700)]
Modules: improved shared dict eviction strategy.
Previously, when a slab allocation failed in evict mode, only 16
entries were evicted with a single retry. This could still result
in SharedMemoryError when the freed slab slots did not match the
requested allocation size class, even though the zone had plenty
of evictable entries.
In practice, it might happen when the following conditions are met:
- The shared zone is full
- evict flag is enabled
- key/value entries differ in size
The allocation now retries in a loop, evicting 16 entries at a time,
until the allocation succeeds or no more entries remain in the expire
tree.
After this change, allocation with evict enabled can only fail when:
- the value is larger than the zone's usable space
- the expire tree has no entries left to evict
- zone metadata overhead leaves insufficient room
Dmitry Volyntsev [Fri, 27 Mar 2026 00:16:11 +0000 (17:16 -0700)]
Modules: fixed double-free in shared dict update with eviction.
Previously, when updating an existing key's string value in a shared
dictionary with timeout and evict enabled, ngx_js_dict_alloc() could
trigger ngx_js_dict_evict() if the zone was full. Since the node being
updated was still in the expire tree, eviction could free it. The
subsequent ngx_slab_free_locked() call in the update path then freed the
already-freed string data, causing the "chunk is already free" alert
followed by a segfault.
The fix removes the node from the expire tree before allocating
memory for the new value, preventing eviction from reaching it.
On allocation failure the node is re-inserted with its original
expiry time.
Dmitry Volyntsev [Thu, 12 Mar 2026 23:50:34 +0000 (16:50 -0700)]
Modules: preserved per-entry TTL on shared dict incr() calls.
Previously, incr() without an explicit timeout argument always
reset the entry expiry to the directive default, discarding any
per-entry timeout set by a prior add(), set(), or incr() call.
This aligns the behavior with Redis INCR and OpenResty
ngx.shared.DICT:incr() where value mutation does not touch the
existing TTL. An explicit timeout argument still updates it.
Dmitry Volyntsev [Thu, 12 Mar 2026 23:48:14 +0000 (16:48 -0700)]
Modules: added ttl() method to shared dictionaries.
The method returns the remaining time-to-live in milliseconds
for a given key, or undefined if the key does not exist or has
expired. Throws TypeError if the dictionary was declared without
the timeout parameter.
Previously, call lowering created FUNCTION_FRAME and METHOD_FRAME before
argument evaluation. This made call ordering observably wrong: for
non-callable callees, the error was thrown before arguments with side
effects were evaluated, violating the ECMAScript specification. It also
prevented await expressions in call arguments, which were rejected at
parse time because suspending inside a half-created frame was not
supported.
The fix evaluates arguments first, then emits the frame, PUT_ARG, and
FUNCTION_CALL. Callee and receiver values are captured into temporaries
before argument evaluation to guard against argument side effects.
Method properties are resolved via PROPERTY_GET before arguments.
METHOD_FRAME is redefined from a composite opcode (property lookup +
callability check + frame creation) to a pure frame-creation opcode
that takes an already-resolved function and explicit "this" value.
The parser always wraps call expressions in a NJS_TOKEN_FUNCTION_CALL
node, removing the NJS_TOKEN_NAME special case.
Previously, the "in" operator swap flag was relayed through generator
context. This broke when call-end handlers switched to
njs_generator_stack_pop(NULL), which released the context before the
swap was read in njs_generate_3addr_operation_end().
The fix derives the swap directly from the node token type
(NJS_TOKEN_IN), eliminating the context relay.
Introduce NJS_TOKEN_OPTIONAL_PRESERVE for optional-chain preserve nodes
instead of reusing OBJECT_VALUE, so OBJECT_VALUE remains strictly an
object/array literal structure token.
Route optional-preserve, object-value, and optional method-preserve
access through dedicated helper functions with narrow assertions,
removing direct u.object/left/right access from general parser and
generator paths.
Previously, grouped optional calls like (o?.m)() resolved the callee
through the optional chain but dispatched via plain FUNCTION_CALL,
losing the original receiver.
The fix stores the receiver on the call node so the upcoming
call-argument reorder can emit METHOD_FRAME with an explicit "this".
Call-expression setup and optional-chain preserve lookup are routed
through named helpers with generator-side validation.
Previously, the generator inferred reference intent from raw AST shape.
Now the parser marks the relevant property node as PROPERTY_REF before
building METHOD_CALL, assignment, or update nodes, and the generator
accepts both PROPERTY and PROPERTY_REF via
njs_generate_is_property_lvalue().
Introduce NJS_TOKEN_PROPERTY_REF as an explicit parser-side marker for
property accesses that carry reference semantics (assignment targets,
delete operands, increment/decrement, method-call receivers).
WebCrypto: validate JWK key type against algorithm in importKey().
Previously, importKey() did not verify that the JWK "kty" field
matched the requested algorithm. For example, importing a JWK with
kty "oct" (symmetric) while specifying an asymmetric algorithm like
ECDH caused a SEGV in EVP_PKEY_free() during cleanup. This happened
because the symmetric key data written into the union's "raw" member
overlapped with the "pkey" pointer, corrupting it.
The fix validates kty before calling any JWK import function:
- "RSA" is only accepted for RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5
- "EC" is only accepted for ECDSA, ECDH
- "oct" is only accepted for HMAC, AES-GCM, AES-CTR, AES-CBC
Dmitry Volyntsev [Thu, 26 Feb 2026 15:45:46 +0000 (07:45 -0800)]
Fixed string offset map corruption in scope values hash.
The issue was introduced in e7caa46d (0.9.5). When compile-time
UTF-8 string constants were copied into the values hash in
njs_scope_value_index(), the string data layout was calculated
incorrectly: the map offset did not account for the null terminator
added in e7caa46d, the "size" variable was overwritten corrupting
the subsequent memcpy, and the offset map was never initialized
to zero.
This caused SEGV/SIGBUS crashes for any multi-byte UTF-8 string
constant with more than 32 characters when accessing a character
at index >= 32 (e.g. via .replace() or bracket notation). The bug
only manifested when the string byte size was 4-byte aligned, as
otherwise alignment padding absorbed the missing byte.
The fix factors out njs_string_data_size() and njs_string_data_init()
helpers shared by njs_string_alloc() and njs_scope_value_index(),
eliminating the duplicated layout logic that caused the divergence.
Fixed logical assignment short-circuit with non-writable properties.
When the logical condition was already satisfied (e.g., falsy for &&=,
truthy for ||=, non-nullish for ??=), the short-circuit jump landed on
the property set instruction instead of past it. This caused spurious
TypeErrors for non-writable, getter-only, and non-extensible property
targets even though no assignment should occur.
This fixes logical assignment introduced in 1a64ba68.
This change fixes 9 more tests in test262.
Dmitry Volyntsev [Wed, 11 Feb 2026 15:13:06 +0000 (07:13 -0800)]
Added support for ||= and &&= logical assignment operators.
Unlike regular compound assignments (+=, -=), these operators
short-circuit: the RHS is not evaluated and no assignment occurs
if the logical condition is already satisfied.
Dmitry Volyntsev [Fri, 13 Feb 2026 05:31:23 +0000 (21:31 -0800)]
Shell: fixed interactive mode detection for piped stdin.
Previously, libedit callback API (rl_callback_read_char) was used for
interactive input processing. Unlike the blocking readline() API, the
callback interface does not work correctly when stdin is a pipe: input
characters are silently dropped and the line handler is never invoked.
Dmitry Volyntsev [Wed, 18 Feb 2026 01:56:22 +0000 (17:56 -0800)]
Replace per-VM indexed array for modules with hash-based lookup.
Previously, evaluated module values were cached in a separate
vm->modules array indexed by module->index assigned at compile time.
This required keeping the array size in sync with shared->module_items,
which was error-prone in interactive mode where new modules could be
compiled across commands.
Instead, store evaluated module values directly in the per-VM module
copy already maintained by vm->modules_hash. This unifies the import
and require() caching paths and eliminates the index-based array along
with module->index and shared->module_items fields.
This fixes `make shell_test` when configured with --debug=YES.
Dmitry Volyntsev [Wed, 25 Feb 2026 07:13:54 +0000 (23:13 -0800)]
Modules: suppressed slab log_nomem for evict shared dict zones.
When evict is enabled, memory allocation failures are expected
and handled by evicting old entries. The slab allocator's
"no memory" log messages are now suppressed for such zones.
Dmitry Volyntsev [Fri, 20 Feb 2026 23:45:25 +0000 (15:45 -0800)]
Modules: fixed expire field truncation in shared dict state files.
The njs_sprintf buffer for the expire field was sized for 10-digit
numbers, but current millisecond timestamps are 13 digits. This caused
silent truncation, making entries appear expired on a full restart.
The issue has been present since eca03622 (0.9.1), which introduced
the shared dictionary state file support.
Dmitry Volyntsev [Tue, 17 Feb 2026 00:20:20 +0000 (16:20 -0800)]
Fixed heap-buffer-overflow in atom hash caused by Symbol().
Previously, there was a key_hash collision between strings and
symbols in the atom hash table that led to use of uninitialized memory
and desynchronization of vm->atom_id_generator.
In the atom hash, string entries used djb_hash(content) as key_hash
while symbol entries used raw atom_id. Since djb_hash produces arbitrary
uint32_t values and atom_ids grow monotonically from NJS_ATOM_SIZE,
these spaces overlapped. When a symbol's atom_id matched an existing
string's djb_hash, njs_flathsh_insert() invoked the test function which
read uninitialized fhq.key fields (the function expected string data,
but the caller never initialized it for symbols). Additionally,
atom_id_generator was incremented before the insert, so on failure the
counter diverged from the actual element count, causing subsequent
njs_atom_to_value() calls to read out of bounds.
The fix is to partition the key_hash space using bit 31.
Dmitry Volyntsev [Tue, 17 Feb 2026 00:16:12 +0000 (16:16 -0800)]
Refactored atom hash into orthogonal find and add operations.
Previously, njs_atom_find_or_add() and njs_atom_find_or_add_string()
duplicated the lookup logic and coupled find with insert. Splitting
them into njs_atom_find() and njs_atom_add() separates concerns:
find does a pure lookup with raw bytes, add inserts a pre-built value.
Dmitry Volyntsev [Thu, 29 Jan 2026 02:36:39 +0000 (18:36 -0800)]
Make Error.stack faster.
Previously, error.stack reported full names for native function (for
example Array.prototype.map -> map), but it was achieved by iteration
through a global object which is slow.
The fix is to report only function name at hand, this loses a bit
of verbosity but make it ~100 times faster.
make benchmark
before
...
exception.stack: 38.421µs, 26027 times/s
exception.native.stack: 226.711µs, 4410 times/s
after
...
exception.stack: 1.239µs, 807356 times/s
exception.native.stack: 2.419µs, 413339 times/s
Zurab Kvachadze [Mon, 26 Jan 2026 14:54:51 +0000 (15:54 +0100)]
auto/cc: Use portable/POSIX 'command -v' instead of 'which'
'which' is not a portable utility as it is not specified by POSIX. Since
auto/cc is already a shell script, use the more direct and portable
'command' builtin to detect $CC.
There are two bugs linked here. The first one is a downstream report of
this issue. The second one is more general information on why 'which'
usage is an issue and should be avoided.
Added "js_load_http_native_module" and "js_load_stream_native_module"
main nginx.conf level directives. The directives load a dynamic
library. For security reason it is only allowed in the main context.
Later, JS code may import modules loaded with these directives
with standard import syntax.
example.conf:
...
js_load_http_native_module /path/to/lib.so;
js_load_http_native_module /path/to/lib2.so as lib2;
http {
js_import main.js;
...
main.js:
import * as lib from 'lib.so';
import * as lib2 from 'lib2';
...
See quickjs.h for the complete QuickJS API reference and
nginx/t/js_native_module.t for a working example.
Modules: extracted config-time merging into separate function.
This introduces ngx_js_merge_conftime_loc_conf() to handle merging
of configuration-time properties.
Normally ngx_js_merge_conf() does all the default value initialization
for child location configurations.
There is a special case for global "http" or "stream" configuration
where the parent configuration needs to be initialized (so it can be
reused by server configurations if no additional directives were
defined in them). But parent configurations are not initialized by
ngx_js_merge_conf().
Most of the ngx_js_loc_conf_t values are only used at runtime, so
only configuration-time values need to be merged in the parent.
The runtime values will be provided from the appropriate
ngx_js_loc_conf_t during request processing.
Previously, configuration-time merging was done inline. Extracting
it into a dedicated function simplifies adding new configuration-time
properties.
Dmitry Volyntsev [Sat, 10 Jan 2026 02:08:56 +0000 (18:08 -0800)]
QuickJS: fixed js_body_filter with multiple chunks.
Previously, last_key atoms was freed too early. Also, js_body_filter.t
is modified to ensure js_body_filter sees multiple data chains to catch
the issue.
Dmitry Volyntsev [Fri, 12 Dec 2025 05:57:18 +0000 (21:57 -0800)]
HTTP: fixed buffer_type inheritance in if blocks.
Previously, when js_body_filter was used inside an if block that
evaluated to true, the data parameter received Buffer type instead
of the expected String type. This happened because buffer_type field
in ngx_http_js_loc_conf_t was not properly initialized, causing the
configuration merge to fail when nginx created a new location context
for if blocks.
Previously, when upstream data was delivered from nginx cache
js_body_filter was not able to process it correctly. In particular,
it was treated as a chain of empty buffers.
The fix is to set r->filter_need_in_memory flag, which ensures
that ngx_http_core_module reads the data into memory before
js_body_filter sees it.
Dmitry Volyntsev [Fri, 24 Oct 2025 03:06:42 +0000 (20:06 -0700)]
Using printing and parsing library from QuickJS.
- Number.prototype.toString(radix)
Improved accuracy for edge cases
Reimplemented using njs_dtoa2() with JS_DTOA_FORMAT_FREE | JS_DTOA_EXP_DISABLED
- Number.prototype.toFixed(frac)
Reimplemented using njs_dtoa2() with JS_DTOA_FORMAT_FIXED
Removed old njs_fixed_dtoa() implementation
- Number.prototype.toPrecision(prec)
Reimplemented using njs_dtoa2() with precision format
Removed old njs_dtoa_precision() implementation
- Number.prototype.toExponential(frac)
Reimplemented using njs_dtoa2() with exponential format
Removed old njs_dtoa_exponential() implementation
- parseInt()
Simplified parsing implementation
Removed custom njs_number_radix_parse() helper
- parseFloat()
Simplified parsing implementation
Removed custom njs_number_bin_parse(), njs_number_oct_parse(),
njs_number_dec_parse() and njs_strtod.c module Better handling of large
numbers and denormal floats and invalid inputs.
Fetch: added forward proxy support with HTTPS tunneling.
Supports Basic authentication via Proxy-Authorization header.
- js_fetch_proxy - configures forward proxy URL. It takes proxy URL
as a parameter. The URL may optionally contain user and password.
Parameter value can contain variables. If value is empty,
forward proxy is disabled.
Dmitry Volyntsev [Tue, 14 Oct 2025 00:37:11 +0000 (17:37 -0700)]
QuickJS: fixed r.subrequest() to a location with JS handler.
Previously, when a subrequest location had a JS handler, an object of a
subrequest JS context was provided as an argument to a parent contexts.
This may cause all sorts of problems due to incorrect reference
counting.
After this change in bellard/quickjs@42eb2795 the bug became apparent.