jerryscript: NULL-pointer dereference in ES-module linker (ecma_module_resolve_import) via duplicate var/namespace-import binding - pre-link crash
Severity: MEDIUM | CVSS 3.1: 6.2 CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H | CWE: CWE-476 | Affected: jerryscript @ b7069350c2e5
Summary
JerryScript fails to reject an illegal duplicate module binding in which a name is first declared with var/function and then re-declared as a namespace import (import * as <name>). Per ECMA-262 a module's top-level binding names must be unique, so this is an early SyntaxError; jerryscript instead accepts it, leaving the binding for that name inconsistent between the module's lexical scope and its import list. When the module's namespace object is later built during linking, ecma_module_resolve_export() (ecma-module.c:446) cannot find an export's local name in the module scope and falls through to ecma_module_resolve_import() (ecma-module.c:330). That function contains an unbounded while (true) loop that walks module_p->imports_p assuming the binding is always present - the only guard is a JERRY_ASSERT (compiled out in release builds). With the binding absent from the import list, the loop advances import_node_p past the end of the list to NULL and dereferences it (import_node_p->module_names_p) at ecma-module.c:342, reading address 0x8. The same root cause (improper duplicate-binding handling in module scope) also produces wild-pointer memory corruption on the module cleanup path (observed: SEGV at a non-NULL garbage address 0x17fff80cc in ecma_module_release_module_names, ecma-module.c:146, via ecma_module_release_module), indicating the flaw is a genuine memory-safety defect rather than a benign missing NULL check. This code path is reached only in module mode (jerry -m / JERRY_PARSE_MODULE); the project's libFuzzer/OSS-Fuzz harness only parses input as a plain script (JERRY_PARSE_NO_OPTS), so it never exercises the module linker.
Affected
- Component: jerryscript@b7069350c2e52e7dc721dfb75f067147bd79b39b - jerry-core/ecma/base/ecma-module.c:342 (ecma_module_resolve_import)
- Repository / commit: https://github.com/jerryscript-project/jerryscript @
b7069350c2e52e7dc721dfb75f067147bd79b39b
- Attacker / interface: Anyone who can supply an ES module that the host application links/evaluates (e.g.
jerry -m attacker.mjs, or an embedder that calls jerry_module_link()/jerry_module_evaluate() on attacker-controlled module source). No authentication or prior state required.
Root cause
jerry-core/ecma/base/ecma-module.c:330 static bool ecma_module_resolve_import (..., ecma_string_t *local_name_p) {
ecma_module_node_t *import_node_p = module_p->imports_p;
while (true) {
JERRY_ASSERT (import_node_p != NULL); /* <- compiled out when JERRY_NDEBUG (release) */
for (ecma_module_names_t *import_names_p = import_node_p->module_names_p; /* <- line 342: NULL deref when import_node_p == NULL */
import_names_p != NULL; import_names_p = import_names_p->next_p) {
if (ecma_compare_ecma_strings (local_name_p, import_names_p->local_name_p)) { ... return true; }
}
import_node_p = import_node_p->next_p; /* <- walks off the end of the list to NULL */
}
}
/* caller: ecma-module.c:446 ecma_module_resolve_export(): when an export's local_name is not found
in current_module_p->scope_p (property_p == NULL) it assumes the name must be an import and calls
ecma_module_resolve_import(...), which the corrupted duplicate-binding state violates. */
Reachability (untrusted source -> sink)
- main-desktop.c:138 main() -> jerryx_source_exec_module(file) [
jerry -m m0.mjs, the shipped CLI]
- jerry-ext/util/sources.c:94 jerry_module_link(module, ...) [public API; default config]
- api/jerryscript.c:610 jerry_module_link -> ecma_module_link [ecma-module.c:1080]
- ecma-module.c:1257 ecma_module_link -> ecma_module_create_namespace_object(m0)
- ecma-module.c:789 -> ecma_module_namespace_object_add_export_if_needed (per export)
- ecma-module.c:641 -> ecma_module_resolve_export(m0, export_name)
- ecma-module.c:446 resolve_export: export local name not in module scope (duplicate-binding corruption) -> ecma_module_resolve_import(...)
- ecma-module.c:342 resolve_import: while(true) walks imports_p past end -> import_node_p == NULL -> READ import_node_p->module_names_p (addr 0x8) -> SIGSEGV.
Guards checked: JERRY_PARSE_MODULE is required (module mode) and is the documented default for jerry -m; JERRY_MODULE_SYSTEM is ON by default (= JERRY_BUILTINS=1). The only NULL guard (JERRY_ASSERT at ecma-module.c:340) is removed by -DJERRY_NDEBUG in every non-debug build, including the default MinSizeRel build. No authentication gate exists; the module source itself is the input.
To reproduce
- git clone https://github.com/jerryscript-project/jerryscript && cd jerryscript
- git checkout b706935
- python3 tools/build.py # default configuration; produces build/bin/jerry with module support (JERRY_MODULE_SYSTEM is on by default)
- mkdir /tmp/poc && printf 'export const q = 1;\n' > /tmp/poc/m1.mjs
- printf 'var z = {};\nimport * as z from "./m1.mjs";\nexport default 1;\n' > /tmp/poc/m0.mjs
- cd /tmp/poc && "$OLDPWD"/build/bin/jerry -m m0.mjs # -> 'Segmentation fault', exit code 139
-
Negative control (proves the duplicate name is causal; exits 0 cleanly):
- printf 'import * as z from "./m1.mjs";\nvar w = 1;\nexport default 1;\n' > /tmp/poc/m0.mjs && cd /tmp/poc && "$OLDPWD"/build/bin/jerry -m m0.mjs ; echo exit=$?
Output
SIGSEGV (exit 139) on the default build; AddressSanitizer 'SEGV on unknown address 0x000000000008 (READ)' at ecma_module_resolve_import (ecma-module.c:342) on the release+ASAN build. Deterministic across 10/10 ASAN runs and 6/6 + 3/3 default-build runs over two independent clean rebuilds.
Demonstrated vs inferred: Demonstrated: deterministic NULL-pointer dereference -> process crash (DoS) reachable from the shipped jerry -m CLI in the default configuration. Inferred (not exploited): the same duplicate-binding root cause additionally corrupts module structures (observed wild-pointer SEGV at 0x17fff80cc in ecma_module_release_module_names on the cleanup path); no controllable read/write or code execution was demonstrated, so severity/CVSS reflect DoS only.
Impact
Denial of service: a single ~60-byte malicious ES module deterministically crashes the engine (SIGSEGV) during jerry_module_link(), terminating the embedding application/process. JerryScript targets embedded/IoT hosts that may load JavaScript modules from update channels or other partially-trusted sources; such a host is crashed by linking one crafted module. Demonstrated impact is process termination (A:H). Because the underlying duplicate-binding flaw also corrupts the module structures (wild-pointer dereference during module release), a correct fix must reject the duplicate binding rather than only NULL-guard the resolver.
Suggested fix
Primary fix: enforce the ECMA-262 early error for duplicate top-level module bindings so that a var/function/let declaration followed by a same-named import * as <name> (and the symmetric case) is rejected as a SyntaxError during scanning/parsing - the named-import clause already performs a SCANNER_TYPE_ERR_REDECLARED check (js-parser-module.c parser_module_parse_import_clause); the namespace-import statement path must perform the equivalent check against existing var/lexical declarations. Defense-in-depth: make ecma_module_resolve_import() tolerate a not-found binding instead of looping on the implicit invariant - replace the while (true) / JERRY_ASSERT (import_node_p != NULL) with while (import_node_p != NULL) and return false (resolution failure) when the binding is not found, so a corrupted/unexpected state cannot dereference NULL in release builds.
References
- CWE-476
- ECMA-262 15.2.1.1 Module Static Semantics: Early Errors (duplicate top-level binding names)
|
static bool |
|
ecma_module_resolve_import (ecma_module_resolve_result_t *resolve_result_p, /**< [in,out] resolve result */ |
|
ecma_module_resolve_set_t *resolve_set_p, /**< resolve set */ |
|
ecma_module_t *module_p, /**< base module */ |
|
ecma_string_t *local_name_p) /**< local name */ |
|
{ |
|
ecma_module_node_t *import_node_p = module_p->imports_p; |
|
|
|
while (true) |
|
{ |
|
JERRY_ASSERT (import_node_p != NULL); |
|
|
|
for (ecma_module_names_t *import_names_p = import_node_p->module_names_p; import_names_p != NULL; |
|
import_names_p = import_names_p->next_p) |
|
{ |
|
if (ecma_compare_ecma_strings (local_name_p, import_names_p->local_name_p)) |
|
{ |
|
ecma_module_t *imported_module_p = ecma_module_get_from_object (import_node_p->u.path_or_module); |
|
|
|
if (ecma_compare_ecma_string_to_magic_id (import_names_p->imex_name_p, LIT_MAGIC_STRING_ASTERIX_CHAR)) |
|
{ |
|
/* Namespace import. */ |
|
ecma_value_t namespace = ecma_make_object_value (imported_module_p->namespace_object_p); |
|
|
|
JERRY_ASSERT (namespace & ECMA_MODULE_NAMESPACE_RESULT_FLAG); |
|
|
|
return ecma_module_resolve_update (resolve_result_p, namespace); |
|
} |
|
|
|
if (!ecma_module_resolve_set_append (resolve_set_p, imported_module_p, import_names_p->imex_name_p) |
|
&& resolve_result_p->result_type == ECMA_MODULE_RESOLVE_NOT_FOUND) |
|
{ |
|
resolve_result_p->result_type = ECMA_MODULE_RESOLVE_CIRCULAR; |
|
} |
|
|
|
return true; |
|
} |
|
} |
|
|
|
import_node_p = import_node_p->next_p; |
|
} |
|
} /* ecma_module_resolve_import */ |
- related-but-distinct: issue#5143 (string double free in ecma_module_find_module - explicitly the pre-master v2.4.0 module implementation, not this code)
Environment
- jerryscript @ commit
b7069350c2e52e7dc721dfb75f067147bd79b39b - default build (./configure && make), default config.
jerryscript: NULL-pointer dereference in ES-module linker (ecma_module_resolve_import) via duplicate var/namespace-import binding - pre-link crash
Severity: MEDIUM | CVSS 3.1: 6.2
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| CWE: CWE-476 | Affected: jerryscript @b7069350c2e5Summary
JerryScript fails to reject an illegal duplicate module binding in which a name is first declared with
var/functionand then re-declared as a namespace import (import * as <name>). Per ECMA-262 a module's top-level binding names must be unique, so this is an early SyntaxError; jerryscript instead accepts it, leaving the binding for that name inconsistent between the module's lexical scope and its import list. When the module's namespace object is later built during linking, ecma_module_resolve_export() (ecma-module.c:446) cannot find an export's local name in the module scope and falls through to ecma_module_resolve_import() (ecma-module.c:330). That function contains an unboundedwhile (true)loop that walks module_p->imports_p assuming the binding is always present - the only guard is a JERRY_ASSERT (compiled out in release builds). With the binding absent from the import list, the loop advances import_node_p past the end of the list to NULL and dereferences it (import_node_p->module_names_p) at ecma-module.c:342, reading address 0x8. The same root cause (improper duplicate-binding handling in module scope) also produces wild-pointer memory corruption on the module cleanup path (observed: SEGV at a non-NULL garbage address 0x17fff80cc in ecma_module_release_module_names, ecma-module.c:146, via ecma_module_release_module), indicating the flaw is a genuine memory-safety defect rather than a benign missing NULL check. This code path is reached only in module mode (jerry -m / JERRY_PARSE_MODULE); the project's libFuzzer/OSS-Fuzz harness only parses input as a plain script (JERRY_PARSE_NO_OPTS), so it never exercises the module linker.Affected
b7069350c2e52e7dc721dfb75f067147bd79b39bjerry -m attacker.mjs, or an embedder that calls jerry_module_link()/jerry_module_evaluate() on attacker-controlled module source). No authentication or prior state required.Root cause
Reachability (untrusted source -> sink)
jerry -m m0.mjs, the shipped CLI]Guards checked: JERRY_PARSE_MODULE is required (module mode) and is the documented default for
jerry -m; JERRY_MODULE_SYSTEM is ON by default (= JERRY_BUILTINS=1). The only NULL guard (JERRY_ASSERT at ecma-module.c:340) is removed by -DJERRY_NDEBUG in every non-debug build, including the default MinSizeRel build. No authentication gate exists; the module source itself is the input.To reproduce
Negative control (proves the duplicate name is causal; exits 0 cleanly):
Output
SIGSEGV (exit 139) on the default build; AddressSanitizer 'SEGV on unknown address 0x000000000008 (READ)' at ecma_module_resolve_import (ecma-module.c:342) on the release+ASAN build. Deterministic across 10/10 ASAN runs and 6/6 + 3/3 default-build runs over two independent clean rebuilds.
Demonstrated vs inferred: Demonstrated: deterministic NULL-pointer dereference -> process crash (DoS) reachable from the shipped
jerry -mCLI in the default configuration. Inferred (not exploited): the same duplicate-binding root cause additionally corrupts module structures (observed wild-pointer SEGV at 0x17fff80cc in ecma_module_release_module_names on the cleanup path); no controllable read/write or code execution was demonstrated, so severity/CVSS reflect DoS only.Impact
Denial of service: a single ~60-byte malicious ES module deterministically crashes the engine (SIGSEGV) during jerry_module_link(), terminating the embedding application/process. JerryScript targets embedded/IoT hosts that may load JavaScript modules from update channels or other partially-trusted sources; such a host is crashed by linking one crafted module. Demonstrated impact is process termination (A:H). Because the underlying duplicate-binding flaw also corrupts the module structures (wild-pointer dereference during module release), a correct fix must reject the duplicate binding rather than only NULL-guard the resolver.
Suggested fix
Primary fix: enforce the ECMA-262 early error for duplicate top-level module bindings so that a
var/function/letdeclaration followed by a same-namedimport * as <name>(and the symmetric case) is rejected as a SyntaxError during scanning/parsing - the named-import clause already performs a SCANNER_TYPE_ERR_REDECLARED check (js-parser-module.c parser_module_parse_import_clause); the namespace-import statement path must perform the equivalent check against existing var/lexical declarations. Defense-in-depth: make ecma_module_resolve_import() tolerate a not-found binding instead of looping on the implicit invariant - replace thewhile (true)/JERRY_ASSERT (import_node_p != NULL)withwhile (import_node_p != NULL)and return false (resolution failure) when the binding is not found, so a corrupted/unexpected state cannot dereference NULL in release builds.References
jerryscript/jerry-core/ecma/base/ecma-module.c
Lines 330 to 371 in b706935
Environment
b7069350c2e52e7dc721dfb75f067147bd79b39b- default build (./configure && make), default config.