diff options
Diffstat (limited to 'js/src/fuzz-tests/testWasm.cpp')
-rw-r--r-- | js/src/fuzz-tests/testWasm.cpp | 448 |
1 files changed, 448 insertions, 0 deletions
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); |