cloudfox-coreshift-core 2.14.0

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/

//! Push-based per-task FPS listener registered with `WindowManager`.

use super::sys::*;
use crate::CoreError;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::raw::{c_char, c_void};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};

// ── FpsListener (task FPS callback) ───────────────────────────────────────

const WINDOW_SERVICE: &[u8] = b"window\0";
const WM_DESCRIPTOR: &[u8] = b"android.view.IWindowManager\0";
const FPS_DESCRIPTOR: &[u8] = b"android.window.ITaskFpsCallback\0";
// The last reported FPS (bit pattern of the f32) is published before the
// eventfd is signalled, so the consumer never reads a stale value. The wake
// eventfd is per-instance (same pattern as TaskStackListener): it is handed
// to AIBinder_new as the callback binder's userdata, so a second
// FpsListener can never rewire an earlier registration's wake into its own
// fd (the callback resolves its own binder's fd via AIBinder_getUserData).
static FPS_VALUE: AtomicU32 = AtomicU32::new(0);
// Distinct from FPS_VALUE's bits: `0.0f32` has bit pattern 0, so a "not
// seen" sentinel of 0 would misread a genuine idle (0-FPS) report as "no
// report yet" — swallowing the sample and (downstream) leaving the first-
// report-after-swap drop armed. The seen flag disambiguates.
static FPS_SEEN: AtomicBool = AtomicBool::new(false);
static FPS_CODE: AtomicU32 = AtomicU32::new(0);
static FPS_READ_I32: AtomicUsize = AtomicUsize::new(0);

// No-op callbacks for the client-only IWindowManager class (we never serve
// transactions on the `window` binder — the class exists only to satisfy
// AIBinder_prepareTransaction's remote-transaction contract).
unsafe extern "C" fn wm_on_create(_: *mut c_void) -> *mut c_void {
    std::ptr::null_mut()
}
unsafe extern "C" fn wm_on_destroy(_: *mut c_void) {}
unsafe extern "C" fn wm_on_transact(
    _: *mut AIBinder,
    _: u32,
    _: *const AParcel,
    _: *mut AParcel,
) -> BinderStatus {
    STATUS_OK
}

// The per-instance wake eventfd is this callback binder's userdata, so
// onCreate must return the args passed to AIBinder_new — AIBinder_getUserData
// returns exactly that value — and onDestroy must reclaim the box (same
// pattern as TaskStackListener). Returning null here would make
// AIBinder_getUserData return null, silently breaking the FPS wake.
unsafe extern "C" fn fps_on_create(userdata: *mut c_void) -> *mut c_void {
    userdata
}
unsafe extern "C" fn fps_on_destroy(userdata: *mut c_void) {
    if !userdata.is_null() {
        unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
    }
}
unsafe extern "C" fn fps_on_transact(
    binder: *mut AIBinder,
    code: u32,
    in_parcel: *const AParcel,
    _: *mut AParcel,
) -> BinderStatus {
    if code != FPS_CODE.load(Ordering::Relaxed) {
        return STATUS_UNKNOWN_TRANSACTION;
    }
    // Reader is published non-zero before the code, so a matching code is
    // never paired with an unset reader.
    let read_addr = FPS_READ_I32.load(Ordering::Relaxed);
    if read_addr != 0 {
        let read_fn: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus =
            unsafe { std::mem::transmute(read_addr) };
        let mut bits: i32 = 0;
        if unsafe { read_fn(in_parcel, &mut bits) } == STATUS_OK {
            // Publish the value before signalling so the consumer always
            // sees the value that triggered the wakeup. Non-finite/negative
            // reports are normalized to 0.0 first (L5) — the stream must
            // never carry "NaN"/"inf".
            FPS_VALUE.store(sanitize_fps(bits as u32), Ordering::Relaxed);
            FPS_SEEN.store(true, Ordering::Release);
            // Per-instance wake: the eventfd is this callback binder's
            // userdata (see FpsListener::open), so two FpsListeners never
            // cross-wire their wakes. A missing slot means the process-wide
            // AIBinder_getUserData symbol has not been cached — drop the
            // signal rather than risk a stale fd.
            let get_user_data = GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner());
            if let Some(get_user_data) = *get_user_data {
                let userdata = unsafe { get_user_data(binder) };
                if !userdata.is_null() {
                    let efd = userdata as *mut OwnedFd;
                    let val: u64 = 1;
                    unsafe {
                        libc::write((*efd).as_raw_fd(), &val as *const u64 as *const c_void, 8)
                    };
                }
            }
        }
    }
    STATUS_OK
}

