fix(lite-backend): catch panics at the wallet-open C entry points

litelib_initialize_new, litelib_initialize_new_from_phrase,
litelib_initialize_existing and litelib_check_server_online had no
catch_unwind (only litelib_execute did), so any panic below them
unwound across the C ABI into the GUI. With rustc 1.63 that is
undefined behaviour; in practice the process aborts with "fatal runtime
error: failed to initiate panic, error 5". The simplest trigger is a
malformed user-entered server URL: get_server_or_default unwraps the
http::Uri parse before anything else runs.

- ffi_guard (next to error_cstring) runs a body under catch_unwind and
  turns a panic into "Error: Rust panic in <name>", which the C++ bridge
  already treats as a failed call. The three init entry points keep
  their extern signatures and #[no_mangle] and call ffi_guard; their old
  bodies are now private *_impl functions.
- litelib_check_server_online wraps its body the same way and returns
  false on a panic.
- litelib_initialize_existing opens the wallet through
  read_from_disk_reporting. After a recovery from the .bak it returns
  "OK: recovered from backup; damaged file kept as <name>" instead of
  "OK". The C++ side treats any non-empty, non-error string as a
  successful open, so this is ABI-compatible.

No ABI or symbol change: the exported symbol inventory
(lite-backend-symbols.txt) is identical. panic = "unwind" stays in the
release profile, and every guard depends on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:22:00 -05:00
co-authored by Claude Opus 5.5
parent 31304a1a63
commit b6cffdf942
+36 -3
View File
@@ -22,6 +22,15 @@ fn error_cstring(msg: &str) -> *mut c_char {
safe_cstring(&format!("Error: {}", msg)).into_raw()
}
/// Run a C entry point's body and turn a Rust panic into an "Error: ..." string (C244): a panic must never unwind
/// across the C ABI. This relies on panic = "unwind" (lib/Cargo.toml [profile.release]).
fn ffi_guard<F: FnOnce() -> *mut c_char>(name: &str, f: F) -> *mut c_char {
match panic::catch_unwind(panic::AssertUnwindSafe(f)) {
Ok(p) => p,
Err(_) => error_cstring(&format!("Rust panic in {}", name)),
}
}
// We'll use a MUTEX to store a global lightclient instance,
// so we don't have to keep creating it. We need to store it here, in rust
// because we can't return such a complex structure back to C++
@@ -70,6 +79,10 @@ pub extern fn blake3_PW(pw: *const c_char) -> *mut c_char{
/// Create a new wallet and return the seed for the newly created wallet.
#[no_mangle]
pub extern fn litelib_initialize_new(dangerous: bool,server: *const c_char) -> *mut c_char {
ffi_guard("litelib_initialize_new", || litelib_initialize_new_impl(dangerous, server))
}
fn litelib_initialize_new_impl(dangerous: bool, server: *const c_char) -> *mut c_char {
let server_str = unsafe {
assert!(!server.is_null());
@@ -126,6 +139,12 @@ pub extern fn litelib_initialize_new(dangerous: bool,server: *const c_char) -> *
#[no_mangle]
pub extern "C" fn litelib_initialize_new_from_phrase(dangerous: bool, server: *const c_char,
seed: *const c_char, birthday: u64, number: u64, overwrite: bool) -> *mut c_char {
ffi_guard("litelib_initialize_new_from_phrase",
|| litelib_initialize_new_from_phrase_impl(dangerous, server, seed, birthday, number, overwrite))
}
fn litelib_initialize_new_from_phrase_impl(dangerous: bool, server: *const c_char,
seed: *const c_char, birthday: u64, number: u64, overwrite: bool) -> *mut c_char {
if server.is_null() || seed.is_null() {
println!("Server or seed is null");
return ptr::null_mut();
@@ -197,6 +216,10 @@ pub extern "C" fn litelib_initialize_new_from_phrase(dangerous: bool, server: *c
// Initialize a new lightclient and store its value
#[no_mangle]
pub extern fn litelib_initialize_existing(dangerous: bool, server: *const c_char) -> *mut c_char {
ffi_guard("litelib_initialize_existing", || litelib_initialize_existing_impl(dangerous, server))
}
fn litelib_initialize_existing_impl(dangerous: bool, server: *const c_char) -> *mut c_char {
let server_str = unsafe {
assert!(!server.is_null());
@@ -212,8 +235,10 @@ pub extern fn litelib_initialize_existing(dangerous: bool, server: *const c_char
}
};
let lightclient = match LightClient::read_from_disk(&config) {
Ok(l) => l,
// s2-D14: the core reports a recovery from the backup; pass it on (the C++ side treats any non-error string as
// a successful open).
let (lightclient, recovered) = match LightClient::read_from_disk_reporting(&config) {
Ok(r) => r,
Err(e) => {
let e_str = CString::new(format!("Error: {}", e)).unwrap();
return e_str.into_raw();
@@ -236,7 +261,10 @@ pub extern fn litelib_initialize_existing(dangerous: bool, server: *const c_char
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
};
let c_str = safe_cstring("OK");
let c_str = match recovered {
Some(note) => safe_cstring(&format!("OK: {}", note)),
None => safe_cstring("OK"),
};
return c_str.into_raw();
}
@@ -286,6 +314,11 @@ pub extern fn litelib_execute(cmd: *const c_char, args: *const c_char) -> *mut c
// Check is Server Connection is fine
#[no_mangle]
pub extern "C" fn litelib_check_server_online(server: *const c_char) -> bool {
// A malformed server URL panics in get_server_or_default (and get_client); never unwind across the C ABI.
panic::catch_unwind(move || litelib_check_server_online_impl(server)).unwrap_or(false)
}
fn litelib_check_server_online_impl(server: *const c_char) -> bool {
let server_str = unsafe {
assert!(!server.is_null());