Skip to content

Commit e69d468

Browse files
committed
fix: Android crash — thread-local arena freed after main() returned (v0.4.29)
- perry-native thread ran main() which initialized module-level arrays on the thread's arena. After main() returned, the thread exited, triggering arena Drop which freed all blocks. UI thread pump ticks then called getLevelInfo() on dangling pointers → SIGSEGV at getLevelInfo+44. - Fix: park the perry-native thread after main() instead of letting it exit, keeping the arena and all module-level objects alive. - Also: -Bsymbolic linker flag prevents ELF symbol interposition.
1 parent 2fd014c commit e69d468

6 files changed

Lines changed: 49 additions & 28 deletions

File tree

CLAUDE.md

Lines changed: 5 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.28
11+
**Current Version:** 0.4.29
1212

1313
## Workflow Requirements
1414

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

141141
## Recent Changes
142142

143+
### v0.4.29
144+
- fix: Android crash in UI pump ticks — perry-native thread exited after `main()` returned, dropping the thread-local arena and freeing all module-level arrays/objects; UI thread's pump tick then called `getLevelInfo()` on dangling pointers → segfault. Fixed by parking the perry-native thread after init instead of letting it exit.
145+
- fix: Android `-Bsymbolic` linker flag prevents ELF symbol interposition (process's `main()` vs perry's `main()`)
146+
143147
### v0.4.28
144148
- fix: module-level arrays/objects with `Unknown`/`Any` HIR type loaded as F64 instead of I64 in functions — `analyze_module_var_types` set `is_union=true` for Unknown/Any, causing `is_pointer && !is_union` to select F64; init stored I64 but functions loaded F64, corrupting pointers on Android (FP flush-to-zero); now arrays/closures/maps/sets/buffers always use I64
145149

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

crates/perry-codegen/src/module_init.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ impl crate::codegen::Compiler {
5050
self.module.declare_function("_perry_user_main", Linkage::Export, &sig)?
5151
} else if self.is_entry_module {
5252
// Entry module: generate "main"
53+
// On Android, -Bsymbolic-functions prevents the process's main() from
54+
// being called instead of ours via ELF symbol interposition.
5355
match self.module.declare_function("main", Linkage::Export, &sig) {
5456
Ok(id) => id,
5557
Err(_) => {

crates/perry-ui-android/src/lib.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeShutdown(
139139

140140
#[cfg(not(test))]
141141
extern "C" {
142-
fn main();
142+
fn main() -> i32;
143143
}
144144

145145
// js_stdlib_init_dispatch and js_stdlib_process_pending — now provided by perry-runtime
@@ -191,9 +191,19 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeMain(
191191
main();
192192
__android_log_print(
193193
3, b"PerryJNI\0".as_ptr(),
194-
b"nativeMain: main() returned\0".as_ptr(),
194+
b"nativeMain: main() returned, parking thread\0".as_ptr(),
195195
);
196196
}
197+
198+
// Park this thread forever — do NOT let it exit.
199+
// Module-level arrays/objects are allocated on this thread's arena.
200+
// If the thread exits, the arena's Drop frees all blocks, turning
201+
// every module-level pointer into a dangling reference. The UI thread's
202+
// pump ticks call into compiled functions (getLevelInfo etc.) that read
203+
// these pointers — segfault if the arena was freed.
204+
loop {
205+
std::thread::park();
206+
}
197207
}
198208

199209
// =============================================================================

crates/perry/src/commands/compile.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4352,6 +4352,11 @@ pub fn run(args: CompileArgs, format: OutputFormat, _use_color: bool, _verbose:
43524352
.arg("-target").arg("aarch64-linux-android24")
43534353
.arg("-Wl,-z,max-page-size=16384")
43544354
.arg("-Wl,-z,separate-loadable-segments")
4355+
// Prevent ELF symbol interposition: bind all symbols within the .so
4356+
// to the .so's own definitions. Without this, PLT calls (e.g. to "main")
4357+
// can resolve to symbols from the host process (app_process/zygote),
4358+
// bypassing perry's module initialization chain.
4359+
.arg("-Wl,-Bsymbolic")
43554360
// Allow unresolved symbols from namespace imports (import * as X).
43564361
// The codegen emits short-name extern refs (__export_X) for namespace
43574362
// imports that may not have a corresponding definition when the module

0 commit comments

Comments
 (0)