Start nested Node entries below the frames a fiber switch left live

A host callback marks where it runs on Node's stack, and a call into
Node.js from a fiber starts below that mark. Runtime code the callback
runs may go deeper on Node's stack before it starts or resumes a fiber,
and those frames stay live while the fiber runs: a call into Node.js
from the fiber overwrote them. scr_switch now reports where it leaves
Node's stack, and the mark moves down to it for the rest of the
callback.

6823 runs a timer callback 200 frames deep that starts an async
function calling into Node.js before its first await.
This commit is contained in:
Chris Tate
2026-10-25 19:54:03 -05:00
parent 27954618c7
commit 1a2eb52016
7 changed files with 59 additions and 2 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6823-node-island-nested-stack/main.ts"
],
"diags": []
}
+5
View File
@@ -28,6 +28,7 @@
#include "scr_runtime.h"
#ifdef SCR_NODE_ISLANDS
#include "scr_node_bridge.h"
#include "scr_node_island.h"
#endif
#include <errno.h>
@@ -1659,6 +1660,10 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
uintptr_t guard = scr_stack_guard;
if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK));
#endif
#ifdef SCR_NODE_ISLANDS
/* Leaving Node's stack keeps this frame and those above it live. */
scr_node_island_leave_stack((uintptr_t)__builtin_frame_address(0));
#endif
#ifdef _WIN32
/* SwitchToFiber snapshots the outgoing fiber's state inside its own
* fiber object — `from` has nothing to record. */
+8 -2
View File
@@ -865,8 +865,10 @@ static bool ni_stack_has(uintptr_t sp) {
return ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK;
}
/* The lowest address of Node's stack its frames use while runtime code
* they called runs (a host callback below), 0 outside them. Runtime code
/* The lowest address of Node's stack in use while runtime code Node's
* frames called runs (a host callback below), 0 outside them: the
* callback's frame, or lower, where that code switched to a fiber's stack
* (scr_node_island_leave_stack) and left its own frames live. Runtime code
* may resume an async fiber there, and the fiber may call into Node again:
* that entry runs on Node's stack below this mark, so every frame of Node
* stays on the one stack V8 scans (heap::base::Stack, bounded by
@@ -883,6 +885,10 @@ static uintptr_t ni_mark = 0;
} while (0)
#define NI_MARK_END (ni_mark = ni_saved_mark)
void scr_node_island_leave_stack(uintptr_t sp) {
if (ni_mark != 0 && ni_stack_has(sp) && sp < ni_mark) ni_mark = sp;
}
/* A nested entry: job(arg) on Node's stack below the mark, from a fiber. */
static void (*ni_nested_job)(void *) = NULL;
static void *ni_nested_arg = NULL;
+5
View File
@@ -49,6 +49,11 @@ void scr_loop_hosted_io_state(struct ScrNodeIoState *state);
void scr_loop_hosted_io(void);
void scr_loop_hosted_microtasks_done(void);
/* Runtime code switches stacks (scr_switch) at `sp`: when that is Node's
* stack, the frames above stay live until it switches back, so a call
* into Node.js from the other stack runs below them. */
void scr_node_island_leave_stack(uintptr_t sp);
/* Process events island code listens to (SCR_NODE_EVENT_* of the bridge
* ABI). Their listeners sit in the runtime's registries (the
* SCR_NODE_ISLANDS build of scr_events.c) at their registration position,
@@ -0,0 +1,8 @@
// Returns its argument after using some of Node's stack.
function down(n, depth) {
return depth === 0 ? n : down(n, depth - 1);
}
exports.echo = function (n) {
return down(n, 500);
};
@@ -0,0 +1,26 @@
// @requires-libnode
// A timer on Node's loop runs runtime code deep below the callback's own
// frame, and an async function it starts calls into Node.js before its
// first await: that call's frames go below the deepest live runtime frame
// on Node's stack, so the frames above it return intact.
import { callSync } from "@scriptc/node";
async function viaNode(n: number): Promise<number> {
const echoed = callSync<number>("./echo.cjs", "echo", n);
await null;
return echoed;
}
const started: Promise<number>[] = [];
function deep(depth: number, n: number): number {
const local = [depth, n, depth * n];
if (depth > 0) return deep(depth - 1, n) + local[2]!;
started.push(viaNode(n));
return local[0]! + local[1]!;
}
console.log("loaded", callSync<number>("./echo.cjs", "echo", 1));
setTimeout(() => {
console.log("deep", deep(200, 3));
void Promise.all(started).then((values) => console.log("echoed", values.join(",")));
}, 0);
@@ -0,0 +1 @@
{ "plain": 3000, "san": 20000 }