/// Push-based per-task FPS listener registered with `WindowManager`.
///
/// Uses `IWindowManager.registerTaskFpsCallback(taskId, callback)`; the
/// daemon hosts the `ITaskFpsCallback` server object and receives
/// `onFpsReported(float)` one-way transactions from the `FpsReporter` at
/// most every ~500 ms.
///
/// The registering UID must hold `ACCESS_FPS_COUNTER` (signature|privileged)
/// — this process typically runs as shell (uid 2000) via `su`.
///
/// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
/// callback fd. It becomes readable whenever `onFpsReported` fires; call
/// [`FpsListener::last_fps`] after the event to read the value.
pub struct FpsListener {
    _lib: DlHandle,
    vt: Vtable,
    window: OwnedBinder,
    cb_binder: *mut AIBinder,
    _wm_class: *mut AIBinder_Class,
    register_code: u32,
    unregister_code: u32,
    task_id: i32,
}
unsafe impl Send for FpsListener {}

impl FpsListener {
    /// Open WindowManager and define the `ITaskFpsCallback` server object.
    ///
    /// Resolves the three tx codes from DEX. Does **not** register a task
    /// yet — call [`FpsListener::register`] once a taskId is known. Starts
    /// the binder thread pool so `onFpsReported` can fire.
    pub fn open() -> Result<(Self, OwnedFd), CoreError> {
        let handle = unsafe {
            libc::dlopen(
                LIBBINDER_PATH.as_ptr() as *const c_char,
                libc::RTLD_NOW | libc::RTLD_LOCAL,
            )
        };
        if handle.is_null() {
            return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
        }
        let lib = DlHandle;
        let vt = load_vtable(handle)?;

        let (register_code, unregister_code, on_fps_code) =
            crate::android::dex::resolve_fps_codes().ok_or_else(|| {
                CoreError::binder(-1, "dex:TRANSACTION_registerTaskFpsCallback not found")
            })?;

        let window = {
            let raw = unsafe { (vt.get_service)(WINDOW_SERVICE.as_ptr() as *const c_char) };
            if raw.is_null() {
                return Err(CoreError::binder(-1, "AServiceManager_getService:window"));
            }
            OwnedBinder {
                ptr: raw,
                dec_strong: vt.dec_strong,
            }
        };

        // Remote transactions require a class on the binder (same
        // AIBinder_prepareTransaction contract as the AM service above).
        let wm_class = unsafe {
            (vt.class_define)(
                WM_DESCRIPTOR.as_ptr() as *const c_char,
                wm_on_create,
                wm_on_destroy,
                wm_on_transact,
            )
        };
        if wm_class.is_null() {
            return Err(CoreError::binder(
                -1,
                "AIBinder_Class_define:IWindowManager",
            ));
        }
        unsafe { (vt.associate_class)(window.ptr, wm_class) };

        let cb_class = unsafe {
            (vt.class_define)(
                FPS_DESCRIPTOR.as_ptr() as *const c_char,
                fps_on_create,
                fps_on_destroy,
                fps_on_transact,
            )
        };
        if cb_class.is_null() {
            return Err(CoreError::binder(
                -1,
                "AIBinder_Class_define:ITaskFpsCallback",
            ));
        }

        // Nonblocking eventfd — the callback only ever writes to it (an
        // eventfd write never blocks), while epoll-based consumers register
        // it edge-triggered and drain to EAGAIN, so a blocking fd would
        // wedge the consumer's drain loop. Matches the obs/fgproc observer
        // eventfds. Each instance owns its own fd; it is handed to the
        // callback binder as userdata (per-instance routing, no process-wide
        // static) and the consumer receives a dup below (C2).
        let owned = unsafe {
            let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
            if raw < 0 {
                return Err(CoreError::sys(*libc::__errno(), "eventfd"));
            }
            OwnedFd::from_raw_fd(raw)
        };

        let consumer = owned
            .try_clone()
            .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:fps"))?;

        let userdata = Box::into_raw(Box::new(owned)) as *mut c_void;
        let cb_binder = unsafe { (vt.new_binder)(cb_class, userdata) };
        if cb_binder.is_null() {
            // Reclaim the userdata box handed to AIBinder_new before bailing.
            unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
            return Err(CoreError::binder(-1, "AIBinder_new:ITaskFpsCallback"));
        }
        unsafe { (vt.associate_class)(cb_binder, cb_class) };

        // Publish the reader and code before the eventfd registration; a
        // matching code is never paired with an unset reader (C2-adjacent).
        FPS_READ_I32.store(vt.read_int32 as usize, Ordering::Relaxed);
        FPS_SEEN.store(false, Ordering::Relaxed);
        FPS_CODE.store(on_fps_code, Ordering::Relaxed);
        FPS_VALUE.store(0, Ordering::Relaxed);
        // The callback resolves AIBinder_getUserData from this vtable; the
        // symbol address is process-wide, so a cached static is safe.
        *GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner()) = Some(vt.get_user_data);

