Skip to content
35 changes: 35 additions & 0 deletions deps/v8/src/base/platform/platform.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,25 @@ namespace {
// A pointer to current thread's stack beginning.
thread_local void* thread_stack_start = nullptr;

// Qualia: embedders that switch stacks on one OS thread (node-fibers with
// coroutines) tell us the start of the stack that is currently executing. When
// set, it replaces the OS thread's stack start everywhere GetStackStart() is
// used: stack limits, IsOnStack() and the conservative stack scan performed by
// cppgc, which would otherwise walk from a coroutine stack up to the thread's
// stack start through unmapped memory.
thread_local void* thread_stack_start_override = nullptr;
// Set once the embedder has used the override at all. Consumers that cache the stack start
// (the conservative stack scan) re-read it per scan when this is set, because a coroutine
// switch can happen under a held Locker without Isolate::Enter() running again.
bool thread_stack_start_override_enabled = false;

} // namespace

// static
Stack::StackSlot Stack::GetStackStartUnchecked() {
if (thread_stack_start_override) {
return thread_stack_start_override;
}
if (!thread_stack_start) {
thread_stack_start = ObtainCurrentThreadStackStart();
}
Expand All @@ -25,6 +40,26 @@ Stack::StackSlot Stack::GetStackStartUnchecked() {
// static
Stack::StackSlot Stack::GetStackStart() { return GetStackStartUnchecked(); }

} // namespace base
} // namespace v8

// Qualia: C entry point for node-fibers (resolved with dlsym, so fibers still
// loads on a node without this patch). Pass nullptr when switching back to the
// OS thread's own stack.
extern "C" __attribute__((visibility("default"))) void
v8_qualia_set_thread_stack_start(void* stack_start) {
v8::base::thread_stack_start_override = stack_start;
v8::base::thread_stack_start_override_enabled = true;
}

extern "C" __attribute__((visibility("default"))) bool
v8_qualia_thread_stack_start_override_enabled() {
return v8::base::thread_stack_start_override_enabled;
}

