Skip to content

Commit f60e334

Browse files
committed
fix: x86_64 ABI mismatch — all extern "C" fn -> bool changed to i32/f64 (v0.4.49)
Bug #13 (SIGSEGV on Contract.call() returning tuple) was caused by the same js_string_equals bool-return ABI mismatch fixed in Bug #12: Cranelift declared I32 return but the Rust function returned bool (1 byte in al, not full eax). Garbage upper bits made isDynamic() return true for tuple types, sending the decoder down the dynamic-encoding path with astronomical offsets → BigInt('0x') → null pointer → SIGSEGV. This commit fixes ALL remaining extern "C" fn -> bool across the codebase: perry-runtime: js_nanbox_is_bigint/pointer/string (→ i32), js_json_is_valid (→ f64 NaN-boxed TAG_TRUE/TAG_FALSE) perry-stdlib: js_cron_validate, js_cron_job_is_running (→ f64) js_sqlite_exec/begin/commit/rollback/close/in_transaction (→ i32) js_moment_is_before/after/same/between/valid (→ f64) js_lodash_in_range (→ f64), js_lodash_is_empty/nil (→ i32) js_argon2_verify_sync/needs_rehash (→ i32) js_commander_get_option_bool (→ f64) js_http_respond_*/server_close (→ f64)
1 parent db84777 commit f60e334

13 files changed

Lines changed: 218 additions & 131 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.48
11+
**Current Version:** 0.4.49
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.49
144+
- fix: x86_64 SIGSEGV when `Contract.call()` returns a tuple — root cause was `js_string_equals` returning `bool` (1 byte in `al`) while Cranelift declared it as `I32` (reads full `eax`); garbage upper bits made `isDynamic("(uint256,uint256)")` return true, causing the decoder to take the dynamic-encoding path with an astronomical offset, leading to invalid BigInt construction and null pointer dereference. Fixed in v0.4.48 commit db84777; this version additionally fixes ALL remaining `extern "C" fn -> bool` ABI mismatches across perry-runtime and perry-stdlib (17 functions: `js_json_is_valid`, `js_nanbox_is_*`, `js_cron_*`, `js_sqlite_*`, `js_moment_*`, `js_lodash_*`, `js_commander_*`, `js_http_*`)
145+
143146
### v0.4.48
144147
- 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)
145148

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.48"
88+
version = "0.4.49"
8989
edition = "2021"
9090
license = "MIT"
9191
repository = "https://github.com/PerryTS/perry"