        unsafe { (vt.set_thread_pool_max)(0) };
        let join_fn = vt.join_thread_pool;
        std::thread::spawn(move || unsafe { join_fn() });

        Ok((
            Self {
                _lib: lib,
                vt,
                window,
                cb_binder,
                _wm_class: wm_class,
                register_code,
                unregister_code,
                task_id: -1,
            },
            consumer,
        ))
    }

    /// Register the callback for `task_id`. If a task was already
    /// registered, it is unregistered first (WindowManager tracks one task
    /// per callback binder).
    pub fn register(&mut self, task_id: i32) -> Result<(), CoreError> {
        if self.task_id == task_id {
            return Ok(());
        }
        if self.task_id >= 0 {
            let _ = self.unregister();
        }

        let _ = transact_write(&self.vt, self.window.ptr, self.register_code, |w| {
            w.write_i32(task_id)?;
            w.write_strong_binder(self.cb_binder)
        })?;
        self.task_id = task_id;
        Ok(())
    }

    /// Unregister the callback from WindowManager. No-op if nothing is
    /// registered.
    pub fn unregister(&mut self) -> Result<(), CoreError> {
        if self.task_id < 0 {
            return Ok(());
        }
        let _ = transact_write(&self.vt, self.window.ptr, self.unregister_code, |w| {
            w.write_strong_binder(self.cb_binder)
        })?;
        self.task_id = -1;
        Ok(())
    }

    /// The most recent `onFpsReported` value (f32), or `None` if no report
    /// has arrived yet. Safe to call at any time; the bit pattern is
    /// published atomically. A genuine 0.0 (idle) report reads as `Some`,
    /// distinguished from the no-report state by `FPS_SEEN`.
    pub fn last_fps(&self) -> Option<f32> {
        fps_from_state(
            FPS_SEEN.load(Ordering::Acquire),
            FPS_VALUE.load(Ordering::Relaxed),
        )
    }

    /// The taskId currently registered, or `None` if none.
    pub fn task_id(&self) -> Option<i32> {
        (self.task_id >= 0).then_some(self.task_id)
    }
}

impl Drop for FpsListener {
    /// Best-effort deregistration from WindowManager so a dropped listener
    /// does not leave the framework delivering `onFpsReported` forever. The
    /// callback binder's local strong ref is intentionally NOT released:
    /// keeping it alive guarantees the per-binder userdata (the OwnedFd)
    /// can never be reclaimed by `on_destroy` while a callback is in
    /// flight, and the framework-side registration has been dropped by the
    /// unregister, so no stale transaction targets this instance.
    fn drop(&mut self) {
        let _ = self.unregister();
    }
}

/// Disambiguate "no report yet" from a genuine 0.0 (idle) report: `seen`
/// tracks whether the callback published a value; `bits` is that value's
/// bit pattern. `0.0f32` has bits 0, so the bits alone cannot tell a real
/// idle sample from an unset slot.
fn fps_from_state(seen: bool, bits: u32) -> Option<f32> {
    if seen {
        Some(f32::from_bits(bits))
    } else {
        None
    }
}

/// Normalize an `onFpsReported` bit pattern for the stream. Real FPS is
/// non-negative and finite; NaN/±Inf (garbage or corruption) and negative
/// values collapse to 0.0 (idle) so the value stream never shows "NaN" or
/// "inf".
fn sanitize_fps(bits: u32) -> u32 {
    let v = f32::from_bits(bits);
    if v.is_finite() && v >= 0.0 {
        bits
    } else {
        0.0f32.to_bits()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fps_zero_report_distinct_from_unseen() {
        assert_eq!(fps_from_state(false, 0), None);
        assert_eq!(fps_from_state(true, 0), Some(0.0));
        assert_eq!(fps_from_state(true, 60.0f32.to_bits()), Some(60.0));
        assert_eq!(fps_from_state(false, 60.0f32.to_bits()), None);
    }

    #[test]
    fn sanitize_fps_rejects_non_finite_and_negative() {
        let de = |bits| f32::from_bits(sanitize_fps(bits));
        assert_eq!(de(0.0f32.to_bits()), 0.0);
        assert_eq!(de(60.0f32.to_bits()), 60.0);
        assert_eq!(de(f32::NAN.to_bits()), 0.0);
        assert_eq!(de(f32::INFINITY.to_bits()), 0.0);
        assert_eq!(de(f32::NEG_INFINITY.to_bits()), 0.0);
        assert_eq!(de((-5.0f32).to_bits()), 0.0);
    }
}