Skip to content

perry-ext-net: outbound TCP panics — LTO dead-strips tokio CONTEXT statics #507

Description

@proggeramlug

Summary

On v0.5.586 (and v0.5.572+), any outbound TCP via net.createConnection
panics on a tokio worker thread before any 'connect'/'error'/'close'
event fires. The script appears to hang (or exits 1 with the panic on
stderr if a top-level await is keeping the runtime alive).

The crate self-diagnoses the cause via an eprintln! left in
crates/perry-ext-net/src/lib.rs:359:

[perry-ext-net] BUG: spawn_socket_runner Handle::try_current returned Err — LTO has likely dead-stripped tokio's CONTEXT statics. This will panic on the subsequent Handle::current().

perry-ext-net is a separate crate from perry-stdlib; each links its
own copy of tokio. LTO sees that perry-ext-net's tokio CONTEXT
thread-locals aren't reachable from a static call graph (the runtime
context is entered over in perry-stdlib's tokio copy) and strips
them. When the spawned socket task on a worker thread reaches into
perry-ext-net's Handle::try_current(), it sees the stripped/null
CONTEXT and returns Err. The next Handle::current() panics with
"there is no reactor running, must be called from the context of a
Tokio 1.x runtime"
.

Net effect: outbound TCP via perry-ext-net is unusable, blocking any
program that opens a socket — drivers (@perryts/mongodb, postgres,
mysql), HTTP clients, fastify HTTP server in any direction that
back-fetches, etc.

Repro

net-lto.ts:

import * as net from 'net';

async function main() {
    console.log('createConnection...');
    const result = await new Promise<string>((resolve) => {
        const s = net.createConnection(27017 as never, '127.0.0.1' as never) as {
            on(ev: 'connect', cb: () => void): void;
            on(ev: 'error', cb: (e: unknown) => void): void;
            end(): void;
        };
        s.on('connect', () => { console.log('CONNECTED'); s.end(); resolve('ok'); });
        s.on('error', (e) => { console.log('ERROR: ' + String(e)); resolve('err'); });
    });
    console.log('done: ' + result);
}

main().catch((e) => console.error('ERR:', e));

Runs against any reachable TCP target — 27017 here is a local mongod
but anything listening works equivalently. The remote being closed
would normally just produce an 'error' event; here it produces a
worker-thread panic instead.

perry run net-lto.ts

Observed (perry 0.5.586)

Running net-lto...

createConnection...
[perry-ext-net] BUG: spawn_socket_runner Handle::try_current returned Err — \
LTO has likely dead-stripped tokio's CONTEXT statics. This will panic on \
the subsequent `Handle::current()`.

thread 'tokio-rt-worker' (5202840) panicked at crates/perry-ext-net/src/lib.rs:366:22:
there is no reactor running, must be called from the context of a Tokio 1.x runtime

Exit code 1. No JS-level event ever fires.

A pre-port build (v0.5.527, before perry-ext-net default-on at v0.5.573)
prints CONNECTED followed by done: ok and exits 0 on the same input.

Background — fixes already attempted

The maintainer has tried four passes under the #421 / #420 umbrella
tracking issues (Hono / Drizzle compile targets):

  • v0.5.578 — fix(perry-ext-net+ffi): tokio features + reactor
  • v0.5.579 — fix(perry-stdlib+ext-net): net pump registration + LTO black-box
  • v0.5.580 — fix(perry-ffi): alloc_buffer through extern js_buffer_alloc
  • v0.5.581 — fix(perry-stdlib+ext-net): HANDLE_METHOD_DISPATCH

The current source contains two std::hint::black_box(...) calls and an
eprintln of try_current()'s debug result specifically to keep the
CONTEXT static from being dead-stripped (lines 358 and 371). Despite
all three, LTO still strips the statics on a release build.

Likely-fruitful directions:

  • Force perry-ext-net and perry-stdlib to share the same tokio
    compilation unit (single tokio crate dependency + same feature flags
    resolved through cargo's unification, no static-link duplication).
  • Or have perry-ext-net obtain its handle from a perry-ffi-exported
    function that lives in perry-stdlib's compilation unit, instead of
    calling Handle::current() from its own (LTO-stripped) tokio copy.
  • Or mark the CONTEXT-touching statics with #[used] / linker
    retention attributes so LTO is forbidden from stripping them.

Environment

  • perry 0.5.586 (built from main, today)
  • macOS 26.4 (arm64)
  • Reference baseline: v0.5.527 (pre-perry-ext-net) — same script connects
    and exits 0.

Where this came from

@perryts/mongodb benchmark cell on Perry-AOT — bench's first
MongoClient.connect(...) triggers the panic before the hello round-trip
completes, so the bench can't run on v0.5.572+. Filed alongside
#471 (which can only be
debugged on a perry build where outbound TCP works).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions