diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/fuzz-tests | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/fuzz-tests')
-rw-r--r-- | js/src/fuzz-tests/README | 31 | ||||
-rw-r--r-- | js/src/fuzz-tests/differential-parsing.js | 148 | ||||
-rw-r--r-- | js/src/fuzz-tests/moz.build | 44 | ||||
-rw-r--r-- | js/src/fuzz-tests/parsing-evaluate.js | 83 | ||||
-rw-r--r-- | js/src/fuzz-tests/testExample.cpp | 61 | ||||
-rw-r--r-- | js/src/fuzz-tests/testRegExp.cpp | 111 | ||||
-rw-r--r-- | js/src/fuzz-tests/testStructuredCloneReader.cpp | 84 | ||||
-rw-r--r-- | js/src/fuzz-tests/testWasm.cpp | 448 | ||||
-rw-r--r-- | js/src/fuzz-tests/tests.cpp | 127 | ||||
-rw-r--r-- | js/src/fuzz-tests/tests.h | 19 | ||||
-rw-r--r-- | js/src/fuzz-tests/util/sanitize.js | 104 |
11 files changed, 1260 insertions, 0 deletions
diff --git a/js/src/fuzz-tests/README b/js/src/fuzz-tests/README new file mode 100644 index 0000000000..2c98c1ffa2 --- /dev/null +++ b/js/src/fuzz-tests/README @@ -0,0 +1,31 @@ +# JS Fuzzing Interface + +This directory contains fuzzing targets that implement the unified fuzzing +interface to be used with libFuzzer or AFL. + +## Building the fuzzing targets + +To include this directory in your JS build, you need to build with Clang +and the --enable-fuzzing flag enabled. The build system will automatically +detect if you are building with afl-clang-fast for AFL or regular Clang +for libFuzzer. + +## Running a fuzzing target + +To run a particular target with libFuzzer, use: + + cd $OBJDIR/dist/bin + FUZZER=YourTargetName ./fuzz-tests + +To run with AFL, use something like + + cd $OBJDIR/dist/bin + FUZZER=YourTargetName MOZ_FUZZ_TESTFILE=input \ + afl-fuzz <regular AFL options> -f input ./fuzz-tests + + +## Writing a fuzzing target + +1. Check testExample.cpp for a target skeleton with comments. + +2. Add your own .cpp file to UNIFIED_SOURCES in moz.build diff --git a/js/src/fuzz-tests/differential-parsing.js b/js/src/fuzz-tests/differential-parsing.js new file mode 100644 index 0000000000..1a61175721 --- /dev/null +++ b/js/src/fuzz-tests/differential-parsing.js @@ -0,0 +1,148 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is used to detect and find cases where the Visage's parser is +// accepting more inputs than SpiderMonkey's parser. +// +// 1. Find new cases: +// +// To find new cases, we have to build with libFuzzer. The JS Shell can easily +// be built with libFuzzer by adding --enable-fuzzing to the configure script +// command line. +// +// Create a directory, and copy all test cases from the JS shell to this +// directory: +// +// $ mkdir fuzzer-input +// $ cd fuzzer-input +// $ find ../ -name \*.js -print0 | xargs -I '{}' -0 -n1 cp '{}' $(pwd) +// +// Once the JS Shell is built, set the FUZZER environment variable to this +// script location. +// +// $ FUZZER="./fuzz-tests/differential-parsing.js" build.dir/dist/bin/js -- \ +// -use_value_profile=1 -print_pcs=1 -timeout=5 -max_len=32 -only_ascii=1 \ +// ./fuzzer-input +// +// 2. Test a crashing test case: +// +// Once a new crashing test case is found, this script can be used to +// reproduce the crashing conditions. +// +// To do so, you need a JS Shell and to load this script and use the testFile +// function with the location of the crashing file. +// +// $ build.dir/dist/bin/js +// js> load("./fuzz-tests/differential-parsing.js"); +// js> testFile("./crash-42"); +// Parse Script C++: fail +// Parse Module C++: fail +// Parse Script Rust: succeed +// Parse Module Rust: fail +// Hit MOZ_CRASH(Rust accept more than C++) +// + +/* global crash, os, parse, timeout */ + +// This global will hold the current fuzzing buffer for each iteration. +var fuzzBuf; + +function timed(sec, f) { + // If the function `f` takes more than 3 seconds, then the evaluation ends + // prematurely and returns in libFuzzer handler without considering this + // test case as interesting. + timeout(sec, function() { + return false; + }); + f(); + + // Remove the timeout handler, to not kill future executions. + timeout(-1); +} + +var parseScriptCpp = { module: false, smoosh: false }; +var parseScriptRust = { module: false, smoosh: true }; +var parseModuleRust = { module: true, smoosh: true }; +var parseModuleCpp = { module: true, smoosh: false }; +function test(code, verbose = false) { + var isScriptCpp = false, + isModuleCpp = false, + isScriptRust = false, + isModuleRust = false; + try { + parse(code, parseScriptCpp); + isScriptCpp = true; + if (verbose) { + console.log("Parse Script C++: succeed"); + } + } catch (exc) { + if (verbose) { + console.log("Parse Script C++: fail"); + } + } + try { + parse(code, parseModuleCpp); + isModuleCpp = true; + if (verbose) { + console.log("Parse Module C++: succeed"); + } + } catch (exc) { + if (verbose) { + console.log("Parse Module C++: fail"); + } + } + try { + parse(code, parseScriptRust); + isScriptRust = true; + if (verbose) { + console.log("Parse Script Rust: succeed"); + } + } catch (exc) { + if (verbose) { + console.log("Parse Script Rust: fail"); + } + } + try { + parse(code, parseModuleRust); + isModuleRust = true; + if (verbose) { + console.log("Parse Module Rust: succeed"); + } + } catch (exc) { + if (verbose) { + console.log("Parse Module Rust: fail"); + } + } + if ((isScriptRust && !isScriptCpp) || (isModuleRust && !isModuleCpp)) { + crash("Rust accept more than C++"); + } +} + +function JSFuzzIterate() { + // This function is called per iteration. You must ensure that: + // + // 1) Each of your actions/decisions is only based on fuzzBuf, + // in particular not on Math.random(), Date/Time or other + // external inputs. + // + // 2) Your actions should be deterministic. The same fuzzBuf + // should always lead to the same set of actions/decisions. + // + // 3) You can modify the global where needed, but ensure that + // each iteration is isolated from one another by cleaning + // any modifications to the global after each iteration. + // In particular, iterations must not depend on or influence + // each other in any way (see also 1)). + // + // 4) You must catch all exceptions. + let code = String.fromCharCode(...fuzzBuf); + timed(3, _ => test(code)); + return 0; +} + +function testFile(file) { + let content = os.file.readFile(file); + test(content, true); +} diff --git a/js/src/fuzz-tests/moz.build b/js/src/fuzz-tests/moz.build new file mode 100644 index 0000000000..dc6e5cb9a5 --- /dev/null +++ b/js/src/fuzz-tests/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoProgram("fuzz-tests", linkage=None) + +include("../js-cxxflags.mozbuild") +include("../js-standalone.mozbuild") + +UNIFIED_SOURCES += [ + "testExample.cpp", + "testRegExp.cpp", + "tests.cpp", + "testStructuredCloneReader.cpp", + "testWasm.cpp", +] + +DEFINES["EXPORT_JS_API"] = True + +LOCAL_INCLUDES += [ + "!..", + "..", +] + +if CONFIG["FUZZING"]: + USE_LIBS += [ + "static:fuzzer-registry", + ] + +if CONFIG["LIBFUZZER"]: + USE_LIBS += [ + "static:fuzzer", + ] + + # Add libFuzzer configuration directives + include("/tools/fuzzing/libfuzzer-config.mozbuild") + +USE_LIBS += [ + "static:js", +] + +DEFINES["topsrcdir"] = "%s/js/src" % TOPSRCDIR diff --git a/js/src/fuzz-tests/parsing-evaluate.js b/js/src/fuzz-tests/parsing-evaluate.js new file mode 100644 index 0000000000..1714e051a9 --- /dev/null +++ b/js/src/fuzz-tests/parsing-evaluate.js @@ -0,0 +1,83 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This fuzzing target aims to stress the SpiderMonkey parser. However, for +// this purpose, it does *not* use `parse()` because some past bugs in the +// parser could only be triggered in the runtime later. Instead, we use +// the `evaluate` function which parses and runs the code. This brings in +// other problems like timeouts and permanent side-effects. We try to minimize +// the amount of permanent side-effects from running the code by running it +// in a fresh global for each iteration. We also use a special function +// called `sanitizeGlobal` to remove any harmful shell functions from the +// global prior to running. Many of these shell functions would otherwise +// have permanent side-effects of some sort or be disruptive to testing like +// increasing the amount of timeouts or leak memory. Finally, the target also +// tries to catch timeouts locally and signal back any timeouts by returning 1 +// from the iteration function. + +// This global will hold the current fuzzing buffer for each iteration. +var fuzzBuf; + +loadRelativeToScript("util/sanitize.js"); + +deterministicgc(true); + +// Set a default value for timeouts to 1 second, but allow this to +// be set on the command line as well using -e fuzzTimeout=VAL. +if (typeof fuzzTimeout === "undefined") { + fuzzTimeout = 1; +} + +function JSFuzzIterate() { + try { + let code = String.fromCharCode(...fuzzBuf); + let result = null; + + // Create a new global and sanitize it such that its potentially permanent + // side-effects are reduced to a minimum. + let global = newGlobal(); + sanitizeGlobal(global); + + // Work around memory leaks when the hook is not set + evaluate(` + setModuleResolveHook(function(module, specifier) { + throw "Module '" + specifier + "' not found"; + }); + setModuleResolveHook = function() {}; + `, { global: global, catchTermination: true }); + + // Start a timer and set a timeout in addition + let lfStart = monotonicNow(); + timeout(fuzzTimeout, function() { return false; }); + + try { + result = evaluate(code, { global: global, catchTermination: true }); + } catch(exc) { + print(exc); + } + + timeout(-1); + let lfStop = monotonicNow(); + + // Reset some things that could have been altered by the code we ran + gczeal(0); + schedulegc(0); + setGCCallback({ action: "majorGC" }); + clearSavedFrames(); + + // If we either ended terminating the script, or we took longer than + // the timeout set (but timeout didn't kick in), then we return 1 to + // signal libFuzzer that the sample just be abandoned. + if (result === "terminated" || (lfStop - lfStart > (fuzzTimeout * 1000 + 200))) { + return 1; + } + + return 0; + } catch(exc) { + print("Caught toplevel exception: " + exc); + } + + return 1; +} diff --git a/js/src/fuzz-tests/testExample.cpp b/js/src/fuzz-tests/testExample.cpp new file mode 100644 index 0000000000..311f230b3a --- /dev/null +++ b/js/src/fuzz-tests/testExample.cpp @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ScopeExit.h" + +#include "jsapi.h" + +#include "fuzz-tests/tests.h" +#include "vm/Interpreter.h" + +#include "vm/JSContext-inl.h" + +using namespace JS; +using namespace js; + +extern JS::PersistentRootedObject gGlobal; +extern JSContext* gCx; + +static int testExampleInit(int* argc, char*** argv) { + /* This function is called once at startup. You can use it to e.g. read + environment variables to initialize additional options you might need. + Note that `gCx` and `gGlobal` are pre-initialized by the harness. + */ + return 0; +} + +static int testExampleFuzz(const uint8_t* buf, size_t size) { + /* If your code directly or indirectly allocates GC memory, then it makes + sense to attempt and collect that after every iteration. This should detect + GC issues as soon as possible (right after your iteration), rather than + later when your code happens to trigger GC coincidentially. You can of + course disable this code + if it is not required in your use case, which will speed up fuzzing. */ + auto gcGuard = mozilla::MakeScopeExit([&] { + JS::PrepareForFullGC(gCx); + JS::NonIncrementalGC(gCx, GC_NORMAL, JS::GCReason::API); + }); + + /* Add code here that processes the given buffer. + While doing so, you need to follow these rules: + + 1. Do not modify or free the buffer. Make a copy if necessary. + 2. This function must always return 0. + 3. Do not crash or abort unless the condition constitutes a bug. + 4. You may use the `gGlobal` and `gCx` variables, they are pre-initialized. + 5. Try to keep the effects of this function contained, such that future + calls to this function are not affected. Otherwise you end up with + non-reproducible testcases and coverage measurements will be incorrect. + */ + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(testExampleInit, /* init function */ + testExampleFuzz, /* fuzzing function */ + Example /* module name */ +); diff --git a/js/src/fuzz-tests/testRegExp.cpp b/js/src/fuzz-tests/testRegExp.cpp new file mode 100644 index 0000000000..edd9157fbe --- /dev/null +++ b/js/src/fuzz-tests/testRegExp.cpp @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ScopeExit.h" + +#include "jsapi.h" + +#include "fuzz-tests/tests.h" +#include "irregexp/RegExpAPI.h" +#include "vm/Interpreter.h" +#include "vm/JSAtom.h" +#include "vm/MatchPairs.h" + +#include "vm/JSContext-inl.h" + +using namespace JS; +using namespace js; + +extern JS::PersistentRootedObject gGlobal; +extern JSContext* gCx; + +static int testRegExpInit(int* argc, char*** argv) { return 0; } + +static int testRegExpFuzz(const uint8_t* buf, size_t size) { + auto gcGuard = mozilla::MakeScopeExit([&] { + JS::PrepareForFullGC(gCx); + JS::NonIncrementalGC(gCx, GC_NORMAL, JS::GCReason::API); + }); + + const uint32_t HEADER_LEN = 2; + if (size <= HEADER_LEN) { + return 0; + } + + uint8_t rawFlags = buf[0]; + int32_t patternLength = buf[1]; + + const uint32_t startIndex = 0; + + RegExpFlags flags(rawFlags & RegExpFlag::AllFlags); + + int32_t inputLength = size - HEADER_LEN - patternLength; + + const char* patternChars = reinterpret_cast<const char*>(buf + HEADER_LEN); + + const char* inputChars; + if (inputLength < 0) { + patternLength = size - HEADER_LEN; + + bool useUnicodeInput = (buf[1] & 1) == 0; + inputChars = useUnicodeInput ? "Привет мир" : "Hello\nworld!"; + inputLength = strlen(inputChars); + } else { + inputChars = patternChars + patternLength; + } + + RootedAtom pattern(gCx, AtomizeUTF8Chars(gCx, patternChars, patternLength)); + if (!pattern) { + ReportOutOfMemory(gCx); + return 0; + } + RootedAtom input(gCx, AtomizeUTF8Chars(gCx, inputChars, inputLength)); + if (!input) { + ReportOutOfMemory(gCx); + return 0; + } + + VectorMatchPairs interpretedMatches; + VectorMatchPairs compiledMatches; + + RegExpRunStatus iStatus = irregexp::ExecuteForFuzzing( + gCx, pattern, input, flags, startIndex, &interpretedMatches, + RegExpShared::CodeKind::Bytecode); + if (iStatus == RegExpRunStatus_Error) { + if (gCx->isThrowingOverRecursed()) { + return 0; + } + gCx->clearPendingException(); + } + RegExpRunStatus cStatus = irregexp::ExecuteForFuzzing( + gCx, pattern, input, flags, startIndex, &compiledMatches, + RegExpShared::CodeKind::Jitcode); + if (cStatus == RegExpRunStatus_Error) { + if (gCx->isThrowingOverRecursed()) { + return 0; + } + gCx->clearPendingException(); + } + + // Use release asserts to enable fuzzing on non-debug builds. + MOZ_RELEASE_ASSERT(iStatus == cStatus); + if (iStatus == RegExpRunStatus_Success) { + MOZ_RELEASE_ASSERT(interpretedMatches.pairCount() == + compiledMatches.pairCount()); + for (uint32_t i = 0; i < interpretedMatches.pairCount(); i++) { + MOZ_RELEASE_ASSERT( + interpretedMatches[i].start == compiledMatches[i].start && + interpretedMatches[i].limit == compiledMatches[i].limit); + } + } + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(testRegExpInit, /* init function */ + testRegExpFuzz, /* fuzzing function */ + RegExp /* module name */ +); diff --git a/js/src/fuzz-tests/testStructuredCloneReader.cpp b/js/src/fuzz-tests/testStructuredCloneReader.cpp new file mode 100644 index 0000000000..0025779b45 --- /dev/null +++ b/js/src/fuzz-tests/testStructuredCloneReader.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ScopeExit.h" + +#include "jsapi.h" + +#include "fuzz-tests/tests.h" +#include "js/StructuredClone.h" +#include "vm/Interpreter.h" + +#include "vm/JSContext-inl.h" + +using namespace js; + +// These are defined and pre-initialized by the harness (in tests.cpp). +extern JS::PersistentRootedObject gGlobal; +extern JSContext* gCx; + +static int testStructuredCloneReaderInit(int* argc, char*** argv) { return 0; } + +static int testStructuredCloneReaderFuzz(const uint8_t* buf, size_t size) { + auto gcGuard = mozilla::MakeScopeExit([&] { + JS::PrepareForFullGC(gCx); + JS::NonIncrementalGC(gCx, GC_NORMAL, JS::GCReason::API); + }); + + if (!size) return 0; + + // Make sure to pad the buffer to a multiple of kSegmentAlignment + const size_t kSegmentAlignment = 8; + size_t buf_size = RoundUp(size, kSegmentAlignment); + + JS::StructuredCloneScope scope = JS::StructuredCloneScope::DifferentProcess; + + auto clonebuf = MakeUnique<JSStructuredCloneData>(scope); + if (!clonebuf || !clonebuf->Init(buf_size)) { + ReportOutOfMemory(gCx); + return 0; + } + + // Copy buffer then pad with zeroes. + if (!clonebuf->AppendBytes((const char*)buf, size)) { + ReportOutOfMemory(gCx); + return 0; + } + char padding[kSegmentAlignment] = {0}; + if (!clonebuf->AppendBytes(padding, buf_size - size)) { + ReportOutOfMemory(gCx); + return 0; + } + + JS::CloneDataPolicy policy; + RootedValue deserialized(gCx); + if (!JS_ReadStructuredClone(gCx, *clonebuf, JS_STRUCTURED_CLONE_VERSION, + scope, &deserialized, policy, nullptr, nullptr)) { + return 0; + } + + /* If we succeeded in deserializing, we should try to reserialize the data. + This has two main advantages: + + 1) It tests parts of the serializer as well. + 2) The deserialized data is actually used, making it more likely to detect + further memory-related problems. + + Tests show that this also doesn't cause a serious performance penalty. + */ + mozilla::Maybe<JSAutoStructuredCloneBuffer> clonebufOut; + + clonebufOut.emplace(scope, nullptr, nullptr); + if (!clonebufOut->write(gCx, deserialized, UndefinedHandleValue, policy)) { + return 0; + } + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(testStructuredCloneReaderInit, + testStructuredCloneReaderFuzz, StructuredCloneReader); diff --git a/js/src/fuzz-tests/testWasm.cpp b/js/src/fuzz-tests/testWasm.cpp new file mode 100644 index 0000000000..a5611419d5 --- /dev/null +++ b/js/src/fuzz-tests/testWasm.cpp @@ -0,0 +1,448 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ScopeExit.h" + +#include "jsapi.h" +#include "jspubtd.h" + +#include "fuzz-tests/tests.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/TypedArrayObject.h" + +#include "wasm/WasmCompile.h" +#include "wasm/WasmCraneliftCompile.h" +#include "wasm/WasmIonCompile.h" +#include "wasm/WasmJS.h" +#include "wasm/WasmTable.h" + +#include "vm/ArrayBufferObject-inl.h" +#include "vm/JSContext-inl.h" + +using namespace js; +using namespace js::wasm; + +// These are defined and pre-initialized by the harness (in tests.cpp). +extern JS::PersistentRootedObject gGlobal; +extern JSContext* gCx; + +static int testWasmInit(int* argc, char*** argv) { + if (!wasm::HasSupport(gCx) || + !GlobalObject::getOrCreateConstructor(gCx, JSProto_WebAssembly)) { + MOZ_CRASH("Failed to initialize wasm support"); + } + + return 0; +} + +static bool emptyNativeFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + return true; +} + +static bool callExportedFunc(HandleFunction func, + MutableHandleValue lastReturnVal) { + // TODO: We can specify a thisVal here. + RootedValue thisVal(gCx, UndefinedValue()); + JS::RootedValueVector args(gCx); + + if (!lastReturnVal.isNull() && !lastReturnVal.isUndefined() && + !args.append(lastReturnVal)) { + return false; + } + + RootedValue returnVal(gCx); + if (!Call(gCx, thisVal, func, args, &returnVal)) { + gCx->clearPendingException(); + } else { + lastReturnVal.set(returnVal); + } + + return true; +} + +template <typename T> +static bool assignImportKind(const Import& import, HandleObject obj, + HandleObject lastExportsObj, + JS::Handle<JS::IdVector> lastExportIds, + size_t* currentExportId, size_t exportsLength, + HandleValue defaultValue) { + bool assigned = false; + while (*currentExportId < exportsLength) { + RootedValue propVal(gCx); + if (!JS_GetPropertyById(gCx, lastExportsObj, + lastExportIds[*currentExportId], &propVal)) { + return false; + } + + (*currentExportId)++; + + if (propVal.isObject() && propVal.toObject().is<T>()) { + if (!JS_SetProperty(gCx, obj, import.field.get(), propVal)) { + return false; + } + + assigned = true; + break; + } + } + if (!assigned) { + if (!JS_SetProperty(gCx, obj, import.field.get(), defaultValue)) { + return false; + } + } + return true; +} + +static int testWasmFuzz(const uint8_t* buf, size_t size) { + auto gcGuard = mozilla::MakeScopeExit([&] { + JS::PrepareForFullGC(gCx); + JS::NonIncrementalGC(gCx, GC_NORMAL, JS::GCReason::API); + }); + + const size_t MINIMUM_MODULE_SIZE = 8; + + // The smallest valid wasm module is 8 bytes and we need 1 byte for size + if (size < MINIMUM_MODULE_SIZE + 1) return 0; + + size_t currentIndex = 0; + + // Store the last non-empty exports object and its enumerated Ids here + RootedObject lastExportsObj(gCx); + JS::Rooted<JS::IdVector> lastExportIds(gCx, JS::IdVector(gCx)); + + // Store the last return value so we can pass it in as an argument during + // the next call (which can be on another module as well). + RootedValue lastReturnVal(gCx); + + while (size - currentIndex >= MINIMUM_MODULE_SIZE + 1) { + // Ensure we have no lingering exceptions from previous modules + gCx->clearPendingException(); + + unsigned char moduleLen = buf[currentIndex]; + currentIndex++; + + if (size - currentIndex < moduleLen) { + moduleLen = size - currentIndex; + } + + if (moduleLen < MINIMUM_MODULE_SIZE) { + continue; + } + + if (currentIndex == 1) { + // If this is the first module we are reading, we use the first + // few bytes to tweak some settings. These are fixed anyway and + // overwritten later on. + uint8_t optByte = (uint8_t)buf[currentIndex]; + + // Note that IonPlatformSupport() and CraneliftPlatformSupport() do not + // take into account whether those compilers support particular features + // that may have been enabled. + bool enableWasmBaseline = ((optByte & 0xF0) == (1 << 7)); + bool enableWasmOptimizing = false; +#ifdef ENABLE_WASM_CRANELIFT + enableWasmOptimizing = + CraneliftPlatformSupport() && ((optByte & 0xF0) == (1 << 5)); +#else + enableWasmOptimizing = + IonPlatformSupport() && ((optByte & 0xF0) == (1 << 6)); +#endif + bool enableWasmAwaitTier2 = (IonPlatformSupport() +#ifdef ENABLE_WASM_CRANELIFT + || CraneliftPlatformSupport() +#endif + ) && + ((optByte & 0xF) == (1 << 3)); + + if (!enableWasmBaseline && !enableWasmOptimizing) { + // If nothing is selected explicitly, enable an optimizing compiler to + // test more platform specific JIT code. However, on some platforms, + // e.g. ARM64 on Windows, we do not have Ion available, so we need to + // switch to baseline instead. + if (IonPlatformSupport() || CraneliftPlatformSupport()) { + enableWasmOptimizing = true; + } else { + enableWasmBaseline = true; + } + } + + if (enableWasmAwaitTier2) { + // Tier 2 needs Baseline + Optimizing + enableWasmBaseline = true; + + if (!enableWasmOptimizing) { + enableWasmOptimizing = true; + } + } + + JS::ContextOptionsRef(gCx) + .setWasmBaseline(enableWasmBaseline) +#ifdef ENABLE_WASM_CRANELIFT + .setWasmCranelift(enableWasmOptimizing) +#else + .setWasmIon(enableWasmOptimizing) +#endif + .setTestWasmAwaitTier2(enableWasmAwaitTier2); + } + + // Expected header for a valid WebAssembly module + uint32_t magic_header = 0x6d736100; + uint32_t magic_version = 0x1; + + // We just skip over the first 8 bytes now because we fill them + // with `magic_header` and `magic_version` anyway. + currentIndex += 8; + moduleLen -= 8; + + RootedWasmInstanceObject instanceObj(gCx); + + MutableBytes bytecode = gCx->new_<ShareableBytes>(); + if (!bytecode || !bytecode->append((uint8_t*)&magic_header, 4) || + !bytecode->append((uint8_t*)&magic_version, 4) || + !bytecode->append(&buf[currentIndex], moduleLen)) { + return 0; + } + + currentIndex += moduleLen; + + ScriptedCaller scriptedCaller; + SharedCompileArgs compileArgs = + CompileArgs::build(gCx, std::move(scriptedCaller)); + if (!compileArgs) { + return 0; + } + + UniqueChars error; + UniqueCharsVector warnings; + SharedModule module = + CompileBuffer(*compileArgs, *bytecode, &error, &warnings); + if (!module) { + continue; + } + + // At this point we have a valid module and we should try to ensure + // that its import requirements are met for instantiation. + const ImportVector& importVec = module->imports(); + + // Empty native function used to fill in function import slots if we + // run out of functions exported by other modules. + JS::RootedFunction emptyFunction(gCx); + emptyFunction = + JS_NewFunction(gCx, emptyNativeFunction, 0, 0, "emptyFunction"); + + if (!emptyFunction) { + return 0; + } + + RootedValue emptyFunctionValue(gCx, ObjectValue(*emptyFunction)); + RootedValue nullValue(gCx, NullValue()); + + RootedObject importObj(gCx, JS_NewPlainObject(gCx)); + + if (!importObj) { + return 0; + } + + size_t exportsLength = lastExportIds.length(); + size_t currentFunctionExportId = 0; + size_t currentTableExportId = 0; + size_t currentMemoryExportId = 0; + size_t currentGlobalExportId = 0; +#ifdef ENABLE_WASM_EXCEPTIONS + size_t currentEventExportId = 0; +#endif + + for (const Import& import : importVec) { + // First try to get the namespace object, create one if this is the + // first time. + RootedValue v(gCx); + if (!JS_GetProperty(gCx, importObj, import.module.get(), &v) || + !v.isObject()) { + // Insert empty object at importObj[import.module.get()] + RootedObject plainObj(gCx, JS_NewPlainObject(gCx)); + + if (!plainObj) { + return 0; + } + + RootedValue plainVal(gCx, ObjectValue(*plainObj)); + if (!JS_SetProperty(gCx, importObj, import.module.get(), plainVal)) { + return 0; + } + + // Get the object we just inserted, store in v, ensure it is an + // object (no proxies or other magic at work). + if (!JS_GetProperty(gCx, importObj, import.module.get(), &v) || + !v.isObject()) { + return 0; + } + } + + RootedObject obj(gCx, &v.toObject()); + bool found = false; + if (JS_HasProperty(gCx, obj, import.field.get(), &found) && !found) { + // Insert i-th export object that fits the type requirement + // at `v[import.field.get()]`. + + switch (import.kind) { + case DefinitionKind::Function: + if (!assignImportKind<JSFunction>( + import, obj, lastExportsObj, lastExportIds, + ¤tFunctionExportId, exportsLength, + emptyFunctionValue)) { + return 0; + } + break; + + case DefinitionKind::Table: + // TODO: Pass a dummy defaultValue + if (!assignImportKind<WasmTableObject>( + import, obj, lastExportsObj, lastExportIds, + ¤tTableExportId, exportsLength, nullValue)) { + return 0; + } + break; + + case DefinitionKind::Memory: + // TODO: Pass a dummy defaultValue + if (!assignImportKind<WasmMemoryObject>( + import, obj, lastExportsObj, lastExportIds, + ¤tMemoryExportId, exportsLength, nullValue)) { + return 0; + } + break; + + case DefinitionKind::Global: + // TODO: Pass a dummy defaultValue + if (!assignImportKind<WasmGlobalObject>( + import, obj, lastExportsObj, lastExportIds, + ¤tGlobalExportId, exportsLength, nullValue)) { + return 0; + } + break; + +#ifdef ENABLE_WASM_EXCEPTIONS + case DefinitionKind::Event: + // TODO: Pass a dummy defaultValue + if (!assignImportKind<WasmExceptionObject>( + import, obj, lastExportsObj, lastExportIds, + ¤tEventExportId, exportsLength, nullValue)) { + return 0; + } + break; +#endif + } + } + } + + Rooted<ImportValues> imports(gCx); + if (!GetImports(gCx, *module, importObj, imports.address())) { + continue; + } + + if (!module->instantiate(gCx, imports.get(), nullptr, &instanceObj)) { + continue; + } + + // At this module we have a valid WebAssembly module instance. + + RootedObject exportsObj(gCx, &instanceObj->exportsObj()); + JS::Rooted<JS::IdVector> exportIds(gCx, JS::IdVector(gCx)); + if (!JS_Enumerate(gCx, exportsObj, &exportIds)) { + continue; + } + + if (!exportIds.length()) { + continue; + } + + // Store the last exports for re-use later + lastExportsObj = exportsObj; + lastExportIds.get() = std::move(exportIds.get()); + + for (size_t i = 0; i < lastExportIds.length(); i++) { + RootedValue propVal(gCx); + if (!JS_GetPropertyById(gCx, exportsObj, lastExportIds[i], &propVal)) { + return 0; + } + + if (propVal.isObject()) { + RootedObject propObj(gCx, &propVal.toObject()); + + if (propObj->is<JSFunction>()) { + RootedFunction func(gCx, &propObj->as<JSFunction>()); + + if (!callExportedFunc(func, &lastReturnVal)) { + return 0; + } + } + + if (propObj->is<WasmTableObject>()) { + Rooted<WasmTableObject*> tableObj(gCx, + &propObj->as<WasmTableObject>()); + size_t tableLen = tableObj->table().length(); + + RootedValue tableGetVal(gCx); + if (!JS_GetProperty(gCx, tableObj, "get", &tableGetVal)) { + return 0; + } + RootedFunction tableGet(gCx, + &tableGetVal.toObject().as<JSFunction>()); + + for (size_t i = 0; i < tableLen; i++) { + JS::RootedValueVector tableGetArgs(gCx); + if (!tableGetArgs.append(NumberValue(uint32_t(i)))) { + return 0; + } + + RootedValue readFuncValue(gCx); + if (!Call(gCx, tableObj, tableGet, tableGetArgs, &readFuncValue)) { + return 0; + } + + if (readFuncValue.isNull()) { + continue; + } + + RootedFunction callee(gCx, + &readFuncValue.toObject().as<JSFunction>()); + + if (!callExportedFunc(callee, &lastReturnVal)) { + return 0; + } + } + } + + if (propObj->is<WasmMemoryObject>()) { + Rooted<WasmMemoryObject*> memory(gCx, + &propObj->as<WasmMemoryObject>()); + size_t byteLen = memory->volatileMemoryLength32(); + if (byteLen) { + // Read the bounds of the buffer to ensure it is valid. + // AddressSanitizer would detect any out-of-bounds here. + uint8_t* rawMemory = memory->buffer().dataPointerEither().unwrap(); + volatile uint8_t rawMemByte = 0; + rawMemByte += rawMemory[0]; + rawMemByte += rawMemory[byteLen - 1]; + } + } + + if (propObj->is<WasmGlobalObject>()) { + Rooted<WasmGlobalObject*> global(gCx, + &propObj->as<WasmGlobalObject>()); + if (global->type() != ValType::I64) { + global->val().get().toJSValue(gCx, &lastReturnVal); + } + } + } + } + } + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(testWasmInit, testWasmFuzz, Wasm); diff --git a/js/src/fuzz-tests/tests.cpp b/js/src/fuzz-tests/tests.cpp new file mode 100644 index 0000000000..d85bad72d3 --- /dev/null +++ b/js/src/fuzz-tests/tests.cpp @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "fuzz-tests/tests.h" + +#include <stdio.h> + +#include "js/AllocPolicy.h" +#include "js/Initialization.h" +#include "js/RootingAPI.h" +#include "vm/JSContext.h" + +#ifdef LIBFUZZER +# include "FuzzerDefs.h" +#endif + +using namespace mozilla; + +JS::PersistentRootedObject gGlobal; +JSContext* gCx = nullptr; + +static const JSClass* getGlobalClass() { + static const JSClass c = {"global", JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + return &c; +} + +static JSObject* jsfuzz_createGlobal(JSContext* cx, JSPrincipals* principals) { + /* Create the global object. */ + JS::RealmOptions options; + options.creationOptions().setStreamsEnabled(true).setWeakRefsEnabled( + JS::WeakRefSpecifier::EnabledWithCleanupSome); + return JS_NewGlobalObject(cx, getGlobalClass(), principals, + JS::FireOnNewGlobalHook, options); +} + +static bool jsfuzz_init(JSContext** cx, JS::PersistentRootedObject* global) { + *cx = JS_NewContext(8L * 1024 * 1024); + if (!*cx) { + return false; + } + + const size_t MAX_STACK_SIZE = 500000; + + JS_SetNativeStackQuota(*cx, MAX_STACK_SIZE); + + js::UseInternalJobQueues(*cx); + if (!JS::InitSelfHostedCode(*cx)) { + return false; + } + global->init(*cx); + *global = jsfuzz_createGlobal(*cx, nullptr); + if (!*global) { + return false; + } + JS::EnterRealm(*cx, *global); + return true; +} + +static void jsfuzz_uninit(JSContext* cx) { + if (cx) { + JS_DestroyContext(cx); + cx = nullptr; + } +} + +#ifdef LIBFUZZER +static void jsfuzz_atexit() { JS_ShutDown(); } +#endif + +int main(int argc, char* argv[]) { + if (!JS_Init()) { + fprintf(stderr, "Error: Call to jsfuzz_init() failed\n"); + return 1; + } + + if (!jsfuzz_init(&gCx, &gGlobal)) { + fprintf(stderr, "Error: Call to jsfuzz_init() failed\n"); + return 1; + } + +#ifdef LIBFUZZER + // This is required because libFuzzer can exit() in various cases + std::atexit(jsfuzz_atexit); +#endif + + const char* fuzzerEnv = getenv("FUZZER"); + if (!fuzzerEnv) { + fprintf(stderr, + "Must specify fuzzing target in FUZZER environment variable\n"); + return 1; + } + + std::string moduleNameStr(getenv("FUZZER")); + + FuzzerFunctions funcs = + FuzzerRegistry::getInstance().getModuleFunctions(moduleNameStr); + FuzzerInitFunc initFunc = funcs.first; + FuzzerTestingFunc testingFunc = funcs.second; + if (initFunc) { + int ret = initFunc(&argc, &argv); + if (ret) { + fprintf(stderr, "Fuzzing Interface: Error: Initialize callback failed\n"); + return ret; + } + } + + if (!testingFunc) { + fprintf(stderr, "Fuzzing Interface: Error: No testing callback found\n"); + return 1; + } + +#ifdef LIBFUZZER + fuzzer::FuzzerDriver(&argc, &argv, testingFunc); +#elif __AFL_COMPILER + testingFunc(nullptr, 0); +#endif + + jsfuzz_uninit(gCx); + + JS_ShutDown(); + + return 0; +} diff --git a/js/src/fuzz-tests/tests.h b/js/src/fuzz-tests/tests.h new file mode 100644 index 0000000000..3935ade748 --- /dev/null +++ b/js/src/fuzz-tests/tests.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef fuzz_tests_tests_h +#define fuzz_tests_tests_h + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "FuzzingInterface.h" + +#include "vm/JSContext.h" + +#endif /* fuzz_tests_tests_h */ diff --git a/js/src/fuzz-tests/util/sanitize.js b/js/src/fuzz-tests/util/sanitize.js new file mode 100644 index 0000000000..59d43a42bd --- /dev/null +++ b/js/src/fuzz-tests/util/sanitize.js @@ -0,0 +1,104 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This function can be used to "sanitize" a new global for fuzzing in such +// a way that permanent side-effects, hangs and behavior that could be harmful +// to libFuzzer targets is reduced to a minimum. +function sanitizeGlobal(g) { + let lfFuncs = { + // Noisy functions (output) + backtrace: function() {}, + getBacktrace: function() {}, + help: function() {}, + print: function(s) { return s.toString(); }, + printErr: function(s) { return s.toString(); }, + putstr: function(s) { return s.toString(); }, + stackDump: function() {}, + dumpHeap: function() {}, + dumpScopeChain: function() {}, + dumpObjectWrappers: function() {}, + dumpGCArenaInfo: function() {}, + printProfilerEvents: function() {}, + + // Harmful functions (hangs, timeouts, leaks) + getLcovInfo: function() {}, + readline: function() {}, + readlineBuf: function() {}, + timeout: function() {}, + quit: function() {}, + interruptIf: function() {}, + terminate: function() {}, + invokeInterruptCallback: function() {}, + setInterruptCallback: function() {}, + intern: function() {}, + evalInWorker: function() {}, + sleep: function() {}, + cacheEntry: function() {}, + streamCacheEntry: function() {}, + createMappedArrayBuffer: function() {}, + wasmCompileInSeparateProcess: function() {}, + gcparam: function() {}, + newGlobal: function() { return g; }, + + // Harmful functions (throw) + assertEq: function(a,b) { return a.toString() == b.toString(); }, + throwError: function() {}, + reportOutOfMemory: function() {}, + throwOutOfMemory: function() {}, + reportLargeAllocationFailure: function() {}, + + // Functions that need limiting + gczeal: function(m, f) { return gczeal(m, 100); }, + startgc: function(n, o) { startgc(n > 20 ? 20 : n, o); }, + gcslice: function(n) { gcslice(n > 20 ? 20 : n); }, + + // Global side-effects + deterministicgc: function() {}, + fullcompartmentchecks: function() {}, + setIonCheckGraphCoherency: function() {}, + enableShellAllocationMetadataBuilder: function() {}, + setTimeResolution: function() {}, + options: function() { return "tracejit,methodjit,typeinfer"; }, + setJitCompilerOption: function() {}, + clearLastWarning: function() {}, + enableSingleStepProfiling: function() {}, + disableSingleStepProfiling: function() {}, + enableGeckoProfiling: function() {}, + enableGeckoProfilingWithSlowAssertions: function() {}, + disableGeckoProfiling: function() {}, + enqueueJob: function() {}, + globalOfFirstJobInQueue: function() {}, + drainJobQueue: function() {}, + setPromiseRejectionTrackerCallback: function() {}, + startTimingMutator: function() {}, + stopTimingMutator: function() {}, + setModuleLoadHook: function() {}, + // Left enabled, as it is required for now to avoid leaks + //setModuleResolveHook: function() {}, + setModuleMetadataHook: function() {}, + setModuleDynamicImportHook: function() {}, + finishDynamicModuleImport: function() {}, + abortDynamicModuleImport: function() {}, + offThreadCompileScript: function() {}, + runOffThreadScript: function() {}, + offThreadCompileModule: function() {}, + finishOffThreadModule: function() {}, + offThreadDecodeScript: function() {}, + runOffThreadDecodedScript: function() {}, + addPromiseReactions: function() {}, + ignoreUnhandledRejections: function() {}, + enableTrackAllocations: function() {}, + disableTrackAllocations: function() {}, + startTraceLogger: function() {}, + stopTraceLogger: function() {}, + setTestFilenameValidationCallback: function() {}, + }; + + for (let lfFunc in lfFuncs) { + g[lfFunc] = lfFuncs[lfFunc]; + } + + return g; +} |