Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
/* JS shell. */
#include "mozilla/AlreadyAddRefed.h" // mozilla::already_AddRefed
#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h" // MOZ_ASSERT, MOZ_ASSERT_IF, MOZ_RELEASE_ASSERT, MOZ_CRASH
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/Compression.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EnumSet.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/mozalloc.h"
#include "mozilla/PodOperations.h"
#include "mozilla/RandomNum.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Sprintf.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtrExtensions.h" // UniqueFreePtr
#include "mozilla/Utf8.h"
#include "mozilla/Variant.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#ifdef XP_WIN
# include <direct.h>
# include <process.h>
#endif
#include <errno.h>
#include <fcntl.h>
#if defined(XP_WIN)
# include <io.h> /* for isatty() */
#endif
#include <locale.h>
#if defined(MALLOC_H)
# include MALLOC_H /* for malloc_usable_size, malloc_size, _msize */
#endif
#include <ctime>
#include <math.h>
#ifndef __wasi__
# include <signal.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <utility>
#ifdef XP_UNIX
# ifndef __wasi__
# include <sys/mman.h>
# include <sys/wait.h>
# endif
# include <sys/stat.h>
# include <unistd.h>
#endif
#ifdef XP_LINUX
# include <sys/prctl.h>
#endif
#include "jsapi.h"
#include "jsfriendapi.h"
#include "jstypes.h"
#ifndef JS_WITHOUT_NSPR
# include "prerror.h"
# include "prlink.h"
#endif
#include "builtin/Array.h"
#include "builtin/MapObject.h"
#include "builtin/ModuleObject.h"
#include "builtin/RegExp.h"
#include "builtin/TestingFunctions.h"
#include "builtin/TestingUtility.h" // js::ParseCompileOptions, js::ParseDebugMetadata, js::CreateScriptPrivate
#include "debugger/DebugAPI.h"
#include "frontend/CompilationStencil.h"
#include "frontend/FrontendContext.h" // AutoReportFrontendContext
#include "frontend/ModuleSharedContext.h"
#include "frontend/Parser.h"
#include "frontend/ScopeBindingCache.h" // js::frontend::ScopeBindingCache
#include "gc/GC.h"
#include "gc/PublicIterators.h"
#ifdef DEBUG
# include "irregexp/RegExpAPI.h"
#endif
#ifdef JS_SIMULATOR_ARM
# include "jit/arm/Simulator-arm.h"
#endif
#ifdef JS_SIMULATOR_MIPS64
# include "jit/mips64/Simulator-mips64.h"
#endif
#ifdef JS_SIMULATOR_LOONG64
# include "jit/loong64/Simulator-loong64.h"
#endif
#ifdef JS_SIMULATOR_RISCV64
# include "jit/riscv64/Simulator-riscv64.h"
#endif
#include "jit/BaselineCompileQueue.h"
#include "jit/CacheIRHealth.h"
#include "jit/InlinableNatives.h"
#include "jit/Ion.h"
#include "jit/JitcodeMap.h"
#include "jit/JitZone.h"
#include "jit/shared/CodeGenerator-shared.h"
#include "js/Array.h" // JS::NewArrayObject
#include "js/ArrayBuffer.h" // JS::{CreateMappedArrayBufferContents,NewMappedArrayBufferWithContents,IsArrayBufferObject,GetArrayBufferLengthAndData}
#include "js/BuildId.h" // JS::BuildIdCharVector, JS::SetProcessBuildIdOp
#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable, JS_CallFunction, JS_CallFunctionValue
#include "js/CharacterEncoding.h" // JS::StringIsASCII
#include "js/CompilationAndEvaluation.h"
#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions, JS::CompileOptions, JS::OwningCompileOptions, JS::DecodeOptions, JS::InstantiateOptions
#include "js/ContextOptions.h" // JS::ContextOptions{,Ref}
#include "js/Debug.h" // JS::dbg::ShouldAvoidSideEffects, JS::ExecutionTrace
#include "js/EnvironmentChain.h" // JS::EnvironmentChain
#include "js/Equality.h" // JS::SameValue
#include "js/ErrorReport.h" // JS::PrintError
#include "js/Exception.h" // JS::StealPendingExceptionStack
#include "js/experimental/BindingAllocs.h" // JS_NewObjectWithGivenProtoAndUseAllocSite
#include "js/experimental/CodeCoverage.h" // js::EnableCodeCoverage
#include "js/experimental/CompileScript.h" // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::HadFrontendErrors, JS::ConvertFrontendErrorsToRuntimeErrors, JS::CompileGlobalScriptToStencil, JS::CompileModuleScriptToStencil
#include "js/experimental/CTypes.h" // JS::InitCTypesClass
#include "js/experimental/Intl.h" // JS::AddMoz{DateTimeFormat,DisplayNames}Constructor
#include "js/experimental/JitInfo.h" // JSJit{Getter,Setter,Method}CallArgs, JSJitGetterInfo, JSJit{Getter,Setter}Op, JSJitInfo
#include "js/experimental/JSStencil.h" // JS::Stencil, JS::DecodeStencil, JS::InstantiateModuleStencil
#include "js/experimental/SourceHook.h" // js::{Set,Forget,}SourceHook
#include "js/experimental/TypedData.h" // JS_NewUint8Array
#include "js/friend/DumpFunctions.h" // JS::FormatStackDump
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit
#include "js/friend/WindowProxy.h" // js::IsWindowProxy, js::SetWindowProxyClass, js::ToWindowProxyIfWindow, js::ToWindowIfWindowProxy
#include "js/GCAPI.h" // JS::AutoCheckCannotGC
#include "js/GCVector.h"
#include "js/GlobalObject.h"
#include "js/Initialization.h"
#include "js/Interrupt.h"
#include "js/JSON.h"
#include "js/MemoryCallbacks.h"
#include "js/MemoryFunctions.h"
#include "js/Modules.h" // JS::GetModulePrivate, JS::SetModule{DynamicImport,Metadata,Resolve}Hook, JS::SetModulePrivate, JS::CompileModule
#include "js/Object.h" // JS::GetClass, JS::GetCompartment, JS::GetReservedSlot, JS::SetReservedSlot
#include "js/Prefs.h"
#include "js/Principals.h"
#include "js/Printer.h" // QuoteString
#include "js/Printf.h"
#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineFunction, JS_DefineFunctions, JS_DefineProperties, JS_DefineProperty, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty, JS_SetElement, JS_SetProperty, JS_SetPropertyById
#include "js/PropertySpec.h"
#include "js/Realm.h"
#include "js/RegExp.h" // JS::ObjectIsRegExp
#include "js/ScriptPrivate.h"
#include "js/SourceText.h" // JS::SourceText
#include "js/StableStringChars.h"
#include "js/Stack.h"
#include "js/StreamConsumer.h"
#include "js/StructuredClone.h"
#include "js/SweepingAPI.h"
#include "js/Transcoding.h" // JS::TranscodeBuffer, JS::TranscodeRange, JS::IsTranscodeFailureResult
#include "js/Warnings.h" // JS::SetWarningReporter
#include "js/WasmFeatures.h" // JS_FOR_WASM_FEATURES
#include "js/WasmModule.h" // JS::WasmModule
#include "js/Wrapper.h"
#include "proxy/DeadObjectProxy.h" // js::IsDeadProxyObject
#include "shell/jsoptparse.h"
#include "shell/jsshell.h"
#include "shell/OSObject.h"
#include "shell/ShellModuleObjectWrapper.h"
#include "shell/WasmTesting.h"
#include "threading/ConditionVariable.h"
#include "threading/ExclusiveData.h"
#include "threading/LockGuard.h"
#include "threading/Thread.h"
#include "util/CompleteFile.h" // js::FileContents, js::ReadCompleteFile
#include "util/DifferentialTesting.h"
#include "util/StringBuilder.h"
#include "util/Text.h"
#include "util/WindowsWrapper.h"
#include "vm/ArgumentsObject.h"
#include "vm/Compression.h"
#include "vm/ErrorObject.h"
#include "vm/ErrorReporting.h"
#include "vm/HelperThreads.h"
#include "vm/JSAtomUtils.h" // AtomizeUTF8Chars, AtomizeString, ToAtom
#include "vm/JSContext.h"
#include "vm/JSFunction.h"
#include "vm/JSObject.h"
#include "vm/JSScript.h"
#include "vm/Logging.h"
#include "vm/ModuleBuilder.h" // js::ModuleBuilder
#include "vm/Modules.h"
#include "vm/Monitor.h"
#include "vm/MutexIDs.h"
#include "vm/PromiseObject.h" // js::PromiseObject
#include "vm/Shape.h"
#include "vm/SharedArrayObject.h"
#include "vm/StencilObject.h" // js::StencilObject
#include "vm/Time.h"
#include "vm/ToSource.h" // js::ValueToSource
#include "vm/TypedArrayObject.h"
#include "vm/WrapperObject.h"
#include "wasm/WasmFeatures.h"
#include "wasm/WasmJS.h"
#include "vm/Compartment-inl.h"
#include "vm/ErrorObject-inl.h"
#include "vm/Interpreter-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/Realm-inl.h"
#include "vm/Stack-inl.h"
#undef compress
using namespace js;
using namespace js::cli;
using namespace js::shell;
using JS::AutoStableStringChars;
using JS::CompileOptions;
using js::shell::RCFile;
using mozilla::ArrayEqual;
using mozilla::AsVariant;
using mozilla::Atomic;
using mozilla::MakeScopeExit;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::NumberEqualsInt32;
using mozilla::TimeDuration;
using mozilla::TimeStamp;
using mozilla::Utf8Unit;
using mozilla::Variant;
bool InitOptionParser(OptionParser& op);
bool SetGlobalOptionsPreJSInit(const OptionParser& op);
bool SetGlobalOptionsPostJSInit(const OptionParser& op);
bool SetContextOptions(JSContext* cx, const OptionParser& op);
bool SetContextWasmOptions(JSContext* cx, const OptionParser& op);
bool SetContextJITOptions(JSContext* cx, const OptionParser& op);
bool SetContextGCOptions(JSContext* cx, const OptionParser& op);
bool InitModuleLoader(JSContext* cx, const OptionParser& op);
#ifdef FUZZING_JS_FUZZILLI
# define REPRL_CRFD 100
# define REPRL_CWFD 101
# define REPRL_DRFD 102
# define REPRL_DWFD 103
# define SHM_SIZE 0x100000
# define MAX_EDGES ((SHM_SIZE - 4) * 8)
struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};
struct shmem_data* __shmem;
uint32_t *__edges_start, *__edges_stop;
void __sanitizer_cov_reset_edgeguards() {
uint64_t N = 0;
for (uint32_t* x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++)
*x = ++N;
}
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start,
uint32_t* stop) {
// Avoid duplicate initialization
if (start == stop || *start) return;
if (__edges_start != NULL || __edges_stop != NULL) {
fprintf(stderr,
"Coverage instrumentation is only supported for a single module\n");
_exit(-1);
}
__edges_start = start;
__edges_stop = stop;
// Map the shared memory region
const char* shm_key = getenv("SHM_ID");
if (!shm_key) {
puts("[COV] no shared memory bitmap available, skipping");
__shmem = (struct shmem_data*)malloc(SHM_SIZE);
} else {
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
if (fd <= -1) {
fprintf(stderr, "Failed to open shared memory region: %s\n",
strerror(errno));
_exit(-1);
}
__shmem = (struct shmem_data*)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (__shmem == MAP_FAILED) {
fprintf(stderr, "Failed to mmap shared memory region\n");
_exit(-1);
}
}
__sanitizer_cov_reset_edgeguards();
__shmem->num_edges = stop - start;
printf("[COV] edge counters initialized. Shared memory: %s with %u edges\n",
shm_key, __shmem->num_edges);
}
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
// There's a small race condition here: if this function executes in two
// threads for the same edge at the same time, the first thread might disable
// the edge (by setting the guard to zero) before the second thread fetches
// the guard value (and thus the index). However, our instrumentation ignores
// the first edge (see libcoverage.c) and so the race is unproblematic.
uint32_t index = *guard;
// If this function is called before coverage instrumentation is properly
// initialized we want to return early.
if (!index) return;
__shmem->edges[index / 8] |= 1 << (index % 8);
*guard = 0;
}
#endif /* FUZZING_JS_FUZZILLI */
enum JSShellExitCode {
EXITCODE_RUNTIME_ERROR = 3,
EXITCODE_FILE_NOT_FOUND = 4,
EXITCODE_OUT_OF_MEMORY = 5,
EXITCODE_TIMEOUT = 6
};
struct ShellLogModule {
// Since ShellLogModules have references to their levels created
// we can't move them.
ShellLogModule(ShellLogModule&&) = delete;
const char* name;
explicit ShellLogModule(const char* name) : name(name) {}
mozilla::AtomicLogLevel level;
};
// If asserts related to this ever fail, simply bump this number.
//
// This is used to construct a mozilla::Array, which is used because a
// ShellLogModule cannot move once constructed to avoid invalidating
// a levelRef.
static const int MAX_LOG_MODULES = 64;
static int initialized_modules = 0;
mozilla::Array<mozilla::Maybe<ShellLogModule>, MAX_LOG_MODULES> logModules;
JS::OpaqueLogger GetLoggerByName(const char* name) {
// Check for pre-existing module
for (auto& logger : logModules) {
if (logger) {
if (logger->name == name) {
return logger.ptr();
}
}
// We've seen all initialized, not there, break out.
if (!logger) break;
}
// Not found, allocate a new module.
MOZ_RELEASE_ASSERT(initialized_modules < MAX_LOG_MODULES - 1);
auto index = initialized_modules++;
logModules[index].emplace(name);
return logModules[index].ptr();
}
mozilla::AtomicLogLevel& GetLevelRef(JS::OpaqueLogger logger) {
ShellLogModule* slm = static_cast<ShellLogModule*>(logger);
return slm->level;
}
void LogPrintVA(const JS::OpaqueLogger logger, mozilla::LogLevel level,
const char* fmt, va_list ap) {
ShellLogModule* mod = static_cast<ShellLogModule*>(logger);
fprintf(stderr, "[%s] ", mod->name);
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
}
JS::LoggingInterface shellLoggingInterface = {GetLoggerByName, LogPrintVA,
GetLevelRef};
static void ToLower(const char* src, char* dest, size_t len) {
for (size_t c = 0; c < len; c++) {
dest[c] = (char)(tolower(src[c]));
}
}
// Run this after initialiation!
void ParseLoggerOptions() {
char* mixedCaseOpts = getenv("MOZ_LOG");
if (!mixedCaseOpts) {
return;
}
// Copy into a new buffer and lower case to do case insensitive matching.
//
// Done this way rather than just using strcasestr because Windows doesn't
// have strcasestr as part of its base C library.
size_t len = strlen(mixedCaseOpts);
mozilla::UniqueFreePtr<char[]> logOpts(
static_cast<char*>(calloc(len + 1, 1)));
if (!logOpts) {
return;
}
ToLower(mixedCaseOpts, logOpts.get(), len);
// This is a really permissive parser, but will suffice!
for (auto& logger : logModules) {
if (logger) {
// Lowercase the logger name for strstr
size_t len = strlen(logger->name);
mozilla::UniqueFreePtr<char[]> lowerName(
static_cast<char*>(calloc(len + 1, 1)));
ToLower(logger->name, lowerName.get(), len);
if (char* needle = strstr(logOpts.get(), lowerName.get())) {
// If the string to enable a logger is present, but no level is provided
// then default to Debug level.
int logLevel = static_cast<int>(mozilla::LogLevel::Debug);
if (char* colon = strchr(needle, ':')) {
// Parse character after colon as log level.
if (*(colon + 1)) {
logLevel = atoi(colon + 1);
}
}
fprintf(stderr, "[JS_LOG] Enabling Logger %s at level %d\n",
logger->name, logLevel);
logger->level = mozilla::ToLogLevel(logLevel);
}
}
}
}
/*
* Limit the timeout to 30 minutes to prevent an overflow on platfoms
* that represent the time internally in microseconds using 32-bit int.
*/
static const double MAX_TIMEOUT_SECONDS = 1800.0;
// Not necessarily in sync with the browser
#ifdef ENABLE_SHARED_MEMORY
# define SHARED_MEMORY_DEFAULT 1
#else
# define SHARED_MEMORY_DEFAULT 0
#endif
// Fuzzing support for JS runtime fuzzing
#ifdef FUZZING_INTERFACES
# include "shell/jsrtfuzzing/jsrtfuzzing.h"
MOZ_RUNINIT static bool fuzzDoDebug = !!getenv("MOZ_FUZZ_DEBUG");
MOZ_RUNINIT static bool fuzzHaveModule = !!getenv("FUZZER");
#endif // FUZZING_INTERFACES
// Code to support GCOV code coverage measurements on standalone shell
#ifdef MOZ_CODE_COVERAGE
# if defined(__GNUC__) && !defined(__clang__)
extern "C" void __gcov_dump();
extern "C" void __gcov_reset();
void counters_dump(int) { __gcov_dump(); }
void counters_reset(int) { __gcov_reset(); }
# else
void counters_dump(int) { /* Do nothing */ }
void counters_reset(int) { /* Do nothing */ }
# endif
static void InstallCoverageSignalHandlers() {
# ifndef XP_WIN
fprintf(stderr, "[CodeCoverage] Setting handlers for process %d.\n",
getpid());
struct sigaction dump_sa;
dump_sa.sa_handler = counters_dump;
dump_sa.sa_flags = SA_RESTART;
sigemptyset(&dump_sa.sa_mask);
mozilla::DebugOnly<int> r1 = sigaction(SIGUSR1, &dump_sa, nullptr);
MOZ_ASSERT(r1 == 0, "Failed to install GCOV SIGUSR1 handler");
struct sigaction reset_sa;
reset_sa.sa_handler = counters_reset;
reset_sa.sa_flags = SA_RESTART;
sigemptyset(&reset_sa.sa_mask);
mozilla::DebugOnly<int> r2 = sigaction(SIGUSR2, &reset_sa, nullptr);
MOZ_ASSERT(r2 == 0, "Failed to install GCOV SIGUSR2 handler");
# endif
}
#endif
// An off-thread parse or decode job.
class js::shell::OffThreadJob {
static constexpr size_t kCompileStackQuota = 128 * sizeof(size_t) * 1024;
static constexpr size_t kThreadStackQuota =
kCompileStackQuota + 128 * sizeof(size_t) * 1024;
enum State {
RUNNING, // Working; no stencil.
DONE, // Finished; have stencil.
CANCELLED // Cancelled due to error.
};
public:
enum class Kind {
CompileScript,
CompileModule,
Decode,
};
OffThreadJob(ShellContext* sc, Kind kind, JS::SourceText<char16_t>&& srcBuf);
OffThreadJob(ShellContext* sc, Kind kind, JS::TranscodeBuffer&& xdrBuf);
~OffThreadJob();
bool init(JSContext* cx, const JS::ReadOnlyCompileOptions& options);
bool dispatch();
static void OffThreadMain(OffThreadJob* self);
void run();
void cancel();
void waitUntilDone();
already_AddRefed<JS::Stencil> stealStencil(JSContext* cx);
public:
const int32_t id;
private:
Kind kind_;
State state_;
JS::FrontendContext* fc_ = nullptr;
JS::OwningCompileOptions options_;
UniquePtr<Thread> thread_;
JS::SourceText<char16_t> srcBuf_;
JS::TranscodeBuffer xdrBuf_;
RefPtr<JS::Stencil> stencil_;
JS::TranscodeResult transcodeResult_ = JS::TranscodeResult::Ok;
};
template <typename T>
static OffThreadJob* NewOffThreadJob(JSContext* cx, OffThreadJob::Kind kind,
JS::ReadOnlyCompileOptions& options,
T&& source) {
ShellContext* sc = GetShellContext(cx);
if (sc->isWorker) {
// Off-thread compilation/decode is used by main-thread, in order to improve
// the responsiveness. It's not used by worker in browser, and there's not
// much reason to support worker here.
JS_ReportErrorASCII(cx, "Off-thread job is not supported in worker");
return nullptr;
}
UniquePtr<OffThreadJob> job(
cx->new_<OffThreadJob>(sc, kind, std::move(source)));
if (!job) {
return nullptr;
}
if (!job->init(cx, options)) {
return nullptr;
}
if (!sc->offThreadJobs.append(job.get())) {
job->cancel();
JS_ReportErrorASCII(cx, "OOM adding off-thread job");
return nullptr;
}
return job.release();
}
static OffThreadJob* GetSingleOffThreadJob(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
const auto& jobs = sc->offThreadJobs;
if (jobs.empty()) {
JS_ReportErrorASCII(cx, "No off-thread jobs are pending");
return nullptr;
}
if (jobs.length() > 1) {
JS_ReportErrorASCII(
cx, "Multiple off-thread jobs are pending: must specify job ID");
return nullptr;
}
return jobs[0];
}
static OffThreadJob* LookupOffThreadJobByID(JSContext* cx, int32_t id) {
if (id <= 0) {
JS_ReportErrorASCII(cx, "Bad off-thread job ID");
return nullptr;
}
ShellContext* sc = GetShellContext(cx);
const auto& jobs = sc->offThreadJobs;
if (jobs.empty()) {
JS_ReportErrorASCII(cx, "No off-thread jobs are pending");
return nullptr;
}
OffThreadJob* job = nullptr;
for (auto someJob : jobs) {
if (someJob->id == id) {
job = someJob;
break;
}
}
if (!job) {
JS_ReportErrorASCII(cx, "Off-thread job not found");
return nullptr;
}
return job;
}
static OffThreadJob* LookupOffThreadJobForArgs(JSContext* cx,
const CallArgs& args,
size_t arg) {
// If the optional ID argument isn't present, get the single pending job.
if (args.length() <= arg) {
return GetSingleOffThreadJob(cx);
}
// Lookup the job using the specified ID.
int32_t id = 0;
RootedValue value(cx, args[arg]);
if (!ToInt32(cx, value, &id)) {
return nullptr;
}
return LookupOffThreadJobByID(cx, id);
}
static void DeleteOffThreadJob(JSContext* cx, OffThreadJob* job) {
ShellContext* sc = GetShellContext(cx);
for (size_t i = 0; i < sc->offThreadJobs.length(); i++) {
if (sc->offThreadJobs[i] == job) {
sc->offThreadJobs.erase(&sc->offThreadJobs[i]);
js_delete(job);
return;
}
}
MOZ_CRASH("Off-thread job not found");
}
static void CancelOffThreadJobsForRuntime(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
while (!sc->offThreadJobs.empty()) {
OffThreadJob* job = sc->offThreadJobs.popCopy();
job->waitUntilDone();
js_delete(job);
}
}
mozilla::Atomic<int32_t> gOffThreadJobSerial(1);
OffThreadJob::OffThreadJob(ShellContext* sc, Kind kind,
JS::SourceText<char16_t>&& srcBuf)
: id(gOffThreadJobSerial++),
kind_(kind),
state_(RUNNING),
options_(JS::OwningCompileOptions::ForFrontendContext()),
srcBuf_(std::move(srcBuf)) {
MOZ_RELEASE_ASSERT(id > 0, "Off-thread job IDs exhausted");
}
OffThreadJob::OffThreadJob(ShellContext* sc, Kind kind,
JS::TranscodeBuffer&& xdrBuf)
: id(gOffThreadJobSerial++),
kind_(kind),
state_(RUNNING),
options_(JS::OwningCompileOptions::ForFrontendContext()),
xdrBuf_(std::move(xdrBuf)) {
MOZ_RELEASE_ASSERT(id > 0, "Off-thread job IDs exhausted");
}
OffThreadJob::~OffThreadJob() {
if (fc_) {
JS::DestroyFrontendContext(fc_);
}
MOZ_ASSERT(state_ != RUNNING);
}
bool OffThreadJob::init(JSContext* cx,
const JS::ReadOnlyCompileOptions& options) {
fc_ = JS::NewFrontendContext();
if (!fc_) {
ReportOutOfMemory(cx);
state_ = CANCELLED;
return false;
}
if (!options_.copy(cx, options)) {
state_ = CANCELLED;
return false;
}
return true;
}
bool OffThreadJob::dispatch() {
thread_ =
js::MakeUnique<Thread>(Thread::Options().setStackSize(kThreadStackQuota));
if (!thread_) {
state_ = CANCELLED;
return false;
}
if (!thread_->init(OffThreadJob::OffThreadMain, this)) {
state_ = CANCELLED;
thread_ = nullptr;
return false;
}
return true;
}
/* static */ void OffThreadJob::OffThreadMain(OffThreadJob* self) {
self->run();
}
void OffThreadJob::run() {
MOZ_ASSERT(state_ == RUNNING);
MOZ_ASSERT(!stencil_);
JS::SetNativeStackQuota(fc_, kCompileStackQuota);
switch (kind_) {
case Kind::CompileScript: {
stencil_ = JS::CompileGlobalScriptToStencil(fc_, options_, srcBuf_);
break;
}
case Kind::CompileModule: {
stencil_ = JS::CompileModuleScriptToStencil(fc_, options_, srcBuf_);
break;
}
case Kind::Decode: {
JS::DecodeOptions decodeOptions(options_);
JS::TranscodeRange range(xdrBuf_.begin(), xdrBuf_.length());
transcodeResult_ = JS::DecodeStencil(fc_, decodeOptions, range,
getter_AddRefs(stencil_));
break;
}
}
state_ = DONE;
}
void OffThreadJob::cancel() {
MOZ_ASSERT(state_ == RUNNING);
MOZ_ASSERT(!stencil_);
MOZ_ASSERT(!thread_, "cannot cancel after starting a thread");
state_ = CANCELLED;
}
void OffThreadJob::waitUntilDone() {
MOZ_ASSERT(state_ != CANCELLED);
thread_->join();
}
already_AddRefed<JS::Stencil> OffThreadJob::stealStencil(JSContext* cx) {
JS::FrontendContext* fc = fc_;
fc_ = nullptr;
auto destroyFrontendContext =
mozilla::MakeScopeExit([&]() { JS::DestroyFrontendContext(fc); });
MOZ_ASSERT(fc);
if (JS::HadFrontendErrors(fc)) {
(void)JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options_);
return nullptr;
}
if (!stencil_ && JS::IsTranscodeFailureResult(transcodeResult_)) {
JS_ReportErrorASCII(cx, "failed to decode cache");
return nullptr;
}
// Report warnings.
if (!JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options_)) {
return nullptr;
}
return stencil_.forget();
}
struct ShellCompartmentPrivate {
GCPtr<ArrayObject*> blackRoot;
GCPtr<ArrayObject*> grayRoot;
};
struct MOZ_STACK_CLASS EnvironmentPreparer
: public js::ScriptEnvironmentPreparer {
explicit EnvironmentPreparer(JSContext* cx) {
js::SetScriptEnvironmentPreparer(cx, this);
}
void invoke(JS::HandleObject global, Closure& closure) override;
};
const char* shell::selfHostedXDRPath = nullptr;
bool shell::encodeSelfHostedCode = false;
bool shell::enableCodeCoverage = false;
bool shell::enableDisassemblyDumps = false;
bool shell::offthreadBaselineCompilation = false;
bool shell::offthreadIonCompilation = false;
JS::DelazificationOption shell::defaultDelazificationMode =
JS::DelazificationOption::OnDemandOnly;
bool shell::enableAsmJS = false;
bool shell::enableWasm = false;
bool shell::enableSharedMemory = SHARED_MEMORY_DEFAULT;
bool shell::enableWasmBaseline = false;
bool shell::enableWasmOptimizing = false;
bool shell::enableWasmVerbose = false;
bool shell::enableTestWasmAwaitTier2 = false;
bool shell::enableSourcePragmas = true;
bool shell::enableAsyncStacks = false;
bool shell::enableAsyncStackCaptureDebuggeeOnly = false;
bool shell::enableToSource = false;
bool shell::enableImportAttributes = false;
#ifdef JS_GC_ZEAL
uint32_t shell::gZealBits = 0;
uint32_t shell::gZealFrequency = 0;
#endif
bool shell::printTiming = false;
RCFile* shell::gErrFile = nullptr;
RCFile* shell::gOutFile = nullptr;
bool shell::reportWarnings = true;
bool shell::compileOnly = false;
bool shell::disableOOMFunctions = false;
bool shell::defaultToSameCompartment = true;
#ifdef DEBUG
bool shell::dumpEntrainedVariables = false;
bool shell::OOM_printAllocationCount = false;
#endif
MOZ_RUNINIT UniqueChars shell::processWideModuleLoadPath;
static bool SetTimeoutValue(JSContext* cx, double t);
static void KillWatchdog(JSContext* cx);
static bool ScheduleWatchdog(JSContext* cx, double t);
static void CancelExecution(JSContext* cx);
enum class ShellGlobalKind {
GlobalObject,
WindowProxy,
};
static JSObject* NewGlobalObject(JSContext* cx, JS::RealmOptions& options,
JSPrincipals* principals, ShellGlobalKind kind,
bool immutablePrototype);
/*
* A toy WindowProxy class for the shell. This is intended for testing code
* where global |this| is a WindowProxy. All requests are forwarded to the
* underlying global and no navigation is supported.
*/
const JSClass ShellWindowProxyClass =
PROXY_CLASS_DEF("ShellWindowProxy", JSCLASS_HAS_RESERVED_SLOTS(1));
JSObject* NewShellWindowProxy(JSContext* cx, JS::HandleObject global) {
MOZ_ASSERT(global->is<GlobalObject>());
js::WrapperOptions options;
options.setClass(&ShellWindowProxyClass);
JSAutoRealm ar(cx, global);
JSObject* obj =
js::Wrapper::New(cx, global, &js::Wrapper::singleton, options);
MOZ_ASSERT_IF(obj, js::IsWindowProxy(obj));
return obj;
}
/*
* A toy principals type for the shell.
*
* In the shell, a principal is simply a 32-bit mask: P subsumes Q if the
* set bits in P are a superset of those in Q. Thus, the principal 0 is
* subsumed by everything, and the principal ~0 subsumes everything.
*
* As a special case, a null pointer as a principal is treated like 0xffff.
*
* The 'newGlobal' function takes an option indicating which principal the
* new global should have; 'evaluate' does for the new code.
*/
class ShellPrincipals final : public JSPrincipals {
uint32_t bits;
static uint32_t getBits(JSPrincipals* p) {
if (!p) {
return 0xffff;
}
return static_cast<ShellPrincipals*>(p)->bits;
}
public:
explicit ShellPrincipals(uint32_t bits, int32_t refcount = 0) : bits(bits) {
this->refcount = refcount;
}
bool write(JSContext* cx, JSStructuredCloneWriter* writer) override {
// The shell doesn't have a read principals hook, so it doesn't really
// matter what we write here, but we have to write something so the
// fuzzer is happy.
return JS_WriteUint32Pair(writer, bits, 0);
}
bool isSystemOrAddonPrincipal() override { return true; }
static void destroy(JSPrincipals* principals) {
MOZ_ASSERT(principals != &fullyTrusted);
MOZ_ASSERT(principals->refcount == 0);
js_delete(static_cast<const ShellPrincipals*>(principals));
}
static bool subsumes(JSPrincipals* first, JSPrincipals* second) {
uint32_t firstBits = getBits(first);
uint32_t secondBits = getBits(second);
return (firstBits | secondBits) == firstBits;
}
static JSSecurityCallbacks securityCallbacks;
// Fully-trusted principals singleton.
static ShellPrincipals fullyTrusted;
};
JSSecurityCallbacks ShellPrincipals::securityCallbacks = {
nullptr, // contentSecurityPolicyAllows
nullptr, // codeForEvalGets
subsumes};
// The fully-trusted principal subsumes all other principals.
MOZ_RUNINIT ShellPrincipals ShellPrincipals::fullyTrusted(-1, 1);
#ifdef EDITLINE
extern "C" {
extern MOZ_EXPORT char* readline(const char* prompt);
extern MOZ_EXPORT void add_history(char* line);
} // extern "C"
#endif
ShellContext::ShellContext(JSContext* cx, IsWorkerEnum isWorker_)
: cx_(nullptr),
isWorker(isWorker_),
lastWarningEnabled(false),
trackUnhandledRejections(true),
timeoutInterval(-1.0),
startTime(PRMJ_Now()),
serviceInterrupt(false),
haveInterruptFunc(false),
interruptFunc(cx, NullValue()),
lastWarning(cx, NullValue()),
promiseRejectionTrackerCallback(cx, NullValue()),
unhandledRejectedPromises(cx),
watchdogLock(mutexid::ShellContextWatchdog),
exitCode(0),
quitting(false),
readLineBufPos(0),
errFilePtr(nullptr),
outFilePtr(nullptr),
offThreadMonitor(mutexid::ShellOffThreadState),
finalizationRegistryCleanupCallbacks(cx) {}
ShellContext* js::shell::GetShellContext(JSContext* cx) {
ShellContext* sc = static_cast<ShellContext*>(JS_GetContextPrivate(cx));
MOZ_ASSERT(sc);
return sc;
}
static void TraceRootArrays(JSTracer* trc, gc::MarkColor color) {
JSRuntime* rt = trc->runtime();
for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) {
auto priv = static_cast<ShellCompartmentPrivate*>(
JS_GetCompartmentPrivate(comp.get()));
if (!priv) {
continue;
}
GCPtr<ArrayObject*>& array =
(color == gc::MarkColor::Black) ? priv->blackRoot : priv->grayRoot;
TraceNullableEdge(trc, &array, "shell root array");
if (array) {
// Trace the array elements as part of root marking.
for (uint32_t i = 0; i < array->getDenseInitializedLength(); i++) {
Value& value = const_cast<Value&>(array->getDenseElement(i));
TraceManuallyBarrieredEdge(trc, &value, "shell root array element");
}
}
}
}
}
static void TraceBlackRoots(JSTracer* trc, void* data) {
TraceRootArrays(trc, gc::MarkColor::Black);
}
static bool TraceGrayRoots(JSTracer* trc, JS::SliceBudget& budget, void* data) {
TraceRootArrays(trc, gc::MarkColor::Gray);
return true;
}
static inline JSString* NewStringCopyUTF8(JSContext* cx, const char* chars) {
return JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(chars, strlen(chars)));
}
static mozilla::UniqueFreePtr<char[]> GetLine(FILE* file, const char* prompt) {
#ifdef EDITLINE
/*
* Use readline only if file is stdin, because there's no way to specify
* another handle. Are other filehandles interactive?
*/
if (file == stdin) {
mozilla::UniqueFreePtr<char[]> linep(readline(prompt));
/*
* We set it to zero to avoid complaining about inappropriate ioctl
* for device in the case of EOF. Looks like errno == 251 if line is
* finished with EOF and errno == 25 (EINVAL on Mac) if there is
* nothing left to read.
*/
if (errno == 251 || errno == 25 || errno == EINVAL) {
errno = 0;
}
if (!linep) {
return nullptr;
}
if (linep[0] != '\0') {
add_history(linep.get());
}
return linep;
}
#endif
size_t len = 0;
if (*prompt != '\0' && gOutFile->isOpen()) {
fprintf(gOutFile->fp, "%s", prompt);
fflush(gOutFile->fp);
}
size_t size = 80;
mozilla::UniqueFreePtr<char[]> buffer(static_cast<char*>(malloc(size)));
if (!buffer) {
return nullptr;
}
char* current = buffer.get();
do {
while (true) {
if (fgets(current, size - len, file)) {
break;
}
if (errno != EINTR) {
return nullptr;
}
}
len += strlen(current);
char* t = buffer.get() + len - 1;
if (*t == '\n') {
/* Line was read. We remove '\n' and exit. */
*t = '\0';
break;
}
if (len + 1 == size) {
size = size * 2;
char* raw = buffer.release();
char* tmp = static_cast<char*>(realloc(raw, size));
if (!tmp) {
free(raw);
return nullptr;
}
buffer.reset(tmp);
}
current = buffer.get() + len;
} while (true);
return buffer;
}
static bool ShellInterruptCallback(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
if (!sc->serviceInterrupt) {
return true;
}
// Reset serviceInterrupt. CancelExecution or InterruptIf will set it to
// true to distinguish watchdog or user triggered interrupts.
// Do this first to prevent other interrupts that may occur while the
// user-supplied callback is executing from re-entering the handler.
sc->serviceInterrupt = false;
bool result;
if (sc->haveInterruptFunc) {
bool wasAlreadyThrowing = cx->isExceptionPending();
JS::AutoSaveExceptionState savedExc(cx);
JSAutoRealm ar(cx, &sc->interruptFunc.toObject());
RootedValue rval(cx);
// Report any exceptions thrown by the JS interrupt callback, but do
// *not* keep it on the cx. The interrupt handler is invoked at points
// that are not expected to throw catchable exceptions, like at
// JSOp::RetRval.
//
// If the interrupted JS code was already throwing, any exceptions
// thrown by the interrupt handler are silently swallowed.
{
Maybe<AutoReportException> are;
if (!wasAlreadyThrowing) {
are.emplace(cx);
}
result = JS_CallFunctionValue(cx, nullptr, sc->interruptFunc,
JS::HandleValueArray::empty(), &rval);
}
savedExc.restore();
if (rval.isBoolean()) {
result = rval.toBoolean();
} else {
result = false;
}
} else {
result = false;
}
if (!result && sc->exitCode == 0) {
static const char msg[] = "Script terminated by interrupt handler.\n";
fputs(msg, stderr);
sc->exitCode = EXITCODE_TIMEOUT;
}
return result;
}
static void GCSliceCallback(JSContext* cx, JS::GCProgress progress,
const JS::GCDescription& desc) {
if (progress == JS::GC_CYCLE_END) {
#if defined(MOZ_MEMORY)
// We call this here to match the browser's DOMGCSliceCallback.
jemalloc_free_dirty_pages();
#endif
}
}
/*
* Some UTF-8 files, notably those written using Notepad, have a Unicode
* Byte-Order-Mark (BOM) as their first character. This is useless (byte-order
* is meaningless for UTF-8) but causes a syntax error unless we skip it.
*/
static void SkipUTF8BOM(FILE* file) {
int ch1 = fgetc(file);
int ch2 = fgetc(file);
int ch3 = fgetc(file);
// Skip the BOM
if (ch1 == 0xEF && ch2 == 0xBB && ch3 == 0xBF) {
return;
}
// No BOM - revert
if (ch3 != EOF) {
ungetc(ch3, file);
}
if (ch2 != EOF) {
ungetc(ch2, file);
}
if (ch1 != EOF) {
ungetc(ch1, file);
}
}
void EnvironmentPreparer::invoke(HandleObject global, Closure& closure) {
MOZ_ASSERT(JS_IsGlobalObject(global));
JSContext* cx = TlsContext.get();
MOZ_ASSERT(!JS_IsExceptionPending(cx));
AutoRealm ar(cx, global);
AutoReportException are(cx);
if (!closure(cx)) {
return;
}
}
static bool RegisterScriptPathWithModuleLoader(JSContext* cx,
HandleScript script,
const char* filename) {
// Set the private value associated with a script to a object containing the
// script's filename so that the module loader can use it to resolve
// relative imports.
RootedString path(cx, NewStringCopyUTF8(cx, filename));
if (!path) {
return false;
}
MOZ_ASSERT(JS::GetScriptPrivate(script).isUndefined());
RootedObject infoObject(cx, js::CreateScriptPrivate(cx, path));
if (!infoObject) {
return false;
}
JS::SetScriptPrivate(script, ObjectValue(*infoObject));
return true;
}
enum class CompileUtf8 {
InflateToUtf16,
DontInflate,
};
[[nodiscard]] static bool RunFile(JSContext* cx, const char* filename,
FILE* file, CompileUtf8 compileMethod,
bool compileOnly, bool fullParse) {
SkipUTF8BOM(file);
int64_t t1 = PRMJ_Now();
RootedScript script(cx);
if (!filename) filename = "-";
{
CompileOptions options(cx);
options.setIntroductionType("js shell file")
.setFileAndLine(filename, 1)
.setIsRunOnce(true)
.setNoScriptRval(true);
if (fullParse) {
options.setForceFullParse();
} else {
options.setEagerDelazificationStrategy(defaultDelazificationMode);
}
if (compileMethod == CompileUtf8::DontInflate) {
script = JS::CompileUtf8File(cx, options, file);
} else {
fprintf(stderr, "(compiling '%s' after inflating to UTF-16)\n", filename);
FileContents buffer(cx);
if (!ReadCompleteFile(cx, file, buffer)) {
return false;
}
size_t length = buffer.length();
auto chars = UniqueTwoByteChars(
UTF8CharsToNewTwoByteCharsZ(
cx,
JS::UTF8Chars(reinterpret_cast<const char*>(buffer.begin()),
buffer.length()),
&length, js::MallocArena)
.get());
if (!chars) {
return false;
}
JS::SourceText<char16_t> source;
if (!source.init(cx, std::move(chars), length)) {
return false;
}
script = JS::Compile(cx, options, source);
}
if (!script) {
return false;
}
}
if (!RegisterScriptPathWithModuleLoader(cx, script, filename)) {
return false;
}
#ifdef DEBUG
if (dumpEntrainedVariables) {
AnalyzeEntrainedVariables(cx, script);
}
#endif
if (!compileOnly) {
if (!JS_ExecuteScript(cx, script)) {
return false;
}
int64_t t2 = PRMJ_Now() - t1;
if (printTiming) {
printf("runtime = %.3f ms\n", double(t2) / PRMJ_USEC_PER_MSEC);
}
}
return true;
}
[[nodiscard]] static bool RunModule(JSContext* cx, const char* filename,
bool compileOnly) {
ShellContext* sc = GetShellContext(cx);
RootedString path(cx, NewStringCopyUTF8(cx, filename));
if (!path) {
return false;
}
path = ResolvePath(cx, path, RootRelative);
if (!path) {
return false;
}
return sc->moduleLoader->loadRootModule(cx, path);
}
static void ShellCleanupFinalizationRegistryCallback(JSFunction* doCleanup,
JSObject* incumbentGlobal,
void* data) {
// In the browser this queues a task. Shell jobs correspond to microtasks so
// we arrange for cleanup to happen after all jobs/microtasks have run. The
// incumbent global is ignored in the shell.
auto sc = static_cast<ShellContext*>(data);
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!sc->finalizationRegistryCleanupCallbacks.append(doCleanup)) {
oomUnsafe.crash("ShellCleanupFinalizationRegistryCallback");
}
}
// Run any FinalizationRegistry cleanup tasks and return whether any ran.
static bool MaybeRunFinalizationRegistryCleanupTasks(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
MOZ_ASSERT(!sc->quitting);
Rooted<ShellContext::FunctionVector> callbacks(cx);
std::swap(callbacks.get(), sc->finalizationRegistryCleanupCallbacks.get());
bool ranTasks = false;
RootedFunction callback(cx);
for (JSFunction* f : callbacks) {
callback = f;
JS::ExposeObjectToActiveJS(callback);
AutoRealm ar(cx, callback);
{
AutoReportException are(cx);
RootedValue unused(cx);
(void)JS_CallFunction(cx, nullptr, callback, HandleValueArray::empty(),
&unused);
}
ranTasks = true;
if (sc->quitting) {
break;
}
}
return ranTasks;
}
static bool EnqueueJob(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!IsFunctionObject(args.get(0))) {
JS_ReportErrorASCII(cx, "EnqueueJob's first argument must be a function");
return false;
}
args.rval().setUndefined();
RootedObject job(cx, &args[0].toObject());
return js::EnqueueJob(cx, job);
}
static void RunShellJobs(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
if (sc->quitting) {
return;
}
while (true) {
// Run microtasks.
js::RunJobs(cx);
if (sc->quitting) {
return;
}
// Run tasks (only finalization registry clean tasks are possible).
bool ranTasks = MaybeRunFinalizationRegistryCleanupTasks(cx);
if (!ranTasks) {
break;
}
}
}
static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (GetShellContext(cx)->quitting) {
JS_ReportErrorASCII(
cx, "Mustn't drain the job queue when the shell is quitting");
return false;
}
if (cx->isEvaluatingModule != 0) {
JS_ReportErrorASCII(
cx,
"Can't drain the job queue when executing the top level of a module");
return false;
}
RunShellJobs(cx);
if (GetShellContext(cx)->quitting) {
return false;
}
args.rval().setUndefined();
return true;
}
static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject job(cx, cx->internalJobQueue->maybeFront());
if (!job) {
JS_ReportErrorASCII(cx, "Job queue is empty");
return false;
}
RootedObject global(cx, &job->nonCCWGlobal());
if (!cx->compartment()->wrap(cx, &global)) {
return false;
}
args.rval().setObject(*global);
return true;
}
static bool TrackUnhandledRejections(JSContext* cx, JS::HandleObject promise,
JS::PromiseRejectionHandlingState state) {
ShellContext* sc = GetShellContext(cx);
if (!sc->trackUnhandledRejections) {
return true;
}
#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
if (cx->runningOOMTest) {
// When OOM happens, we cannot reliably track the set of unhandled
// promise rejections. Throw error only when simulated OOM is used
// *and* promises are used in the test.
JS_ReportErrorASCII(
cx,
"Can't track unhandled rejections while running simulated OOM "
"test. Call ignoreUnhandledRejections before using oomTest etc.");
return false;
}
#endif
if (!sc->unhandledRejectedPromises) {
sc->unhandledRejectedPromises = SetObject::create(cx);
if (!sc->unhandledRejectedPromises) {
return false;
}
}
RootedValue promiseVal(cx, ObjectValue(*promise));
AutoRealm ar(cx, sc->unhandledRejectedPromises);
if (!cx->compartment()->wrap(cx, &promiseVal)) {
return false;
}
switch (state) {
case JS::PromiseRejectionHandlingState::Unhandled:
if (!sc->unhandledRejectedPromises->add(cx, promiseVal)) {
return false;
}
break;
case JS::PromiseRejectionHandlingState::Handled:
bool deleted = false;
if (!sc->unhandledRejectedPromises->delete_(cx, promiseVal, &deleted)) {
return false;
}
// We can't MOZ_ASSERT(deleted) here, because it's possible we failed to
// add the promise in the first place, due to OOM.
break;
}
return true;
}
static void ForwardingPromiseRejectionTrackerCallback(
JSContext* cx, bool mutedErrors, JS::HandleObject promise,
JS::PromiseRejectionHandlingState state, void* data) {
AutoReportException are(cx);
if (!TrackUnhandledRejections(cx, promise, state)) {
return;
}
RootedValue callback(cx,
GetShellContext(cx)->promiseRejectionTrackerCallback);
if (callback.isNull()) {
return;
}
AutoRealm ar(cx, &callback.toObject());
FixedInvokeArgs<2> args(cx);
args[0].setObject(*promise);
args[1].setInt32(static_cast<int32_t>(state));
if (!JS_WrapValue(cx, args[0])) {
return;
}
RootedValue rval(cx);
(void)Call(cx, callback, UndefinedHandleValue, args, &rval);
}
static bool SetPromiseRejectionTrackerCallback(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!IsFunctionObject(args.get(0))) {
JS_ReportErrorASCII(
cx,
"setPromiseRejectionTrackerCallback expects a function as its sole "
"argument");
return false;
}
GetShellContext(cx)->promiseRejectionTrackerCallback = args[0];
args.rval().setUndefined();
return true;
}
static const char* telemetryNames[static_cast<int>(JSMetric::Count)] = {
#define LIT(NAME, _) #NAME,
FOR_EACH_JS_METRIC(LIT)
#undef LIT
};
// Telemetry can be executed from multiple threads, and the callback is
// responsible to avoid contention on the recorded telemetry data.
class MOZ_RAII AutoLockTelemetry : public LockGuard<Mutex> {
using Base = LockGuard<Mutex>;
Mutex& getMutex() {
static Mutex mutex(mutexid::ShellTelemetry);
return mutex;
}
public:
AutoLockTelemetry() : Base(getMutex()) {}
};
using TelemetrySamples = mozilla::Vector<uint32_t, 0, js::SystemAllocPolicy>;
MOZ_CONSTINIT static mozilla::Array<UniquePtr<TelemetrySamples>,
size_t(JSMetric::Count)>
recordedTelemetrySamples;
static void AccumulateTelemetryDataCallback(JSMetric id, uint32_t sample) {
AutoLockTelemetry alt;
TelemetrySamples* samples = recordedTelemetrySamples[size_t(id)].get();
if (!samples) {
return;
}
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!samples->append(sample)) {
oomUnsafe.crash("AccumulateTelemetrySamplesCallback");
}
}
static bool LookupTelemetryName(JSContext* cx, Handle<Value> nameValue,
JSMetric* resultOut) {
static_assert(size_t(JSMetric::Count) == std::size(telemetryNames));
MOZ_ASSERT(resultOut);
JSString* str = ToString(cx, nameValue);
if (!str) {
return false;
}
UniqueChars name = EncodeLatin1(cx, str);
if (!name) {
return false;
}
for (size_t i = 0; i < std::size(telemetryNames); i++) {
if (strcmp(telemetryNames[i], name.get()) == 0) {
*resultOut = JSMetric(i);
return true;
}
}
return false;
}
static bool StartRecordingTelemetry(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "startRecordingTelemetry");
return false;
}
JSMetric id;
if (!LookupTelemetryName(cx, args[0], &id)) {
JS_ReportErrorASCII(cx, "Telemetry probe name not found");
return false;
}
if (recordedTelemetrySamples[size_t(id)]) {
JS_ReportErrorASCII(cx, "Already recording telemetry for this probe");
return false;
}
auto samples = MakeUnique<TelemetrySamples>();
if (!samples) {
ReportOutOfMemory(cx);
return false;
}
recordedTelemetrySamples[size_t(id)] = std::move(samples);
args.rval().setUndefined();
return true;
}
static bool StopRecordingTelemetry(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "stopRecordingTelemetry");
return false;
}
JSMetric id;
if (!LookupTelemetryName(cx, args[0], &id)) {
JS_ReportErrorASCII(cx, "Telemetry probe name not found");
return false;
}
if (!recordedTelemetrySamples[size_t(id)]) {
JS_ReportErrorASCII(cx, "Not recording telemetry for this probe");
return false;
}
recordedTelemetrySamples[size_t(id)].reset();
args.rval().setUndefined();
return true;
}
static bool GetTelemetrySamples(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "getTelemetrySamples");
return false;
}
JSMetric id;
if (!LookupTelemetryName(cx, args[0], &id)) {
JS_ReportErrorASCII(cx, "Telemetry probe name not found");
return false;
}
TelemetrySamples* samples = recordedTelemetrySamples[size_t(id)].get();
if (!samples) {
args.rval().setUndefined();
return true;
}
Rooted<ArrayObject*> array(cx, NewDenseEmptyArray(cx));
if (!array) {
return false;
}
for (uint32_t sample : *samples) {
if (!NewbornArrayPush(cx, array, NumberValue(sample))) {
return false;
}
}
args.rval().setObject(*array);
return true;
}
// Use Counter introspection
MOZ_RUNINIT static Mutex useCounterLock(mutexid::ShellUseCounters);
class MOZ_RAII AutoLockUseCounters : public LockGuard<Mutex> {
using Base = LockGuard<Mutex>;
public:
AutoLockUseCounters() : Base(useCounterLock) {}
};
using UseCounterArray =
mozilla::Array<uint32_t, static_cast<size_t>(JSUseCounter::COUNT)>;
static UseCounterArray useCounterResults;
static void SetUseCounterCallback(JSObject* obj, JSUseCounter counter) {
MOZ_RELEASE_ASSERT(obj);
AutoLockUseCounters aluc;
// Maybe should ensure obj is a global object?
useCounterResults[static_cast<size_t>(counter)]++;
}
static bool GetUseCounterResults(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
if (!obj) {
return false;
}
// Make a private copy holding the lock then release, because we can't
// hold this mutex while doing JS_DefineProperty, which holds MemoryTracker
// mutex.
UseCounterArray local;
{
AutoLockUseCounters aluc;
local = useCounterResults;
}
RootedValue val(cx);
#define ADD_VALUE(ENUM, NAME) \
val.setInt32(local[static_cast<size_t>(JSUseCounter::ENUM)]); \
if (!JS_DefineProperty(cx, obj, #NAME, val, JSPROP_ENUMERATE)) { \
return false; \
}
FOR_EACH_JS_USE_COUNTER(ADD_VALUE);
args.rval().setObject(*obj);
return true;
}
static bool BoundToAsyncStack(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedValue function(cx, GetFunctionNativeReserved(&args.callee(), 0));
RootedObject options(
cx, &GetFunctionNativeReserved(&args.callee(), 1).toObject());
Rooted<SavedFrame*> stack(cx, nullptr);
bool isExplicit;
RootedValue v(cx);
if (!JS_GetProperty(cx, options, "stack", &v)) {
return false;
}
if (!v.isObject() || !v.toObject().is<SavedFrame>()) {
JS_ReportErrorASCII(cx,
"The 'stack' property must be a SavedFrame object.");
return false;
}
stack = &v.toObject().as<SavedFrame>();
if (!JS_GetProperty(cx, options, "cause", &v)) {
return false;
}
RootedString causeString(cx, ToString(cx, v));
if (!causeString) {
return false;
}
UniqueChars cause = JS_EncodeStringToUTF8(cx, causeString);
if (!cause) {
MOZ_ASSERT(cx->isExceptionPending());
return false;
}
if (!JS_GetProperty(cx, options, "explicit", &v)) {
return false;
}
isExplicit = v.isUndefined() ? true : ToBoolean(v);
auto kind =
(isExplicit ? JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT
: JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::IMPLICIT);
JS::AutoSetAsyncStackForNewCalls asasfnckthxbye(cx, stack, cause.get(), kind);
return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(),
args.rval());
}
static bool BindToAsyncStack(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 2) {
JS_ReportErrorASCII(cx, "bindToAsyncStack takes exactly two arguments.");
return false;
}
if (!args[0].isObject() || !IsCallable(args[0])) {
JS_ReportErrorASCII(
cx, "bindToAsyncStack's first argument should be a function.");
return false;
}
if (!args[1].isObject()) {
JS_ReportErrorASCII(
cx, "bindToAsyncStack's second argument should be an object.");
return false;
}
RootedFunction bound(cx, NewFunctionWithReserved(cx, BoundToAsyncStack, 0, 0,
"bindToAsyncStack thunk"));
if (!bound) {
return false;
}
SetFunctionNativeReserved(bound, 0, args[0]);
SetFunctionNativeReserved(bound, 1, args[1]);
args.rval().setObject(*bound);
return true;
}
#ifdef JS_HAS_INTL_API
static bool AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.get(0).isObject()) {
JS_ReportErrorASCII(cx, "addIntlExtras must be passed an object");
return false;
}
JS::RootedObject intl(cx, &args[0].toObject());
static const JSFunctionSpec funcs[] = {
JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0),
JS_FS_END,
};
if (!JS_DefineFunctions(cx, intl, funcs)) {
return false;
}
if (!JS::AddMozDateTimeFormatConstructor(cx, intl)) {
return false;
}
if (!JS::AddMozDisplayNamesConstructor(cx, intl)) {
return false;
}
args.rval().setUndefined();
return true;
}
#endif // JS_HAS_INTL_API
[[nodiscard]] static bool EvalUtf8AndPrint(JSContext* cx, const char* bytes,
size_t length, int lineno,
bool compileOnly) {
// Eval.
JS::CompileOptions options(cx);
options.setIntroductionType("js shell interactive")
.setIsRunOnce(true)
.setFileAndLine("typein", lineno)
.setEagerDelazificationStrategy(defaultDelazificationMode);
JS::SourceText<Utf8Unit> srcBuf;
if (!srcBuf.init(cx, bytes, length, JS::SourceOwnership::Borrowed)) {
return false;
}
RootedScript script(cx, JS::Compile(cx, options, srcBuf));
if (!script) {
return false;
}
if (compileOnly) {
return true;
}
RootedValue result(cx);
if (!JS_ExecuteScript(cx, script, &result)) {
return false;
}
if (!result.isUndefined() && gOutFile->isOpen()) {
// Print.
RootedString str(cx, JS_ValueToSource(cx, result));
if (!str) {
return false;
}
UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, str);
if (!utf8chars) {
return false;
}
fprintf(gOutFile->fp, "%s\n", utf8chars.get());
}
return true;
}
[[nodiscard]] static bool ReadEvalPrintLoop(JSContext* cx, FILE* in,
bool compileOnly) {
ShellContext* sc = GetShellContext(cx);
int lineno = 1;
bool hitEOF = false;
do {
/*
* Accumulate lines until we get a 'compilable unit' - one that either
* generates an error (before running out of source) or that compiles
* cleanly. This should be whenever we get a complete statement that
* coincides with the end of a line.
*/
int startline = lineno;
using CharBuffer = Vector<char, 32>;
RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment());
CharBuffer buffer(cx);
do {
ScheduleWatchdog(cx, -1);
sc->serviceInterrupt = false;
errno = 0;
mozilla::UniqueFreePtr<char[]> line =
GetLine(in, startline == lineno ? "js> " : "");
if (!line) {
if (errno) {
if (UniqueChars error = SystemErrorMessage(cx, errno)) {
JS_ReportErrorUTF8(cx, "%s", error.get());
}
return false;
}
hitEOF = true;
break;
}
if (!buffer.append(line.get(), strlen(line.get())) ||
!buffer.append('\n')) {
return false;
}
lineno++;
if (!ScheduleWatchdog(cx, sc->timeoutInterval)) {
hitEOF = true;
break;
}
} while (!JS_Utf8BufferIsCompilableUnit(cx, cx->global(), buffer.begin(),
buffer.length()));
if (hitEOF && buffer.empty()) {
break;
}
{
// Report exceptions but keep going.
AutoReportException are(cx);
(void)EvalUtf8AndPrint(cx, buffer.begin(), buffer.length(), startline,
compileOnly);
}
// If a let or const fail to initialize they will remain in an unusable
// without further intervention. This call cleans up the global scope,
// setting uninitialized lexicals to undefined so that they may still
// be used. This behavior is _only_ acceptable in the context of the repl.
if (JS::ForceLexicalInitialization(cx, globalLexical) &&
gErrFile->isOpen()) {
fputs(
"Warning: According to the standard, after the above exception,\n"
"Warning: the global bindings should be permanently uninitialized.\n"
"Warning: We have non-standard-ly initialized them to `undefined`"
"for you.\nWarning: This nicety only happens in the JS shell.\n",
stderr);
}
RunShellJobs(cx);
} while (!hitEOF && !sc->quitting);
if (gOutFile->isOpen()) {
fprintf(gOutFile->fp, "\n");
}
return true;
}
enum FileKind {
PreludeScript, // UTF-8 script, fully-parsed, to avoid conflicting
// configurations.
FileScript, // UTF-8, directly parsed as such
FileScriptUtf16, // FileScript, but inflate to UTF-16 before parsing
FileModule,
};
[[nodiscard]] static bool Process(JSContext* cx, const char* filename,
bool forceTTY, FileKind kind) {
FILE* file;
if (forceTTY || !filename || strcmp(filename, "-") == 0) {
file = stdin;
} else {
file = OpenFile(cx, filename, "rb");
if (!file) {
return false;
}
}
AutoCloseFile autoClose(file);
bool fullParse = false;
if (!forceTTY && !isatty(fileno(file))) {
// It's not interactive - just execute it.
switch (kind) {
case PreludeScript:
fullParse = true;
if (!RunFile(cx, filename, file, CompileUtf8::DontInflate, compileOnly,
fullParse)) {
return false;
}
break;
case FileScript:
if (!RunFile(cx, filename, file, CompileUtf8::DontInflate, compileOnly,
fullParse)) {
return false;
}
break;
case FileScriptUtf16:
if (!RunFile(cx, filename, file, CompileUtf8::InflateToUtf16,
compileOnly, fullParse)) {
return false;
}
break;
case FileModule:
if (!RunModule(cx, filename, compileOnly)) {
return false;
}
break;
default:
MOZ_CRASH("Impossible FileKind!");
}
} else {
// It's an interactive filehandle; drop into read-eval-print loop.
MOZ_ASSERT(kind == FileScript);
if (!ReadEvalPrintLoop(cx, file, compileOnly)) {
return false;
}
}
#ifdef FUZZING_JS_FUZZILLI
fprintf(stderr, "executionHash is 0x%x with %d inputs\n", cx->executionHash,
cx->executionHashInputs);
#endif
return true;
}
#ifdef XP_WIN
# define GET_FD_FROM_FILE(a) int(_get_osfhandle(fileno(a)))
#else
# define GET_FD_FROM_FILE(a) fileno(a)
#endif
static void freeExternalCallback(void* contents, void* userData) {
MOZ_ASSERT(!userData);
js_free(contents);
}
static bool CreateExternalArrayBuffer(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(
cx, my_GetErrorMessage, nullptr,
args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"createExternalArrayBuffer");
return false;
}
int32_t bytes = 0;
if (!ToInt32(cx, args[0], &bytes)) {
return false;
}
if (bytes < 0) {
JS_ReportErrorASCII(cx, "Size must be non-negative");
return false;
}
void* buffer = js_calloc(bytes);
if (!buffer) {
JS_ReportOutOfMemory(cx);
return false;
}
UniquePtr<void, JS::BufferContentsDeleter> ptr{buffer,
{&freeExternalCallback}};
RootedObject arrayBuffer(
cx, JS::NewExternalArrayBuffer(cx, bytes, std::move(ptr)));
if (!arrayBuffer) {
return false;
}
args.rval().setObject(*arrayBuffer);
return true;
}
static bool CreateMappedArrayBuffer(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1 || args.length() > 3) {
JS_ReportErrorNumberASCII(
cx, my_GetErrorMessage, nullptr,
args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"createMappedArrayBuffer");
return false;
}
RootedString rawFilenameStr(cx, JS::ToString(cx, args[0]));
if (!rawFilenameStr) {
return false;
}
// It's a little bizarre to resolve relative to the script, but for testing
// I need a file at a known location, and the only good way I know of to do
// that right now is to include it in the repo alongside the test script.
Rooted<JSString*> filenameStr(
cx, ResolvePath(cx, rawFilenameStr, ScriptRelative));
if (!filenameStr) {
return false;
}
UniqueChars filename = JS_EncodeStringToUTF8(cx, filenameStr);
if (!filename) {
return false;
}
uint32_t offset = 0;
if (args.length() >= 2) {
if (!JS::ToUint32(cx, args[1], &offset)) {
return false;
}
}
bool sizeGiven = false;
uint32_t size;
if (args.length() >= 3) {
if (!JS::ToUint32(cx, args[2], &size)) {
return false;
}
sizeGiven = true;
if (size == 0) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_BAD_ARRAY_LENGTH);
return false;
}
}
FILE* file = OpenFile(cx, filename.get(), "rb");
if (!file) {
return false;
}
AutoCloseFile autoClose(file);
struct stat st;
if (fstat(fileno(file), &st) < 0) {
JS_ReportErrorASCII(cx, "Unable to stat file");
return false;
}
if ((st.st_mode & S_IFMT) != S_IFREG) {
JS_ReportErrorASCII(cx, "Path is not a regular file");
return false;
}
if (!sizeGiven) {
if (off_t(offset) >= st.st_size) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_OFFSET_LARGER_THAN_FILESIZE);
return false;
}
size = st.st_size - offset;
}
void* contents =
JS::CreateMappedArrayBufferContents(GET_FD_FROM_FILE(file), offset, size);
if (!contents) {
JS_ReportErrorASCII(cx,
"failed to allocate mapped array buffer contents "
"(possibly due to bad alignment)");
return false;
}
RootedObject obj(cx,
JS::NewMappedArrayBufferWithContents(cx, size, contents));
if (!obj) {
return false;
}
args.rval().setObject(*obj);
return true;
}
#undef GET_FD_FROM_FILE
class UserBufferObject : public NativeObject {
static const uint32_t BUFFER_SLOT = 0;
static const uint32_t BYTE_LENGTH_SLOT = 1;
static const uint32_t RESERVED_SLOTS = 2;
static constexpr auto BufferMemoryUse = MemoryUse::Embedding1;
static void finalize(JS::GCContext* gcx, JSObject* obj);
public:
static const JSClassOps classOps_;
static const JSClass class_;
[[nodiscard]] static UserBufferObject* create(JSContext* cx,
size_t byteLength);
void* buffer() const {
auto& buffer = getReservedSlot(BUFFER_SLOT);
if (buffer.isUndefined()) {
return nullptr;
}
return buffer.toPrivate();
}
size_t byteLength() const {
return size_t(getReservedSlot(BYTE_LENGTH_SLOT).toPrivate());
}
};
const JSClassOps UserBufferObject::classOps_ = {
nullptr, // addProperty
nullptr, // delProperty
nullptr, // enumerate
nullptr, // newEnumerate
nullptr, // resolve
nullptr, // mayResolve
UserBufferObject::finalize, // finalize
nullptr, // call
nullptr, // construct
nullptr, // trace
};
const JSClass UserBufferObject::class_ = {
"UserBufferObject",
JSCLASS_HAS_RESERVED_SLOTS(UserBufferObject::RESERVED_SLOTS) |
JSCLASS_BACKGROUND_FINALIZE,
&UserBufferObject::classOps_,
};
UserBufferObject* UserBufferObject::create(JSContext* cx, size_t byteLength) {
void* buffer = js_calloc(byteLength);
if (!buffer) {
JS_ReportOutOfMemory(cx);
return nullptr;
}
UniquePtr<void, JS::FreePolicy> ptr(buffer);
auto* userBuffer = NewObjectWithGivenProto<UserBufferObject>(cx, nullptr);
if (!userBuffer) {
return nullptr;
}
InitReservedSlot(userBuffer, BUFFER_SLOT, ptr.release(), byteLength,
BufferMemoryUse);
userBuffer->initReservedSlot(BYTE_LENGTH_SLOT, PrivateValue(byteLength));
return userBuffer;
}
void UserBufferObject::finalize(JS::GCContext* gcx, JSObject* obj) {
auto* userBuffer = &obj->as<UserBufferObject>();
if (auto* buffer = userBuffer->buffer()) {
gcx->free_(userBuffer, buffer, userBuffer->byteLength(), BufferMemoryUse);
}
}
static bool CreateUserArrayBuffer(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(
cx, my_GetErrorMessage, nullptr,
args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"createUserArrayBuffer");
return false;
}
int32_t bytes = 0;
if (!ToInt32(cx, args[0], &bytes)) {
return false;
}
if (bytes < 0) {
JS_ReportErrorASCII(cx, "Size must be non-negative");
return false;
}
Rooted<UserBufferObject*> userBuffer(cx, UserBufferObject::create(cx, bytes));
if (!userBuffer) {
return false;
}
Rooted<JSObject*> arrayBuffer(
cx, JS::NewArrayBufferWithUserOwnedContents(cx, userBuffer->byteLength(),
userBuffer->buffer()));
if (!arrayBuffer) {
return false;
}
// Create a strong reference from |arrayBuffer| to |userBuffer|. This ensures
// |userBuffer| can't outlive |arrayBuffer|. That way we don't have to worry
// about detaching the ArrayBuffer object when |userBuffer| gets finalized.
// The reference is made through a private name, because we don't want to
// expose |userBuffer| to user-code.
auto* privateName = NewPrivateName(cx, cx->names().empty_.toHandle());
if (!privateName) {
return false;
}
Rooted<PropertyKey> id(cx, PropertyKey::Symbol(privateName));
Rooted<JS::Value> userBufferVal(cx, ObjectValue(*userBuffer));
if (!js::DefineDataProperty(cx, arrayBuffer, id, userBufferVal, 0)) {
return false;
}
args.rval().setObject(*arrayBuffer);
return true;
}
static bool AddPromiseReactions(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 3) {
JS_ReportErrorNumberASCII(
cx, my_GetErrorMessage, nullptr,
args.length() < 3 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"addPromiseReactions");
return false;
}
RootedObject promise(cx);
if (args[0].isObject()) {
promise = &args[0].toObject();
}
if (!promise || !JS::IsPromiseObject(promise)) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "addPromiseReactions");
return false;
}
RootedObject onResolve(cx);
if (args[1].isObject()) {
onResolve = &args[1].toObject();
}
RootedObject onReject(cx);
if (args[2].isObject()) {
onReject = &args[2].toObject();
}
if (!onResolve || !onResolve->is<JSFunction>() || !onReject ||
!onReject->is<JSFunction>()) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "addPromiseReactions");
return false;
}
return JS::AddPromiseReactions(cx, promise, onResolve, onReject);
}
static bool IgnoreUnhandledRejections(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
ShellContext* sc = GetShellContext(cx);
sc->trackUnhandledRejections = false;
args.rval().setUndefined();
return true;
}
static bool Options(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JS::ContextOptions oldContextOptions = JS::ContextOptionsRef(cx);
for (unsigned i = 0; i < args.length(); i++) {
RootedString str(cx, JS::ToString(cx, args[i]));
if (!str) {
return false;
}
Rooted<JSLinearString*> opt(cx, str->ensureLinear(cx));
if (!opt) {
return false;
}
if (StringEqualsLiteral(opt, "throw_on_asmjs_validation_failure")) {
JS::ContextOptionsRef(cx).toggleThrowOnAsmJSValidationFailure();
} else {
UniqueChars optChars = QuoteString(cx, opt, '"');
if (!optChars) {
return false;
}
JS_ReportErrorASCII(cx,
"unknown option name %s."
" The valid name is "
"throw_on_asmjs_validation_failure.",
optChars.get());
return false;
}
}
UniqueChars names = DuplicateString("");
bool found = false;
if (names && oldContextOptions.throwOnAsmJSValidationFailure()) {
names = JS_sprintf_append(std::move(names), "%s%s", found ? "," : "",
"throw_on_asmjs_validation_failure");
found = true;
}
if (!names) {
JS_ReportOutOfMemory(cx);
return false;
}
JSString* str = JS_NewStringCopyZ(cx, names.get());
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}
static bool LoadScript(JSContext* cx, unsigned argc, Value* vp,
bool scriptRelative) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedString str(cx);
for (unsigned i = 0; i < args.length(); i++) {
str = JS::ToString(cx, args[i]);
if (!str) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "load");
return false;
}
str = ResolvePath(cx, str, scriptRelative ? ScriptRelative : RootRelative);
if (!str) {
JS_ReportErrorASCII(cx, "unable to resolve path");
return false;
}
UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
if (!filename) {
return false;
}
errno = 0;
CompileOptions opts(cx);
opts.setIntroductionType("js shell load")
.setIsRunOnce(true)
.setNoScriptRval(true)
.setEagerDelazificationStrategy(defaultDelazificationMode);
RootedValue unused(cx);
if (!(compileOnly
? JS::CompileUtf8Path(cx, opts, filename.get()) != nullptr
: JS::EvaluateUtf8Path(cx, opts, filename.get(), &unused))) {
return false;
}
}
args.rval().setUndefined();
return true;
}
static bool Load(JSContext* cx, unsigned argc, Value* vp) {
return LoadScript(cx, argc, vp, false);
}
static bool LoadScriptRelativeToScript(JSContext* cx, unsigned argc,
Value* vp) {
return LoadScript(cx, argc, vp, true);
}
static void my_LargeAllocFailCallback() {
JSContext* cx = TlsContext.get();
if (!cx) {
return;
}
MOZ_ASSERT(!JS::RuntimeHeapIsBusy());
JS::PrepareForFullGC(cx);
cx->runtime()->gc.gc(JS::GCOptions::Shrink,
JS::GCReason::SHARED_MEMORY_LIMIT);
}
static const uint32_t CacheEntry_SOURCE = 0;
static const uint32_t CacheEntry_BYTECODE = 1;
static const uint32_t CacheEntry_OPTIONS = 2;
// Some compile options can't be combined differently between save and load.
//
// CacheEntries store a CacheOption set, and on load an exception is thrown
// if the entries are incompatible.
enum CacheOptions : uint32_t {
IsRunOnce,
NoScriptRval,
Global,
NonSyntactic,
SourceIsLazy,
ForceFullParse,
};
struct CacheOptionSet : public mozilla::EnumSet<CacheOptions> {
using mozilla::EnumSet<CacheOptions>::EnumSet;
explicit CacheOptionSet(const CompileOptions& options) : EnumSet() {
initFromOptions(options);
}
void initFromOptions(const CompileOptions& options) {
if (options.noScriptRval) {
*this += CacheOptions::NoScriptRval;
}
if (options.isRunOnce) {
*this += CacheOptions::IsRunOnce;
}
if (options.sourceIsLazy) {
*this += CacheOptions::SourceIsLazy;
}
if (options.forceFullParse()) {
*this += CacheOptions::ForceFullParse;
}
if (options.nonSyntacticScope) {
*this += CacheOptions::NonSyntactic;
}
}
};
static bool CacheOptionsCompatible(const CacheOptionSet& a,
const CacheOptionSet& b) {
// If the options are identical, they are trivially compatible.
return a == b;
}
static const JSClass CacheEntry_class = {
"CacheEntryObject",
JSCLASS_HAS_RESERVED_SLOTS(3),
};
static bool CacheEntry(JSContext* cx, unsigned argc, JS::Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1 || !args[0].isString()) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "CacheEntry");
return false;
}
RootedObject obj(cx, JS_NewObject(cx, &CacheEntry_class));
if (!obj) {
return false;
}
JS::SetReservedSlot(obj, CacheEntry_SOURCE, args[0]);
JS::SetReservedSlot(obj, CacheEntry_BYTECODE, UndefinedValue());
// Fill in empty option set.
CacheOptionSet defaultOptions;
JS::SetReservedSlot(obj, CacheEntry_OPTIONS,
Int32Value(defaultOptions.serialize()));
args.rval().setObject(*obj);
return true;
}
static bool CacheEntry_isCacheEntry(JSObject* cache) {
return cache->hasClass(&CacheEntry_class);
}
static JSString* CacheEntry_getSource(JSContext* cx, HandleObject cache) {
MOZ_ASSERT(CacheEntry_isCacheEntry(cache));
Value v = JS::GetReservedSlot(cache, CacheEntry_SOURCE);
if (!v.isString()) {
JS_ReportErrorASCII(
cx, "CacheEntry_getSource: Unexpected type of source reserved slot.");
return nullptr;
}
return v.toString();
}
static bool CacheEntry_compatible(JSContext* cx, HandleObject cache,
const CacheOptionSet& currentOptionSet) {
CacheOptionSet cacheEntryOptions;
MOZ_ASSERT(CacheEntry_isCacheEntry(cache));
Value v = JS::GetReservedSlot(cache, CacheEntry_OPTIONS);
cacheEntryOptions.deserialize(v.toInt32());
if (!CacheOptionsCompatible(cacheEntryOptions, currentOptionSet)) {
JS_ReportErrorASCII(cx,
"CacheEntry_compatible: Incompatible cache contents");
return false;
}
return true;
}
static uint8_t* CacheEntry_getBytecode(JSContext* cx, HandleObject cache,
size_t* length) {
MOZ_ASSERT(CacheEntry_isCacheEntry(cache));
Value v = JS::GetReservedSlot(cache, CacheEntry_BYTECODE);
if (!v.isObject() || !v.toObject().is<ArrayBufferObject>()) {
JS_ReportErrorASCII(
cx,
"CacheEntry_getBytecode: Unexpected type of bytecode reserved slot.");
return nullptr;
}
ArrayBufferObject* arrayBuffer = &v.toObject().as<ArrayBufferObject>();
*length = arrayBuffer->byteLength();
return arrayBuffer->dataPointer();
}
static bool CacheEntry_setBytecode(JSContext* cx, HandleObject cache,
const CacheOptionSet& cacheOptions,
uint8_t* buffer, uint32_t length) {
MOZ_ASSERT(CacheEntry_isCacheEntry(cache));
using BufferContents = ArrayBufferObject::BufferContents;
BufferContents contents = BufferContents::createMallocedUnknownArena(buffer);
Rooted<ArrayBufferObject*> arrayBuffer(
cx, ArrayBufferObject::createForContents(cx, length, contents));
if (!arrayBuffer) {
return false;
}
JS::SetReservedSlot(cache, CacheEntry_BYTECODE, ObjectValue(*arrayBuffer));
JS::SetReservedSlot(cache, CacheEntry_OPTIONS,
Int32Value(cacheOptions.serialize()));
return true;
}
static bool ConvertTranscodeResultToJSException(JSContext* cx,
JS::TranscodeResult rv) {
switch (rv) {
case JS::TranscodeResult::Ok:
return true;
default:
[[fallthrough]];
case JS::TranscodeResult::Failure:
MOZ_ASSERT(!cx->isExceptionPending());
JS_ReportErrorASCII(cx, "generic warning");
return false;
case JS::TranscodeResult::Failure_BadBuildId:
MOZ_ASSERT(!cx->isExceptionPending());
JS_ReportErrorASCII(cx, "the build-id does not match");
return false;
case JS::TranscodeResult::Failure_AsmJSNotSupported:
MOZ_ASSERT(!cx->isExceptionPending());
JS_ReportErrorASCII(cx, "Asm.js is not supported by XDR");
return false;
case JS::TranscodeResult::Failure_BadDecode:
MOZ_ASSERT(!cx->isExceptionPending());
JS_ReportErrorASCII(cx, "XDR data corruption");
return false;
case JS::TranscodeResult::Throw:
MOZ_ASSERT(cx->isExceptionPending());
return false;
}
}
static void SetQuitting(JSContext* cx, int32_t code) {
ShellContext* sc = GetShellContext(cx);
js::StopDrainingJobQueue(cx);
sc->exitCode = code;
sc->quitting = true;
}
static void UnsetQuitting(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
js::RestartDrainingJobQueue(cx);
sc->exitCode = 0;
sc->quitting = false;
}
static bool Evaluate(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1 || args.length() > 2) {
JS_ReportErrorNumberASCII(
cx, my_GetErrorMessage, nullptr,
args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"evaluate");
return false;
}
RootedString code(cx, nullptr);
RootedObject cacheEntry(cx, nullptr);
if (args[0].isString()) {
code = args[0].toString();
} else if (args[0].isObject() &&
CacheEntry_isCacheEntry(&args[0].toObject())) {
cacheEntry = &args[0].toObject();
code = CacheEntry_getSource(cx, cacheEntry);
if (!code) {
return false;
}
}
if (!code || (args.length() == 2 && args[1].isPrimitive())) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "evaluate");
return false;
}
RootedObject opts(cx);
if (args.length() == 2) {
if (!args[1].isObject()) {
JS_ReportErrorASCII(cx, "evaluate: The 2nd argument must be an object");
return false;
}
opts = &args[1].toObject();
}
RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
MOZ_ASSERT(global);
// Check "global" property before everything to use the given global's
// option as the default value.
Maybe<CompileOptions> maybeOptions;
if (opts) {
RootedValue v(cx);
if (!JS_GetProperty(cx, opts, "global", &v)) {
return false;
}
if (!v.isUndefined()) {
if (v.isObject()) {
global = js::CheckedUnwrapDynamic(&v.toObject(), cx,
/* stopAtWindowProxy = */ false);
if (!global) {
return false;
}
}
if (!global || !(JS::GetClass(global)->flags & JSCLASS_IS_GLOBAL)) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"\"global\" passed to evaluate()", "not a global object");
return false;
}
JSAutoRealm ar(cx, global);
maybeOptions.emplace(cx);
}
}
if (!maybeOptions) {
// If "global" property is not given, use the current global's option as
// the default value.
maybeOptions.emplace(cx);
}
CompileOptions& options = maybeOptions.ref();
UniqueChars fileNameBytes;
RootedString displayURL(cx);
RootedString sourceMapURL(cx);
bool catchTermination = false;
bool loadBytecode = false;
bool saveBytecodeWithDelazifications = false;
bool execute = true;
bool assertEqBytecode = false;
JS::EnvironmentChain envChain(cx, JS::SupportUnscopables::No);
RootedObject callerGlobal(cx, cx->global());
options.setIntroductionType("js shell evaluate")
.setFileAndLine("@evaluate", 1)
.setDeferDebugMetadata();
RootedValue privateValue(cx);
RootedString elementAttributeName(cx);
if (opts) {
if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) {
return false;
}
if (!ParseDebugMetadata(cx, opts, &privateValue, &elementAttributeName)) {
return false;
}
if (!ParseSourceOptions(cx, opts, &displayURL, &sourceMapURL)) {
return false;
}
RootedValue v(cx);
if (!JS_GetProperty(cx, opts, "catchTermination", &v)) {
return false;
}
if (!v.isUndefined()) {
catchTermination = ToBoolean(v);
}
if (!JS_GetProperty(cx, opts, "loadBytecode", &v)) {
return false;
}
if (!v.isUndefined()) {
loadBytecode = ToBoolean(v);
}
if (!JS_GetProperty(cx, opts, "saveBytecodeWithDelazifications", &v)) {
return false;
}
if (!v.isUndefined()) {
saveBytecodeWithDelazifications = ToBoolean(v);
}
if (!JS_GetProperty(cx, opts, "execute", &v)) {
return false;
}
if (!v.isUndefined()) {
execute = ToBoolean(v);
}
if (!JS_GetProperty(cx, opts, "assertEqBytecode", &v)) {
return false;
}
if (!v.isUndefined()) {
assertEqBytecode = ToBoolean(v);
}
if (!JS_GetProperty(cx, opts, "envChainObject", &v)) {
return false;
}
if (!v.isUndefined()) {
if (!v.isObject()) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"\"envChainObject\" passed to evaluate()", "not an object");
return false;
}
RootedObject obj(cx, &v.toObject());
{
// This may be a CCW, so try to unwrap before checking
// if it is an unqualified variables object. We still append
// the original object to the environment chain however.
JSObject* unwrappedObj = js::UncheckedUnwrap(obj, cx);
if (unwrappedObj->isUnqualifiedVarObj()) {
JS_ReportErrorASCII(
cx,
"\"envChainObject\" passed to evaluate() should not be an "
"unqualified variables object");
return false;
}
}
if (!envChain.append(obj)) {
return false;
}
}
if (!JS_GetProperty(cx, opts, "supportUnscopables", &v)) {
return false;
}
if (!v.isUndefined()) {
envChain.setSupportUnscopables(JS::SupportUnscopables(ToBoolean(v)));
}
// We cannot load or save the bytecode if we have no object where the
// bytecode cache is stored.
if (loadBytecode || saveBytecodeWithDelazifications) {
if (!cacheEntry) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "evaluate");
return false;
}
}
}
if (envChain.length() != 0) {
// Wrap the envChainObject list into target realm.
JSAutoRealm ar(cx, global);
for (size_t i = 0; i < envChain.length(); ++i) {
if (!JS_WrapObject(cx, envChain.chain()[i])) {
return false;
}
}
options.setNonSyntacticScope(true);
}
// The `loadBuffer` we use below outlives the Stencil we generate so we can
// use its contents directly in the Stencil.
options.borrowBuffer = true;
// We need to track the options used to generate bytecode for a CacheEntry to
// avoid mismatches. This is primarily a concern when fuzzing the jsshell.
CacheOptionSet cacheOptions;
cacheOptions.initFromOptions(options);
JS::TranscodeBuffer loadBuffer;
JS::TranscodeBuffer saveBuffer;
if (loadBytecode) {
size_t loadLength = 0;
uint8_t* loadData = nullptr;
if (!CacheEntry_compatible(cx, cacheEntry, cacheOptions)) {
return false;
}
loadData = CacheEntry_getBytecode(cx, cacheEntry, &loadLength);
if (!loadData) {
return false;
}
if (!loadBuffer.append(loadData, loadLength)) {
JS_ReportOutOfMemory(cx);
return false;
}
}
{
JSAutoRealm ar(cx, global);
RefPtr<JS::Stencil> stencil;
if (loadBytecode) {
JS::TranscodeRange range(loadBuffer.begin(), loadBuffer.length());
JS::DecodeOptions decodeOptions(options);
JS::TranscodeResult rv =
JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil));
if (JS::IsTranscodeFailureResult(rv)) {
JS_ReportErrorASCII(cx, "failed to decode cache");
return false;
}
if (!ConvertTranscodeResultToJSException(cx, rv)) {
return false;
}
} else {
AutoStableStringChars linearChars(cx);
if (!linearChars.initTwoByte(cx, code)) {
return false;
}
JS::SourceText<char16_t> srcBuf;
if (!srcBuf.initMaybeBorrowed(cx, linearChars)) {
return false;
}
stencil = JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
if (!stencil) {
return false;
}
}
if (!js::ValidateLazinessOfStencilAndGlobal(cx, stencil.get())) {
return false;
}
JS::InstantiateOptions instantiateOptions(options);
RootedScript script(
cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
if (!script) {
return false;
}
AutoReportFrontendContext fc(cx);
if (!SetSourceOptions(cx, &fc, script->scriptSource(), displayURL,
sourceMapURL)) {
return false;
}
if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, privateValue,
elementAttributeName, nullptr, nullptr)) {
return false;
}
if (saveBytecodeWithDelazifications) {
bool alreadyStarted;
if (!JS::StartCollectingDelazifications(cx, script, stencil,
alreadyStarted)) {
return false;
}
MOZ_ASSERT(!alreadyStarted);
}
if (execute) {
if (!(envChain.empty()
? JS_ExecuteScript(cx, script, args.rval())
: JS_ExecuteScript(cx, envChain, script, args.rval()))) {
if (catchTermination && !JS_IsExceptionPending(cx)) {
ShellContext* sc = GetShellContext(cx);
if (sc->quitting) {
UnsetQuitting(cx);
}
JSAutoRealm ar1(cx, callerGlobal);
JSString* str = JS_NewStringCopyZ(cx, "terminated");
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}
return false;
}
}
// Serialize the encoded bytecode, recorded before the execution, into a
// buffer which can be deserialized linearly.
if (saveBytecodeWithDelazifications) {
if (!FinishCollectingDelazifications(cx, script, saveBuffer)) {
return false;
}
}
}
if (saveBytecodeWithDelazifications) {
// If we are both loading and saving, we assert that we are going to
// replace the current bytecode by the same stream of bytes.
if (loadBytecode && assertEqBytecode) {
if (saveBuffer.length() != loadBuffer.length()) {
char loadLengthStr[16];
SprintfLiteral(loadLengthStr, "%zu", loadBuffer.length());
char saveLengthStr[16];
SprintfLiteral(saveLengthStr, "%zu", saveBuffer.length());
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_CACHE_EQ_SIZE_FAILED, loadLengthStr,
saveLengthStr);
return false;
}
if (!ArrayEqual(loadBuffer.begin(), saveBuffer.begin(),
loadBuffer.length())) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_CACHE_EQ_CONTENT_FAILED);
return false;
}
}
size_t saveLength = saveBuffer.length();
if (saveLength >= INT32_MAX) {
JS_ReportErrorASCII(cx, "Cannot save large cache entry content");
return false;
}
uint8_t* saveData = saveBuffer.extractOrCopyRawBuffer();
if (!CacheEntry_setBytecode(cx, cacheEntry, cacheOptions, saveData,
saveLength)) {
js_free(saveData);
return false;
}
}
return JS_WrapValue(cx, args.rval());
}
JSString* js::shell::FileAsString(JSContext* cx, JS::HandleString pathnameStr) {
UniqueChars pathname = JS_EncodeStringToUTF8(cx, pathnameStr);
if (!pathname) {
return nullptr;
}
FILE* file = OpenFile(cx, pathname.get(), "rb");
if (!file) {
return nullptr;
}
AutoCloseFile autoClose(file);
struct stat st;
if (fstat(fileno(file), &st) != 0) {
JS_ReportErrorUTF8(cx, "can't stat %s", pathname.get());
return nullptr;
}
if ((st.st_mode & S_IFMT) != S_IFREG) {
JS_ReportErrorUTF8(cx, "can't read non-regular file %s", pathname.get());
return nullptr;
}
size_t len;
if (!FileSize(cx, pathname.get(), file, &len)) {
return nullptr;
}
UniqueChars buf(js_pod_malloc<char>(len + 1));
if (!buf) {
JS_ReportErrorUTF8(cx, "out of memory reading %s", pathname.get());
return nullptr;
}
if (!ReadFile(cx, pathname.get(), file, buf.get(), len)) {
return nullptr;
}
UniqueTwoByteChars ucbuf(
JS::LossyUTF8CharsToNewTwoByteCharsZ(cx, JS::UTF8Chars(buf.get(), len),
&len, js::MallocArena)
.get());
if (!ucbuf) {
JS_ReportErrorUTF8(cx, "Invalid UTF-8 in file '%s'", pathname.get());
return nullptr;
}
return JS_NewUCStringCopyN(cx, ucbuf.get(), len);
}
/*
* Function to run scripts and return compilation + execution time. Semantics
* are closely modelled after the equivalent function in WebKit, as this is used
* to produce benchmark timings by SunSpider.
*/
static bool Run(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "run");
return false;
}
RootedString str(cx, JS::ToString(cx, args[0]));
if (!str) {
return false;
}
args[0].setString(str);
str = FileAsString(cx, str);
if (!str) {
return false;
}
AutoStableStringChars linearChars(cx);
if (!linearChars.initTwoByte(cx, str)) {
return false;
}
JS::SourceText<char16_t> srcBuf;
if (!srcBuf.initMaybeBorrowed(cx, linearChars)) {
return false;
}
RootedScript script(cx);
int64_t startClock = PRMJ_Now();
{
UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
if (!filename) {
return false;
}
JS::CompileOptions options(cx);
options.setIntroductionType("js shell run")
.setFileAndLine(filename.get(), 1)
.setIsRunOnce(true)
.setNoScriptRval(true)
.setEagerDelazificationStrategy(defaultDelazificationMode);
script = JS::Compile(cx, options, srcBuf);
if (!script) {
return false;
}
}
if (!JS_ExecuteScript(cx, script)) {
return false;
}
int64_t endClock = PRMJ_Now();
args.rval().setDouble((endClock - startClock) / double(PRMJ_USEC_PER_MSEC));
return true;
}
static int js_fgets(char* buf, int size, FILE* file) {
int n, i, c;
bool crflag;
n = size - 1;
if (n < 0) {
return -1;
}
// Use the fastest available getc.
auto fast_getc =
#if defined(HAVE_GETC_UNLOCKED)
getc_unlocked
#elif defined(HAVE__GETC_NOLOCK)
_getc_nolock
#else
getc
#endif
;
crflag = false;
for (i = 0; i < n && (c = fast_getc(file)) != EOF; i++) {
buf[i] = c;
if (c == '\n') { // any \n ends a line
i++; // keep the \n; we know there is room for \0
break;
}
if (crflag) { // \r not followed by \n ends line at the \r
ungetc(c, file);
break; // and overwrite c in buf with \0
}
crflag = (c == '\r');
}
buf[i] = '\0';
return i;
}
/*
* function readline()
* Provides a hook for scripts to read a line from stdin.
*/
static bool ReadLine(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
static constexpr size_t BUFSIZE = 256;
FILE* from = stdin;
size_t buflength = 0;
size_t bufsize = BUFSIZE;
char* buf = (char*)JS_malloc(cx, bufsize);
if (!buf) {
JS_ReportOutOfMemory(cx);
return false;
}
auto freeBuf = mozilla::MakeScopeExit([&]() {
JS_free(cx, buf);
buf = nullptr;
});
bool sawNewline = false;
size_t gotlength;
while ((gotlength = js_fgets(buf + buflength, bufsize - buflength, from)) >
0) {
buflength += gotlength;
/* Are we done? */
if (buf[buflength - 1] == '\n') {
buf[buflength - 1] = '\0';
sawNewline = true;
break;
} else if (buflength < bufsize - 1) {
break;
}
/* Else, grow our buffer for another pass. */
bufsize *= 2;
if (bufsize <= buflength) {
JS_ReportAllocationOverflow(cx);
return false;
}
char* tmp = static_cast<char*>(JS_realloc(cx, buf, bufsize / 2, bufsize));
if (!tmp) {
JS_ReportOutOfMemory(cx);
return false;
}
buf = tmp;
}
/* Treat the empty string specially. */
if (buflength == 0) {
args.rval().set(feof(from) ? NullValue() : JS_GetEmptyStringValue(cx));
return true;
}
/*
* Turn buf into a JSString. Note that buflength includes the trailing null
* character.
*/
JSString* str =
JS_NewStringCopyN(cx, buf, sawNewline ? buflength - 1 : buflength);
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}
/*
* function readlineBuf()
* Provides a hook for scripts to emulate readline() using a string object.
*/
static bool ReadLineBuf(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
ShellContext* sc = GetShellContext(cx);
if (!args.length()) {
if (!sc->readLineBuf) {
JS_ReportErrorASCII(cx,
"No source buffer set. You must initially "
"call readlineBuf with an argument.");
return false;
}
char* currentBuf = sc->readLineBuf.get() + sc->readLineBufPos;
size_t buflen = strlen(currentBuf);
if (!buflen) {
args.rval().setNull();
return true;
}
size_t len = 0;
while (len < buflen) {
if (currentBuf[len] == '\n') {
break;
}
len++;
}
JSString* str = JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(currentBuf, len));
if (!str) {
return false;
}
if (currentBuf[len] == '\0') {
sc->readLineBufPos += len;
} else {
sc->readLineBufPos += len + 1;
}
args.rval().setString(str);
return true;
}
if (args.length() == 1) {
sc->readLineBuf = nullptr;
sc->readLineBufPos = 0;
RootedString str(cx, JS::ToString(cx, args[0]));
if (!str) {
return false;
}
sc->readLineBuf = JS_EncodeStringToUTF8(cx, str);
if (!sc->readLineBuf) {
return false;
}
args.rval().setUndefined();
return true;
}
JS_ReportErrorASCII(cx, "Must specify at most one argument");
return false;
}
static bool PutStr(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 0) {
if (!gOutFile->isOpen()) {
JS_ReportErrorASCII(cx, "output file is closed");
return false;
}
RootedString str(cx, JS::ToString(cx, args[0]));
if (!str) {
return false;
}
UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
if (!bytes) {
return false;
}
fputs(bytes.get(), gOutFile->fp);
fflush(gOutFile->fp);
}
args.rval().setUndefined();
return true;
}
static bool Now(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
double now = PRMJ_Now() / double(PRMJ_USEC_PER_MSEC);
args.rval().setDouble(now);
return true;
}
static bool CpuNow(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
double now = double(std::clock()) / double(CLOCKS_PER_SEC);
args.rval().setDouble(now);
return true;
}
static bool PrintInternal(JSContext* cx, const CallArgs& args, RCFile* file) {
if (!file->isOpen()) {
JS_ReportErrorASCII(cx, "output file is closed");
return false;
}
for (unsigned i = 0; i < args.length(); i++) {
RootedString str(cx, JS::ToString(cx, args[i]));
if (!str) {
return false;
}
UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
if (!bytes) {
return false;
}
fprintf(file->fp, "%s%s", i ? " " : "", bytes.get());
}
fputc('\n', file->fp);
fflush(file->fp);
args.rval().setUndefined();
return true;
}
static bool Print(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
#ifdef FUZZING_INTERFACES
if (fuzzHaveModule && !fuzzDoDebug) {
// When fuzzing and not debugging, suppress any print() output,
// as it slows down fuzzing and makes libFuzzer's output hard
// to read.
args.rval().setUndefined();
return true;
}
#endif // FUZZING_INTERFACES
return PrintInternal(cx, args, gOutFile);
}
static bool PrintErr(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
return PrintInternal(cx, args, gErrFile);
}
static bool Help(JSContext* cx, unsigned argc, Value* vp);
static bool Quit(JSContext* cx, unsigned argc, Value* vp) {
// Print a message to stderr in differential testing to help jsfunfuzz
// find uncatchable-exception bugs.
if (js::SupportDifferentialTesting()) {
fprintf(stderr, "quit called\n");
}
CallArgs args = CallArgsFromVp(argc, vp);
int32_t code;
if (!ToInt32(cx, args.get(0), &code)) {
return false;
}
// The fuzzers check the shell's exit code and assume a value >= 128 means
// the process crashed (for instance, SIGSEGV will result in code 139). On
// POSIX platforms, the exit code is 8-bit and negative values can also
// result in an exit code >= 128. We restrict the value to range [0, 127] to
// avoid false positives.
if (code < 0 || code >= 128) {
JS_ReportErrorASCII(cx, "quit exit code should be in range 0-127");
return false;
}
SetQuitting(cx, code);
JS::ReportUncatchableException(cx);
return false;
}
static bool StartTimingMutator(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() > 0) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_TOO_MANY_ARGS, "startTimingMutator");
return false;
}
if (!cx->runtime()->gc.stats().startTimingMutator()) {
JS_ReportErrorASCII(
cx, "StartTimingMutator should only be called from outside of GC");
return false;
}
args.rval().setUndefined();
return true;
}
static bool StopTimingMutator(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() > 0) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_TOO_MANY_ARGS, "stopTimingMutator");
return false;
}
double mutator_ms, gc_ms;
if (!cx->runtime()->gc.stats().stopTimingMutator(mutator_ms, gc_ms)) {
JS_ReportErrorASCII(cx,
"stopTimingMutator called when not timing the mutator");
return false;
}
double total_ms = mutator_ms + gc_ms;
if (total_ms > 0 && gOutFile->isOpen()) {
fprintf(gOutFile->fp, "Mutator: %.3fms (%.1f%%), GC: %.3fms (%.1f%%)\n",
mutator_ms, mutator_ms / total_ms * 100.0, gc_ms,
gc_ms / total_ms * 100.0);
}
args.rval().setUndefined();
return true;
}
static const char* ToSource(JSContext* cx, HandleValue vp, UniqueChars* bytes) {
RootedString str(cx, JS_ValueToSource(cx, vp));
if (str) {
*bytes = JS_EncodeStringToUTF8(cx, str);
if (*bytes) {
return bytes->get();
}
}
JS_ClearPendingException(cx);
return "<<error converting value to string>>";
}
static bool AssertEq(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!(args.length() == 2 || (args.length() == 3 && args[2].isString()))) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
(args.length() < 2) ? JSSMSG_NOT_ENOUGH_ARGS
: (args.length() == 3) ? JSSMSG_INVALID_ARGS
: JSSMSG_TOO_MANY_ARGS,
"assertEq");
return false;
}
bool same;
if (!JS::SameValue(cx, args[0], args[1], &same)) {
return false;
}
if (!same) {
UniqueChars bytes0, bytes1;
const char* actual = ToSource(cx, args[0], &bytes0);
const char* expected = ToSource(cx, args[1], &bytes1);
if (args.length() == 2) {
JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
JSSMSG_ASSERT_EQ_FAILED, actual, expected);
} else {
RootedString message(cx, args[2].toString());
UniqueChars bytes2 = QuoteString(cx, message);
if (!bytes2) {
return false;
}
JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
JSSMSG_ASSERT_EQ_FAILED_MSG, actual, expected,
bytes2.get());
}
return false;
}
args.rval().setUndefined();
return true;
}
static JSScript* GetTopScript(JSContext* cx) {
NonBuiltinScriptFrameIter iter(cx);
return iter.done() ? nullptr : iter.script();
}
static bool GetScriptAndPCArgs(JSContext* cx, CallArgs& args,
MutableHandleScript scriptp, int32_t* ip) {
RootedScript script(cx, GetTopScript(cx));
*ip = 0;
if (!args.get(0).isUndefined()) {
HandleValue v = args[0];
unsigned intarg = 0;
if (v.isObject() && JS::GetClass(&v.toObject())->isJSFunction()) {
script = TestingFunctionArgumentToScript(cx, v);
if (!script) {
return false;
}
intarg++;
}
if (!args.get(intarg).isUndefined()) {
if (!JS::ToInt32(cx, args[intarg], ip)) {
return false;
}
if ((uint32_t)*ip >= script->length()) {
JS_ReportErrorASCII(cx, "Invalid PC");
return false;
}
}
}
scriptp.set(script);
return true;
}
static bool LineToPC(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_LINE2PC_USAGE);
return false;
}
RootedScript script(cx, GetTopScript(cx));
int32_t lineArg = 0;
if (args[0].isObject() && args[0].toObject().is<JSFunction>()) {
script = TestingFunctionArgumentToScript(cx, args[0]);
if (!script) {
return false;
}
lineArg++;
}
uint32_t lineno;
if (!ToUint32(cx, args.get(lineArg), &lineno)) {
return false;
}
jsbytecode* pc = LineNumberToPC(script, lineno);
if (!pc) {
return false;
}
args.rval().setInt32(script->pcToOffset(pc));
return true;
}
static bool PCToLine(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedScript script(cx);
int32_t i;
unsigned lineno;
if (!GetScriptAndPCArgs(cx, args, &script, &i)) {
return false;
}
lineno = PCToLineNumber(script, script->offsetToPC(i));
if (!lineno) {
return false;
}
args.rval().setInt32(lineno);
return true;
}
#if defined(DEBUG) || defined(JS_JITSPEW)
static bool Notes(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JSSprinter sprinter(cx);
if (!sprinter.init()) {
return false;
}
for (unsigned i = 0; i < args.length(); i++) {
RootedScript script(cx, TestingFunctionArgumentToScript(cx, args[i]));
if (!script) {
return false;
}
if (!JSScript::dumpSrcNotes(cx, script, &sprinter)) {
return false;
}
}
JSString* str = sprinter.release(cx);
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}
namespace {
struct DisassembleOptionParser {
unsigned argc;
Value* argv;
JSScript::DumpOptions options;
DisassembleOptionParser(unsigned argc, Value* argv)
: argc(argc), argv(argv) {}
bool parse(JSContext* cx) {
options.recursive = false;
/* Read options off early arguments */
while (argc > 0 && argv[0].isString()) {
JSString* str = argv[0].toString();
JSLinearString* linearStr = JS_EnsureLinearString(cx, str);
if (!linearStr) {
return false;
}
if (JS_LinearStringEqualsLiteral(linearStr, "-r")) {
options.recursive = true;
} else {
break;
}
argv++;
argc--;
}
return true;
}
};
} /* anonymous namespace */
static bool DisassembleToSprinter(JSContext* cx, unsigned argc, Value* vp,
StringPrinter* sp) {
CallArgs args = CallArgsFromVp(argc, vp);
DisassembleOptionParser p(args.length(), args.array());
if (!p.parse(cx)) {
return false;
}
if (p.argc == 0) {
/* Without arguments, disassemble the current script. */
RootedScript script(cx, GetTopScript(cx));
if (script) {
JSAutoRealm ar(cx, script);
if (!JSScript::dump(cx, script, p.options, sp)) {
return false;
}
}
} else {
for (unsigned i = 0; i < p.argc; i++) {
RootedFunction fun(cx);
RootedScript script(cx);
RootedValue value(cx, p.argv[i]);
if (value.isObject() && value.toObject().is<ShellModuleObjectWrapper>()) {
auto* module = value.toObject().as<ShellModuleObjectWrapper>().get();
script = module->maybeScript();
if (!script) {
JS_ReportErrorASCII(cx, "module does not have an associated script");
return false;
}
} else {
script = TestingFunctionArgumentToScript(cx, value, fun.address());
if (!script) {
return false;
}
}
if (!JSScript::dump(cx, script, p.options, sp)) {
return false;
}
}
}
return true;
}
static bool DisassembleToString(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JSSprinter sprinter(cx);
if (!sprinter.init()) {
return false;
}
if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) {
return false;
}
JSString* str = sprinter.release(cx);
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}
static bool Disassemble(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!gOutFile->isOpen()) {
JS_ReportErrorASCII(cx, "output file is closed");
return false;
}
Sprinter sprinter(cx);
if (!sprinter.init()) {
return false;
}
if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) {
return false;
}
JS::UniqueChars str = sprinter.release();
if (!str) {
return false;
}
fprintf(gOutFile->fp, "%s\n", str.get());
args.rval().setUndefined();
return true;
}
static bool DisassFile(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!gOutFile->isOpen()) {
JS_ReportErrorASCII(cx, "output file is closed");
return false;
}
/* Support extra options at the start, just like Disassemble. */
DisassembleOptionParser p(args.length(), args.array());
if (!p.parse(cx)) {
return false;
}
if (!p.argc) {
args.rval().setUndefined();
return true;
}
// We should change DisassembleOptionParser to store CallArgs.
Rooted<JSString*> str(
cx, JS::ToString(cx, HandleValue::fromMarkedLocation(&p.argv[0])));
if (!str) {
return false;
}
UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
if (!filename) {
return false;
}
RootedScript script(cx);
{
CompileOptions options(cx);
options.setIntroductionType("js shell disFile")
.setFileAndLine(filename.get(), 1)
.setIsRunOnce(true)
.setNoScriptRval(true)
.setEagerDelazificationStrategy(defaultDelazificationMode);
script = JS::CompileUtf8Path(cx, options, filename.get());
if (!script) {
return false;
}
}
Sprinter sprinter(cx);
if (!sprinter.init()) {
return false;
}
if (!JSScript::dump(cx, script, p.options, &sprinter)) {
return false;
}
JS::UniqueChars chars = sprinter.release();
if (!chars) {
return false;
}
fprintf(gOutFile->fp, "%s\n", chars.get());
args.rval().setUndefined();
return true;
}
static bool DisassWithSrc(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!gOutFile->isOpen()) {
JS_ReportErrorASCII(cx, "output file is closed");
return false;
}
const size_t lineBufLen = 512;
unsigned len, line1, line2, bupline;
char linebuf[lineBufLen];
static const char sep[] = ";-------------------------";
RootedScript script(cx);
for (unsigned i = 0; i < args.length(); i++) {
script = TestingFunctionArgumentToScript(cx, args[i]);
if (!script) {
return false;
}
if (!script->filename()) {
JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
JSSMSG_FILE_SCRIPTS_ONLY);
return false;
}
FILE* file = OpenFile(cx, script->filename(), "rb");
if (!file) {
return false;
}
auto closeFile = MakeScopeExit([file] { fclose(file); });
jsbytecode* pc = script->code();
jsbytecode* end = script->codeEnd();
Sprinter sprinter(cx);
if (!sprinter.init()) {
return false;
}
/* burn the leading lines */
line2 = PCToLineNumber(script, pc);
for (line1 = 0; line1 < line2 - 1; line1++) {
char* tmp = fgets(linebuf, lineBufLen, file);
if (!tmp) {
JS_ReportErrorUTF8(cx, "failed to read %s fully", script->filename());
return false;
}
}
bupline = 0;
while (pc < end) {
line2 = PCToLineNumber(script, pc);
if (line2 < line1) {
if (bupline != line2) {
bupline = line2;
sprinter.printf("%s %3u: BACKUP\n", sep, line2);
}
} else {
if (bupline && line1 == line2) {
sprinter.printf("%s %3u: RESTORE\n", sep, line2);
}
bupline = 0;
while (line1 < line2) {
if (!fgets(linebuf, lineBufLen, file)) {
JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
JSSMSG_UNEXPECTED_EOF, script->filename());
return false;
}
line1++;
sprinter.printf("%s %3u: %s", sep, line1, linebuf);
}
}
len =
Disassemble1(cx, script, pc, script->pcToOffset(pc), true, &sprinter);
if (!len) {
return false;
}
pc += len;
}
JS::UniqueChars str = sprinter.release();
if (!str) {
return false;
}
fprintf(gOutFile->fp, "%s\n", str.get());
}
args.rval().setUndefined();
return true;
}
#endif /* defined(DEBUG) || defined(JS_JITSPEW) */
#ifdef JS_CACHEIR_SPEW
static bool CacheIRHealthReport(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
js::jit::CacheIRHealth cih;
RootedScript script(cx);
// In the case that we are calling this function from the shell and
// the environment variable is not set, AutoSpewChannel automatically
// sets and unsets the proper channel for the duration of spewing
// a health report.
AutoSpewChannel channel(cx, SpewChannel::CacheIRHealthReport, script);
if (!argc) {
// Calling CacheIRHealthReport without any arguments will create health
// reports for all scripts in the zone.
if (jit::JitZone* jitZone = cx->zone()->jitZone()) {
jitZone->forEachJitScript([&](jit::JitScript* jitScript) {
script = jitScript->owningScript();
if (!script->selfHosted()) {
cih.healthReportForScript(cx, script, js::jit::SpewContext::Shell);
}
});
}
} else {
RootedValue value(cx, args.get(0));
if (value.isObject() && value.toObject().is<ShellModuleObjectWrapper>()) {
script =
value.toObject().as<ShellModuleObjectWrapper>().get()->maybeScript();
if (!script) {
JS_ReportErrorASCII(cx, "module does not have an associated script");
return false;
}
} else {
script = TestingFunctionArgumentToScript(cx, args.get(0));
if (!script) {
return false;
}
}
cih.healthReportForScript(cx, script, js::jit::SpewContext::Shell);
}
args.rval().setUndefined();
return true;
}
#endif /* JS_CACHEIR_SPEW */
/* Pretend we can always preserve wrappers for dummy DOM objects. */
static bool DummyPreserveWrapperCallback(JSContext* cx, HandleObject obj) {
return true;
}
static bool DummyHasReleasedWrapperCallback(HandleObject obj) { return true; }
#ifdef FUZZING_JS_FUZZILLI
static bool fuzzilli_hash(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setUndefined();
if (argc != 1) {
return true;
}
uint32_t hash;
JS::Handle<JS::Value> v = args.get(0);
if (v.isInt32()) {
int32_t i = v.toInt32();
hash = FuzzilliHashDouble((double)i);
} else if (v.isDouble()) {
double d = v.toDouble();
d = JS::CanonicalizeNaN(d);
hash = FuzzilliHashDouble(d);
} else if (v.isNull()) {
hash = FuzzilliHashDouble(1.0);
} else if (v.isUndefined()) {
hash = FuzzilliHashDouble(2.0);
} else if (v.isBoolean()) {
hash = FuzzilliHashDouble(3.0 + v.toBoolean());
} else if (v.isBigInt()) {
JS::BigInt* bigInt = v.toBigInt();
hash = FuzzilliHashBigInt(bigInt);
} else if (v.isObject()) {
JSObject& obj = v.toObject();
FuzzilliHashObject(cx, &obj);
return true;
} else {
hash = 0;
}
cx->executionHashInputs += 1;
cx->executionHash = mozilla::RotateLeft(cx->executionHash + hash, 1);
return true;
}
// We have to assume that the fuzzer will be able to call this function e.g. by
// enumerating the properties of the global object and eval'ing them. As such
// this function is implemented in a way that requires passing some magic value
// as first argument (with the idea being that the fuzzer won't be able to
// generate this value) which then also acts as a selector for the operation
// to perform.
static bool Fuzzilli(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedString arg(cx, JS::ToString(cx, args.get(0)));
if (!arg) {
return false;
}
Rooted<JSLinearString*> operation(cx, StringToLinearString(cx, arg));
if (!operation) {
return false;
}
if (StringEqualsAscii(operation, "FUZZILLI_CRASH")) {
int type;
if (!ToInt32(cx, args.get(1), &type)) {
return false;
}
// With this, we can test the various ways the JS shell can crash and make
// sure that Fuzzilli is able to detect all of these failures properly.
switch (type) {
case 0:
*((int*)0x41414141) = 0x1337;
break;
case 1:
MOZ_RELEASE_ASSERT(false);
break;
case 2:
MOZ_ASSERT(false);
break;
case 3:
__asm__("int3");
break;
default:
exit(1);
}
} else if (StringEqualsAscii(operation, "FUZZILLI_PRINT")) {
static FILE* fzliout = fdopen(REPRL_DWFD, "w");
if (!fzliout) {
fprintf(
stderr,
"Fuzzer output channel not available, printing to stdout instead\n");
fzliout = stdout;
}
RootedString str(cx, JS::ToString(cx, args.get(1)));
if (!str) {
return false;
}
UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
if (!bytes) {
return false;
}
fprintf(fzliout, "%s\n", bytes.get());
fflush(fzliout);
} else if (StringEqualsAscii(operation, "FUZZILLI_RANDOM")) {
// This is an entropy source which can be called during fuzzing.
// Its currently used to tests whether Fuzzilli detects non-deterministic
// behavior.
args.rval().setInt32(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
return true;
}
args.rval().setUndefined();
return true;
}
static bool FuzzilliReprlGetAndRun(JSContext* cx) {
size_t scriptSize = 0;
unsigned action;
MOZ_RELEASE_ASSERT(read(REPRL_CRFD, &action, 4) == 4);
if (action == 'cexe') {
MOZ_RELEASE_ASSERT(read(REPRL_CRFD, &scriptSize, 8) == 8);
} else {
fprintf(stderr, "Unknown action: %u\n", action);
_exit(-1);
}
CompileOptions options(cx);
options.setIntroductionType("reprl")
.setFileAndLine("reprl", 1)
.setIsRunOnce(true)
.setNoScriptRval(true)
.setEagerDelazificationStrategy(defaultDelazificationMode);
char* scriptSrc = static_cast<char*>(js_malloc(scriptSize));
char* ptr = scriptSrc;
size_t remaining = scriptSize;
while (remaining > 0) {
ssize_t rv = read(REPRL_DRFD, ptr, remaining);
if (rv <= 0) {
fprintf(stderr, "Failed to load script\n");
_exit(-1);
}
remaining -= rv;
ptr += rv;
}
JS::SourceText<Utf8Unit> srcBuf;
if (!srcBuf.init(cx, scriptSrc, scriptSize,
JS::SourceOwnership::TakeOwnership)) {
return false;
}
RootedScript script(cx, JS::Compile(cx, options, srcBuf));
if (!script) {
return false;
}
if (!JS_ExecuteScript(cx, script)) {
return false;
}
return true;
}
#endif /* FUZZING_JS_FUZZILLI */
static bool FuzzilliUseReprlMode(OptionParser* op) {
#ifdef FUZZING_JS_FUZZILLI
// Check if we should use REPRL mode
bool reprl_mode = op->getBoolOption("reprl");
if (reprl_mode) {
// Check in with parent
char helo[] = "HELO";
if (write(REPRL_CWFD, helo, 4) != 4 || read(REPRL_CRFD, helo, 4) != 4) {
reprl_mode = false;
}
if (memcmp(helo, "HELO", 4) != 0) {
fprintf(stderr, "Invalid response from parent\n");
_exit(-1);
}
}
return reprl_mode;
#else
return false;
#endif /* FUZZING_JS_FUZZILLI */
}
static bool Crash(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
MOZ_CRASH("forced crash");
}
RootedString message(cx, JS::ToString(cx, args[0]));
if (!message) {
return false;
}
UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, message);
if (!utf8chars) {
return false;
}
if (args.get(1).isObject()) {
RootedValue v(cx);
RootedObject opts(cx, &args[1].toObject());
if (!JS_GetProperty(cx, opts, "suppress_minidump", &v)) {
return false;
}
if (v.isBoolean() && v.toBoolean()) {
js::NoteIntentionalCrash();
}
}
#ifndef DEBUG
MOZ_ReportCrash(utf8chars.get(), __FILE__, __LINE__);
#endif
MOZ_CRASH_UNSAFE(utf8chars.get());
}
static bool GetSLX(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedScript script(cx);
script = TestingFunctionArgumentToScript(cx, args.get(0));
if (!script) {
return false;
}
args.rval().setInt32(GetScriptLineExtent(script));
return true;
}
static bool ThrowError(JSContext* cx, unsigned argc, Value* vp) {
JS_ReportErrorASCII(cx, "This is an error");
return false;
}
static bool CopyErrorReportToObject(JSContext* cx, JSErrorReport* report,
HandleObject obj) {
RootedString nameStr(cx);
if (report->exnType == JSEXN_WARN) {
nameStr = JS_NewStringCopyZ(cx, "Warning");
if (!nameStr) {
return false;
}
} else {
nameStr = GetErrorTypeName(cx, report->exnType);
// GetErrorTypeName doesn't set an exception, but
// can fail for InternalError or non-error objects.
if (!nameStr) {
nameStr = cx->runtime()->emptyString;
}
}
RootedValue nameVal(cx, StringValue(nameStr));
if (!DefineDataProperty(cx, obj, cx->names().name, nameVal)) {
return false;
}
RootedString messageStr(cx, report->newMessageString(cx));
if (!messageStr) {
return false;
}
RootedValue messageVal(cx, StringValue(messageStr));
if (!DefineDataProperty(cx, obj, cx->names().message, messageVal)) {
return false;
}
RootedValue linenoVal(cx, Int32Value(report->lineno));
if (!DefineDataProperty(cx, obj, cx->names().lineNumber, linenoVal)) {
return false;
}
RootedValue columnVal(cx, Int32Value(report->column.oneOriginValue()));
if (!DefineDataProperty(cx, obj, cx->names().columnNumber, columnVal)) {
return false;
}
RootedObject notesArray(cx, CreateErrorNotesArray(cx, report));
if (!notesArray) {
return false;
}
RootedValue notesArrayVal(cx, ObjectValue(*notesArray));
return DefineDataProperty(cx, obj, cx->names().notes, notesArrayVal);
}
static bool CreateErrorReport(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// We don't have a stack here, so just initialize with null.
JS::ExceptionStack exnStack(cx, args.get(0), nullptr);
JS::ErrorReportBuilder report(cx);
if (!report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) {
return false;
}
MOZ_ASSERT(!report.report()->isWarning());
RootedObject obj(cx, JS_NewPlainObject(cx));
if (!obj) {
return false;
}
RootedString toString(cx, NewStringCopyUTF8Z(cx, report.toStringResult()));
if (!toString) {
return false;
}
if (!JS_DefineProperty(cx, obj, "toStringResult", toString,
JSPROP_ENUMERATE)) {
return false;
}
if (!CopyErrorReportToObject(cx, report.report(), obj)) {
return false;
}
args.rval().setObject(*obj);
return true;
}
#define LAZY_STANDARD_CLASSES
/* A class for easily testing the inner/outer object callbacks. */
struct ComplexObject {
bool isInner;
bool frozen;
JSObject* inner;
JSObject* outer;
};
static bool sandbox_enumerate(JSContext* cx, JS::HandleObject obj,
JS::MutableHandleIdVector properties,
bool enumerableOnly) {
RootedValue v(cx);
if (!JS_GetProperty(cx, obj, "lazy", &v)) {
return false;
}
if (!ToBoolean(v)) {
return true;
}
return JS_NewEnumerateStandardClasses(cx, obj, properties, enumerableOnly);
}
static bool sandbox_resolve(JSContext* cx, HandleObject obj, HandleId id,
bool* resolvedp) {
RootedValue v(cx);
if (!JS_GetProperty(cx, obj, "lazy", &v)) {
return false;
}
if (ToBoolean(v)) {
return JS_ResolveStandardClass(cx, obj, id, resolvedp);
}
return true;
}
static const JSClassOps sandbox_classOps = {
nullptr, // addProperty
nullptr, // delProperty
nullptr, // enumerate
sandbox_enumerate, // newEnumerate
sandbox_resolve, // resolve
nullptr, // mayResolve
nullptr, // finalize
nullptr, // call
nullptr, // construct
JS_GlobalObjectTraceHook, // trace
};
static const JSClass sandbox_class = {
"sandbox",
JSCLASS_GLOBAL_FLAGS,
&sandbox_classOps,
};
static void SetStandardRealmOptions(JS::RealmOptions& options) {
options.creationOptions()
.setSharedMemoryAndAtomicsEnabled(enableSharedMemory)
.setCoopAndCoepEnabled(false)
.setToSourceEnabled(enableToSource);
}
[[nodiscard]] static bool CheckRealmOptions(JSContext* cx,
JS::RealmOptions& options,
JSPrincipals* principals) {
JS::RealmCreationOptions& creationOptions = options.creationOptions();
if (creationOptions.compartmentSpecifier() !=
JS::CompartmentSpecifier::ExistingCompartment) {
return true;
}
JS::Compartment* comp = creationOptions.compartment();
// All realms in a compartment must be either system or non-system.
bool isSystem =
principals && principals == cx->runtime()->trustedPrincipals();
if (isSystem != IsSystemCompartment(comp)) {
JS_ReportErrorASCII(cx,
"Cannot create system and non-system realms in the "
"same compartment");
return false;
}
// Debugger visibility is per-compartment, not per-realm, so make sure the
// requested visibility matches the existing compartment's.
if (creationOptions.invisibleToDebugger() != comp->invisibleToDebugger()) {
JS_ReportErrorASCII(cx,
"All the realms in a compartment must have "
"the same debugger visibility");
return false;
}
return true;
}
static JSObject* NewSandbox(JSContext* cx, bool lazy) {
JS::RealmOptions options;
SetStandardRealmOptions(options);
if (defaultToSameCompartment) {
options.creationOptions().setExistingCompartment(cx->global());
} else {
options.creationOptions().setNewCompartmentAndZone();
}
JSPrincipals* principals = nullptr;
if (!CheckRealmOptions(cx, options, principals)) {
return nullptr;
}
RootedObject obj(cx,
JS_NewGlobalObject(cx, &sandbox_class, principals,
JS::DontFireOnNewGlobalHook, options));
if (!obj) {
return nullptr;
}
{
JSAutoRealm ar(cx, obj);
if (!lazy && !JS::InitRealmStandardClasses(cx)) {
return nullptr;
}
RootedValue value(cx, BooleanValue(lazy));
if (!JS_DefineProperty(cx, obj, "lazy", value,
JSPROP_PERMANENT | JSPROP_READONLY)) {
return nullptr;
}
JS_FireOnNewGlobalObject(cx, obj);
}
if (!cx->compartment()->wrap(cx, &obj)) {
return nullptr;
}
return obj;
}
static bool EvalInContext(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "evalcx", 1)) {
return false;
}
RootedString str(cx, ToString(cx, args[0]));
if (!str) {
return false;
}
RootedObject sobj(cx);
if (args.hasDefined(1)) {
sobj = ToObject(cx, args[1]);
if (!sobj) {
return false;
}
}
AutoStableStringChars strChars(cx);
if (!strChars.initTwoByte(cx, str)) {
return false;
}
mozilla::Range<const char16_t> chars = strChars.twoByteRange();
size_t srclen = chars.length();
const char16_t* src = chars.begin().get();
bool lazy = false;
if (srclen == 4) {
if (src[0] == 'l' && src[1] == 'a' && src[2] == 'z' && src[3] == 'y') {
lazy = true;
srclen = 0;
}
}
if (!sobj) {
sobj = NewSandbox(cx, lazy);
if (!sobj) {
return false;
}
}
if (srclen == 0) {
args.rval().setObject(*sobj);
return true;
}
JS::AutoFilename filename;
uint32_t lineno;
DescribeScriptedCaller(&filename, cx, &lineno);
{
sobj = UncheckedUnwrap(sobj, true);
JSAutoRealm ar(cx, sobj);
sobj = ToWindowIfWindowProxy(sobj);
if (!JS_IsGlobalObject(sobj)) {
JS_ReportErrorASCII(cx, "Invalid scope argument to evalcx");
return false;
}
JS::CompileOptions opts(cx);
opts.setFileAndLine(filename.get(), lineno)
.setEagerDelazificationStrategy(defaultDelazificationMode);
JS::SourceText<char16_t> srcBuf;
if (!srcBuf.init(cx, src, srclen, JS::SourceOwnership::Borrowed) ||
!JS::Evaluate(cx, opts, srcBuf, args.rval())) {
return false;
}
}
if (!cx->compartment()->wrap(cx, args.rval())) {
return false;
}
return true;
}
static bool EnsureGeckoProfilingStackInstalled(JSContext* cx,
ShellContext* sc) {
if (cx->geckoProfiler().infraInstalled()) {
MOZ_ASSERT(sc->geckoProfilingStack);
return true;
}
MOZ_ASSERT(!sc->geckoProfilingStack);
sc->geckoProfilingStack = MakeUnique<ProfilingStack>();
if (!sc->geckoProfilingStack) {
JS_ReportOutOfMemory(cx);
return false;
}
SetContextProfilingStack(cx, sc->geckoProfilingStack.get());
return true;
}
struct WorkerInput {
JSRuntime* parentRuntime;
UniqueTwoByteChars chars;
size_t length;
WorkerInput(JSRuntime* parentRuntime, UniqueTwoByteChars chars, size_t length)
: parentRuntime(parentRuntime), chars(std::move(chars)), length(length) {}
};
static void DestroyShellCompartmentPrivate(JS::GCContext* gcx,
JS::Compartment* compartment) {
auto priv = static_cast<ShellCompartmentPrivate*>(
JS_GetCompartmentPrivate(compartment));
js_delete(priv);
}
static void SetWorkerContextOptions(JSContext* cx);
static bool ShellBuildId(JS::BuildIdCharVector* buildId);
static constexpr size_t gWorkerStackSize = 2 * 128 * sizeof(size_t) * 1024;
static void WorkerMain(UniquePtr<WorkerInput> input) {
MOZ_ASSERT(input->parentRuntime);
JSContext* cx = JS_NewContext(8L * 1024L * 1024L, input->parentRuntime);
if (!cx) {
return;
}
auto destroyContext = MakeScopeExit([cx] { JS_DestroyContext(cx); });
UniquePtr<ShellContext> sc =
MakeUnique<ShellContext>(cx, ShellContext::Worker);
if (!sc || !sc->registerWithCx(cx)) {
return;
}
if (!JS::InitSelfHostedCode(cx)) {
return;
}
EnvironmentPreparer environmentPreparer(cx);
do {
JS::RealmOptions realmOptions;
SetStandardRealmOptions(realmOptions);
RootedObject global(cx, NewGlobalObject(cx, realmOptions, nullptr,
ShellGlobalKind::WindowProxy,
/* immutablePrototype = */ true));
if (!global) {
break;
}
JSAutoRealm ar(cx, global);
JS::ConstUTF8CharsZ path(processWideModuleLoadPath.get(),
strlen(processWideModuleLoadPath.get()));
RootedString moduleLoadPath(cx, JS_NewStringCopyUTF8Z(cx, path));
if (!moduleLoadPath) {
return;
}
sc->moduleLoader = js::MakeUnique<ModuleLoader>();
if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) {
return;
}
JS::CompileOptions options(cx);
options.setFileAndLine("<string>", 1)
.setIsRunOnce(true)
.setEagerDelazificationStrategy(defaultDelazificationMode);
AutoReportException are(cx);
JS::SourceText<char16_t> srcBuf;
if (!srcBuf.init(cx, input->chars.get(), input->length,
JS::SourceOwnership::Borrowed)) {
break;
}
RootedScript script(cx, JS::Compile(cx, options, srcBuf));
if (!script) {
break;
}
RootedValue result(cx);
JS_ExecuteScript(cx, script, &result);
} while (0);
KillWatchdog(cx);
}
// Workers can spawn other workers, so we need a lock to access workerThreads.
static Mutex* workerThreadsLock = nullptr;
MOZ_RUNINIT static Vector<UniquePtr<js::Thread>, 0, SystemAllocPolicy>
workerThreads;
class MOZ_RAII AutoLockWorkerThreads : public LockGuard<Mutex> {
using Base = LockGuard<Mutex>;
public:
AutoLockWorkerThreads() : Base(*workerThreadsLock) {
MOZ_ASSERT(workerThreadsLock);
}
};
static bool EvalInWorker(JSContext* cx, unsigned argc, Value* vp) {
if (!CanUseExtraThreads()) {
JS_ReportErrorASCII(cx, "Can't create threads with --no-threads");
return false;
}
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.get(0).isString()) {
JS_ReportErrorASCII(cx, "Invalid arguments");
return false;
}
#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
if (cx->runningOOMTest) {
JS_ReportErrorASCII(
cx, "Can't create threads while running simulated OOM test");
return false;
}
#endif
if (!args[0].toString()->ensureLinear(cx)) {
return false;
}
if (!workerThreadsLock) {
workerThreadsLock = js_new<Mutex>(mutexid::ShellWorkerThreads);
if (!workerThreadsLock) {
ReportOutOfMemory(cx);
return false;
}
}
JSLinearString* str = &args[0].toString()->asLinear();
UniqueTwoByteChars chars(js_pod_malloc<char16_t>(str->length()));
if (!chars) {
ReportOutOfMemory(cx);
return false;
}
CopyChars(chars.get(), *str);
auto input = js::MakeUnique<WorkerInput>(JS_GetParentRuntime(cx),
std::move(chars), str->length());
if (!input) {
ReportOutOfMemory(cx);
return false;
}
UniquePtr<Thread> thread;
{
AutoEnterOOMUnsafeRegion oomUnsafe;
thread = js::MakeUnique<Thread>(
Thread::Options().setStackSize(gWorkerStackSize + 512 * 1024));
if (!thread || !thread->init(WorkerMain, std::move(input))) {
oomUnsafe.crash("EvalInWorker");
}
}
AutoLockWorkerThreads alwt;
if (!workerThreads.append(std::move(thread))) {
ReportOutOfMemory(cx);
thread->join();
return false;
}
args.rval().setUndefined();
return true;
}
static bool ShapeOf(JSContext* cx, unsigned argc, JS::Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.get(0).isObject()) {
JS_ReportErrorASCII(cx, "shapeOf: object expected");
return false;
}
JSObject* obj = &args[0].toObject();
args.rval().set(JS_NumberValue(double(uintptr_t(obj->shape()) >> 3)));
return true;
}
static bool Sleep_fn(JSContext* cx, unsigned argc, Value* vp) {
ShellContext* sc = GetShellContext(cx);
CallArgs args = CallArgsFromVp(argc, vp);
TimeDuration duration = TimeDuration::FromSeconds(0.0);
if (args.length() > 0) {
double t_secs;
if (!ToNumber(cx, args[0], &t_secs)) {
return false;
}
if (std::isnan(t_secs)) {
JS_ReportErrorASCII(cx, "sleep interval is not a number");
return false;
}
duration = TimeDuration::FromSeconds(std::max(0.0, t_secs));
const TimeDuration MAX_TIMEOUT_INTERVAL =
TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS);
if (duration > MAX_TIMEOUT_INTERVAL) {
JS_ReportErrorASCII(cx, "Excessive sleep interval");
return false;
}
}
{
LockGuard<Mutex> guard(sc->watchdogLock);
TimeStamp toWakeup = TimeStamp::Now() + duration;
for (;;) {
sc->sleepWakeup.wait_for(guard, duration);
if (sc->serviceInterrupt) {
break;
}
auto now = TimeStamp::Now();
if (now >= toWakeup) {
break;
}
duration = toWakeup - now;
}
}
if (sc->serviceInterrupt) {
JS::ReportUncatchableException(cx);
return false;
}
args.rval().setUndefined();
return true;
}
static void KillWatchdog(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
Maybe<Thread> thread;
{
LockGuard<Mutex> guard(sc->watchdogLock);
std::swap(sc->watchdogThread, thread);
if (thread) {
// The watchdog thread becoming Nothing is its signal to exit.
sc->watchdogWakeup.notify_one();
}
}
if (thread) {
thread->join();
}
MOZ_ASSERT(!sc->watchdogThread);
}
static void WatchdogMain(JSContext* cx) {
ThisThread::SetName("JS Watchdog");
ShellContext* sc = GetShellContext(cx);
{
LockGuard<Mutex> guard(sc->watchdogLock);
while (sc->watchdogThread) {
auto now = TimeStamp::Now();
if (sc->watchdogTimeout && now >= sc->watchdogTimeout.value()) {
/*
* The timeout has just expired. Request an interrupt callback
* outside the lock.
*/
sc->watchdogTimeout = Nothing();
{
UnlockGuard unlock(guard);
CancelExecution(cx);
}
/* Wake up any threads doing sleep. */
sc->sleepWakeup.notify_all();
} else {
if (sc->watchdogTimeout) {
/*
* Time hasn't expired yet. Simulate an interrupt callback
* which doesn't abort execution.
*/
JS_RequestInterruptCallback(cx);
}
TimeDuration sleepDuration = sc->watchdogTimeout
? TimeDuration::FromSeconds(0.1)
: TimeDuration::Forever();
sc->watchdogWakeup.wait_for(guard, sleepDuration);
}
}
}
}
static bool ScheduleWatchdog(JSContext* cx, double t) {
ShellContext* sc = GetShellContext(cx);
if (t <= 0) {
LockGuard<Mutex> guard(sc->watchdogLock);
sc->watchdogTimeout = Nothing();
return true;
}
#ifdef __wasi__
return false;
#endif
auto interval = TimeDuration::FromSeconds(t);
auto timeout = TimeStamp::Now() + interval;
LockGuard<Mutex> guard(sc->watchdogLock);
if (!sc->watchdogThread) {
MOZ_ASSERT(!sc->watchdogTimeout);
sc->watchdogThread.emplace();
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!sc->watchdogThread->init(WatchdogMain, cx)) {
oomUnsafe.crash("watchdogThread.init");
}
} else if (!sc->watchdogTimeout || timeout < sc->watchdogTimeout.value()) {
sc->watchdogWakeup.notify_one();
}
sc->watchdogTimeout = Some(timeout);
return true;
}
static void KillWorkerThreads(JSContext* cx) {
MOZ_ASSERT_IF(!CanUseExtraThreads(), workerThreads.empty());
if (!workerThreadsLock) {
MOZ_ASSERT(workerThreads.empty());
return;
}
while (true) {
// We need to leave the AutoLockWorkerThreads scope before we call
// js::Thread::join, to avoid deadlocks when AutoLockWorkerThreads is
// used by the worker thread.
UniquePtr<Thread> thread;
{
AutoLockWorkerThreads alwt;
if (workerThreads.empty()) {
break;
}
thread = std::move(workerThreads.back());
workerThreads.popBack();
}
thread->join();
}
workerThreads.clearAndFree();
js_delete(workerThreadsLock);
workerThreadsLock = nullptr;
}
static void CancelExecution(JSContext* cx) {
ShellContext* sc = GetShellContext(cx);
sc->serviceInterrupt = true;
JS_RequestInterruptCallback(cx);
}
static bool SetTimeoutValue(JSContext* cx, double t) {
if (std::isnan(t)) {
JS_ReportErrorASCII(cx, "timeout is not a number");
return false;
}
const TimeDuration MAX_TIMEOUT_INTERVAL =
TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS);
if (TimeDuration::FromSeconds(t) > MAX_TIMEOUT_INTERVAL) {
JS_ReportErrorASCII(cx, "Excessive timeout value");
return false;
}
GetShellContext(cx)->timeoutInterval = t;
if (!ScheduleWatchdog(cx, t)) {
JS_ReportErrorASCII(cx, "Failed to create the watchdog");
return false;
}
return true;
}
static bool Timeout(JSContext* cx, unsigned argc, Value* vp) {
ShellContext* sc = GetShellContext(cx);
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
args.rval().setNumber(sc->timeoutInterval);
return true;
}
if (args.length() > 2) {
JS_ReportErrorASCII(cx, "Wrong number of arguments");
return false;
}
double t;
if (!ToNumber(cx, args[0], &t)) {
return false;
}
if (args.length() > 1) {
RootedValue value(cx, args[1]);
if (!value.isObject() || !value.toObject().is<JSFunction>()) {
JS_ReportErrorASCII(cx, "Second argument must be a timeout function");
return false;
}
sc->interruptFunc = value;
sc->haveInterruptFunc = true;
}
args.rval().setUndefined();
return SetTimeoutValue(cx, t);
}
static bool InterruptIf(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorASCII(cx, "Wrong number of arguments");
return false;
}
if (ToBoolean(args[0])) {
GetShellContext(cx)->serviceInterrupt = true;
JS_RequestInterruptCallback(cx);
}
args.rval().setUndefined();
return true;
}
static bool InvokeInterruptCallbackWrapper(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorASCII(cx, "Wrong number of arguments");
return false;
}
GetShellContext(cx)->serviceInterrupt = true;
JS_RequestInterruptCallback(cx);
bool interruptRv = CheckForInterrupt(cx);
// The interrupt handler could have set a pending exception. Since we call
// back into JS, don't have it see the pending exception. If we have an
// uncatchable exception that's not propagating a debug mode forced
// return, return.
if (!interruptRv && !cx->isExceptionPending() &&
!cx->isPropagatingForcedReturn()) {
return false;
}
JS::AutoSaveExceptionState savedExc(cx);
FixedInvokeArgs<1> iargs(cx);
iargs[0].setBoolean(interruptRv);
RootedValue rv(cx);
if (!js::Call(cx, args[0], UndefinedHandleValue, iargs, &rv)) {
return false;
}
args.rval().setUndefined();
return interruptRv;
}
static bool SetInterruptCallback(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportErrorASCII(cx, "Wrong number of arguments");
return false;
}
RootedValue value(cx, args[0]);
if (!value.isObject() || !value.toObject().is<JSFunction>()) {
JS_ReportErrorASCII(cx, "Argument must be a function");
return false;
}
GetShellContext(cx)->interruptFunc = value;
GetShellContext(cx)->haveInterruptFunc = true;
args.rval().setUndefined();
return true;
}
#ifdef DEBUG
// var s0 = "A".repeat(10*1024);
// interruptRegexp(/a(bc|bd)/, s0);
// first arg is regexp
// second arg is string
static bool InterruptRegexp(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
ShellContext* sc = GetShellContext(cx);
RootedObject callee(cx, &args.callee());
if (args.length() != 2) {
ReportUsageErrorASCII(cx, callee, "Wrong number of arguments.");
return false;
}
if (!(args[0].isObject() && args[0].toObject().is<RegExpObject>())) {
ReportUsageErrorASCII(cx, callee,
"First argument must be a regular expression.");
return false;
}
if (!args[1].isString()) {
ReportUsageErrorASCII(cx, callee, "Second argument must be a String.");
return false;
}
// Set interrupt flags
sc->serviceInterrupt = true;
js::irregexp::IsolateSetShouldSimulateInterrupt(cx->isolate);
RootedObject regexp(cx, &args[0].toObject());
RootedString string(cx, args[1].toString());
int32_t lastIndex = 0;
return js::RegExpMatcherRaw(cx, regexp, string, lastIndex, nullptr,
args.rval());
}
#endif
static bool CheckRegExpSyntax(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject callee(cx, &args.callee());
if (args.length() != 1) {
ReportUsageErrorASCII(cx, callee, "Wrong number of arguments.");
return false;
}
if (!args[0].isString()) {
ReportUsageErrorASCII(cx, callee, "First argument must be a string.");
return false;
}
RootedString string(cx, args[0].toString());
AutoStableStringChars stableChars(cx);
if (!stableChars.initTwoByte(cx, string)) {
return false;
}
const char16_t* chars = stableChars.twoByteRange().begin().get();
size_t length = string->length();
Rooted<JS::Value> error(cx);
if (!JS::CheckRegExpSyntax(cx, chars, length, JS::RegExpFlag::NoFlags,
&error)) {
return false;
}
args.rval().set(error);
return true;
}
static bool IsPrefAvailable(const char* pref) {
if (!fuzzingSafe) {
// All prefs in fuzzing unsafe mode are enabled.
return true;
}
#define WASM_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, FLAG_PRED, \
FLAG_FORCE_ON, FLAG_FUZZ_ON, PREF) \
if constexpr (!FLAG_FUZZ_ON) { \
if (strcmp("wasm_" #PREF, pref) == 0) { \
return false; \
} \
}
JS_FOR_WASM_FEATURES(WASM_FEATURE)
#undef WASM_FEATURE
return true;
}
template <typename T>
static bool ParsePrefValue(const char* name, const char* val, T* result) {
if constexpr (std::is_same_v<T, bool>) {
if (strcmp(val, "true") == 0) {
*result = true;
return true;
}
if (strcmp(val, "false") == 0) {
*result = false;
return true;
}
fprintf(stderr, "Invalid value for boolean pref %s: %s\n", name, val);
return false;
} else {
static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, uint32_t>);
char* end;
long v = strtol(val, &end, 10);
if (end != val + strlen(val) || static_cast<long>(static_cast<T>(v)) != v) {
fprintf(stderr, "Invalid value for integer pref %s: %s\n", name, val);
return false;
}
*result = static_cast<T>(v);
return true;
}
}
static bool SetPrefToTrueForBool(const char* name) {
// Search for a matching pref and try to set it to a default value for the
// type.
#define CHECK_PREF(NAME, CPP_NAME, TYPE, SETTER, IS_STARTUP_PREF) \
if (strcmp(name, NAME) == 0) { \
if constexpr (std::is_same_v<TYPE, bool>) { \
JS::Prefs::SETTER(true); \
return true; \
} else { \
fprintf(stderr, "Pref %s must have a value specified.\n", name); \
return false; \
} \
}
FOR_EACH_JS_PREF(CHECK_PREF)
#undef CHECK_PREF
// Nothing matched. If --fuzzing-safe is used, return true after printing a
// message, to continue execution without breaking fuzzing when a pref is
// removed.
if (fuzzingSafe) {
fprintf(stderr, "Warning: Ignoring unknown pref name: %s\n", name);
return true;
}
fprintf(stderr, "Invalid pref name: %s\n", name);
return false;
}
template <typename T>
static bool ConvertPrefValue(JSContext* cx, HandleValue val, T* result) {
if constexpr (std::is_same_v<T, bool>) {
*result = ToBoolean(val);
return true;
} else if constexpr (std::is_same_v<T, int32_t>) {
return ToInt32(cx, val, result);
} else {
static_assert(std::is_same_v<T, uint32_t>);
return ToUint32(cx, val, result);
}
}
static bool SetPrefValue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "setPrefValue", 2)) {
return false;
}
if (!args[0].isString()) {
JS_ReportErrorASCII(cx, "expected string argument");
return false;
}
Rooted<JSLinearString*> name(cx, args[0].toString()->ensureLinear(cx));
if (!name) {
return false;
}
// Search for a matching pref and try to set it to the provided value.
#define CHECK_PREF(NAME, CPP_NAME, TYPE, SETTER, IS_STARTUP_PREF) \
if (IsPrefAvailable(NAME) && StringEqualsAscii(name, NAME)) { \
if (IS_STARTUP_PREF) { \
JS_ReportErrorASCII(cx, "%s is a startup pref and can't be set", NAME); \
return false; \
} \
TYPE v; \
if (!ConvertPrefValue<TYPE>(cx, args[1], &v)) { \
return false; \
} \
JS::Prefs::SETTER(v); \
args.rval().setUndefined(); \
return true; \
}