Hardening the XS JavaScript Engine
Updated: April 10, 2022
The XS JavaScript engine is at the core of everything Moddable does. The Moddable SDK is a standards-based runtime environment for embedded systems built on XS. Organizations around the world use the Moddable SDK as foundation for consumer and industrial IoT products. Every project Moddable delivers for its clients has XS as its starting point. Moddable is committed to ensuring that XS is as robust as possible so that projects built on XS are as reliable and secure as possible.
It has proven impossible for any organization to identify all the issues in any JavaScript engine. Even the JavaScript engines used in major web browsers regularly receive reports of issues from third party organizations, researchers, and independent developers. This article collects together information to assist those that want to contribute to discovering issues in XS. Moddable is committed to working with experts to identify and resolve issues in XS, particularly those that result in language conformance issues and security vulnerabilities.
This article is organized into the following sections:
Technical Introduction to XS
XS supports the same industry standard JavaScript language used in web browsers. While the JavaScript engines in a browser are optimized for speed, XS is optimized to minimize resource use – especially RAM – so that it can execute complex scripts on resource constrained hardware. The different focus of XS makes its implementation different too.
XS does not aim to run all JavaScript that is supported in a web browser; it does aim to run JavaScript as defined by the language standard. XS takes advantage of some relatively obscure options in the JavaScript language specification. For example, it does not provide the source code for JavaScript functions via Function.prototype.toString
. It also does not implement all of Annex B as this is only required in web browsers.
The XS documentation directory in the Moddable SDK contains extensive information about many aspects of XS. It is a great resource for learning more about the implementation of XS and how XS is used in products. A good starting point is the presentation given to Ecma TC39 about the history, motivations, and data structures of XS.
XS is implemented in C for maximum portability. There is no C++ and no assembly language. It does use some GCC built-ins, when available, to optimize certain operations.
XS compiles scripts to byte code which is then executed by a run loop. There is no JIT or other dynamic optimization. This greatly simplifies the engine implementation thereby eliminating many sources of issues.
XS stores strings internally in UTF-8 format. The use of UTF-8 generally reduces memory use compared to the UTF-16 format assumed by the JavaScript language specification. It also simplifies interacts with the many native APIs which operate on UTF-8 strings directly. This has caused some conformance issues. All strings entering into XS must be checked to ensure they are valid UTF-8.
XS stores JavaScript numbers internally as either 32-bit signed integer values and 64-bit double floating point values. The language standard defines numbers as doubles: the use of integers is an optimization for embedded devices that lack floating point hardware. The internal storage format of number is not generally observable by scripts, but has been a source of conformance issues in some edge cases.
XS has its own Regular Expression engine. This provides a much smaller code footprint than other engines while also using relatively little memory.
XS uses a mark and sweep garbage collector for simplicity. This works well for the small memory size of embedded systems. There is no reference counting and no generational collecting. The garbage collector is a so-called "stop the world" collector, so no other activity occurs in the heap when collecting. The garbage collector combines all free blocks by compacting the heap. This maximizes memory available to scripts by eliminating memory fragmentation. This behavior of the garbage collector is not observable by scripts. The compaction feature, however, has been a source of vulnerabilities.
XS extends some of the built-in JavaScript objects with additional methods. For example, it adds methods for integer division (Math.idiv
). The language specification allows these extensions as long as a script can delete them without breaking the behavior of standard language features.
Testing Advice
Getting started with testing XS can be a challenge. The following sections provide advice to help. The guidance is based on experience testing XS and a review of the issues already reported. It is likely that there are approaches to testing and areas of testing that remain to be identified.
xst
is a command line tool for running JavaScript tests on XS. It is available for macOS, Windows, and Linux. Moddable uses xst
to run the test262 JavaScript language conformance test suite on XS. Details on how to build xst
and how to use it to run test262 are provided here.
Building and running xst
yourself is the best way to be sure you are running the latest version. Moddable also provides a binary distribution of xst
through jvsu, which is particularly useful for comparing conformance test results across several engines.
Resource Exhaustion and Implementation Limits
Tests eventually encounter resource exhaustion, such as out of memory, and implementation limits, such as strings being limited to 2 GB. These are generally not issues, but how they are handled by XS may be. If XS fails to detect a limit has been reached, it can lead to problems.
When XS detects resource exhaustion or that it is has reached an implementation limit, it calls the fxAbort
function which determines what happens next. On embedded systems, fxAbort
generally restarts the system to guarantee it returns to a known good state. The implementation of fxAbort
in xst
throws a JavaScript exception with a RangeError
when memory is exhausted, the JavaScript stack would overflow, or the C stack is close to overflowing. Some test suites require this behavior for certain tests to pass. This behavior also provides a clean exit which helps when performing automated testing.
Once fxAbort
is called, XS has done its job. Any issues that occurs after that are because of choices made in the host implementation of fxAbort
. If you encounter such problems, please report them. If possible, they will be fixed to ensure a smoother testing experience. However, they are not generally critical issues as they impact testing, not production systems. When building with ASAN enabled, XS defines `mxASANStackMargin` to give ASAN additional native stack space when `fxAbort` is called.
The goal of conformance testing is to ensure that XS implements all the behaviors of JavaScript as defined by the language standard. This is essential to ensuring that JavaScript code runs consistently across all engines and environments. To ensure conformance, Moddable regularly runs XS against the test262 tests for language and built-ins. You can run the tests yourself using xst
by following the instructions in the XS Conformance document. XS does not pass 100% of the tests (no engine does!). The conformance document has a section that briefly explains the failures. This is a helpful resource to avoid reporting known issues.
One common source of conformance issues are the limits built into the language. For example, very large and very small numeric values have been a source of conformance issues.
Older features of the language sometimes are sometimes not as well covered by test262 as newer features, and therefore issues can be overlooked.
JavaScript has many dynamic features which cause a function to be invoked during the execution of another function. This happens commonly with type coercion. If the callback function changes the object being operated on, it can lead to issues. While the expected behavior is generally well documented in Ecma-262, implementing that correctly has proven to be challenging for all JavaScript engines.
The jsvu tool is valuable for conformance testing by making it easy to run a single script across multiple JavaScript engines when used with eshost-cli. If you believe you have identified a conformance issue with XS, this is a quick way to see how it compares with other engines. Developers exploring issues with other engines using eshost-cli have noticed issues with XS and reported those.
Vulnerability Testing
The goal of vulnerability testing is to identify places where the implementation performs unsafe operations, such as reading or writing memory that should be inaccessible to a script. Vulnerabilities can lead to incorrect results, unpredictable execution, crashes, and security breaches. Testing for vulnerabilities requires both technical skill and creativity. This section describes some details about XS that may be useful in looking for vulnerabilities.
Moddable is committed to resolving all reported vulnerabilities. That said, many vulnerabilities do not have an obvious path to a security exploit. A vulnerability that leads to a crash can force a device to reboot but does not necessarily not open the device to being taken over. Only a handful of reported vulnerabilities could be easily exploited. Moddable is particularly interested in reports that show how a vulnerability could be exploited.
xst
is the primary tool for vulnerability testing. Moddable recommends using a debug build of xst
as this already has the AddressSanitizer (ASAN) enabled on macOS and Linux builds.
The compacting feature of the memory manager can lead to bugs. If a relocatable pointer is not refreshed after the heap is compacted, the engine code may use a stale address. This can lead to read/write access to unintended memory. These problems are difficult to reproduce because they depend on when the garbage collector runs.
Because XS has its own sub-allocator, ASAN cannot detect all out-of-bounds memory accesses. The XS memory manager has a mode where it uses malloc
instead of the XS sub-allocator. Set mxNoChunks
to 1
in xsMemory.c to enable this mode. As of XS 11.6, mxNoChunks
implements a technique designed to detect use of dangling pointers. Each time the garbage collector runs, every chunk is moved to a new address by allocating a new memory block with malloc
, copying the data, and releasing the previous block. This takes time but allows many more dangling pointers to be detected by ASAN. The effectiveness of this technique can be increased by modifying XS to garbage collect more frequently as the mxStress
option does.
Because it targets resource constrained devices, XS uses 32-bit integers for buffer sizes. These values can overflow, leading to crashes and out-of-bounds memory access. XS uses fxAddChunkSizes
and fxMultiplyChunkSizes
functions internally to detect integer overflow when calculating buffer sizes and offsets. These functions work reliably, but when they are not used when needed, issues can appear.
XS contains three separate parsers: the JavaScript language itself, JSON (a subset of JavaScript that more-or-less requires a separate parser for conformance), and Regular Expressions. Parsers are a well known source of vulnerabilities. Native stack overflows are one potential parser vulnerability which XS only partially defends against. Issue reports here are still expected.
When the execution of one JavaScript functions invokes another JavaScript function, there is a great deal of potential for trouble. For example, if the comparison function passed to sort an Array shrinks the length of the array, XS must not access elements that are no longer part of the array. These malicious callbacks are a common source of vulnerabilities in JavaScript engines.
Fuzzing Support
The xst
tool can be used for fuzz testing. It may be necessary to modify xst
to implement the requirements of a particular fuzzing engine.
xst
implements support for the Fuzzilli fuzzer published by Google Project Zero. This support is active when xst
is built with the FUZZILLI
flag set. The documentation for the XS Profile in the Fuzzilli repository explains how to build xst
for use with Fuzzilli and how to invoke it.
Reporting Issues
Issues may be reported directly to the Moddable repository on GitHub. At this time, issues may be reported publicly. If you prefer to report the issue privately, please send it by email to info@moddable.com. If you are unsure if a behavior is an issue or not, please report it. A good way to do that is using Discussions on our GitHub repository.
It is a lot of work to find an issue. It is important to report the issue as clearly as practical so that it can be quickly assessed and resolved. Here are some things you can do in your issue report to help. These are guidelines, not requirements: do the best you can.
- A concise description of the problem
- A simple proof-of-concept snippet of code that demonstrates the issue. (Direct output of complex code from fuzzers can slow down verification the report!)
- If possible, write the proof-of-concept as a test262 test. This helps with our goal of having regression tests for all reported issues.
- A citation of the relevant specification text from Ecma-262, if it is a conformance issue.
- The XS version number and/or commit used
- Any other details that might help to understand the issue
Acknowledgements
Moddable greatly appreciates the efforts of everyone who has reported issues about XS. The following sections list those individuals together with links to their reports. All reports here have been confirmed as valid, and most have been fixed. If there are any errors or omissions, please let us know.
bakkot (7) |
82 |
For-in prints deleted keys |
87 |
Numeric keys larger than 2**24 - 2*20 + 1 are given as the empty string |
115 |
Correctness issue when shadowing parameters which are mutated |
155 |
Object rest incorrectly accepts complex assignment targets |
285 |
JSON.parse passes wrong argument to reviver in base case when deleting properties |
678 |
new BigInt64Array(new Int32Array(0)) should throw |
704 |
correctness: -(1 << -1) evaluates to -2147483648, but should evaluate to 2147483648 |
dckc (6) |
258 |
Symbol.keyFor() as in @agoric/marshal gives undefined |
259 |
cannot coerce undefined to object! in template literal |
565 |
SyntaxError from const { default: greeting } = { default: 1 } |
577 |
Map get fails on BigInt key |
621 |
Symbol.for('bar').toString() missing 'bar' (missing [[Description]] value) |
652 |
bigint string form is codepoint rather than numeral in OS210603 |
devsnek (2) |
297 |
fxIsFunction gives wrong result for revoked proxy |
464 |
iteratorRecord.[[Done]] is not respected |
dtex (1) |
282 |
Constructor that returns an async IIFE fails |
gibson042 (6) |
665 |
code points above Latin1 are not recognized as white space |
726 |
(
,) is incorrectly treated as valid |
876 |
function name is set incorrectly on a method for a property key that is a registered symbol |
879 |
Change Date.parse to round milliseconds down |
885 |
ToNumber incorrectly accepts "INFINITY" |
886 |
TypedArrays incorrectly write to "NaN" properties |
hax (1) |
284 |
thisArg of accessors in strict mode should not be coercion ToObject |
jugglinmike (4) |
315 |
XS allows trailing comma in Expressions |
316 |
XS misinterprets await in arrow functions |
328 |
XS misinterprets combinations of AwaitExpression and async arrow functions |
335 |
incorrectly interprets zero when used as a multiplication factor |
littledan (1) |
13 |
Variable-length RegExp lookbehind does not work |
lll000111 (1) |
140 |
"cannot coerce undefined to object" - Spread into array syntax issues |
mathiasbynens (3) |
112 |
JSON.stringify correctness issue |
137 |
JSON.parse('-0') should evaluate to -0 |
674 |
Number.prototype.toLocaleString complains about radix |
mhofman (1) |
727 |
TypeError: super: no constructor when prototype.constructor deleted |
mhofman (1) |
818 |
Async generator not closed when yielding rejected promise |
michaelfig (1) |
322 |
async function: silent swallowing of the "override mistake" |
mzgoddard (1) |
330 |
Misinterprets arrow functions without parentheses in many expressions |
Reinose (19) |
400 |
Semantics of PropertyDefinitionEvaluation in Object Initializer |
401 |
Array length property is 0 if it is defined with specific syntax |
403 |
Optional chains are always evaluated |
404 |
Array.prototype.toString behaves differently against the specification |
405 |
Map iterator and Set iterator have excessive properties |
406 |
String.prototype.split behaves differently against the specification |
407 |
Number.prototype.toString radix violation does not covered |
409 |
For-in statement over a string does not work |
410 |
Cannot parse Destructuring assignment - ObjectAssignmentPattern |
411 |
Array.prototype.slice does not work as specification |
412 |
Switch statement contains class declaration is not parsed |
413 |
new.target in generator function does not work |
414 |
Destructuring Assignment - DestructuringAssignmentTarget early error does not occur |
415 |
do-while statement parsing error |
416 |
Cannot parse Destructuring assignment - ArrayAssignmentPattern |
417 |
Function.prototype.bind has excessive properties |
418 |
Cannot parse Destructuring assignment when undefined exists |
420 |
Generator Object has unwanted Symbol Property |
421 |
Property descriptor for set/get is different |
rwaldron (1) |
451 |
Valid switch + nested class syntax producing a SyntaxError exception |
tevador (5) |
58 |
Incorrect string representation of 'NaN' |
59 |
Numeric literals not parsed according to specification |
60 |
JSON.stringify incorrectly considers object cyclic |
62 |
Integer overflow |
64 |
Eval preventing const declaration in the caller scope |
YaoHouyou (2) |
526 |
The syntaxError about illegal token '}' is not thrown as expected |
556 |
An issue about calling Object.prototype.hasOwnProperty without parameters |
Vulnerability Reports
bakkot (1) |
680 |
SIGBUS when overloading regexp.prototype.exec |
dckc (2) |
567 |
unbounded heap growth crashes xst, xsnap (SIGSEGV) |
642 |
print(5n ** 5n ** 6n) crashes xst (in GC?) |
devsnek (1) |
113 |
Array#sort aborts when elements are deleted |
eternalsakura (9) |
735 |
AddressSanitizer: FPE on unknown address 0x000000773e98 |
738 |
AddressSanitizer: Null pointer dereference in fx_Array_prototype_slice |
739 |
stack-overflow in xs/sources/xsType.c:518 in fxOrdinaryGetProperty |
742 |
AddressSanitizer: memcpy-param-overlap: memory ranges overlap in fx_Array_prototype_slice |
743 |
AddressSanitizer: stack-overflow /home/sakura/moddable/xs/sources/xsMemory.c:651 in fxMarkInstance |
744 |
AddressSanitizer: SEGV xs/sources/xsNumber.c:457:16 in fx_Number_prototype_toString |
747 |
AddressSanitizer: stack-overflow xs/sources/xsMemory.c:950 in fxMarkValue |
748 |
Null pointer dereference in fx_Function_prototype_hasInstance |
749 |
AddressSanitizer: SEGV in xs/sources/xsDataView.c:2709:8 in fxFloat32Getter |
gibson042 (1) |
698 |
SIGBUS when overloading regexp.prototype.exec |
hope-fly (8) |
750 |
SEGV xs/sources/xsProxy.c:506 in fxProxyGetPrototype |
751 |
Heap-buffer-overflow in __libc_start_main |
759 |
Heap-buf-overflow (/usr/local/bin/xst+0x4ec9ab) in __asan_memcpy |
760 |
A weird stack overflow when compiled with ASAN |
766 |
SEGV xs/sources/xsArray.c:2237:7 in fx_Array_prototype_sort |
768 |
SEGV (/usr/local/bin/xst+0xdfee5f) in _fini |
769 |
Negative-size-param (/usr/local/bin/xst+0x4ed5ec) in __asan_memmove |
774 |
SEGV xs/sources/xsDataView.c:559:24 in fx_ArrayBuffer_prototype_concat |
jessysaurusrex (17) |
779 |
AddressSanitizer: stack-overflow xsMemory.c:950 in fxMarkValue |
780 |
AddressSanitizer: BUS xsRun.c:772 in fxRunID |
781 |
AddressSanitizer: SEGV xsArray.c:523 in fxGetArrayLimit |
782 |
AddressSanitizer: SEGV xsFunction.c:546 in fx_Function_prototype_hasInstance |
783 |
AddressSanitizer: SEGV xsArray.c:2577 in fx_ArrayIterator_prototype_next |
784 |
AddressSanitizer: SEGV xsMemory.c:1711 in fxSweepValue |
785 |
AddressSanitizer: heap-buffer-overflow xsMemory.c:955 in fxMarkValue |
788 |
SEGV xsRun.c:4140 in fxRunID |
789 |
heap-buffer-overflow xsProperty.c:300 in fxGetIndexProperty |
790 |
SEGV xsMemory.c:1692 in fxSweepValue |
792 |
SEGV xsAPI.c:948 in fxGetAll |
795 |
SEGV xsObject.c:492 in fx_Object_copy |
797 |
SEGV xsMemory.c:947 in fxMarkValue |
798 |
stack-overflow xsMemory.c:945 in fxMarkValue |
805 |
stack-overflow xsAPI.c in fxThrowMessage |
807 |
BUS xsRun.c:774 in fxRunID |
815 |
stack-overflow xsMemory.c:640 in fxMarkInstance |
kvenux (21) |
431 |
Heap buffer overflow at moddable/xs/sources/xsDebug.c:783 |
432 |
Heap buffer overflow at moddable/xs/sources/xsSyntaxical.c:3562 |
440 |
SEGV at moddable/xs/sources/xsCommon.c:916 |
441 |
SEGV at moddable/xs/sources/xsProxy.c:171 |
442 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3419 |
446 |
SEGV at moddable/xs/sources/xsProxy.c:171 (similar with #441) |
447 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3620 |
448 |
SEGV at moddable/xs/sources/xsRun.c:697 |
449 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3277 |
450 |
SEGV at moddable/xs/sources/xsFunction.c:389 |
452 |
SEGV at moddable/xs/sources/xsArray.c:2139 |
453 |
SEGV at moddable/xs/sources/xsArray.c:2414 |
454 |
Heap buffer overflow at moddable/xs/sources/xsLexical.c:760 |
460 |
SEGV at moddable/xs/sources/xsString.c:1610 |
461 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3583 |
462 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3148 |
463 |
SEGV at moddable/xs/sources/xsSyntaxical.c:3499 |
469 |
SEGV at moddable/xs/sources/xsPromise.c:431 |
483 |
heap-buffer-overflow at xs/sources/xsBigInt.c:1354 |
484 |
SEGV at xs/sources/xsAll.c:161 |
485 |
SEGV at /xs/sources/xsBigInt.c:1182 |
MKGaru (1) |
540 |
Variables declared with let are treated as const |
natashenka (2) |
107 |
Memory Corruption in Array.prototype.map |
123 |
Memory Corruption in Array.prototype.copyWithin |
rain6851 (16) |
353 |
NULL pointer dereference |
364 |
stack overflow |
374 |
Access of Uninitialized Pointer |
375 |
double free |
376 |
free invalid pointer |
377 |
heap overflow |
378 |
null pointer dereference |
379 |
null pointer dereference |
580 |
heap-buffer-overflow(fx_ArrayBuffer) |
581 |
heap-buffer-overflow(fx_String_prototype_pad) |
582 |
heap-buffer-overflow(fx_String_prototype_repeat) |
583 |
heap-buffer-overflow(fxIDToString) |
585 |
over access(fxEnvironmentGetProperty) |
586 |
stack-overflow |
587 |
stack-overflow(fxBinaryExpressionNodeDistribute) |
635 |
heap-buffer-overflow(fx_RegExp_prototype_get_flags) |
Moddable's Vulnerability Disclosure Policy
Moddable's vulnerability disclosure policy is based on the policy generated by disclose.io. Our intention in adopting the work of disclose.io is to give security researchers a policy that is both comprehensive and familiar. Because it is a widely used, the disclose.io policy has a broader scope than is strictly necessary for XS. Testing of XS should generally be limited to execution of a test environment such as xst
.
Introduction
Moddable Tech, Inc. welcomes feedback from security researchers and the general public to help improve our security. If you believe you have discovered a vulnerability, privacy issue, exposed data, or other security issues in any of our assets, we want to hear from you. This policy outlines steps for reporting vulnerabilities to us, what we expect, and what you can expect from us.
Systems in Scope
This policy applies to any digital assets owned, operated, or maintained by Moddable Tech, Inc.
Out of Scope
This policy does not apply to assets or other equipment not owned by parties participating in this policy.
Vulnerabilities discovered or suspected in out-of-scope systems should be reported to the appropriate vendor or applicable authority.
Our Commitments
When working with us, according to this policy, you can expect us to:
- Respond to your report promptly, and work with you to understand and validate your report;
- Strive to keep you informed about the progress of a vulnerability as it is processed;
- Work to remediate discovered vulnerabilities in a timely manner, within our operational constraints; and
- Extend Safe Harbor for your vulnerability research that is related to this policy.
Our Expectations
In participating in our vulnerability disclosure program in good faith, we ask that you:
- Play by the rules, including following this policy and any other relevant agreements. If there is any inconsistency between this policy and any other applicable terms, the terms of this policy will prevail;
- Report any vulnerability you’ve discovered promptly;
- Avoid violating the privacy of others, disrupting our systems, destroying data, and/or harming user experience;
- Use only the Official Channels to discuss vulnerability information with us;
- Provide us a reasonable amount of time (at least 60 days from the initial report) to resolve the issue before you disclose it publicly;
- Perform testing only on in-scope systems, and respect systems and activities which are out-of-scope;
- If a vulnerability provides unintended access to data: Limit the amount of data you access to the minimum required for effectively demonstrating a Proof of Concept; and cease testing and submit a report immediately if you encounter any user data during testing, such as Personally Identifiable Information (PII), Personal Healthcare Information (PHI), credit card data, or proprietary information;
- You should only interact with test accounts you own or with explicit permission from the account holder; and
- Do not engage in extortion.
Official Channels
Please report security issues by creating a new issue report in the public Moddable SDK repository on GitHub. Issues may also be reported via email to info@moddable.com. Please provide all relevant information. The more details you provide, the easier it will be for us to triage and fix the issue.
At this time, all issues may be reported publicly. This may change in the future.
Safe Harbor
When conducting vulnerability research, according to this policy, we consider this research conducted under this policy to be:
- Authorized concerning any applicable anti-hacking laws, and we will not initiate or support legal action against you for accidental, good-faith violations of this policy;
- Authorized concerning any relevant anti-circumvention laws, and we will not bring a claim against you for circumvention of technology controls;
- Exempt from restrictions in our Terms of Service (TOS) and/or Acceptable Usage Policy (AUP) that would interfere with conducting security research, and we waive those restrictions on a limited basis; and
- Lawful, helpful to the overall security of the Internet, and conducted in good faith.
You are expected, as always, to comply with all applicable laws. If legal action is initiated by a third party against you and you have complied with this policy, we will take steps to make it known that your actions were conducted in compliance with this policy.
If at any time you have concerns or are uncertain whether your security research is consistent with this policy, please submit a report through one of our Official Channels before going any further.
Note that the Safe Harbor applies only to legal claims under the control of the organization participating in this policy, and that the policy does not bind independent third parties.