From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- js/src/fuzz-tests/testWasm.cpp | 569 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 569 insertions(+) create mode 100644 js/src/fuzz-tests/testWasm.cpp (limited to 'js/src/fuzz-tests/testWasm.cpp') diff --git a/js/src/fuzz-tests/testWasm.cpp b/js/src/fuzz-tests/testWasm.cpp new file mode 100644 index 0000000000..719c38174a --- /dev/null +++ b/js/src/fuzz-tests/testWasm.cpp @@ -0,0 +1,569 @@ +/* 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 "js/CallAndConstruct.h" +#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetProperty, JS_GetPropertyById, JS_HasProperty, JS_SetProperty +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/TypedArrayObject.h" + +#include "wasm/WasmCompile.h" +#include "wasm/WasmFeatures.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 bool gIsWasmSmith = false; +extern "C" { +size_t gluesmith(uint8_t* data, size_t size, uint8_t* out, size_t maxsize); +} + +static int testWasmInit(int* argc, char*** argv) { + if (!wasm::HasSupport(gCx)) { + MOZ_CRASH("Wasm is not supported"); + } + + JS::ContextOptionsRef(gCx) +#define WASM_FEATURE(NAME, LOWER_NAME, STAGE, COMPILE_PRED, COMPILER_PRED, \ + FLAG_PRED, FLAG_FORCE_ON, FLAG_FUZZ_ON, SHELL, PREF) \ + .setWasm##NAME(FLAG_FUZZ_ON) + JS_FOR_WASM_FEATURES(WASM_FEATURE) +#undef WASM_FEATURE + ; + + if (!GlobalObject::getOrCreateConstructor(gCx, JSProto_WebAssembly)) { + MOZ_CRASH("Failed to initialize wasm engine"); + } + + return 0; +} + +static int testWasmSmithInit(int* argc, char*** argv) { + gIsWasmSmith = true; + return testWasmInit(argc, argv); +} + +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 +static bool assignImportKind(const Import& import, HandleObject obj, + HandleObject lastExportsObj, + JS::Handle lastExportIds, + size_t* currentExportId, size_t exportsLength, + HandleValue defaultValue) { + RootedId fieldName(gCx); + if (!import.field.toPropertyKey(gCx, &fieldName)) { + return false; + } + 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()) { + if (!JS_SetPropertyById(gCx, obj, fieldName, propVal)) { + return false; + } + + assigned = true; + break; + } + } + if (!assigned) { + if (!JS_SetPropertyById(gCx, obj, fieldName, defaultValue)) { + return false; + } + } + return true; +} + +static bool FuzzerBuildId(JS::BuildIdCharVector* buildId) { + const char buildid[] = "testWasmFuzz"; + return buildId->append(buildid, sizeof(buildid)); +} + +static int testWasmFuzz(const uint8_t* buf, size_t size) { + auto gcGuard = mozilla::MakeScopeExit([&] { + JS::PrepareForFullGC(gCx); + JS::NonIncrementalGC(gCx, JS::GCOptions::Normal, JS::GCReason::API); + }); + + JS::SetProcessBuildIdOp(FuzzerBuildId); + + 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 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(); + + uint16_t moduleLen; + if (gIsWasmSmith) { + // Jump over the optByte. Unlike with the regular format, for + // wasm-smith we are fixing this and use byte 0 as opt-byte. + // Eventually this will also be changed for the regular format. + if (!currentIndex) { + currentIndex++; + } + + // Caller ensures the structural soundness of the input here + moduleLen = *((uint16_t*)&buf[currentIndex]); + currentIndex += 2; + } else { + moduleLen = buf[currentIndex]; + currentIndex++; + } + + if (size - currentIndex < moduleLen) { + moduleLen = size - currentIndex; + } + + if (moduleLen < MINIMUM_MODULE_SIZE) { + continue; + } + + if (currentIndex == 1 || (gIsWasmSmith && currentIndex == 3)) { + // 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; + if (gIsWasmSmith) { + optByte = (uint8_t)buf[0]; + } else { + optByte = (uint8_t)buf[currentIndex]; + } + + // Note that IonPlatformSupport() does not take into account whether + // the compiler supports particular features that may have been enabled. + bool enableWasmBaseline = ((optByte & 0xF0) == (1 << 7)); + bool enableWasmOptimizing = + IonPlatformSupport() && ((optByte & 0xF0) == (1 << 6)); + bool enableWasmAwaitTier2 = + (IonPlatformSupport()) && ((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()) { + enableWasmOptimizing = true; + } else { + enableWasmBaseline = true; + } + } + + if (enableWasmAwaitTier2) { + // Tier 2 needs Baseline + Optimizing + enableWasmBaseline = true; + + if (!enableWasmOptimizing) { + enableWasmOptimizing = true; + } + } + + JS::ContextOptionsRef(gCx) + .setWasmBaseline(enableWasmBaseline) + .setWasmIon(enableWasmOptimizing) + .setTestWasmAwaitTier2(enableWasmAwaitTier2); + } + + // Expected header for a valid WebAssembly module + uint32_t magic_header = 0x6d736100; + uint32_t magic_version = 0x1; + + if (gIsWasmSmith) { + // When using wasm-smith, magic values should already be there. + // Checking this to make sure the data passed is sane. + MOZ_RELEASE_ASSERT(*(uint32_t*)(&buf[currentIndex]) == magic_header, + "Magic header mismatch!"); + MOZ_RELEASE_ASSERT(*(uint32_t*)(&buf[currentIndex + 4]) == magic_version, + "Magic version mismatch!"); + } + + // We just skip over the first 8 bytes now because we fill them + // with `magic_header` and `magic_version` anyway. + currentIndex += 8; + moduleLen -= 8; + + Rooted instanceObj(gCx); + + MutableBytes bytecode = gCx->new_(); + 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; + FeatureOptions options; + SharedCompileArgs compileArgs = + CompileArgs::buildAndReport(gCx, std::move(scriptedCaller), options); + if (!compileArgs) { + return 0; + } + + UniqueChars error; + UniqueCharsVector warnings; + SharedModule module = + CompileBuffer(*compileArgs, *bytecode, &error, &warnings); + if (!module) { + // We should always have a valid module if we are using wasm-smith. Check + // that no error is reported, signalling an OOM. + MOZ_RELEASE_ASSERT(!gIsWasmSmith || !error); + 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; + size_t currentTagExportId = 0; + + for (const Import& import : importVec) { + RootedId moduleName(gCx); + if (!import.module.toPropertyKey(gCx, &moduleName)) { + return false; + } + RootedId fieldName(gCx); + if (!import.field.toPropertyKey(gCx, &fieldName)) { + return false; + } + + // First try to get the namespace object, create one if this is the + // first time. + RootedValue v(gCx); + if (!JS_GetPropertyById(gCx, importObj, moduleName, &v) || + !v.isObject()) { + // Insert empty object at importObj[moduleName] + RootedObject plainObj(gCx, JS_NewPlainObject(gCx)); + + if (!plainObj) { + return 0; + } + + RootedValue plainVal(gCx, ObjectValue(*plainObj)); + if (!JS_SetPropertyById(gCx, importObj, moduleName, 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_GetPropertyById(gCx, importObj, moduleName, &v) || + !v.isObject()) { + return 0; + } + } + + RootedObject obj(gCx, &v.toObject()); + bool found = false; + if (JS_HasPropertyById(gCx, obj, fieldName, &found) && !found) { + // Insert i-th export object that fits the type requirement + // at `v[fieldName]`. + + switch (import.kind) { + case DefinitionKind::Function: + if (!assignImportKind( + import, obj, lastExportsObj, lastExportIds, + ¤tFunctionExportId, exportsLength, + emptyFunctionValue)) { + return 0; + } + break; + + case DefinitionKind::Table: + // TODO: Pass a dummy defaultValue + if (!assignImportKind( + import, obj, lastExportsObj, lastExportIds, + ¤tTableExportId, exportsLength, nullValue)) { + return 0; + } + break; + + case DefinitionKind::Memory: + // TODO: Pass a dummy defaultValue + if (!assignImportKind( + import, obj, lastExportsObj, lastExportIds, + ¤tMemoryExportId, exportsLength, nullValue)) { + return 0; + } + break; + + case DefinitionKind::Global: + // TODO: Pass a dummy defaultValue + if (!assignImportKind( + import, obj, lastExportsObj, lastExportIds, + ¤tGlobalExportId, exportsLength, nullValue)) { + return 0; + } + break; + + case DefinitionKind::Tag: + // TODO: Pass a dummy defaultValue + if (!assignImportKind( + import, obj, lastExportsObj, lastExportIds, + ¤tTagExportId, exportsLength, nullValue)) { + return 0; + } + break; + } + } + } + + Rooted 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 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()) { + RootedFunction func(gCx, &propObj->as()); + + if (!callExportedFunc(func, &lastReturnVal)) { + return 0; + } + } + + if (propObj->is()) { + Rooted tableObj(gCx, + &propObj->as()); + size_t tableLen = tableObj->table().length(); + + RootedValue tableGetVal(gCx); + if (!JS_GetProperty(gCx, tableObj, "get", &tableGetVal)) { + return 0; + } + RootedFunction tableGet(gCx, + &tableGetVal.toObject().as()); + + 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()); + + if (!callExportedFunc(callee, &lastReturnVal)) { + return 0; + } + } + } + + if (propObj->is()) { + Rooted memory(gCx, + &propObj->as()); + size_t byteLen = memory->volatileMemoryLength(); + 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]; + (void)rawMemByte; + } + } + + if (propObj->is()) { + Rooted global(gCx, + &propObj->as()); + if (global->type() != ValType::I64) { + global->val().get().toJSValue(gCx, &lastReturnVal); + } + } + } + } + } + + return 0; +} + +static int testWasmSmithFuzz(const uint8_t* buf, size_t size) { + // Define maximum sizes for the input to wasm-smith as well + // as the resulting modules. The input to output size factor + // of wasm-smith is somewhat variable but a factor of 4 seems + // to roughly work out. The logic below also assumes that these + // are powers of 2. + const size_t maxInputSize = 1024; + const size_t maxModuleSize = 4096; + + size_t maxModules = size / maxInputSize + 1; + + // We need 1 leading byte for options and 2 bytes for size per module + uint8_t* out = + new uint8_t[1 + maxModules * (maxModuleSize + sizeof(uint16_t))]; + + auto deleteGuard = mozilla::MakeScopeExit([&] { delete[] out; }); + + // Copy the opt-byte. + out[0] = buf[0]; + + size_t outIndex = 1; + size_t currentIndex = 1; + + while (currentIndex < size) { + size_t remaining = size - currentIndex; + + // We need to have at least a size and some byte to read. + if (remaining <= sizeof(uint16_t)) { + break; + } + + // Determine size of the next input, limited to `maxInputSize`. + uint16_t inSize = + (*((uint16_t*)&buf[currentIndex]) & (maxInputSize - 1)) + 1; + remaining -= sizeof(uint16_t); + currentIndex += sizeof(uint16_t); + + // Cap to remaining bytes. + inSize = remaining >= inSize ? inSize : remaining; + + size_t outSize = + gluesmith((uint8_t*)&buf[currentIndex], inSize, + out + outIndex + sizeof(uint16_t), maxModuleSize); + + if (!outSize) { + break; + } + + currentIndex += inSize; + + // Write the size of the resulting module to our output buffer. + *(uint16_t*)(&out[outIndex]) = (uint16_t)outSize; + outIndex += sizeof(uint16_t) + outSize; + } + + // If we lack at least one module, don't do anything. + if (outIndex == 1) { + return 0; + } + + return testWasmFuzz(out, outIndex); +} + +MOZ_FUZZING_INTERFACE_RAW(testWasmInit, testWasmFuzz, Wasm); +MOZ_FUZZING_INTERFACE_RAW(testWasmSmithInit, testWasmSmithFuzz, WasmSmith); -- cgit v1.2.3