Skip to content

Native server SIGSEGV at jwt.sign(...) (jsonwebtoken) after resumed async-step body — follow-up to #859 #915

Description

@proggeramlug

Filed as follow-up to #859 (which is fixed for what it targeted — minimal arrow-after-resumed-async-step repro now passes byte-identical to tsx). The shop-admin signup at v0.5.953 still SIGSEGVs, but at a different site with a different crash signature, and I've now reduced it to an 11-line MySQL-free standalone.

Minimal repro (11 lines, no MySQL, no @perryts/*, no argon2)

import Fastify from "fastify";
import jwt from "jsonwebtoken";
const app = Fastify({ logger: false });
async function delay() { return Promise.resolve(); }
async function getThing() { await delay(); return undefined; }
app.get("/healthz", async () => ({ ok: true }));
app.post("/go", async (_req, reply) => {
  await getThing();
  const token = jwt.sign({ sub: "x" }, "secret", { algorithm: "HS256" });
  void reply.code(201);
  return { ok: true, len: token.length };
});
await app.listen({ host: "127.0.0.1", port: 18099 });
console.log("listening");
PERRY_ALLOW_UNIMPLEMENTED=1 perry compile jwt-min.ts -o /tmp/jwt-min
/tmp/jwt-min &
sleep 1
curl -X POST http://127.0.0.1:18099/go
# → curl: (52) Empty reply from server; exit 139.

tsx jwt-min.ts returns {"ok":true,"len":89} HTTP 201. 3/3 native runs reliably SIGSEGV.

Crash signature

thread #1, EXC_BAD_ACCESS (code=1, address=0x39edd942)
    frame #0: jwt-min + 0x10000a4b0    ldr w1, [x0, #0x4]
(lldb) register read x0
      x0 = 0x0000000039edd93e   ← 32-bit garbage interpreted as object pointer

Same instruction (ldr w1, [x0, #0x4]) and same dereference pattern as the shop-admin signup crash (which fired at address=0xe6). Different small-int garbage in x0 per run, but the load-32-bit-at-offset-4-of-x0 shape is identical.

Load-bearing ingredients (drop any → no crash)

Numbered against my local bisect (m1–m5 + k variants in the issue-859-repro/ tree of the user repo):

Change Result
Replace jwt.sign with validator.isEmail (also a NATIVE_MODULES binding) in the exact same shape ALIVE 201
Replace jwt.sign with a top-level function syncWork(s) {...} ALIVE 201
Replace await getThing() with await delay() inline (drop the nested fn) ALIVE 201
Replace await getThing() with await Promise.resolve() inline ALIVE 201 (server stays up, request hangs)
import * as jwt (namespace) vs import jwt (default) both fail with same crash
import { sign } (named, call sign(...) directly) fails identically
jwt.sign("string", ...) vs jwt.sign({...}, ...) both fail
HS256 vs ES256 algorithm both fail

So the exact minimum trigger is:

  1. Fastify async route handler
  2. Nested user async function awaited from the handler (await getThing()), whose body itself awaits another named async function (await delay())
  3. A jsonwebtoken sign call after that — call shape doesn't matter (default-import method, named-import, namespace; HS256, ES256; string or object claims).

The bug is specific to jsonwebtoken as the imported callee; validator.isEmail (also in perry_api_manifest::NATIVE_MODULES) in the exact same surrounding shape does not crash. So this is a jsonwebtoken perry-stdlib binding interacting badly with the resumed-async-step body — analogous to #859's "module-level const arrow callee" pattern but for a NATIVE_MODULES binding rather than a user-defined arrow.

What I've ruled out

Files

issue-859-repro/jwt-min.ts in the user repo, alongside the previously-committed minimal repro for the closed #859 case (minimal.ts, now passing). No external setup — just fastify + jsonwebtoken in node_modules, perry 0.5.953.

Note on the original shop-admin signup

Same instruction, same crash class, same surrounding 7-await preamble. DB after the crash now shows users=1, accounts=1, sessions=1, members=1, auditLog=0createUser through issueSession all commit, issueAccessToken (the jwt.sign call) is where it dies. So fixing this should close the residual shop-admin blocker.

Local checkout: clean at perry HEAD (v0.5.953), no source edits.

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