namespace v8 {
namespace base {

// static
int OS::GetCurrentThreadId() {
static thread_local int id = GetCurrentThreadIdInternal();
Expand Down
6 changes: 6 additions & 0 deletions deps/v8/src/base/platform/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,13 @@ class V8_BASE_EXPORT Thread {
static LocalStorageKey CreateThreadLocalKey();
static void DeleteThreadLocalKey(LocalStorageKey key);
static void* GetThreadLocal(LocalStorageKey key);
static int GetThreadLocalInt(LocalStorageKey key) {
return static_cast<int>(reinterpret_cast<intptr_t>(GetThreadLocal(key)));
}
static void SetThreadLocal(LocalStorageKey key, void* value);
static void SetThreadLocalInt(LocalStorageKey key, int value) {
SetThreadLocal(key, reinterpret_cast<void*>(static_cast<intptr_t>(value)));
}
static bool HasThreadLocal(LocalStorageKey key) {
return GetThreadLocal(key) != nullptr;
}
Expand Down
12 changes: 9 additions & 3 deletions deps/v8/src/execution/isolate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,12 @@ void Isolate::Iterate(RootVisitor* v, ThreadLocalTop* thread) {
FullObjectSlot(reinterpret_cast<Address>(&(block->message_obj_))));
}

v->VisitRootPointer(
Root::kStackRoots, nullptr,
FullObjectSlot(continuation_preserved_embedder_data_address()));
// Qualia: continuation_preserved_embedder_data_ lives in IsolateData, not in
// ThreadLocalTop, so it is visited once from Iterate(RootVisitor*) below
// instead of once per archived thread here. Visiting the same root slot more
// than once per GC is not safe: the scavenger would copy a young object a
// second time from its to-space copy (node-fibers archives a thread state per
// coroutine via Locker/Unlocker, which made this reachable).

// Iterate over pointers on native execution stack.
#if V8_ENABLE_WEBASSEMBLY
Expand All @@ -648,6 +651,9 @@ void Isolate::Iterate(RootVisitor* v, ThreadLocalTop* thread) {
}

void Isolate::Iterate(RootVisitor* v) {
v->VisitRootPointer(
Root::kStackRoots, nullptr,
FullObjectSlot(continuation_preserved_embedder_data_address()));
ThreadLocalTop* current_t = thread_local_top();
Iterate(v, current_t);
}
Expand Down
7 changes: 6 additions & 1 deletion deps/v8/src/execution/thread-id.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,27 @@ namespace internal {

namespace {

thread_local int thread_id = 0;
DEFINE_LAZY_LEAKY_OBJECT_GETTER(base::Thread::LocalStorageKey, GetThreadIdKey,
base::Thread::CreateThreadLocalKey())

std::atomic<int> next_thread_id{1};

} // namespace

// static
ThreadId ThreadId::TryGetCurrent() {
int thread_id = base::Thread::GetThreadLocalInt(*GetThreadIdKey());
return thread_id == 0 ? Invalid() : ThreadId(thread_id);
}

// static
int ThreadId::GetCurrentThreadId() {
auto key = *GetThreadIdKey();
int thread_id = base::Thread::GetThreadLocalInt(key);
if (thread_id == 0) {
thread_id = next_thread_id.fetch_add(1);
CHECK_LE(1, thread_id);
base::Thread::SetThreadLocalInt(key, thread_id);
}
return thread_id;
}
Expand Down
15 changes: 13 additions & 2 deletions deps/v8/src/heap/base/stack.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#include "src/base/sanitizer/tsan.h"
#include "src/heap/base/memory-tagging.h"

// Qualia (node-fibers): defined in src/base/platform/platform.cc.
extern "C" bool v8_qualia_thread_stack_start_override_enabled();

namespace heap::base {

// Function with architecture-specific implementation:
Expand Down Expand Up @@ -163,8 +166,16 @@ void Stack::IteratePointersUntilMarker(StackVisitor* visitor) const {
// may not be tagging its portion of the stack, higher frames from the OS or
// libc could be using stack tagging.)
SuspendTagCheckingScope s;
IteratePointersInStack(visitor, current_segment_);
IteratePointersInUnsafeStackIfNecessary(visitor, current_segment_);
Segment segment = current_segment_;
if (v8_qualia_thread_stack_start_override_enabled()) {
// node-fibers switches coroutine stacks under a held Locker without re-entering the
// isolate, so the start recorded by Isolate::Enter() may belong to another coroutine's
// stack. Re-read it (the override points at the running coroutine's stack top) so the
// scan stays within the stack the marker was set on.
segment.start = v8::base::Stack::GetStackStart();
}
IteratePointersInStack(visitor, segment);
IteratePointersInUnsafeStackIfNecessary(visitor, segment);
if (scan_simulator_callback_) {
scan_simulator_callback_(visitor);
}
Expand Down
37 changes: 25 additions & 12 deletions deps/v8/src/wasm/wasm-engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,6 @@ class WasmEngine::LogCodesTask : public CancelableTask {
};

namespace {
void CheckNoArchivedThreads(Isolate* isolate) {
class ArchivedThreadsVisitor : public ThreadVisitor {
void VisitThread(Isolate* isolate, ThreadLocalTop* top) override {
// Archived threads are rarely used, and not combined with Wasm at the
// moment. Implement this and test it properly once we have a use case for
// that.
FATAL("archived threads in combination with wasm not supported");
}
} archived_threads_visitor;
isolate->thread_manager()->IterateArchivedThreads(&archived_threads_visitor);
}

class WasmGCForegroundTask : public CancelableTask {
public:
Expand Down Expand Up @@ -1788,6 +1777,26 @@ void ReportLiveCodeFromFrameForGC(
live_wasm_code.insert(static_cast<WasmToJsFrame*>(frame)->wasm_code());
}
}

// Qualia (node-fibers): every suspended fiber is a Locker-archived thread whose JS stack may
// hold live wasm frames. Upstream refuses to run wasm code GC in that situation
// (FATAL "archived threads in combination with wasm not supported"); instead, report the live
// code from each archived thread's frames, exactly as the GC root visitor walks them in
// Isolate::Iterate(RootVisitor*, ThreadLocalTop*).
class ArchivedThreadsLiveCodeVisitor : public ThreadVisitor {
public:
explicit ArchivedThreadsLiveCodeVisitor(
std::unordered_set<wasm::WasmCode*>& live_wasm_code)
: live_wasm_code_(live_wasm_code) {}
void VisitThread(Isolate* isolate, ThreadLocalTop* top) override {
for (StackFrameIterator it(isolate, top); !it.done(); it.Advance()) {
ReportLiveCodeFromFrameForGC(isolate, it.frame(), live_wasm_code_);
}
}

private:
std::unordered_set<wasm::WasmCode*>& live_wasm_code_;
};
} // namespace

void WasmEngine::ReportLiveCodeFromStackForGC(Isolate* isolate) {
Expand All @@ -1814,7 +1823,11 @@ void WasmEngine::ReportLiveCodeFromStackForGC(Isolate* isolate) {
ReportLiveCodeFromFrameForGC(isolate, frame, live_wasm_code);
}

CheckNoArchivedThreads(isolate);
{
ArchivedThreadsLiveCodeVisitor archived_threads_visitor(live_wasm_code);
isolate->thread_manager()->IterateArchivedThreads(
&archived_threads_visitor);
}

// Flush the code lookup cache, since it may refer to some code we
// are going to release.
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ ObjectDefineProperty(process, 'moduleLoadList', {
// more, we just implement them as legacy wrappers instead. See the
// legacyWrapperList.
const processBindingAllowList = new SafeSet([
// Qualia: node-fibers (fibers_sync.js setupAsyncHacks) saves/restores the async id stack across
// fiber switches through process.binding('async_wrap'). Upstream dropped it from this list; without
// it the hack silently disables itself and the stack corrupts on the first yield.
'async_wrap',
'buffer',
'cares_wrap',
'config',
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ async function restart(child, trigger) {
process.stdout.write(`${blue}Change detected in ${inspect(trigger)}${white}\n`);
}
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
if (process.send) {
// A parent process (e.g. meteor-lite dev-run) uses this to know the child is going down
// before it starts seeing the new child's watch:require / watch:import messages.
process.send({ 'watch:restarting': {} });
}
await stop(child);
return start();
} finally {
Expand Down