-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathjsvm.cc
More file actions
358 lines (321 loc) · 13.9 KB
/
Copy pathjsvm.cc
File metadata and controls
358 lines (321 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
// jsvm - a minimal V8 shell.
//
// Reads one script from a file or stdin, evaluates it, prints the completion
// value, exits. Enforces a wall-clock timeout and a heap ceiling. Optionally
// consumes a build-time startup snapshot and a persistent code cache.
//
// Deliberately absent: module loader, timers, fetch, fs, process. Add them in
// bindings.cc and rebuild the snapshot.
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"
#include "src/bindings.h"
#ifdef JSVM_EMBEDDED_SNAPSHOT
// Defined in snapshot_blob.S via .incbin. Assembly symbols are unmangled, so
// these must have C linkage.
extern "C" const unsigned char kJsvmSnapshotData[];
extern "C" const unsigned int kJsvmSnapshotSize;
#endif
namespace {
struct Options {
int timeout_ms = 5000;
size_t heap_mb = 128;
std::string script_path; // empty => read stdin
std::string cache_path; // empty => no code cache
// Script origin reported to V8. Empty => derive from script_path, else
// "<stdin>". Worth overriding because the origin is not cosmetic: V8 writes
// it into Error().stack as the resource name, so a script that reads its own
// stack consumes the origin as input. Pinning it makes that input a constant
// instead of an accident of how the script was invoked.
std::string origin;
bool quiet = false;
};
// Terminates execution once the deadline passes. TerminateExecution is one of
// the few V8 entry points that is safe to call from another thread.
class Watchdog {
public:
Watchdog(v8::Isolate* isolate, int timeout_ms) {
if (timeout_ms <= 0) return;
thread_ = std::thread([this, isolate, timeout_ms] {
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeout_ms);
while (!done_.load(std::memory_order_acquire)) {
if (std::chrono::steady_clock::now() >= deadline) {
fired_.store(true, std::memory_order_release);
isolate->TerminateExecution();
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
});
}
~Watchdog() {
done_.store(true, std::memory_order_release);
if (thread_.joinable()) thread_.join();
}
bool fired() const { return fired_.load(std::memory_order_acquire); }
private:
std::atomic<bool> done_{false};
std::atomic<bool> fired_{false};
std::thread thread_;
};
// NO AddNearHeapLimitCallback here, deliberately.
//
// At V8 14.0.126 with this args.gn, any near-heap-limit callback crashes the
// process with SIGILL (exit 132) and no output. The trap is a ud1 inside
// v8::internal::Heap::InvokeNearHeapLimitCallback itself, reached via
// CollectGarbage -> PerformHeapLimitCheck, and it fires even when the callback
// body does nothing but `return current_limit * 2` -- so it is not about the
// limit value, TerminateExecution, or RemoveNearHeapLimitCallback, all three of
// which were tested and ruled out. Whether it is a V8 bug or an interaction
// with one of the feature-strip flags was not determined.
//
// The ceiling itself still applies: constraints.ConfigureDefaultsFromHeapSize
// bounds the old generation, and V8 prints its own "Last few GCs" report on
// exhaustion. It then aborts without consulting the OOM handler installed in
// main(), so exit 125 is NOT produced -- heap exhaustion shows up as a crash
// with a legible V8 report on stderr. Callers must not rely on 125.
// Compiles source, consuming a code cache if one is present and still valid.
v8::MaybeLocal<v8::Script> CompileScript(v8::Local<v8::Context> context,
const std::string& source,
const std::string& origin_name,
const std::string& cache_path,
bool* cache_used) {
v8::Isolate* isolate = context->GetIsolate();
// VERSION RISK: ScriptOrigin dropped its leading Isolate* parameter in
// V8 ~10.x. If this fails to compile, add `isolate,` as the first argument.
v8::ScriptOrigin origin(jsvm::Str(isolate, origin_name));
std::string cached_bytes;
v8::ScriptCompiler::CachedData* cached = nullptr;
if (!cache_path.empty() && jsvm::ReadFile(cache_path, &cached_bytes) &&
!cached_bytes.empty()) {
// Source ends up owning this; do not free it here.
cached = new v8::ScriptCompiler::CachedData(
reinterpret_cast<const uint8_t*>(cached_bytes.data()),
static_cast<int>(cached_bytes.size()),
v8::ScriptCompiler::CachedData::BufferNotOwned);
}
v8::ScriptCompiler::Source script_source(jsvm::Str(isolate, source), origin,
cached);
const auto options = cached ? v8::ScriptCompiler::kConsumeCodeCache
: v8::ScriptCompiler::kNoCompileOptions;
v8::MaybeLocal<v8::Script> script =
v8::ScriptCompiler::Compile(context, &script_source, options);
// A stale cache is not an error: V8 rejects it and compiles from source.
*cache_used = cached != nullptr && !cached->rejected;
return script;
}
void SaveCodeCache(v8::Local<v8::Script> script, const std::string& path) {
if (path.empty()) return;
std::unique_ptr<v8::ScriptCompiler::CachedData> data(
v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
if (!data) return;
jsvm::WriteFile(path, reinterpret_cast<const char*>(data->data),
static_cast<size_t>(data->length));
}
void Usage() {
fprintf(stderr,
"usage: jsvm [options] [script.js]\n"
" reads stdin when no script is given\n"
"\n"
" --timeout=MS wall-clock limit, 0 disables (default 5000)\n"
" --heap=MB old-space ceiling (default 128)\n"
" --cache=PATH read/write a persistent code cache\n"
" --origin=NAME script origin V8 reports in Error().stack\n"
" (default: the script path, or <stdin>)\n"
" --quiet do not print the completion value\n"
" --v8=FLAG pass FLAG through to V8, repeatable\n"
" e.g. --v8=--max-semi-space-size=32\n");
}
} // namespace
int main(int argc, char* argv[]) {
Options opt;
std::vector<std::string> v8_flags;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg.rfind("--timeout=", 0) == 0) {
opt.timeout_ms = std::atoi(arg.c_str() + 10);
} else if (arg.rfind("--heap=", 0) == 0) {
opt.heap_mb = std::strtoul(arg.c_str() + 7, nullptr, 10);
} else if (arg.rfind("--cache=", 0) == 0) {
opt.cache_path = arg.substr(8);
} else if (arg.rfind("--origin=", 0) == 0) {
opt.origin = arg.substr(9);
} else if (arg.rfind("--v8=", 0) == 0) {
v8_flags.push_back(arg.substr(5));
} else if (arg == "--quiet") {
opt.quiet = true;
} else if (arg == "-h" || arg == "--help") {
Usage();
return 0;
} else if (arg.rfind("--", 0) == 0) {
fprintf(stderr, "jsvm: unknown option %s\n", arg.c_str());
return 2;
} else {
opt.script_path = arg;
}
}
// All flag setting must precede V8::Initialize().
//
// --single-threaded drops the background compile and concurrent-marking
// threads. For sub-100ms scripts that is a net win on startup; for anything
// long-running, remove it (and switch to NewDefaultPlatform below).
// Defined in bindings.cc and applied identically by mksnapshot_tool: the
// snapshot will not deserialize if the two flag sets disagree.
v8::V8::SetFlagsFromString(jsvm::kV8Flags);
// NOTE: a --v8= flag that V8 folds into its snapshot hash will likewise make
// the embedded blob unusable. If a passthrough flag makes startup fail, that
// is the first thing to suspect.
for (const std::string& flag : v8_flags) {
v8::V8::SetFlagsFromString(flag.c_str());
}
std::string source;
if (opt.script_path.empty()) {
source = jsvm::ReadStdin();
} else if (!jsvm::ReadFile(opt.script_path, &source)) {
fprintf(stderr, "jsvm: cannot read %s\n", opt.script_path.c_str());
return 2;
}
if (source.empty()) {
fprintf(stderr, "jsvm: empty script\n");
return 2;
}
// Must match the --single-threaded flag above.
std::unique_ptr<v8::Platform> platform =
v8::platform::NewSingleThreadedDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
v8::Isolate::CreateParams params;
params.array_buffer_allocator =
v8::ArrayBuffer::Allocator::NewDefaultAllocator();
params.constraints.ConfigureDefaultsFromHeapSize(
2u << 20, static_cast<size_t>(opt.heap_mb) << 20);
#ifdef JSVM_EMBEDDED_SNAPSHOT
v8::StartupData blob{reinterpret_cast<const char*>(kJsvmSnapshotData),
static_cast<int>(kJsvmSnapshotSize)};
params.snapshot_blob = &blob;
params.external_references = jsvm::kExternalReferences;
#endif
int exit_code = 0;
bool snapshot_ok = true;
v8::Isolate* isolate = v8::Isolate::New(params);
{
v8::Isolate::Scope isolate_scope(isolate);
// Without these, a V8 CHECK failure is a bare SIGILL with nothing on stdout
// or stderr -- indistinguishable from a miscompiled binary. V8 calls them
// immediately before aborting.
isolate->SetFatalErrorHandler([](const char* location, const char* message) {
fprintf(stderr, "jsvm: V8 fatal error at %s: %s\n",
location ? location : "?", message ? message : "?");
fflush(stderr);
});
// V8 calls this immediately before aborting. Exiting from it turns what
// would be a bare abort() into the documented heap-ceiling exit code.
isolate->SetOOMErrorHandler([](const char* location,
const v8::OOMDetails& details) {
fprintf(stderr, "jsvm: V8 %s OOM at %s%s%s\n",
details.is_heap_oom ? "heap" : "process",
location ? location : "?", details.detail ? ": " : "",
details.detail ? details.detail : "");
fflush(stderr);
fflush(stdout);
_exit(125);
});
v8::HandleScope handle_scope(isolate);
#ifdef JSVM_EMBEDDED_SNAPSHOT
// Context::New(isolate), NOT Context::FromSnapshot(isolate, 0).
// mksnapshot_tool registers the context with SetDefaultContext, and the
// default context is what a bare Context::New deserializes. FromSnapshot(n)
// indexes only contexts registered with AddContext() -- with none added,
// index 0 is empty and you get "Empty MaybeLocal" with no explanation.
// Globals and preloaded JS are already on the heap: nothing to install.
//
// Not ToLocalChecked: an empty MaybeLocal also covers a blob V8 rejected
// (flag-set hash, version, or external-reference mismatch), and the raw
// fatal error names none of those.
// Context::New returns Local, not MaybeLocal: it yields an empty Local on
// failure rather than a MaybeLocal to unwrap.
v8::Local<v8::Context> context = v8::Context::New(isolate);
if (context.IsEmpty()) {
fprintf(stderr,
"jsvm: snapshot rejected by V8 (%u bytes). The blob and this "
"binary must come from one build, with identical V8 flags and "
"kExternalReferences.\n",
kJsvmSnapshotSize);
// Cannot return here: the Isolate::Scope and HandleScope above are still
// live and must unwind before the isolate is disposed at the bottom.
snapshot_ok = false;
}
if (snapshot_ok) {
#else
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
jsvm::InstallGlobals(isolate, global);
v8::Local<v8::Context> context =
v8::Context::New(isolate, nullptr, global);
// Opens the same block the snapshot branch does, so the two paths close
// symmetrically below. Always taken here.
if (snapshot_ok) {
#endif
v8::Context::Scope context_scope(context);
v8::TryCatch try_catch(isolate);
const std::string origin =
!opt.origin.empty()
? opt.origin
: (opt.script_path.empty() ? "<stdin>" : opt.script_path);
bool cache_used = false;
v8::Local<v8::Script> script;
if (!CompileScript(context, source, origin, opt.cache_path, &cache_used)
.ToLocal(&script)) {
jsvm::ReportException(isolate, &try_catch);
exit_code = 1;
} else {
Watchdog watchdog(isolate, opt.timeout_ms);
v8::Local<v8::Value> result;
const bool ok = script->Run(context).ToLocal(&result);
// Settle promise jobs the script queued before deciding it finished.
if (ok) {
isolate->PerformMicrotaskCheckpoint();
while (v8::platform::PumpMessageLoop(platform.get(), isolate)) {
isolate->PerformMicrotaskCheckpoint();
}
}
if (!ok) {
if (watchdog.fired()) {
fprintf(stderr, "jsvm: timeout after %d ms\n", opt.timeout_ms);
exit_code = 124; // matches coreutils timeout(1)
} else {
// Heap exhaustion never reaches here: the OOM handler exits 125 from
// inside V8, before the script gets a chance to unwind.
jsvm::ReportException(isolate, &try_catch);
exit_code = 1;
}
} else {
if (!opt.quiet && !result->IsUndefined()) {
printf("%s\n", jsvm::ToStdString(isolate, result).c_str());
}
// Written after Run() so lazily-compiled inner functions are captured.
if (!cache_used) SaveCodeCache(script, opt.cache_path);
}
}
// TryCatch must go out of scope before the isolate is torn down, hence the
// enclosing block.
if (try_catch.HasTerminated()) isolate->CancelTerminateExecution();
} // snapshot_ok
}
if (!snapshot_ok) exit_code = 70; // EX_SOFTWARE: bad build, not bad script
isolate->Dispose();
v8::V8::Dispose();
v8::V8::DisposePlatform(); // was ShutdownPlatform() pre-V8 10.x
delete params.array_buffer_allocator;
fflush(stdout);
return exit_code;
}