File
Blob: src/workerd/jsg/setup.c++
| 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 | |
| 32 | namespace workerd::jsg { |
| 33 | |
| 34 | static bool v8Initialized = false; |
| 35 | static V8System::FatalErrorCallback* v8FatalErrorCallback = nullptr; |
| 36 | static 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 | } |
| 44 | static void v8DcheckError(const char* file, int line, const char* message) { |
| 45 | reportV8FatalError(kj::str(file, ':', line), message); |
| 46 | } |
| 47 | |
| 48 | class 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 | |
| 57 | const PlatformDisposer PlatformDisposer::instance{}; |
| 58 | |
| 59 | kj::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 | |
| 69 | static 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 | |
| 76 | V8System::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 | |
| 87 | V8System::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 | |
| 99 | V8System::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 | |
| 106 | void 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 | |
| 200 | V8System::~V8System() noexcept(false) { |
| 201 | v8::V8::Dispose(); |
| 202 | v8::V8::DisposePlatform(); |
| 203 | cppgc::ShutdownProcess(); |
| 204 | } |
| 205 | |
| 206 | void V8System::setFatalErrorCallback(FatalErrorCallback* callback) { |
| 207 | v8FatalErrorCallback = callback; |
| 208 | } |
| 209 | |
| 210 | IsolateBase& IsolateBase::from(v8::Isolate* isolate) { |
| 211 | return *static_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE)); |
| 212 | } |
| 213 | |
| 214 | void 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 | |
| 228 | void IsolateBase::jsgGetMemoryInfo(MemoryTracker& tracker) const { |
| 229 | tracker.trackField("heapTracer", heapTracer); |
| 230 | } |
| 231 | |
| 232 | void 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 | |
| 239 | void IsolateBase::deferDestruction(v8::Global<v8::Data> item) { |
| 240 | deferDestruction(Item(GlobalToDelete(kj::mv(item)))); |
| 241 | } |
| 242 | |
| 243 | kj::Arc<const ExternalMemoryTarget> IsolateBase::getExternalMemoryTarget() { |
| 244 | return externalMemoryTarget.addRef(); |
| 245 | } |
| 246 | |
| 247 | void IsolateBase::terminateExecution() const { |
| 248 | ptr->TerminateExecution(); |
| 249 | } |
| 250 | |
| 251 | void 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 | |
| 263 | HeapTracer::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 | |
| 298 | void HeapTracer::destroy() { |
| 299 | DISALLOW_KJ_IO_DESTRUCTORS_SCOPE; |
| 300 | KJ_DEFER(isolate = nullptr); |
| 301 | } |
| 302 | |
| 303 | HeapTracer& HeapTracer::getTracer(v8::Isolate* isolate) { |
| 304 | return IsolateBase::from(isolate).heapTracer; |
| 305 | } |
| 306 | |
| 307 | void 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 | |
| 328 | bool 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 | |
| 335 | namespace { |
| 336 | std::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 | } |
| 344 | static 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 |
| 375 | IsolateBase::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 | |
| 448 | IsolateBase::~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 | |
| 463 | v8::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 | |
| 468 | void 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 | |
| 498 | void IsolateBase::fatalError(const char* location, const char* message) { |
| 499 | reportV8FatalError(location, message); |
| 500 | } |
| 501 | void 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 | |
| 513 | v8::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 | |
| 564 | bool 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 | |
| 571 | void 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 | |
| 674 | void* 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 | |
| 682 | void 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 |
| 690 | kj::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 |
| 698 | kj::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 |