Skip to content

notify_window_resized panics (unwrap on None) on Meta Quest 3 HorizonOS — fires onNativeWindowResized outside documented window lifecycle #247

@jamesdowzard

Description

@jamesdowzard

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 onNativeWindowCreatedonNativeWindowDestroyed 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:

  1. Build APK from bevy_oxr Android example.
  2. Sideload to Quest 3: adb install -r example.apk.
  3. Launch via VR intent: adb shell am start -n org.bevyengine.example_openxr_android/android.app.NativeActivity.
  4. 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:

  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.

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/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions