From b45cb28ece745a818bebd4098696d1459111ca03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 18 May 2026 12:25:00 +0200 Subject: [PATCH] =?UTF-8?q?fix(jsruntime):=20hono=20SIGSEGV=20=E2=80=94=20?= =?UTF-8?q?guard=20small-handle=20pointers=20in=20native=5Fobject=5Fto=5Fv?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #987 unblocked hono compilation by bundling transitive ESM imports, but the binary then SIGSEGV'd at `app.fetch(req)` because `native_object_to_v8` treated bridge "small handles" (low-value integers stored in JS_HANDLE_TAG) as raw pointers to ObjectHeader, dereferencing invalid memory. Added a small-handle guard in bridge.rs that returns the handle as a V8 number instead of attempting pointer indirection. Hono no longer crashes; next blocker is V8 fallback's missing `Response` global (separate concern). --- CHANGELOG.md | 67 +++++++++++++ CLAUDE.md | 2 +- Cargo.lock | 136 +++++++++++++-------------- Cargo.toml | 2 +- crates/perry-jsruntime/src/bridge.rs | 83 ++++++++++++++++ 5 files changed, 220 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5603e6b09..a33898f4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,73 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.1005 — fix(jsruntime): hono `app.fetch(req)` SIGSEGV — guard small-handle pointers in `native_object_to_v8` + +Hono's compat-sweep fixture +(`/tmp/perry-compat-sweep/hono/entry.ts`) compiled cleanly (PR #987 fixed +the bundle resolution) but crashed with `rc=139` (SIGSEGV) and empty +stdout the moment the program reached `app.fetch(req)`. + +Root cause: perry-stdlib's Web Fetch handles +(`Request` / `Response` / `Headers` / `Blob`) are returned by their +constructors as `POINTER_TAG`-tagged f64 values whose lower 48 bits hold +a small registry id (1, 2, 3, ...) — see +`perry_stdlib::fetch::handle_to_f64`. The runtime side already accounts +for this with a `(obj as usize) < 0x100000` carve-out that routes +small-handle reads through `HANDLE_PROPERTY_DISPATCH` +(`crates/perry-runtime/src/object.rs:4665`). But the V8 bridge in +`perry_jsruntime::bridge::native_object_to_v8` had no such guard: it +unconditionally computed `gc_header_ptr = (ptr as usize) +.wrapping_sub(GC_HEADER_SIZE)` and dereferenced after a +`> 0x1000` bounds check. With `ptr = 1`, the subtraction wraps to +`0xffff_ffff_ffff_fff8`, which is `> 0x1000` as unsigned, so the +dereference proceeds and the loaded address (`0xfffffffffffffffa` for +`gc_header.gc_flags`) faults. The fault was silent because the binary +strips symbols by default. + +The lldb backtrace pins it to +`native_object_to_v8` ← `js_call_method` closure ← `js_call_method` ← +`perry_closure_t5_fetch_ts__5`, and the args buffer at the crash site +contains `0x7FFD_0000_0000_0001` (POINTER_TAG | 1) — exactly the value +`new Request("...")` returned. + +This patch: + +1. Adds a small-handle guard at the top of `native_object_to_v8`: any + `ptr < 0x10_0000` short-circuits to a new + `materialize_web_fetch_handle` helper instead of dereferencing. +2. The helper probes `perry_stdlib::dispatch_request_property` and + `dispatch_response_property` to identify the handle kind, then + builds a `v8::Object` snapshot exposing the scalar properties + (`url`/`method`/`body` for Request, `status`/`statusText`/`ok` for + Response) so V8-side code (hono, sveltekit, etc.) can read them via + plain property access. +3. Unknown small ids return `v8::null` — safe no-op, no segfault. + +After this fix, the hono fixture no longer crashes; it reaches the +expected next blocker (hono's `c.text(...)` building a `new Response(...)` +in the V8 context, where `Response` isn't a registered global). Exit +code goes from `139` to `0` with a clean V8 `ReferenceError: Response is +not defined` instead of a silent SIGSEGV. + +Followup work tracked separately: + +- Method bridging for Web Fetch handles (`req.text()`, + `res.arrayBuffer()`, `headers.get(k)`) needs a v8 Proxy that calls + back through `HANDLE_METHOD_DISPATCH`. +- Registering `Response` / `Request` / `Headers` / `URL` as globals in + the V8 context so hono's response construction works end-to-end. + +Validation: + +- Bisection harness `/tmp/hono-bisect/{t1..t5}.ts` — minimal `import`, + ctor, `app.get(...)`, `new Request(...)`, and `app.fetch(req)` — + isolates the crash to `app.fetch(req)`; t5 now exits cleanly. +- `/tmp/perry-compat-sweep/hono/entry.ts` exit code: 139 → 0. +- `/tmp/req_smoke.ts` (perry-native `new Request(...) + req.url/method/body`) + still prints expected values via the unchanged perry-native path. +- Spot-checked unrelated gap suites (`test_gap_closures.ts`) — pass. + ## v0.5.1004 — fix(runtime): `js_async_step_chain` awaits V8 Promise handles instead of treating them as primitives PR #1004 landed the marshalling layer for jose JWT (HS256) sign/verify diff --git a/CLAUDE.md b/CLAUDE.md index f3d4f3564f..6ecc62fa94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1004 +**Current Version:** 0.5.1005 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 536de7ea45..d28f5c288e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4905,7 +4905,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "base64", @@ -4960,14 +4960,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "log", @@ -4980,7 +4980,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-hir", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-hir", @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-dispatch", @@ -5007,7 +5007,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-hir", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "base64", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-hir", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "serde", "serde_json", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1004" +version = "0.5.1005" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5056,7 +5056,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "clap", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "argon2", "perry-ffi", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "reqwest", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "bcrypt", "perry-ffi", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "rusqlite", @@ -5104,7 +5104,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "scraper", @@ -5112,14 +5112,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "chrono", "cron", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "chrono", "perry-ffi", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "rust_decimal", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "serde_json", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5160,21 +5160,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "bytes", "http-body-util", @@ -5188,7 +5188,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lazy_static", "perry-ffi", @@ -5199,7 +5199,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5211,7 +5211,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "bytes", "http-body-util", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lazy_static", "perry-ffi", @@ -5240,7 +5240,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "base64", "jsonwebtoken", @@ -5251,7 +5251,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lru", "perry-ffi", @@ -5259,7 +5259,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "chrono", "perry-ffi", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "bson", "futures-util", @@ -5279,7 +5279,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "chrono", "perry-ffi", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "nanoid", "perry-ffi", @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "rustls", @@ -5309,7 +5309,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lettre", "perry-ffi", @@ -5319,7 +5319,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "sqlx", @@ -5328,7 +5328,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "governor", "perry-ffi", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "base64", "image", @@ -5345,14 +5345,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "lazy_static", "perry-ffi", @@ -5360,7 +5360,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "uuid", @@ -5368,7 +5368,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "perry-ffi", "regex", @@ -5378,7 +5378,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "futures-util", "lazy_static", @@ -5389,7 +5389,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "flate2", "perry-ffi", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-api-manifest", @@ -5420,7 +5420,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "bytes", @@ -5446,7 +5446,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-diagnostics", @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "base64", @@ -5482,7 +5482,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -5552,7 +5552,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "perry-hir", @@ -5562,7 +5562,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5570,11 +5570,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1004" +version = "0.5.1005" [[package]] name = "perry-ui-android" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "itoa", "jni", @@ -5589,7 +5589,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "rand 0.8.6", "serde", @@ -5599,7 +5599,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5618,7 +5618,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "block2", "libc", @@ -5633,7 +5633,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "block2", "libc", @@ -5651,11 +5651,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.1004" +version = "0.5.1005" [[package]] name = "perry-ui-tvos" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "block2", "libc", @@ -5670,7 +5670,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "block2", "libc", @@ -5685,7 +5685,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "block2", "libc", @@ -5698,7 +5698,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "libc", "perry-runtime", @@ -5712,7 +5712,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "base64", "ed25519-dalek", @@ -5726,7 +5726,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1004" +version = "0.5.1005" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 13423c4bc9..b31f0973ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.1004" +version = "0.5.1005" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-jsruntime/src/bridge.rs b/crates/perry-jsruntime/src/bridge.rs index 12cb998f49..726f68f479 100644 --- a/crates/perry-jsruntime/src/bridge.rs +++ b/crates/perry-jsruntime/src/bridge.rs @@ -898,6 +898,67 @@ fn native_promise_to_v8<'s>( v8_promise.into() } +/// Materialize a snapshot v8 Object for a perry-stdlib Web Fetch handle +/// (Request / Response). Properties are extracted via the public dispatch +/// helpers in `perry_stdlib::fetch`. Headers/Blob ids return `v8::null` +/// for now — they expose methods, not scalar properties, and adding method +/// bridging requires a Proxy + HANDLE_METHOD_DISPATCH callback (future work). +fn materialize_web_fetch_handle<'s>( + scope: &mut v8::PinScope<'s, '_>, + handle_id: usize, +) -> v8::Local<'s, v8::Value> { + if handle_id == 0 { + return v8::null(scope).into(); + } + + // Try Request first — read a probe property to confirm membership. + if let Some(url_f64) = perry_stdlib::dispatch_request_property(handle_id, "url") { + let obj = v8::Object::new(scope); + let url_v8 = native_to_v8(scope, url_f64); + if let Some(k) = v8::String::new(scope, "url") { + obj.set(scope, k.into(), url_v8); + } + if let Some(method_f64) = perry_stdlib::dispatch_request_property(handle_id, "method") { + let m = native_to_v8(scope, method_f64); + if let Some(k) = v8::String::new(scope, "method") { + obj.set(scope, k.into(), m); + } + } + if let Some(body_f64) = perry_stdlib::dispatch_request_property(handle_id, "body") { + let b = native_to_v8(scope, body_f64); + if let Some(k) = v8::String::new(scope, "body") { + obj.set(scope, k.into(), b); + } + } + return obj.into(); + } + + // Then Response. + if let Some(status_f64) = perry_stdlib::dispatch_response_property(handle_id, "status") { + let obj = v8::Object::new(scope); + let status_v8 = native_to_v8(scope, status_f64); + if let Some(k) = v8::String::new(scope, "status") { + obj.set(scope, k.into(), status_v8); + } + if let Some(st_f64) = perry_stdlib::dispatch_response_property(handle_id, "statusText") { + let v = native_to_v8(scope, st_f64); + if let Some(k) = v8::String::new(scope, "statusText") { + obj.set(scope, k.into(), v); + } + } + if let Some(ok_f64) = perry_stdlib::dispatch_response_property(handle_id, "ok") { + let v = native_to_v8(scope, ok_f64); + if let Some(k) = v8::String::new(scope, "ok") { + obj.set(scope, k.into(), v); + } + } + return obj.into(); + } + + // Unknown handle id — return null (safe fallback, no segfault). + v8::null(scope).into() +} + /// Convert a native object pointer to a V8 object fn native_object_to_v8<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -907,6 +968,28 @@ fn native_object_to_v8<'s>( return v8::null(scope).into(); } + // perry-stdlib's Web Fetch handles (Request / Response / Headers / Blob) + // arrive here NaN-boxed as POINTER_TAG values whose lower 48 bits hold a + // small registry id (1, 2, 3, ...) instead of a real heap pointer (see + // `perry_stdlib::fetch::handle_to_f64`). Mirror the perry-runtime side + // small-handle threshold (`object.rs:4665`, `< 0x100000`): below that, + // the value is a handle id, not a dereferenceable pointer. Without this + // guard the `gc_header_ptr = ptr - 8` arithmetic below wraps to a huge + // unsigned value, passes the `> 0x1000` bounds check, and segfaults when + // we deref `gc_header` (the hono `app.fetch(req)` crash where `req` came + // back from `new Request(...)` as `0x7FFD_0000_0000_0001`). + // + // For Request and Response we materialize a real v8 Object so V8-side code + // (hono, sveltekit, etc.) can read `request.url` / `response.status` etc. + // The synthesized object is a snapshot — methods like `req.text()` and + // streaming semantics aren't bridged here yet (would require a Proxy that + // calls back through HANDLE_METHOD_DISPATCH). For unknown small ids fall + // through to `v8::null` rather than crashing. + let ptr_usize = ptr as usize; + if ptr_usize < 0x10_0000 { + return materialize_web_fetch_handle(scope, ptr_usize); + } + // Issue (jose JWT blocker): Uint8Array / TypedArray pointers crossing // into V8 used to fall through to the generic `v8::Array` branch, // which turned a perry Uint8Array into a v8 Array. Libraries running