Summary
WaitableNativeActivityState::notify_window_resized at android-activity/src/native_activity/glue.rs:447 calls guard.window.as_ref().unwrap() unconditionally. On Meta Quest 3 running HorizonOS 12 (UP1A.231005.007.A1), onNativeWindowResized is fired by the Android runtime outside the documented onNativeWindowCreated…onNativeWindowDestroyed window, so guard.window is None, the unwrap() panics, and since the panic crosses the extern "C" FFI boundary into on_native_window_resized, the crate's panic_cannot_unwind shim catches it and forwards to abort(). The app is killed with SIGABRT before reaching xrCreateInstance.
This affects any android-activity 0.6.x consumer using the native-activity feature on Quest 3 HorizonOS. notify_window_redraw_needed at line 451 has the same pattern and will fail in the same way under the same conditions.
Relevant source
// android-activity/src/native_activity/glue.rs:441-449
pub fn notify_window_resized(&self, native_window: *mut ndk_sys::ANativeWindow) {
let mut guard = self.mutex.lock().unwrap();
// set_window always syncs .pending_window back to .window before returning. This callback
// from Android can never arrive at an interim state, and validates that Android:
// 1. Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
// 2. Doesn't call it on a bogus window pointer that we don't know about.
debug_assert_eq!(guard.window.as_ref().unwrap().ptr().as_ptr(), native_window);
guard.write_cmd(AppCmd::WindowResized);
}
The debug_assert_eq! is debug-only, but the guard.window.as_ref().unwrap() it wraps is unconditional — panics in every build when guard.window == None.
Reproduction
Hardware: Meta Quest 3 (Qualcomm XR2 Gen 2), HorizonOS 12.
Software: bevy_oxr main branch, Android example (crates/bevy_openxr/examples/android/), built with cargo apk build --release using NDK 27 and android-activity 0.6.1 (latest on crates.io at time of writing).
Steps:
- Build APK from
bevy_oxr Android example.
- Sideload to Quest 3:
adb install -r example.apk.
- Launch via VR intent:
adb shell am start -n org.bevyengine.example_openxr_android/android.app.NativeActivity.
- App crashes ~1.4 seconds after launch.
Backtrace
From adb logcat on affected device (full log can be provided if useful):
04-20 05:37:53.407 F/libc(27055): Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 27055 (_openxr_android), pid 27055 (_openxr_android)
04-20 05:37:54.053 F/DEBUG(27196): Cmdline: org.bevyengine.example_openxr_android
04-20 05:37:54.053 F/DEBUG(27196): signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
#01 pc 3695fe8 libbevy_openxr_android.so (std::sys::pal::unix::abort_internal)
#02 pc 369d774 libbevy_openxr_android.so (std::process::abort)
#03 pc 369eb98 libbevy_openxr_android.so (std::panicking::panic_with_hook)
#04 pc 369e77c libbevy_openxr_android.so (std::panicking::panic_handler)
#05 pc 3698eb4 libbevy_openxr_android.so (std::sys::backtrace::__rust_end_short_backtrace)
#06 pc 368a548 libbevy_openxr_android.so (__rustc::rust_begin_unwind)
#07 pc 36d5434 libbevy_openxr_android.so (core::panicking::panic_nounwind_fmt)
#08 pc 36d5398 libbevy_openxr_android.so (core::panicking::panic_nounwind)
#09 pc 36d553c libbevy_openxr_android.so (core::panicking::panic_cannot_unwind)
#10 pc 337537c libbevy_openxr_android.so (android_activity::activity_impl::glue::on_native_window_resized)
Frame #10 unambiguously identifies on_native_window_resized as the panic source. panic_cannot_unwind (frame #9) is the crate's FFI shim catching the unwind.
Root cause
The comment at glue.rs:443-446 asserts the contract the code depends on:
set_window always syncs .pending_window back to .window before returning. This callback from Android can never arrive at an interim state, and validates that Android:
- Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
- Doesn't call it on a bogus window pointer that we don't know about.
Meta's HorizonOS violates point 1. This is not hypothetical — the backtrace above proves it happens on retail Quest 3 hardware with current HorizonOS.
Proposed fix
Tolerate None / mismatched windows with a log::warn! rather than aborting the process. Consistent with the direction in #80 ("Don't abort the whole process if we see a Rust panic").
pub fn notify_window_resized(&self, native_window: *mut ndk_sys::ANativeWindow) {
let mut guard = self.mutex.lock().unwrap();
match guard.window.as_ref() {
Some(w) if w.ptr().as_ptr() == native_window => {
guard.write_cmd(AppCmd::WindowResized);
}
Some(w) => {
log::warn!(
"NativeWindowResized ignored: expected window {:p}, got {:p}",
w.ptr().as_ptr(),
native_window
);
}
None => {
log::warn!(
"NativeWindowResized ignored: no current window (lifecycle quirk, observed on Meta Quest 3 HorizonOS 12)"
);
}
}
}
Same pattern applies to notify_window_redraw_needed at line 451.
Happy to put up a PR if the maintainers agree with the direction. Wanted to file the bug first in case there's context I'm missing (e.g., the current unwrap() was an intentional invariant-guard and tolerating None would mask a deeper issue).
Environment
android-activity 0.6.1 (crates.io, latest)
- Rust 1.85
- NDK 27
- Target:
aarch64-linux-android
- Device: Meta Quest 3, HorizonOS 12 (build
UP1A.231005.007.A1)
- Reproducing app:
bevy_oxr Android example at crates/bevy_openxr/examples/android/
Summary
WaitableNativeActivityState::notify_window_resizedatandroid-activity/src/native_activity/glue.rs:447callsguard.window.as_ref().unwrap()unconditionally. On Meta Quest 3 running HorizonOS 12 (UP1A.231005.007.A1),onNativeWindowResizedis fired by the Android runtime outside the documentedonNativeWindowCreated…onNativeWindowDestroyedwindow, soguard.windowisNone, theunwrap()panics, and since the panic crosses theextern "C"FFI boundary intoon_native_window_resized, the crate'spanic_cannot_unwindshim catches it and forwards toabort(). The app is killed withSIGABRTbefore reachingxrCreateInstance.This affects any
android-activity 0.6.xconsumer using thenative-activityfeature on Quest 3 HorizonOS.notify_window_redraw_neededat line 451 has the same pattern and will fail in the same way under the same conditions.Relevant source
The
debug_assert_eq!is debug-only, but theguard.window.as_ref().unwrap()it wraps is unconditional — panics in every build whenguard.window == None.Reproduction
Hardware: Meta Quest 3 (Qualcomm XR2 Gen 2), HorizonOS 12.
Software:
bevy_oxrmain branch, Android example (crates/bevy_openxr/examples/android/), built withcargo apk build --releaseusing NDK 27 andandroid-activity 0.6.1(latest on crates.io at time of writing).Steps:
bevy_oxrAndroid example.adb install -r example.apk.adb shell am start -n org.bevyengine.example_openxr_android/android.app.NativeActivity.Backtrace
From
adb logcaton affected device (full log can be provided if useful):Frame #10 unambiguously identifies
on_native_window_resizedas the panic source.panic_cannot_unwind(frame #9) is the crate's FFI shim catching the unwind.Root cause
The comment at
glue.rs:443-446asserts the contract the code depends on:Meta's HorizonOS violates point 1. This is not hypothetical — the backtrace above proves it happens on retail Quest 3 hardware with current HorizonOS.
Proposed fix
Tolerate
None/ mismatched windows with alog::warn!rather than aborting the process. Consistent with the direction in #80 ("Don't abort the whole process if we see a Rust panic").Same pattern applies to
notify_window_redraw_neededat line 451.Happy to put up a PR if the maintainers agree with the direction. Wanted to file the bug first in case there's context I'm missing (e.g., the current
unwrap()was an intentional invariant-guard and toleratingNonewould mask a deeper issue).Environment
android-activity0.6.1 (crates.io, latest)aarch64-linux-androidUP1A.231005.007.A1)bevy_oxrAndroid example atcrates/bevy_openxr/examples/android/