Skip to content
File

Blob: src/workerd/jsg/setup.c++

33.7 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#if __APPLE__
6// We need to define `_XOPEN_SOURCE` to get `ucontext_t` on Mac.
7#define _XOPEN_SOURCE
8#endif
9 
10#include "setup.h"
11 
12#include "libplatform/libplatform.h"
13 
14#include <v8-cppgc.h>
15#include <v8-initialization.h>
16 
17#if !_WIN32
18#include <cxxabi.h>
19#include <ucontext.h>
20#endif
21 
22#ifdef WORKERD_ICU_DATA_EMBED
23#include "icu-data-file.embed.h"
24 
25#include <unicode/udata.h>
26#endif
27 
28#if defined(__APPLE__) && defined(__aarch64__)
29#include <mach/mach.h>
30#endif
31 
32namespace workerd::jsg {
33 
34static bool v8Initialized = false;
35static V8System::FatalErrorCallback* v8FatalErrorCallback = nullptr;
36static void reportV8FatalError(kj::StringPtr location, kj::StringPtr message) {
37 if (v8FatalErrorCallback == nullptr) {
38 KJ_LOG(FATAL, "V8 fatal error", location, message);
39 abort();
40 } else {
41 v8FatalErrorCallback(location, message);
42 }
43}
44static void v8DcheckError(const char* file, int line, const char* message) {
45 reportV8FatalError(kj::str(file, ':', line), message);
46}
47 
48class PlatformDisposer final: public kj::Disposer {
49 public:
50 virtual void disposeImpl(void* pointer) const override {
51 delete static_cast<v8::Platform*>(pointer);
52 }
53 
54 static const PlatformDisposer instance;
55};
56 
57const PlatformDisposer PlatformDisposer::instance{};
58 
59kj::Own<v8::Platform> defaultPlatform(uint backgroundThreadCount) {
60 return kj::Own<v8::Platform>(
61 v8::platform::NewDefaultPlatform(backgroundThreadCount, // default thread pool size
62 v8::platform::IdleTaskSupport::kDisabled, // TODO(perf): investigate enabling
63 v8::platform::InProcessStackDumping::kDisabled, // KJ's stack traces are better
64 nullptr) // default TracingController
65 .release(),
66 PlatformDisposer::instance);
67}
68 
69static kj::Own<v8::Platform> userPlatform(v8::Platform& platform) {
70 // Make a fake kj::Own that wraps a user-specified platform reference. V8's default platform can
71 // only be created with manual memory management, so V8System::Platform needs to be able to store
72 // a smart pointer. However, requiring user platforms to come in via kj::Owns feels unnatural.
73 return kj::Own<v8::Platform>(&platform, kj::NullDisposer::instance);
74}
75 
76V8System::V8System(kj::ArrayPtr<const kj::StringPtr> flags) {
77 auto platform = defaultPlatform(0);
78 auto defaultPlatformPtr = platform.get();
79 init(kj::mv(platform), flags, [defaultPlatformPtr](v8::Isolate* isolate) {
80 return v8::platform::PumpMessageLoop(
81 defaultPlatformPtr, isolate, v8::platform::MessageLoopBehavior::kDoNotWait);
82 }, [defaultPlatformPtr](v8::Isolate* isolate) {
83 v8::platform::NotifyIsolateShutdown(defaultPlatformPtr, isolate);
84 });
85}
86 
87V8System::V8System(v8::Platform& platformParam,
88 kj::ArrayPtr<const kj::StringPtr> flags,
89 v8::Platform* defaultPlatformPtr) {
90 KJ_REQUIRE_NONNULL(defaultPlatformPtr);
91 init(userPlatform(platformParam), flags, [defaultPlatformPtr](v8::Isolate* isolate) {
92 return v8::platform::PumpMessageLoop(
93 defaultPlatformPtr, isolate, v8::platform::MessageLoopBehavior::kDoNotWait);
94 }, [defaultPlatformPtr](v8::Isolate* isolate) {
95 v8::platform::NotifyIsolateShutdown(defaultPlatformPtr, isolate);
96 });
97}
98 
99V8System::V8System(v8::Platform& platformParam,
100 kj::ArrayPtr<const kj::StringPtr> flags,
101 PumpMsgLoopType pumpMsgLoopFn,
102 ShutdownIsolateType shutdownIsolateFn) {
103 init(userPlatform(platformParam), flags, kj::mv(pumpMsgLoopFn), kj::mv(shutdownIsolateFn));
104}
105 
106void V8System::init(kj::Own<v8::Platform> platformParam,
107 kj::ArrayPtr<const kj::StringPtr> flags,
108 PumpMsgLoopType pumpMsgLoopFn,
109 ShutdownIsolateType shutdownIsolateFn) {
110 platformInner = kj::mv(platformParam);
111 platformWrapper = kj::heap<V8PlatformWrapper>(*platformInner);
112 pumpMsgLoop = kj::mv(pumpMsgLoopFn);
113 shutdownIsolate = kj::mv(shutdownIsolateFn);
114 
115#if V8_HAS_STACK_START_MARKER
116 v8::StackStartMarker::EnableForProcess();
117#endif
118 
119 v8::V8::SetDcheckErrorHandler(&v8DcheckError);
120 v8::V8::SetFatalErrorHandler(&v8DcheckError);
121 
122 // Note that v8::V8::SetFlagsFromString() simply ignores flags it doesn't recognize, which means
123 // typos don't generate any error. SetFlagsFromCommandLine() has the `remove_flags` option which
124 // leaves behind the flags V8 didn't recognize, so we'd like to use that for error checking
125 // purposes. Unfortunately, the interface is rather awkward, since it assumes you're going to
126 // run it on the raw argv array.
127 //
128 // Especially annoying is that V8 expects an array of `char*` -- not `const`. It won't actually
129 // modify the strings, so we'll just const_cast them here...
130 int argc = flags.size() + 1;
131 KJ_STACK_ARRAY(char*, argv, flags.size() + 2, 32, 32);
132 argv[0] = const_cast<char*>("fake-binary-name");
133 for (auto i: kj::zeroTo(flags.size())) {
134 argv[i + 1] = const_cast<char*>(flags[i].cStr());
135 }
136 argv[argc] = nullptr; // V8 probably doesn't need this but technically argv is NULL-terminated.
137 
138 v8::V8::SetFlagsFromCommandLine(&argc, argv.begin(), true);
139 
140 KJ_REQUIRE(argc == 1, "unrecognized V8 flag", argv[1]);
141 
142 // At present, we're not confident the JSG GC integration works with incremental marking. We have
143 // seen bugs in the past that were fixed by adding this flag, although that was a long time ago
144 // and the code has changed a lot since then. Since Worker heaps are generally relatively small
145 // (limited to 128MB in Cloudflare Workers), incremental marking is probably not a win anyway,
146 // and can be disabled. If we want to support significantly larger heaps, we may want to revisit
147 // this. We'll want to do some stress testing first, and fix any bugs seen.
148 //
149 // (It turns out you can call v8::V8::SetFlagsFromString() as many times as you want to add
150 // more flags.)
151 v8::V8::SetFlagsFromString("--noincremental-marking");
152 
153 // These features are completed and enabled by default in Chrome, but not
154 // in V8. Follows Node.js: https://github.com/nodejs/node/pull/58154
155 v8::V8::SetFlagsFromString("--js-explicit-resource-management");
156 v8::V8::SetFlagsFromString("--js-float16array");
157 
158 // Enable source phase imports for WebAssembly modules
159 v8::V8::SetFlagsFromString("--js-source-phase-imports");
160 
161#ifdef __APPLE__
162 // On macOS arm64, we find that V8 can be collecting pages that contain compiled code when
163 // handling requests in short succession. There are some specific differences for macOS arm64
164 // that may be a factor:
165 // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/11.5.150.4/src/heap/heap.h#2523
166 //
167 // Bugs attributable to this are https://github.com/cloudflare/workers-sdk/issues/2386 and
168 // CUSTESC-29094.
169 v8::V8::SetFlagsFromString("--single-threaded-gc");
170#endif // __APPLE__
171 
172 if (isPredictableModeForTest() || isGcStressModeForTest()) {
173 v8::V8::SetFlagsFromString("--expose-gc");
174 }
175 
176#ifdef WORKERD_ICU_DATA_EMBED
177 // V8's bazel build files currently don't support the option to embed ICU data, so we do it
178 // ourselves. `ICU_DATA_FILE`, if defined, will refer to a `kj::ArrayPtr<const byte>` containing
179 // the data.
180 UErrorCode err = U_ZERO_ERROR;
181 udata_setCommonData(ICU_DATA_FILE.begin(), &err);
182 udata_setFileAccess(UDATA_ONLY_PACKAGES, &err);
183 KJ_ASSERT(err == U_ZERO_ERROR);
184#else
185 // We instruct V8 to compile in this data file, so passing nullptr should work here. If V8 is
186 // built incorrectly, this will crash.
187 v8::V8::InitializeICUDefaultLocation(nullptr);
188#endif
189 
190 v8::V8::InitializePlatform(platformWrapper.get());
191 
192 // A recent change in v8 initializes cppgc in V8::Initialize if it's not already initialized
193 // Hence the ordering here is important
194 cppgc::InitializeProcess(platformWrapper->GetPageAllocator());
195 
196 v8::V8::Initialize();
197 v8Initialized = true;
198}
199 
200V8System::~V8System() noexcept(false) {
201 v8::V8::Dispose();
202 v8::V8::DisposePlatform();
203 cppgc::ShutdownProcess();
204}
205 
206void V8System::setFatalErrorCallback(FatalErrorCallback* callback) {
207 v8FatalErrorCallback = callback;
208}
209 
210IsolateBase& IsolateBase::from(v8::Isolate* isolate) {
211 return *static_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
212}
213 
214void IsolateBase::buildEmbedderGraph(v8::Isolate* isolate, v8::EmbedderGraph* graph, void* data) {
215 try {
216 const auto base = static_cast<IsolateBase*>(data);
217 MemoryTracker tracker(isolate, graph);
218 tracker.track(base);
219 } catch (...) {
220 // Generating the heap snapshot should be a safe process that does not
221 // throw any exceptions. We'll treat any exception here as fatal, including
222 // JsExceptionThrown. Note that we're not entered into any particular v8::Context
223 // here so pulling out the details of the exception would be tricky anyway.
224 kj::throwFatalException(kj::getCaughtExceptionAsKj());
225 }
226}
227 
228void IsolateBase::jsgGetMemoryInfo(MemoryTracker& tracker) const {
229 tracker.trackField("heapTracer", heapTracer);
230}
231 
232void IsolateBase::deferDestruction(Item item) {
233 KJ_REQUIRE_NONNULL(ptr, "tried to defer destruction after V8 isolate was destroyed");
234 KJ_REQUIRE(queueState == QueueState::ACTIVE, "tried to defer destruction during isolate shutdown",
235 queueState);
236 queue.lockExclusive()->push(kj::mv(item));
237}
238 
239void IsolateBase::deferDestruction(v8::Global<v8::Data> item) {
240 deferDestruction(Item(GlobalToDelete(kj::mv(item))));
241}
242 
243kj::Arc<const ExternalMemoryTarget> IsolateBase::getExternalMemoryTarget() {
244 return externalMemoryTarget.addRef();
245}
246 
247void IsolateBase::terminateExecution() const {
248 ptr->TerminateExecution();
249}
250 
251void IsolateBase::applyDeferredActions() {
252 // Clear the deferred destruction queue.
253 {
254 // Safe to destroy the popped batch outside of the lock because the lock is only actually used
255 // to guard the push buffer.
256 DISALLOW_KJ_IO_DESTRUCTORS_SCOPE;
257 auto drop = queue.lockExclusive()->pop();
258 }
259 
260 externalMemoryTarget->applyDeferredMemoryUpdate();
261}
262 
263HeapTracer::HeapTracer(v8::Isolate* isolate)
264 // Historically V8 would call IsRoot() to scan references, and then call ResetRoot() on those
265 // where IsRoot() returned false. Currently, V8 allows marking a reference as "droppable", and
266 // assumes droppable references are not roots. This way V8 only calls ResetRoot() on droppable
267 // references, and doesn't even call `IsRoot()` on anything else. See comment about droppable
268 // references in Wrappable::attachWrapper() for details.
269 : isolate(isolate) {
270 isolate->AddGCPrologueCallback(
271 [](v8::Isolate* isolate, v8::GCType type, v8::GCCallbackFlags flags, void* data) {
272 // We can expect that any freelisted shims will be collected during a major GC, because
273 // they are not in use therefore not reachable. We should therefore clear the freelist now,
274 // before the trace starts.
275 //
276 // Note that we cannot simply depend on the destructor of CppgcShim to remove objects from
277 // the freelist, because destructors do not actually run at trace time. They may be deferred
278 // to run some time after the trace is done. If we accidentally reuse a shim during that
279 // time, we'll have a problem as the shim will still be destroyed as it was already
280 // determined to be unreachable.
281 //
282 // We must clear the freelist in the GC prologue, not the epilogue, because when building in
283 // ASAN mode, V8 will poison the objects' memory, so our attempt to clear the freelist after
284 // the fact will trigger a spurious ASAN failure.
285 static_cast<HeapTracer*>(data)->clearFreelistedShims();
286 }, this, v8::GCType::kGCTypeMarkSweepCompact);
287 
288 isolate->AddGCEpilogueCallback(
289 [](v8::Isolate* isolate, v8::GCType type, v8::GCCallbackFlags flags, void* data) {
290 auto& self = *static_cast<HeapTracer*>(data);
291 for (Wrappable* wrappable: self.detachLater) {
292 wrappable->detachWrapper(true);
293 }
294 self.detachLater.clear();
295 }, this, v8::GCType::kGCTypeAll);
296}
297 
298void HeapTracer::destroy() {
299 DISALLOW_KJ_IO_DESTRUCTORS_SCOPE;
300 KJ_DEFER(isolate = nullptr);
301}
302 
303HeapTracer& HeapTracer::getTracer(v8::Isolate* isolate) {
304 return IsolateBase::from(isolate).heapTracer;
305}
306 
307void HeapTracer::ResetRoot(const v8::TracedReference<v8::Value>& handle) {
308 // V8 calls this to tell us when our wrapper can be dropped. See comment about droppable
309 // references in Wrappable::attachWrapper() for details.
310 v8::HandleScope scope(isolate);
311 auto& wrappable = *static_cast<Wrappable*>(
312 handle.As<v8::Object>().Get(isolate)->GetAlignedPointerFromInternalField(
313 Wrappable::WRAPPED_OBJECT_FIELD_INDEX,
314 static_cast<v8::EmbedderDataTypeTag>(Wrappable::WRAPPED_OBJECT_FIELD_INDEX)));
315 
316 // V8 gets angry if we do not EXPLICITLY call `Reset()` on the wrapper. If we merely destroy it
317 // (which is what `detachWrapper()` will do) it is not satisfied, and will come back and try to
318 // visit the reference again, but it will DCHECK-fail on that second attempt because the
319 // reference is in an inconsistent state at that point.
320 KJ_ASSERT_NONNULL(wrappable.wrapper).Reset();
321 
322 // We don't want to call `detachWrapper()` now because it may create new handles (specifically,
323 // if the wrappable has strong references, which means that its outgoing references need to be
324 // upgraded to strong).
325 detachLater.add(&wrappable);
326}
327 
328bool HeapTracer::TryResetRoot(const v8::TracedReference<v8::Value>& handle) {
329 // This method is potentially called on a separate thread. Our ResetRoot() implementation,
330 // though, only works on the main thread. Return false to request V8 schedule the call for the
331 // main thread later on.
332 return false;
333}
334 
335namespace {
336std::unique_ptr<v8::CppHeap> newCppHeap(V8PlatformWrapper* system) {
337 return jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
338 v8::CppHeapCreateParams heapParams{{}};
339 heapParams.marking_support = cppgc::Heap::MarkingType::kAtomic;
340 heapParams.sweeping_support = cppgc::Heap::SweepingType::kAtomic;
341 return v8::CppHeap::Create(system, heapParams);
342 });
343}
344static v8::Isolate* newIsolate(
345 v8::Isolate::CreateParams&& params, v8::CppHeap* cppHeap, v8::IsolateGroup group) {
346 return jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) -> v8::Isolate* {
347 // We currently don't attempt to support incremental marking or sweeping. We probably could
348 // support them, but it will take some careful investigation and testing. It's not clear if
349 // this would be a win anyway, since Worker heaps are relatively small and therefore doing a
350 // full atomic mark-sweep usually doesn't require much of a pause.
351 //
352 // We probably won't ever support concurrent marking or sweeping because concurrent GC is
353 // only expected to be a win if there are idle CPU cores available. Workers normally run on
354 // servers that are handling many requests at once, thus it's expected CPU cores will be
355 // fully utilized. This differs from browser environments, where a user is typically doing
356 // only one thing at a time and thus likely has CPU cores to spare.
357 
358 // V8 takes ownership of the v8::CppHeap.
359 params.cpp_heap = cppHeap;
360 
361 if (params.array_buffer_allocator == nullptr &&
362 params.array_buffer_allocator_shared == nullptr) {
363#ifdef V8_COMPRESS_POINTERS_IN_MULTIPLE_CAGES
364 params.array_buffer_allocator_shared = std::shared_ptr<v8::ArrayBuffer::Allocator>(
365 v8::ArrayBuffer::Allocator::NewDefaultAllocator(group));
366#else
367 params.array_buffer_allocator_shared = std::shared_ptr<v8::ArrayBuffer::Allocator>(
368 v8::ArrayBuffer::Allocator::NewDefaultAllocator());
369#endif
370 }
371 return v8::Isolate::New(group, params);
372 });
373}
374} // namespace
375IsolateBase::IsolateBase(V8System& system,
376 v8::Isolate::CreateParams&& createParams,
377 kj::Own<IsolateObserver> observer,
378 kj::Own<ExternalStringAllocator> externalStringAllocator,
379 v8::IsolateGroup group)
380 : v8System(system),
381 cppHeap(newCppHeap(const_cast<V8PlatformWrapper*>(system.platformWrapper.get()))),
382 ptr(newIsolate(kj::mv(createParams), cppHeap.release(), group)),
383 externalMemoryTarget(kj::arc<ExternalMemoryTarget>(ptr)),
384 envAsyncContextKey(kj::refcounted<AsyncContextFrame::StorageKey>()),
385 exportsAsyncContextKey(kj::refcounted<AsyncContextFrame::StorageKey>()),
386 heapTracer(ptr),
387 observer(kj::mv(observer)),
388 externalStringAllocator(kj::mv(externalStringAllocator)) {
389 jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
390 ptr->SetEmbedderRootsHandler(&heapTracer);
391 
392 ptr->SetFatalErrorHandler(&fatalError);
393 ptr->SetOOMErrorHandler(&oomError);
394 // We also set the global OOM error handler. This is a bit of
395 // a hack: Later in the run the allocation of a sandbox may fail
396 // due to OOM. In that case we want our handler to be called
397 // even though there is no current isolate.
398 v8::V8::SetFatalMemoryErrorCallback(&oomError);
399 
400 ptr->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit);
401 ptr->SetData(SET_DATA_ISOLATE_BASE, this);
402 
403 ptr->SetModifyCodeGenerationFromStringsCallback(&modifyCodeGenCallback);
404 ptr->SetAllowWasmCodeGenerationCallback(&allowWasmCallback);
405 
406 // We don't support SharedArrayBuffer so Atomics.wait() doesn't make sense, and might allow DoS
407 // attacks.
408 ptr->SetAllowAtomicsWait(false);
409 
410 ptr->SetJitCodeEventHandler(v8::kJitCodeEventDefault, &jitCodeEvent);
411 
412 // V8 10.5 introduced this API which is used to resolve the promise returned by
413 // WebAssembly.compile(). For some reason, the default implementation of the callback does not
414 // work -- the promise is never resolved. The only thing the default version does differently
415 // is it creates a `MicrotasksScope` with `kDoNotRunMicrotasks`. I do not understand what that
416 // is even supposed to do, but it seems related to `MicrotasksPolicy::kScoped`, which we don't
417 // use, we use `kExplicit`. Replacing the callback seems to solve the problem?
418 ptr->SetWasmAsyncResolvePromiseCallback(
419 [](v8::Isolate* isolate, v8::Local<v8::Context> context,
420 v8::Local<v8::Promise::Resolver> resolver, v8::Local<v8::Value> result,
421 v8::WasmAsyncSuccess success) {
422 switch (success) {
423 case v8::WasmAsyncSuccess::kSuccess:
424 resolver->Resolve(context, result).FromJust();
425 break;
426 case v8::WasmAsyncSuccess::kFail:
427 resolver->Reject(context, result).FromJust();
428 break;
429 }
430 });
431 
432 ptr->GetHeapProfiler()->AddBuildEmbedderGraphCallback(buildEmbedderGraph, this);
433 
434 {
435 // We don't need a v8::Locker here since there's no way another thread could be using the
436 // isolate yet, but we do need v8::Isolate::Scope.
437 v8::Isolate::Scope isolateScope(ptr);
438 v8::HandleScope scope(ptr);
439 
440 // Create opaqueTemplate
441 auto opaqueTemplate = v8::FunctionTemplate::New(ptr, &throwIllegalConstructor);
442 opaqueTemplate->InstanceTemplate()->SetInternalFieldCount(Wrappable::INTERNAL_FIELD_COUNT);
443 this->opaqueTemplate.Reset(ptr, opaqueTemplate);
444 }
445 });
446}
447 
448IsolateBase::~IsolateBase() noexcept(false) {
449 // Ensure objects that outlive the isolate won't attempt to modify external memory
450 // on the now-destroyed isolate.
451 externalMemoryTarget->detach();
452 
453 jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
454 // Terminate the v8::platform's task queue associated with this isolate
455 v8System.shutdownIsolate(ptr);
456 ptr->Dispose();
457 ptr = nullptr;
458 // TODO(cleanup): meaningless after V8 13.4 is released.
459 cppHeap.reset();
460 });
461}
462 
463v8::Local<v8::FunctionTemplate> IsolateBase::getOpaqueTemplate(v8::Isolate* isolate) {
464 return static_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE))
465 ->opaqueTemplate.Get(isolate);
466}
467 
468void IsolateBase::dropWrappers(kj::FunctionParam<void()> drop) {
469 KJ_REQUIRE(queueState == QueueState::ACTIVE);
470 queueState = QueueState::DROPPING;
471 // Delete all wrappers.
472 jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
473 v8::Locker lock(ptr);
474 v8::Isolate::Scope isolateScope(ptr);
475 
476 // Make sure everything in the deferred destruction queue is dropped.
477 applyDeferredActions();
478 
479 // We MUST call heapTracer.destroy(), but we can't do it yet because destroying other handles
480 // may call into the heap tracer.
481 KJ_DEFER(heapTracer.destroy());
482 
483 // Make sure v8::Globals are destroyed under lock (but not until later).
484 KJ_DEFER(opaqueTemplate.Reset());
485 KJ_DEFER(workerEnvObj.Reset());
486 KJ_DEFER(workerExportsObj.Reset());
487 
488 // Make sure the TypeWrapper is destroyed under lock by declaring a new copy of the variable
489 // that is destroyed before the lock is released.
490 drop();
491 
492 // Destroy all wrappers.
493 heapTracer.clearWrappers();
494 queueState = QueueState::DROPPED;
495 });
496}
497 
498void IsolateBase::fatalError(const char* location, const char* message) {
499 reportV8FatalError(location, message);
500}
501void IsolateBase::oomError(const char* location, const v8::OOMDetails& oom) {
502 kj::StringPtr detailPrefix, detail;
503 if (oom.detail != nullptr) {
504 detailPrefix = "; detail: "_kj;
505 detail = oom.detail;
506 }
507 auto message = kj::str(oom.is_heap_oom ? ": allocation failed: JavaScript heap out of memory"_kj
508 : ": allocation failed: process out of memory"_kj,
509 detailPrefix, detail);
510 reportV8FatalError(location, message);
511}
512 
513v8::ModifyCodeGenerationFromStringsResult IsolateBase::modifyCodeGenCallback(
514 v8::Local<v8::Context> context, v8::Local<v8::Value> source, bool isCodeLike) {
515 // For undefined sources (e.g. eval() with no argument or eval(undefined)),
516 // there is no code generation from strings. V8 returns undefined as-is per spec.
517 // We allow it through without further checks.
518 // Note: Wasm compilation uses a separate callback (AllowWasmCodeGenerationCallback).
519 if (source->IsUndefined()) {
520 return {.codegen_allowed = true, .modified_source = {}};
521 }
522 
523 // Allow empty-body, no-parameter Function constructor calls: `new Function()`, or
524 // `class Foo extends Function { constructor() { super(); } }`.
525 //
526 // V8 synthesizes the full source string before calling this callback (see
527 // CreateDynamicFunction in v8/src/builtins/builtins-function.cc). When called with
528 // no arguments (argc == 0), isCodeLike is true and the source is the exact string
529 // below — which contains no user-provided code.
530 //
531 // Security notes:
532 // - isCodeLike is false on the eval() path, so this check cannot be reached via
533 // eval(). An attacker cannot use eval("<evil> {\n\n}") to bypass this.
534 // - isCodeLike is also false when any arguments are plain strings (not CodeLike
535 // objects), so new Function('a', 'b', undefined) cannot reach this check either.
536 // - The exact string match ensures no user content (parameters or body) is present.
537 // - We intentionally only match the no-parameter, no-body case. Calls like
538 // new Function('a', 'b') are always blocked since the last argument becomes the
539 // body via ToString(), producing a non-matching source string.
540 //
541 // NOTE: This pattern is tied to V8's CreateDynamicFunction format in
542 // builtins-function.cc:46-71 and must be reviewed during V8 updates. If the format
543 // changes, the setup-test and worker-test will fail, signaling that this constant
544 // needs updating.
545 static constexpr auto kEmptyFunctionSource = "(function anonymous(\n) {\n\n})"_kj;
546 if (isCodeLike && source->IsString() &&
547 kj::str(source.As<v8::String>()) == kEmptyFunctionSource) {
548 return {.codegen_allowed = true, .modified_source = {}};
549 }
550 
551 v8::Isolate* isolate = v8::Isolate::GetCurrent();
552 auto& base = IsolateBase::from(isolate);
553 if (base.evalAllowed) {
554 // If eval is allowed, notify the observer so that it can take any action necessary.
555 // Once possible action is logging the source to be evaluated for auditing purposes.
556 // TODO(cleanup): Consider making it so that `onDynamicEval()` returns true or false
557 // depending on whether eval should be allowed or not.
558 base.observer->onDynamicEval(context, source, isCodeLike ? IsCodeLike::YES : IsCodeLike::NO);
559 }
560 
561 return {.codegen_allowed = base.evalAllowed, .modified_source = {}};
562}
563 
564bool IsolateBase::allowWasmCallback(v8::Local<v8::Context> context, v8::Local<v8::String> source) {
565 // Don't allow WASM unless arbitrary eval() is allowed.
566 IsolateBase* self =
567 static_cast<IsolateBase*>(v8::Isolate::GetCurrent()->GetData(SET_DATA_ISOLATE_BASE));
568 return self->evalAllowed;
569}
570 
571void IsolateBase::jitCodeEvent(const v8::JitCodeEvent* event) noexcept {
572 // We register this callback with V8 in order to build a mapping of code addresses to source
573 // code locations, which we use when reporting stack traces during crashes.
574 
575 IsolateBase* self = static_cast<IsolateBase*>(event->isolate->GetData(SET_DATA_ISOLATE_BASE));
576 auto& codeMap = self->codeMap;
577 
578 // Pointer comparison between pointers not from the same array is UB so we'd better operate on
579 // uintptr_t instead.
580 uintptr_t startAddr = reinterpret_cast<uintptr_t>(event->code_start);
581 
582 struct UserData {
583 // The type we'll use in JitCodeEvent::user_data...
584 
585 kj::Vector<CodeBlockInfo::PositionMapping> mapping;
586 };
587 
588 switch (event->type) {
589 case v8::JitCodeEvent::CODE_ADDED: {
590 // Usually CODE_ADDED comes after CODE_END_LINE_INFO_RECORDING, but sometimes it doesn't,
591 // particularly in the case of Wasm where it appears no line info is provided.
592 auto& info = codeMap.findOrCreate(
593 startAddr, [&]() { return decltype(self->codeMap)::Entry{startAddr, CodeBlockInfo()}; });
594 info.size = event->code_len;
595 info.name = kj::str(kj::arrayPtr(event->name.str, event->name.len));
596 info.type = event->code_type;
597 break;
598 }
599 
600 case v8::JitCodeEvent::CODE_MOVED:
601 KJ_IF_SOME(entry, codeMap.findEntry(startAddr)) {
602 auto info = kj::mv(entry.value);
603 codeMap.erase(entry);
604 codeMap.upsert(reinterpret_cast<uintptr_t>(event->new_code_start), kj::mv(info),
605 [&](CodeBlockInfo& existing, CodeBlockInfo&& replacement) {
606 // It seems sometimes V8 tells us that it "moved" a block to a location that already
607 // existed. Why? Who knows? There's no documentation. Let's do the best we can, which is:
608 // replace the existing with the new values, unless the new values are not initialized.
609 // (E.g. maybe the reason the block already exists is because CODE_ADDED or
610 // CODE_END_LINE_INFO_RECORDING was already delivered to the new location for some
611 // reason...)
612 if (replacement.type != kj::none) {
613 existing.size = replacement.size;
614 existing.type = replacement.type;
615 existing.name = kj::mv(replacement.name);
616 }
617 if (replacement.mapping != nullptr) {
618 existing.mapping = kj::mv(replacement.mapping);
619 }
620 });
621 } else {
622 // TODO(someday): Figure out why this triggers. As of v8 10.3 it actually happens in one
623 // of our tests. This API is very undocumented, though, so I'm not sure what I should do.
624 // Change this back to DEBUG_FAIL_PROD_LOG once debugged. (It was reduced to INFO logging
625 // to avoid bothering users of workerd.)
626 KJ_LOG(INFO, "CODE_MOVED for unknown code block?");
627 }
628 break;
629 
630 case v8::JitCodeEvent::CODE_REMOVED:
631 if (!codeMap.erase(startAddr)) {
632 DEBUG_FATAL_RELEASE_LOG(ERROR, "CODE_REMOVED for unknown code block?");
633 }
634 break;
635 
636 case v8::JitCodeEvent::CODE_ADD_LINE_POS_INFO:
637 // V8 reports multiple "position types", POSITION and STATEMENT_POSITION. These are intended
638 // to produce two different mappings from instructions to locations. POSITION points to
639 // a specific expression while STATEMENT_POSITION only points to the enclosing statement.
640 // For our purposes, the former is strictly more useful than the latter, so we ignore
641 // STATEMENT_POSITION.
642 if (event->line_info.position_type == v8::JitCodeEvent::POSITION) {
643 UserData* data = static_cast<UserData*>(event->user_data);
644 data->mapping.add(CodeBlockInfo::PositionMapping{
645 static_cast<uint>(event->line_info.offset), static_cast<uint>(event->line_info.pos)});
646 }
647 break;
648 
649 case v8::JitCodeEvent::CODE_START_LINE_INFO_RECORDING: {
650 UserData* data = new UserData();
651 data->mapping.reserve(256);
652 
653 // Yes we are actually supposed to const_cast the event in order to set the user_data. This
654 // is nuts but it's what other users of this interface inside the V8 codebase actually do.
655 const_cast<v8::JitCodeEvent*>(event)->user_data = data;
656 break;
657 }
658 
659 case v8::JitCodeEvent::CODE_END_LINE_INFO_RECORDING: {
660 // Sometimes CODE_END_LINE_INFO_RECORDING comes after CODE_ADDED, in particular with
661 // modules.
662 auto& info = codeMap.findOrCreate(
663 startAddr, [&]() { return decltype(self->codeMap)::Entry{startAddr, CodeBlockInfo()}; });
664 
665 UserData* data = static_cast<UserData*>(event->user_data);
666 info.mapping = data->mapping.releaseAsArray();
667 delete data;
668 
669 break;
670 }
671 }
672}
673 
674void* getJsCageBase() {
675 if (!v8Initialized) return nullptr;
676 v8::Isolate* isolate = v8::Isolate::TryGetCurrent();
677 if (isolate == nullptr) return nullptr;
678 // Returns null if setJsCageBase was never called.
679 return isolate->GetData(SET_DATA_CAGE_BASE);
680}
681 
682void setJsCageBase(void* cageBase) {
683 if (!v8Initialized) return;
684 v8::Isolate* isolate = v8::Isolate::TryGetCurrent();
685 if (isolate == nullptr) return;
686 isolate->SetData(SET_DATA_CAGE_BASE, cageBase);
687}
688 
689#if _WIN32
690kj::Maybe<kj::StringPtr> getJsStackTrace(void* ucontext, kj::ArrayPtr<char> scratch) {
691 // This function is only called by the internal build which just targets Linux.
692 // Windows doesn't provide ucontext, so we'd need to rewrite this function's signature
693 // if we were to support it. `v8/src/libsampler/sampler.cc` provides a suitable
694 // implementation we could use.
695 KJ_UNIMPLEMENTED("getJsStackTrace() is not implemented on Windows");
696}
697#else
698kj::Maybe<kj::StringPtr> getJsStackTrace(void* ucontext, kj::ArrayPtr<char> scratch) {
699 if (!v8Initialized) {
700 return kj::none;
701 }
702 v8::Isolate* isolate = v8::Isolate::TryGetCurrent();
703 if (isolate == nullptr) {
704 return kj::none;
705 }
706 
707 char* pos = scratch.begin();
708 char* limit = scratch.end() - 1;
709 auto appendText = [&](const auto&... params) {
710 pos = kj::_::fillLimited(pos, limit, kj::toCharSequence(params)...);
711 };
712 
713 v8::RegisterState state;
714 auto& mcontext = static_cast<ucontext_t*>(ucontext)->uc_mcontext;
715#if defined(__APPLE__) && defined(__x86_64__)
716 state.pc = reinterpret_cast<void*>(mcontext->__ss.__rip);
717 state.sp = reinterpret_cast<void*>(mcontext->__ss.__rsp);
718 state.fp = reinterpret_cast<void*>(mcontext->__ss.__rbp);
719#elif defined(__APPLE__) && defined(__aarch64__)
720 state.pc = reinterpret_cast<void*>(arm_thread_state64_get_pc(mcontext->__ss));
721 state.sp = reinterpret_cast<void*>(arm_thread_state64_get_sp(mcontext->__ss));
722 state.fp = reinterpret_cast<void*>(arm_thread_state64_get_fp(mcontext->__ss));
723#elif defined(__linux__) && defined(__x86_64__)
724 state.pc = reinterpret_cast<void*>(mcontext.gregs[REG_RIP]);
725 state.sp = reinterpret_cast<void*>(mcontext.gregs[REG_RSP]);
726 state.fp = reinterpret_cast<void*>(mcontext.gregs[REG_RBP]);
727#elif defined(__linux__) && defined(__aarch64__)
728 state.pc = reinterpret_cast<void*>(mcontext.pc);
729 state.sp = reinterpret_cast<void*>(mcontext.sp);
730 state.fp = reinterpret_cast<void*>(mcontext.regs[29]);
731 state.lr = reinterpret_cast<void*>(mcontext.regs[30]);
732#else
733#error "Please add architecture support. See FillRegisterState() in v8/src/libsampler/sampler.cc"
734#endif
735 
736 v8::SampleInfo sampleInfo;
737 void* traceSpace[32]{};
738 isolate->GetStackSample(state, traceSpace, kj::size(traceSpace), &sampleInfo);
739 
740 kj::StringPtr vmState = "??";
741 switch (sampleInfo.vm_state) {
742 case v8::StateTag::JS:
743 vmState = "js";
744 break;
745 case v8::StateTag::GC:
746 vmState = "gc";
747 break;
748 case v8::StateTag::PARSER:
749 vmState = "parser";
750 break;
751 case v8::StateTag::BYTECODE_COMPILER:
752 vmState = "bytecode_compiler";
753 break;
754 case v8::StateTag::COMPILER:
755 vmState = "compiler";
756 break;
757 case v8::StateTag::OTHER:
758 vmState = "other";
759 break;
760 case v8::StateTag::EXTERNAL:
761 vmState = "external";
762 break;
763 case v8::StateTag::ATOMICS_WAIT:
764 vmState = "atomics_wait";
765 break;
766 case v8::StateTag::IDLE:
767 vmState = "idle";
768 break;
769 case v8::StateTag::IDLE_EXTERNAL:
770 vmState = "idle_external";
771 break;
772 case v8::StateTag::LOGGING:
773 vmState = "logging";
774 break;
775 }
776 appendText("js: (", vmState, ")");
777 
778 auto& codeMap = static_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE))->codeMap;
779 
780 for (auto i: kj::zeroTo(sampleInfo.frames_count)) {
781 uintptr_t addr = reinterpret_cast<uintptr_t>(traceSpace[i]);
782 auto range = codeMap.range(0, addr + 1);
783 bool matched = false;
784 kj::StringPtr prevName = nullptr;
785 if (range.begin() != range.end()) {
786 auto iter = range.end();
787 --iter;
788 auto& entry = *iter;
789 if (entry.key + entry.value.size > addr) {
790 // Yay, a match. Binary search it. We're looking for the first entry that is greater than
791 // the target address (then we'll back up one).
792 uint offset = addr - entry.key;
793 auto& mapping = entry.value.mapping;
794 size_t l = 0;
795 size_t r = mapping.size();
796 while (l < r) {
797 size_t mid = (l + r) / 2;
798 if (mapping[mid].instructionOffset <= offset) {
799 l = mid + 1;
800 } else {
801 r = mid;
802 }
803 }
804 
805 matched = true;
806 appendText(' ');
807 if (entry.value.name != prevName) {
808 appendText('\'', entry.value.name, '\'');
809 prevName = entry.value.name;
810 }
811 if (l > 0) {
812 appendText('@', mapping[l - 1].sourceOffset);
813 } else {
814 appendText("@?");
815 }
816 }
817 }
818 
819 if (!matched) {
820 appendText(" @?");
821 }
822 }
823 
824 *pos = '\0';
825 return kj::StringPtr(scratch.begin(), pos - scratch.begin());
826}
827#endif
828 
829} // namespace workerd::jsg