Source code

Revision control

Copy as Markdown

Other Tools

/* 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/Prefs.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");
}
#define WASM_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, FLAG_PRED, \
FLAG_FORCE_ON, FLAG_FUZZ_ON, PREF) \
JS::Prefs::setAtStartup_wasm_##PREF(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 <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) {
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<T>()) {
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<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();
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<WasmInstanceObject*> 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;
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<JSFunction>(
import, obj, lastExportsObj, lastExportIds,
&currentFunctionExportId, exportsLength,
emptyFunctionValue)) {
return 0;
}
break;
case DefinitionKind::Table:
// TODO: Pass a dummy defaultValue
if (!assignImportKind<WasmTableObject>(
import, obj, lastExportsObj, lastExportIds,
&currentTableExportId, exportsLength, nullValue)) {
return 0;
}
break;
case DefinitionKind::Memory:
// TODO: Pass a dummy defaultValue
if (!assignImportKind<WasmMemoryObject>(
import, obj, lastExportsObj, lastExportIds,
&currentMemoryExportId, exportsLength, nullValue)) {
return 0;
}
break;
case DefinitionKind::Global:
// TODO: Pass a dummy defaultValue
if (!assignImportKind<WasmGlobalObject>(
import, obj, lastExportsObj, lastExportIds,
&currentGlobalExportId, exportsLength, nullValue)) {
return 0;
}
break;
case DefinitionKind::Tag:
// TODO: Pass a dummy defaultValue
if (!assignImportKind<WasmTagObject>(
import, obj, lastExportsObj, lastExportIds,
&currentTagExportId, exportsLength, nullValue)) {
return 0;
}
break;
}
}
}
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->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<WasmGlobalObject>()) {
Rooted<WasmGlobalObject*> global(gCx,
&propObj->as<WasmGlobalObject>());
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);