crates/perry-runtime/src/json.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,14 +698,20 @@ pub unsafe extern "C" fn js_json_stringify_null() -> *mut StringHeader {
698698

699699
/// Check if a string is valid JSON
700700
#[no_mangle]
701-
pub unsafe extern "C" fn js_json_is_valid(text_ptr: *const StringHeader) -> bool {
701+
pub unsafe extern "C" fn js_json_is_valid(text_ptr: *const StringHeader) -> f64 {
702+
const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004;
703+
const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003;
702704
if text_ptr.is_null() {
703-
return false;
705+
return f64::from_bits(TAG_FALSE);
704706
}
705707
let len = (*text_ptr).length as usize;
706708
let data_ptr = (text_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
707709
let bytes = std::slice::from_raw_parts(data_ptr, len);
708-
serde_json::from_slice::<serde_json::Value>(bytes).is_ok()
710+
if serde_json::from_slice::<serde_json::Value>(bytes).is_ok() {
711+
f64::from_bits(TAG_TRUE)
712+
} else {
713+
f64::from_bits(TAG_FALSE)
714+
}
709715
}
710716

711717
// ─── Utility functions ────────────────────────────────────────────────────────

crates/perry-runtime/src/value.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,9 @@ pub unsafe extern "C" fn js_dynamic_bitxor(a: f64, b: f64) -> f64 {
660660

661661
/// Check if an f64 value (interpreted as NaN-boxed) represents a BigInt.
662662
#[no_mangle]
663-
pub extern "C" fn js_nanbox_is_bigint(value: f64) -> bool {
663+
pub extern "C" fn js_nanbox_is_bigint(value: f64) -> i32 {
664664
let jsval = JSValue::from_bits(value.to_bits());
665-
jsval.is_bigint()
665+
if jsval.is_bigint() { 1 } else { 0 }
666666
}
667667

668668
/// Extract a BigInt pointer from a NaN-boxed f64 value.
@@ -682,9 +682,9 @@ pub extern "C" fn js_nanbox_get_bigint(value: f64) -> i64 {
682682

683683
/// Check if an f64 value (interpreted as NaN-boxed) represents a pointer.
684684
#[no_mangle]
685-
pub extern "C" fn js_nanbox_is_pointer(value: f64) -> bool {
685+
pub extern "C" fn js_nanbox_is_pointer(value: f64) -> i32 {
686686
let jsval = JSValue::from_bits(value.to_bits());
687-
jsval.is_pointer()
687+
if jsval.is_pointer() { 1 } else { 0 }
688688
}
689689

690690
/// Extract a pointer from a NaN-boxed f64 value.
@@ -780,9 +780,9 @@ pub extern "C" fn js_get_string_pointer_unified(value: f64) -> i64 {
780780

781781
/// Check if a NaN-boxed f64 value represents a string.
782782
#[no_mangle]
783-
pub extern "C" fn js_nanbox_is_string(value: f64) -> bool {
783+
pub extern "C" fn js_nanbox_is_string(value: f64) -> i32 {
784784
let jsval = JSValue::from_bits(value.to_bits());
785-
jsval.is_string()
785+
if jsval.is_string() { 1 } else { 0 }
786786
}
787787

788788
/// Convert a NaN-boxed f64 value to a string pointer.

crates/perry-stdlib/src/argon2.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,47 +129,47 @@ pub unsafe extern "C" fn js_argon2_verify(
129129
pub unsafe extern "C" fn js_argon2_verify_sync(
130130
hash_ptr: *const StringHeader,
131131
password_ptr: *const StringHeader,
132-
) -> bool {
132+
) -> i32 {
133133
let hash_str = match string_from_header(hash_ptr) {
134134
Some(h) => h,
135-
None => return false,
135+
None => return 0,
136136
};
137137

138138
let password = match string_from_header(password_ptr) {
139139
Some(p) => p,
140-
None => return false,
140+
None => return 0,
141141
};
142142

143143
let parsed_hash = match PasswordHash::new(&hash_str) {
144144
Ok(h) => h,
145-
Err(_) => return false,
145+
Err(_) => return 0,
146146
};
147147

148148
let argon2 = Argon2::default();
149-
argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok()
149+
if argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok() { 1 } else { 0 }
150150
}
151151

152152
/// argon2.needsRehash(hash) -> boolean
153153
///
154154
/// Check if a hash needs to be rehashed (e.g., due to outdated parameters).
155155
#[no_mangle]
156-
pub unsafe extern "C" fn js_argon2_needs_rehash(hash_ptr: *const StringHeader) -> bool {
156+
pub unsafe extern "C" fn js_argon2_needs_rehash(hash_ptr: *const StringHeader) -> i32 {
157157
let hash_str = match string_from_header(hash_ptr) {
158158
Some(h) => h,
159-
None => return true,
159+
None => return 1,
160160
};
161161

162162
// Parse the hash to check its parameters
163163
match PasswordHash::new(&hash_str) {
164164
Ok(parsed) => {
165165
// Check if algorithm is argon2id
166166
if parsed.algorithm.as_str() != "argon2id" {
167-
return true;
167+
return 1;
168168
}
169169
// In a real implementation, we'd check memory cost, time cost, etc.
170170
// For now, we assume current defaults are acceptable
171-
false
171+
0
172172
}
173-
Err(_) => true, // Invalid hash needs rehashing
173+
Err(_) => 1, // Invalid hash needs rehashing
174174
}
175175
}

crates/perry-stdlib/src/commander.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,19 +281,24 @@ pub unsafe extern "C" fn js_commander_get_option_number(handle: Handle, name_ptr
281281

282282
/// Get a specific option value as boolean
283283
#[no_mangle]
284-
pub unsafe extern "C" fn js_commander_get_option_bool(handle: Handle, name_ptr: *const StringHeader) -> bool {
284+
pub unsafe extern "C" fn js_commander_get_option_bool(handle: Handle, name_ptr: *const StringHeader) -> f64 {
285+
const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004;
286+
const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003;
287+
285288
let name = match string_from_header(name_ptr) {
286289
Some(n) => n,
287-
None => return false,
290+
None => return f64::from_bits(TAG_FALSE),
288291
};
289292

290293
if let Some(cmd) = get_handle_mut::<CommanderHandle>(handle) {
291294
if let Some(value) = cmd.parsed_values.get(&name) {
292-
return value == "true" || value == "1";
295+
if value == "true" || value == "1" {
296+
return f64::from_bits(TAG_TRUE);
297+
}
293298
}
294299
}
295300

296-
false
301+
f64::from_bits(TAG_FALSE)
297302
}
298303

299304
/// Get positional arguments count

0 commit comments

Comments
 (0)