Skip to content

Commit 10dead0

Browse files
committed
fix: x86_64 SIGSEGV in Contract() with multi-module ethkit — Export linkage for all wrappers, keys_array GcHeader guard (v0.4.48)
Two fixes for x86_64-specific crash in ethkit Contract() with 20 compiled modules: 1. Wrapper functions for FuncRef callbacks (e.g., .map(resolveType)) now use Linkage::Export instead of Linkage::Local. The wrapper names are already scoped with module prefix (__wrapper_{prefix}__{name}) preventing symbol collisions. Export linkage ensures func_addr produces linker-resolved absolute addresses, which is more reliable than Cranelift-internal resolution for Local symbols on x86_64 ELF. 2. Added cross-platform GcHeader type validation for keys_array pointers in js_object_get_field_by_name. Linux lacked the macOS-only ASCII heuristic guard, so corrupted keys_array pointers crashed with SIGSEGV instead of returning undefined.
1 parent 04f7998 commit 10dead0

5 files changed

Lines changed: 49 additions & 31 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
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 Cranelift for code generation.
1010

11-
**Current Version:** 0.4.47
11+
**Current Version:** 0.4.48
1212

1313
## Workflow Requirements
1414

@@ -140,6 +140,9 @@ Projects can list npm packages to compile natively instead of routing to V8. Con
140140

141141
## Recent Changes
142142

143+
### v0.4.48
144+
- fix: x86_64 SIGSEGV in `Contract()` with 20-module ethkit — wrapper functions for FuncRef callbacks (e.g., `.map(resolveType)`) now use `Linkage::Export` instead of `Linkage::Local`; module-scoped names prevent collisions while Export linkage ensures correct `func_addr` resolution on x86_64 ELF; also added cross-platform GcHeader validation for `keys_array` in `js_object_get_field_by_name` to catch corrupted object pointers (Linux lacked the macOS-only ASCII heuristic guard)
145+
143146
### v0.4.47
144147
- fix: module-local function wrappers use `Linkage::Local` — prevents cross-module symbol collisions when two modules share filename + function names (e.g., two `contract.ts` files both with `resolveType`); fixes x86_64 wrong dispatch in large module graphs
145148
- feat: `Promise.race` implemented — `js_promise_race` runtime function with resolve/reject handlers; settles with first promise that completes

Cargo.lock

Lines changed: 24 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ opt-level = "s" # Optimize for size in stdlib
8585
opt-level = 3
8686

8787
[workspace.package]
88-
version = "0.4.47"
88+
version = "0.4.48"
8989
edition = "2021"
9090
license = "MIT"
9191
repository = "https://github.com/PerryTS/perry"

crates/perry-codegen/src/closures.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,11 +1983,13 @@ impl crate::codegen::Compiler {
19831983
} else {
19841984
format!("__wrapper_{}__{}", self.module_symbol_prefix, func.name)
19851985
};
1986-
// Only exported functions need Export linkage for their wrappers (cross-module calls).
1987-
// Module-local function wrappers use Local linkage to prevent symbol collisions when
1988-
// two modules have the same filename and function names (e.g., two contract.ts files
1989-
// each with resolveType). With Export linkage the linker could resolve the wrong one.
1990-
let linkage = if func.is_exported { Linkage::Export } else { Linkage::Local };
1986+
// All wrapper functions use Export linkage. The wrapper name is scoped with the
1987+
// module prefix (__wrapper_{prefix}__{name}), preventing symbol collisions even when
1988+
// two modules define functions with the same name. Export linkage ensures that
1989+
// func_addr produces a linker-resolved absolute address, which is more reliable
1990+
// than Cranelift-internal resolution for Local symbols — particularly on x86_64 ELF
1991+
// where Local func_addr relocations can produce incorrect addresses in large binaries.
1992+
let linkage = Linkage::Export;
19911993
let wrapper_id = self.module.declare_function(&wrapper_name, linkage, &sig)?;
19921994
// Track whether we need to NaN-box the return value (always needed since we return f64)
19931995
let needs_return_boxing = original_return_abi == types::I64;

crates/perry-runtime/src/object.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,19 @@ pub extern "C" fn js_object_get_field_by_name(obj: *const ObjectHeader, key: *co
10601060
}
10611061
}
10621062

1063+
// Cross-platform safety: validate keys_array has a valid GcHeader.
1064+
// If the keys_array pointer is corrupt (e.g., due to a stale reference after GC,
1065+
// or a func_addr relocation issue on x86_64), the GcHeader check catches it
1066+
// before we dereference the array contents.
1067+
{
1068+
let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
1069+
let keys_gc_type = (*keys_gc).obj_type;
1070+
// keys_array must be GC_TYPE_ARRAY (arena-allocated array)
1071+
if keys_gc_type != crate::gc::GC_TYPE_ARRAY {
1072+
return JSValue::undefined();
1073+
}
1074+
}
1075+
10631076
// Fast path: check field index cache (keys_array_ptr + key_hash → field_index)
10641077
// Objects with the same shape share the same keys_array, so we cache per-shape lookups.
10651078
let key_bytes = std::slice::from_raw_parts(

0 commit comments

Comments
 (0)