mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Exit cleanly with Node loaded and keep nested entries on Node's stack (NI-7c, NI-3b) (#1905)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6870-node-island-server-fibers/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -195,6 +195,26 @@ bool TakeFunction(Isolate *isolate, Local<Context> context, Local<Object> from,
|
||||
return true;
|
||||
}
|
||||
|
||||
// OpenSSL registers OPENSSL_cleanup with atexit on its first
|
||||
// initialization. In a scriptc program that loaded Node, threads Node owns
|
||||
// can still be inside OpenSSL when the process's exit handlers run: the
|
||||
// threads node:tls starts to parse the root certificates (long with
|
||||
// NODE_USE_SYSTEM_CA=1; RunLoop waits for them, but an exit that does not
|
||||
// end the loop does not) are joined only by Node's per-process teardown,
|
||||
// which cannot run here (see Stop). OPENSSL_cleanup
|
||||
// freed OpenSSL's locks and method stores under them (SIGSEGV/SIGABRT at
|
||||
// exit in up to half the runs, NI-7c). Initializing OpenSSL first with
|
||||
// OPENSSL_INIT_NO_ATEXIT keeps that handler from being registered: OpenSSL's
|
||||
// process-lifetime state is left to process exit, like the rest of Node's
|
||||
// per-process state. Packs whose libnode does not export the symbol keep
|
||||
// OpenSSL's default.
|
||||
void KeepOpenSSLAtExit() {
|
||||
using InitCrypto = int (*)(uint64_t, const void *);
|
||||
constexpr uint64_t kNoAtExit = 0x00080000L; // OPENSSL_INIT_NO_ATEXIT (OpenSSL 3)
|
||||
auto init = reinterpret_cast<InitCrypto>(dlsym(RTLD_DEFAULT, "OPENSSL_init_crypto"));
|
||||
if (init != nullptr) init(kNoAtExit, nullptr);
|
||||
}
|
||||
|
||||
uint32_t ExportIdOf(Isolate *isolate, Local<Context> context, Local<Value> v);
|
||||
|
||||
// isExport(value): whether a value is the representative of a native value
|
||||
@@ -214,6 +234,7 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
Dl_info info;
|
||||
if (dladdr(reinterpret_cast<void *>(&napi_create_function), &info) && info.dli_fname)
|
||||
dlopen(info.dli_fname, RTLD_NOW | RTLD_NOLOAD | RTLD_GLOBAL);
|
||||
KeepOpenSSLAtExit();
|
||||
std::vector<std::string> args = {argv0};
|
||||
// The program is not a node process: NODE_OPTIONS from its environment
|
||||
// does not apply, and Node leaves the process's resource limits and code
|
||||
@@ -433,10 +454,18 @@ void OnCap(uv_timer_t *) {
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void OnPrepare(uv_prepare_t *) { SyncIo(SCR_NODE_IO_PREPARE); }
|
||||
// Before each poll phase, while the runtime has live work: one pass over
|
||||
// its dispatch stations, as the runtime's own loop makes one per turn.
|
||||
// Some of their work no fd announces (a server's deferred 'listening' or
|
||||
// 'close', a socket's queued writes), so waiting for an fd could block the
|
||||
// loop forever with that work pending.
|
||||
void OnPrepare(uv_prepare_t *handle) {
|
||||
if (uv_has_ref(reinterpret_cast<uv_handle_t *>(handle))) Dispatch();
|
||||
SyncIo(SCR_NODE_IO_PREPARE);
|
||||
}
|
||||
|
||||
// The end of an iteration: the next one may dispatch again (from its idle
|
||||
// phase when work is ready, else from its poll phase).
|
||||
// or prepare phase, else from its poll phase).
|
||||
void OnCheck(uv_check_t *) {
|
||||
g_dispatched = false;
|
||||
SyncIo(SCR_NODE_IO_CHECK);
|
||||
@@ -616,6 +645,29 @@ void Checkpoint(int microtasks_first) {
|
||||
node::CallbackScope scope(e.isolate, Object::New(e.isolate), node::async_context{0, 0});
|
||||
}
|
||||
|
||||
// Loading node:tls starts threads that parse the root certificates into
|
||||
// function-local statics (bundled, NODE_EXTRA_CA_CERTS, and the system
|
||||
// store with NODE_USE_SYSTEM_CA=1, which takes long). Only Node's
|
||||
// CleanupCachedRootCertificates joins them. Once exit() begins, the
|
||||
// statics' destructors (registered when the threads initialize them, so
|
||||
// they run before any handler the program registered earlier) free the
|
||||
// vectors under a thread that is still parsing, and OpenSSL's state goes
|
||||
// away under it: heap corruption and SIGSEGV/SIGABRT at exit (NI-7c). So
|
||||
// when the loop is done, before the program returns, the bridge waits for
|
||||
// them: building a root store reads the same statics under the same
|
||||
// conditions, and initializing a static blocks until the thread that runs
|
||||
// it is done. A store built earlier means the statics were initialized
|
||||
// then. Programs that never loaded node:tls do nothing here.
|
||||
void AwaitCertificateLoading(Enter &e) {
|
||||
TryCatch try_catch(e.isolate);
|
||||
static const char kSource[] =
|
||||
"(() => { try { if (process.moduleLoadList.includes('NativeModule tls'))"
|
||||
" process.getBuiltinModule('node:tls').createSecureContext(); } catch {} })()";
|
||||
Local<v8::Script> script;
|
||||
if (v8::Script::Compile(e.context, Utf8(e.isolate, kSource)).ToLocal(&script))
|
||||
(void)script->Run(e.context);
|
||||
}
|
||||
|
||||
int RunLoop(int continue_turn) {
|
||||
Enter e;
|
||||
// The rest of the turn the switch interrupted: libuv's UV_RUN_ONCE skips
|
||||
@@ -624,7 +676,9 @@ int RunLoop(int continue_turn) {
|
||||
uv_run(g_setup->event_loop(), UV_RUN_ONCE);
|
||||
g_platform->DrainTasks(e.isolate);
|
||||
}
|
||||
return node::SpinEventLoop(g_setup->env()).FromMaybe(0);
|
||||
int code = node::SpinEventLoop(g_setup->env()).FromMaybe(0);
|
||||
AwaitCertificateLoading(e);
|
||||
return code;
|
||||
}
|
||||
|
||||
// An island listener of a process event: through MakeCallback, so a throw
|
||||
@@ -653,9 +707,16 @@ void Stop() {
|
||||
delete g_locker;
|
||||
g_locker = nullptr;
|
||||
g_setup.reset();
|
||||
// As Node's own exit does (DefaultProcessExitHandlerInternal): join
|
||||
// libuv's thread pool, whose work (pbkdf2, key generation, ...) may be
|
||||
// inside OpenSSL, before the process's exit handlers clean OpenSSL up.
|
||||
uv_library_shutdown();
|
||||
v8::V8::Dispose();
|
||||
v8::V8::DisposePlatform();
|
||||
node::TearDownOncePerProcess();
|
||||
// Node's per-process teardown is left to process exit: Stop runs from an
|
||||
// exit handler registered when Node started, after the destructors of
|
||||
// the certificate statics (registered later) already ran, so its
|
||||
// certificate cleanup would free them a second time.
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1701,9 +1701,26 @@ static void *scr_win_self(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* The lowest frame of Node's stack that is still live while a fiber runs:
|
||||
* where runtime code on Node's stack (a job or host callback, however deep
|
||||
* it went) last switched to a fiber. A call into Node from that fiber runs
|
||||
* on Node's stack below it (scr_node_island.c), never over those frames.
|
||||
* 0: none. scr_node_stack_has (set when Node starts) tells Node's stack. */
|
||||
uintptr_t scr_node_stack_low = 0;
|
||||
bool (*scr_node_stack_has)(uintptr_t sp) = NULL;
|
||||
#endif
|
||||
|
||||
static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
||||
#ifdef SCR_ASAN_FIBERS
|
||||
ScrFiber *from_fiber = scr_current;
|
||||
#endif
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
uintptr_t node_low = scr_node_stack_low;
|
||||
if (to_fiber != NULL && scr_node_stack_has != NULL) {
|
||||
uintptr_t here = (uintptr_t)__builtin_frame_address(0);
|
||||
if (scr_node_stack_has(here)) scr_node_stack_low = here;
|
||||
}
|
||||
#endif
|
||||
scr_current = to_fiber;
|
||||
scr_exc_swap_cell(to_fiber ? &to_fiber->exc : NULL);
|
||||
@@ -1756,8 +1773,59 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
SCR_STACK_GUARD_SET(guard);
|
||||
#endif
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
scr_node_stack_low = node_low; /* back on the stack that switched away */
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* A nested entry into Node (scr_node_island.c) runs on Node's stack in a
|
||||
* context of its own while the fiber that made the call stays suspended in
|
||||
* it. Runtime code that Node calls back there runs as the loop's code does:
|
||||
* no current fiber, the main exception cell and ALS slot. It may resume
|
||||
* other fibers, which saves the resumer's context in scr_loop_ctx (and,
|
||||
* under ASan, its fake stack in the main slot); those belong to whoever
|
||||
* resumed the calling fiber, so they are saved here and put back when the
|
||||
* nested entry returns, or the calling fiber's next park would return to a
|
||||
* context that no longer exists. */
|
||||
typedef struct {
|
||||
ScrFiber *current;
|
||||
ScrAlsCtx **als;
|
||||
ScrCtx loop_ctx;
|
||||
#ifdef SCR_ASAN_FIBERS
|
||||
void *main_fake;
|
||||
#endif
|
||||
} ScrNestedSave;
|
||||
|
||||
void *scr_loop_nested_enter(void) {
|
||||
ScrNestedSave *s = malloc(sizeof *s);
|
||||
if (s == NULL) scr_oom();
|
||||
s->current = scr_current;
|
||||
s->als = scr_als_active;
|
||||
s->loop_ctx = scr_loop_ctx;
|
||||
#ifdef SCR_ASAN_FIBERS
|
||||
s->main_fake = scr_main_fake_stack;
|
||||
scr_main_fake_stack = NULL;
|
||||
#endif
|
||||
scr_current = NULL;
|
||||
scr_exc_swap_cell(NULL);
|
||||
scr_als_active = &scr_als_main_slot;
|
||||
return s;
|
||||
}
|
||||
|
||||
void scr_loop_nested_leave(void *save) {
|
||||
ScrNestedSave *s = save;
|
||||
scr_loop_ctx = s->loop_ctx;
|
||||
#ifdef SCR_ASAN_FIBERS
|
||||
scr_main_fake_stack = s->main_fake;
|
||||
#endif
|
||||
scr_current = s->current;
|
||||
scr_exc_swap_cell(s->current ? &s->current->exc : NULL);
|
||||
scr_als_active = s->als;
|
||||
free(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void scr_promise_settle_wake(ScrPromise *p);
|
||||
|
||||
/* Copies src's settlement into a still-pending dst (payload RETAINED —
|
||||
|
||||
@@ -93,6 +93,25 @@ void __sanitizer_finish_switch_fiber(void *fake_stack_save, const void **bottom_
|
||||
size_t *size_old);
|
||||
void __lsan_register_root_region(const void *p, size_t size) __attribute__((weak));
|
||||
void __asan_unpoison_memory_region(void const volatile *addr, size_t size);
|
||||
|
||||
/* LeakSanitizer's report at exit, with Node loaded: Node keeps its root
|
||||
* certificate stores (the bundled roots, the system's with
|
||||
* NODE_USE_SYSTEM_CA, the extra ones of NODE_EXTRA_CA_CERTS) for the
|
||||
* process's life and never frees them, and OpenSSL keeps the compression
|
||||
* methods it loads with its default library context. Only those loaders
|
||||
* are suppressed; a leak anywhere else, in libnode or in the program,
|
||||
* still reports. */
|
||||
__attribute__((used)) const char *__lsan_default_suppressions(void) {
|
||||
return "leak:node::crypto::LoadCACertificates\n"
|
||||
"leak:node::crypto::GetBundledRootCertificates\n"
|
||||
"leak:node::crypto::LoadSystemCACertificates\n"
|
||||
"leak:node::crypto::GetExtraCACertificates\n"
|
||||
"leak:ossl_load_builtin_compressions\n";
|
||||
}
|
||||
|
||||
/* Without the table of suppressions used, which LeakSanitizer otherwise
|
||||
* prints to stderr at every exit (LSAN_OPTIONS still overrides it). */
|
||||
__attribute__((used)) const char *__lsan_default_options(void) { return "print_suppressions=0"; }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -954,8 +973,15 @@ static void ni_nested_trampoline(void) {
|
||||
static void ni_on_node_stack_below_mark(void (*job)(void *), void *arg) {
|
||||
ucontext_t nested, caller;
|
||||
char *lo = ni_stack + NI_GUARD;
|
||||
/* Below the red zone and 16-byte aligned, as makecontext expects. */
|
||||
uintptr_t hi = (ni_mark - 512) & ~(uintptr_t)15;
|
||||
/* Below every live frame of Node's stack: the mark the host callback
|
||||
* recorded, or lower where runtime code it ran went deeper before it
|
||||
* switched to this fiber (an async call's eager start, a job: the
|
||||
* spawner's frame and its saved context are still in use there). Below
|
||||
* the red zone and the swap's own frame, 16-byte aligned, as makecontext
|
||||
* expects. */
|
||||
uintptr_t top = ni_mark;
|
||||
if (scr_node_stack_low != 0 && scr_node_stack_low < top) top = scr_node_stack_low;
|
||||
uintptr_t hi = (top - 1024) & ~(uintptr_t)15;
|
||||
ni_make_context(&nested, lo, hi - (uintptr_t)lo, ni_nested_trampoline);
|
||||
#ifdef NI_ASAN
|
||||
ni_nested_top = hi;
|
||||
@@ -965,6 +991,7 @@ static void ni_on_node_stack_below_mark(void (*job)(void *), void *arg) {
|
||||
ni_nested_return = &caller;
|
||||
uintptr_t guard = scr_stack_guard;
|
||||
SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)lo, hi - (uintptr_t)lo));
|
||||
void *loop_save = scr_loop_nested_enter();
|
||||
#ifdef NI_ASAN
|
||||
void *caller_fake = NULL;
|
||||
__sanitizer_start_switch_fiber(&caller_fake, lo, hi - (uintptr_t)lo);
|
||||
@@ -975,6 +1002,7 @@ static void ni_on_node_stack_below_mark(void (*job)(void *), void *arg) {
|
||||
size_t size;
|
||||
__sanitizer_finish_switch_fiber(caller_fake, &bottom, &size);
|
||||
#endif
|
||||
scr_loop_nested_leave(loop_save);
|
||||
SCR_STACK_GUARD_SET(guard);
|
||||
}
|
||||
|
||||
@@ -994,6 +1022,7 @@ static void ni_on_node_stack(void (*job)(void *), void *arg) {
|
||||
ni_stack = mmap(NULL, NI_STACK, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (ni_stack == MAP_FAILED) ni_oom();
|
||||
(void)mprotect(ni_stack, NI_GUARD, PROT_NONE);
|
||||
scr_node_stack_has = &ni_stack_has;
|
||||
#ifdef NI_ASAN
|
||||
if (__lsan_register_root_region) __lsan_register_root_region(ni_stack, NI_STACK);
|
||||
#endif
|
||||
|
||||
@@ -43,6 +43,16 @@ ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name,
|
||||
struct ScrNodeBridge;
|
||||
struct ScrNodeIoState;
|
||||
void scr_loop_host(const struct ScrNodeBridge *bridge, double since_ms);
|
||||
/* The lowest live frame of Node's stack while a fiber runs (the SCR_NODE_ISLANDS
|
||||
* build of scr_async.c records it at each switch from Node's stack to a
|
||||
* fiber; 0: none), and the test for Node's stack it uses. */
|
||||
extern uintptr_t scr_node_stack_low;
|
||||
extern bool (*scr_node_stack_has)(uintptr_t sp);
|
||||
/* Around a nested entry: the runtime's scheduling state of the suspended
|
||||
* calling fiber, saved and put back (the SCR_NODE_ISLANDS build of
|
||||
* scr_async.c). */
|
||||
void *scr_loop_nested_enter(void);
|
||||
void scr_loop_nested_leave(void *save);
|
||||
bool scr_loop_is_hosted(void);
|
||||
void scr_loop_hosted_run(uint32_t kind, uint32_t token);
|
||||
void scr_loop_hosted_io_state(struct ScrNodeIoState *state);
|
||||
|
||||
@@ -1424,6 +1424,10 @@ static const ScrDynHandleOps nh_ops = {
|
||||
* own objects. */
|
||||
static void nh_cleanup(void) {
|
||||
nh_drain_freed();
|
||||
/* What the last conversion for the export unit kept alive. */
|
||||
NhKeep last = nh_out_keep;
|
||||
memset(&nh_out_keep, 0, sizeof nh_out_keep);
|
||||
nh_keep_done(&last);
|
||||
if (nh_inspect_fn != NULL) {
|
||||
scr_dyn_release(nh_inspect_fn);
|
||||
nh_inspect_fn = NULL;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// @requires-libnode
|
||||
// An HTTP server whose async request handler awaits island promises, calls
|
||||
// island functions, extends an island class and arms timers, while an
|
||||
// island client drives it (a long-running service's shape): the handler's
|
||||
// continuations run as runtime jobs that Node's loop
|
||||
// dispatches, possibly on an async fiber's stack, and every entry into Node
|
||||
// from there must run on Node's stack, where V8 scans it during the
|
||||
// collections the island's allocations cause.
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { churn, delay, depth, get, Handler } from "server-kit";
|
||||
|
||||
class CountingHandler extends Handler {
|
||||
served = 0;
|
||||
async handle(path: string): Promise<string> {
|
||||
this.served++;
|
||||
return `${await super.handle(path)}#${this.served}`;
|
||||
}
|
||||
}
|
||||
|
||||
const handler = new CountingHandler("kit");
|
||||
const timers: string[] = [];
|
||||
|
||||
// Called from a continuation (a runtime job on Node's stack): its eager
|
||||
// start runs on a fiber and calls into the island, deep, before its first
|
||||
// await returns to the caller's frame on Node's stack.
|
||||
async function listFiles(path: string, wait: (n: number) => Promise<number>): Promise<unknown> {
|
||||
const out: string[] = [];
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
out.push(`${path}:${depth(300 + i)}`);
|
||||
}
|
||||
const more = await wait(out.length);
|
||||
out.push(String(more));
|
||||
} finally {
|
||||
out.push("done");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Starts listFiles a few native frames deeper on the current stack.
|
||||
function startDeep(levels: number, path: string): Promise<unknown> {
|
||||
const parts = [path, String(levels), path.length.toString(16)];
|
||||
if (levels > 0) return startDeep(levels - 1, parts[0]!);
|
||||
return listFiles(parts.join("").slice(0, path.length), (n) => delay(1, n));
|
||||
}
|
||||
|
||||
async function respond(path: string): Promise<string> {
|
||||
const waited = await delay(1, path);
|
||||
const files = (await startDeep(24, waited)) as string[];
|
||||
let total = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
total += churn(i) > 0 ? 1 : 0;
|
||||
await delay(0);
|
||||
}
|
||||
const handled = await handler.handle(waited);
|
||||
await new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
timers.push(path);
|
||||
resolve();
|
||||
}, 1),
|
||||
);
|
||||
return `${handled} churn=${total} files=${files.length}`;
|
||||
}
|
||||
|
||||
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
const path = req.url ?? "/";
|
||||
respond(path).then(
|
||||
(text) => {
|
||||
res.writeHead(200, { "content-type": "text/plain" });
|
||||
res.end(text);
|
||||
},
|
||||
(error: unknown) => {
|
||||
res.writeHead(500);
|
||||
res.end(String(error));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address !== null ? address.port : 0;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const reply = await get(port, `/r${i}`);
|
||||
if (i % 10 === 0 || reply.status !== 200) console.log(reply.status, reply.body);
|
||||
}
|
||||
console.log("timers", timers.length, "served", handler.served, "count", handler.count);
|
||||
server.close(() => console.log("closed"));
|
||||
}
|
||||
|
||||
void main();
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export interface Reply {
|
||||
status: number;
|
||||
body: string;
|
||||
}
|
||||
export declare function get(port: number, path: string): Promise<Reply>;
|
||||
export declare function delay<T>(ms: number, value?: T): Promise<T>;
|
||||
export declare function churn(seed: number): number;
|
||||
export declare function depth(n: number): number;
|
||||
export declare class Handler {
|
||||
name: string;
|
||||
count: number;
|
||||
constructor(name: string);
|
||||
handle(path: string): Promise<string>;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
// An island package shaped like a service's HTTP helpers: a
|
||||
// client that requests over HTTP and answers promises, a base class that
|
||||
// native code extends, and work that allocates enough to collect garbage.
|
||||
const http = require("node:http");
|
||||
|
||||
function get(port, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.get({ host: "127.0.0.1", port, path, agent: false }, (res) => {
|
||||
let body = "";
|
||||
res.setEncoding("utf8");
|
||||
res.on("data", (chunk) => (body += chunk));
|
||||
res.on("end", () => resolve({ status: res.statusCode, body }));
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function delay(ms, value) {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
||||
}
|
||||
|
||||
// Allocates and drops a few megabytes, so collections run while native
|
||||
// callers are on the stack.
|
||||
function churn(seed) {
|
||||
let keep = 0;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const a = new Array(64).fill(seed + i);
|
||||
keep += a.length + JSON.stringify({ i, a: a.slice(0, 4) }).length;
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
|
||||
// Recursion deep enough to use tens of kilobytes of the stack it runs on.
|
||||
function depth(n) {
|
||||
const pad = [n, n + 1, n + 2, n + 3];
|
||||
return n <= 0 ? pad.length : depth(n - 1) + (pad[n % 4] > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
class Handler {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.count = 0;
|
||||
}
|
||||
async handle(path) {
|
||||
this.count++;
|
||||
await delay(1);
|
||||
return `${this.name}:${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { get, delay, churn, depth, Handler };
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "server-kit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-server-fibers", "private": true, "type": "module", "scriptc": { "packages": { "server-kit": "node" } } }
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Island work that keeps OpenSSL busy while the program exits
|
||||
// (exit-tls.mjs): loading node:tls starts Node's off-thread loading of the
|
||||
// root certificates (system ones with NODE_USE_SYSTEM_CA=1, extra ones with
|
||||
// NODE_EXTRA_CA_CERTS); pbkdf2 and key generation run on libuv's pool.
|
||||
"use strict";
|
||||
const crypto = require("node:crypto");
|
||||
const tls = require("node:tls");
|
||||
|
||||
exports.warm = function warm(mode) {
|
||||
if (mode === "load") return tls.DEFAULT_MIN_VERSION.length > 0;
|
||||
tls.createSecureContext();
|
||||
const roots = tls.rootCertificates.length > 0;
|
||||
for (let i = 0; i < 8; i++) crypto.pbkdf2("password", "salt", 20000, 64, "sha512", () => {});
|
||||
if (mode === "keys") crypto.generateKeyPair("ec", { namedCurve: "P-256" }, () => {});
|
||||
return roots;
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Exit while Node has OpenSSL work in flight (node-island-exit.test.ts):
|
||||
// Node must stop its threads before the process's exit handlers tear
|
||||
// OpenSSL down. argv[2]: "early" (exit right after Node starts, while it
|
||||
// loads the system root certificates on a thread of its own with
|
||||
// NODE_USE_SYSTEM_CA=1), "load" (node:tls loaded, extra certificates
|
||||
// loading off-thread), "exit" (process.exit with pool work in flight),
|
||||
// "keys", or "return" (fall off the end). A "-return" suffix ends the
|
||||
// program by returning instead of process.exit (C exit and its handlers).
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const arg = process.argv[2] ?? "exit";
|
||||
const returns = arg.endsWith("-return") || arg === "return";
|
||||
const mode = arg.replace(/-return$/, "");
|
||||
if (mode === "early") {
|
||||
console.log("roots", requireModule("./helper.cjs") !== null);
|
||||
} else {
|
||||
const lib = requireModule("./exit-tls.cjs");
|
||||
console.log("roots", lib.warm(mode));
|
||||
}
|
||||
if (!returns) process.exit(0);
|
||||
@@ -2,3 +2,24 @@
|
||||
# (scripts/ci-test-plan.mjs). One `leak:<function>` line per intentional
|
||||
# leak, under a comment that gives the reason. Empty: no corpus program
|
||||
# leaks.
|
||||
|
||||
# Node islands (scope-B NI-7c): OpenSSL inside libnode is initialized with
|
||||
# OPENSSL_INIT_NO_ATEXIT (scr_node_bridge.cc, KeepOpenSSLAtExit), so its
|
||||
# process-lifetime state is never freed: OPENSSL_cleanup at exit raced the
|
||||
# threads Node owns that can still be inside OpenSSL.
|
||||
leak:OPENSSL_init_crypto
|
||||
# Node islands (scope-B NI-7c): Node's root certificates (bundled, system
|
||||
# store, NODE_EXTRA_CA_CERTS) live in function-local statics that Node's
|
||||
# per-process teardown frees; a scriptc program leaves that teardown to
|
||||
# process exit (scr_node_bridge.cc, Stop), and the vectors' own destructors
|
||||
# drop the last pointers at exit.
|
||||
leak:node::crypto::LoadCACertificates
|
||||
leak:node::crypto::LoadSystemCACertificates
|
||||
leak:node::crypto::GetBundledRootCertificates
|
||||
leak:node::crypto::GetSystemStoreCACertificates
|
||||
leak:node::crypto::GetExtraCACertificates
|
||||
leak:node::crypto::GetOpenSSLSystemCertificates
|
||||
leak:node::crypto::LoadCertsFromBIO
|
||||
# The certificates' lazily decoded keys, cached in them when a root store
|
||||
# is built.
|
||||
leak:node::crypto::NewRootCertStore
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Exit with Node loaded (scope-B NI-7c): island code loads node:tls (Node
|
||||
* then loads the root certificates on threads of its own) or leaves OpenSSL
|
||||
* work on libuv's thread pool, and the program exits at once. The bridge's
|
||||
* stop runs Node's own exit sequence (join the pool, dispose the platform,
|
||||
* join the certificate threads) before the process's exit handlers run
|
||||
* OpenSSL's cleanup; without it a certificate thread inside OpenSSL crashed
|
||||
* the exit (SIGSEGV/SIGABRT in up to half the runs).
|
||||
* Many runs per mode; SCRIPTC_NODE_ISLAND_EXIT_RUNS scales them. Needs a
|
||||
* libnode pack in SCRIPTC_LIBNODE. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
import { libnodePack } from "./libnode.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const fixture = join(repoRoot, "tests/fixtures/node-island/exit-tls.mjs");
|
||||
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/node-island-exit");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
const pack = libnodePack();
|
||||
const runs = Number(process.env["SCRIPTC_NODE_ISLAND_EXIT_RUNS"] ?? (sanitize ? "10" : "40"));
|
||||
|
||||
let binary = "";
|
||||
// Node loads the system and extra root certificates on their own threads
|
||||
// when node:tls loads, with these set: the threads the exit must wait for.
|
||||
const caFile = ["/etc/ssl/certs/ca-certificates.crt", "/etc/ssl/cert.pem"].find((f) =>
|
||||
existsSync(f),
|
||||
);
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
NODE_USE_SYSTEM_CA: "1",
|
||||
...(caFile === undefined ? {} : { NODE_EXTRA_CA_CERTS: caFile }),
|
||||
};
|
||||
|
||||
async function runOnce(mode: string): Promise<{ code: number | string; stdout: string }> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(binary, [mode], { encoding: "utf8", env });
|
||||
return { code: 0, stdout };
|
||||
} catch (err) {
|
||||
const e = err as { code?: number; signal?: string; stdout?: string };
|
||||
return { code: e.signal ?? e.code ?? "?", stdout: e.stdout ?? "" };
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(pack === null)("Node islands: exit with OpenSSL work in flight", () => {
|
||||
beforeAll(async () => {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(fixture, {
|
||||
outPath: join(outDir, "exit-tls"),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
binary = result.binaryPath;
|
||||
}, 300_000);
|
||||
|
||||
test.each([
|
||||
"early",
|
||||
"load",
|
||||
"exit",
|
||||
"keys",
|
||||
"return",
|
||||
"early-return",
|
||||
"load-return",
|
||||
"keys-return",
|
||||
])(
|
||||
"%s: every run exits 0",
|
||||
async (mode) => {
|
||||
const outcomes: (number | string)[] = [];
|
||||
for (let i = 0; i < runs; i += 4) {
|
||||
const batch = await Promise.all(
|
||||
Array.from({ length: Math.min(4, runs - i) }, () => runOnce(mode)),
|
||||
);
|
||||
for (const r of batch) {
|
||||
outcomes.push(r.code);
|
||||
expect(r.stdout).toBe(`roots true\n`);
|
||||
}
|
||||
}
|
||||
expect(outcomes.filter((c) => c !== 0)).toEqual([]);
|
||||
},
|
||||
600_000,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user