cloudfox-coreshift-core 2.14.0

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
// 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/

//! DisplayManager binder client.
//!
//! Display-state callbacks (`IDisplayManagerCallback`), Android 14
//! `DisplayInfo` parcel decoding for FPS normalization, and the
//! power-service `isInteractive` probe.

use super::sys::*;
use crate::CoreError;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
use std::os::raw::{c_char, c_void};
use std::sync::Mutex;

/// Decoded subset of the Android 14 `android.view.DisplayInfo` parcel used
/// for FPS normalization.
struct ParsedDisplayInfo {
    active_mode_id: i32,
    vsync_rate: f32,
    peak_refresh_rate: f32,
    render_frame_rate: f32,
}

/// Walk the `IDisplayManager.getDisplayInfo` reply parcel following the
/// Android 14 `android.view.DisplayInfo.writeToParcel` field order.
///
/// Field order (Android 14):
/// layerStack, flags, type, displayId, displayGroupId (5×i32), then a
/// nullable `DisplayAddress` parcelable (`writeParcelable` → a String16
/// class name, or a single `-1` marker for null), a nullable
/// `DeviceProductInfo` parcelable, then `name` as a **String8** (Java
/// `writeString8`), 8 logical-dimension i32s, the `DisplayCutout`
/// ParcelableWrapper, rotation/modeId/renderFrameRate/defaultModeId, and
/// `supportedModes`. Only fields up to `supportedModes` are consumed — the
/// tail (color modes, HDR, rounded corners, …) is never touched.
///
/// `AParcel_readString` decodes String16 only; `name` is String8, so it is
/// consumed with `skip_string8`. Null strings and null parcelables carry a
/// `-1` length marker, which `string_alloc` now accepts so they decode to
/// `None` rather than a hard `STATUS_UNEXPECTED_NULL`.
///
/// Validity gate: any value shape outside the expected one returns a
/// `CoreError` tagged `display_info` so on-device logs identify the layout
/// mismatch.
fn parse_display_info(r: &ParcelReader<'_>) -> Result<ParsedDisplayInfo, CoreError> {
    // 1. layerStack/type/displayId prefix: layerStack, flags, type,
    //    displayId, displayGroupId.
    r.skip_i32s(5)?;

    // 2. address: nullable DisplayAddress parcelable. Non-null writes the
    //    class name as a String16 then the body; null writes a -1 marker.
    //    Body differs by subtype: Physical → 1×i64 display id, Network →
    //    String16 mac address. Dispatch on the decoded class name so a
    //    network display cannot desync the walk.
    if let Some(addr_class) = r.read_string()? {
        if addr_class.ends_with("$Physical") {
            r.read_int64()?;
        } else if addr_class.ends_with("$Network") {
            r.skip_string16()?;
        } else {
            return Err(CoreError::binder(
                -1,
                "display_info:unsupported DisplayAddress",
            ));
        }
    }

    // 3. deviceProductInfo: nullable parcelable (same null probe). Body:
    //    mName (String16), mManufacturerPnpId (String16), then three
    //    readValue() fields — mProductId (String), mModelYear (Integer),
    //    mManufactureDate (nullable ManufactureDate parcelable) — and a
    //    final mConnectionToSinkType i32. The ManufactureDate parcelable is
    //    length-prefixed, so skip_value's VAL_PARCELABLE case consumes it
    //    wholesale including its internal writeValue fields.
    if let Some(_dpi_class) = r.read_string()? {
        r.skip_string16()?; // mName
        r.skip_string16()?; // mManufacturerPnpId
        r.skip_value()?; // mProductId
        r.skip_value()?; // mModelYear
        r.skip_value()?; // mManufactureDate
        r.read_i32()?; // mConnectionToSinkType
    }

    // 4. name — Java `writeString8`, a String8 (byte length + UTF-8), may
    //    be null. Must NOT be read with read_string() (String16 only).
    r.skip_string8()?;

    // 5. Logical dimensions: appWidth, appHeight, smallestNominalAppWidth,
    //    smallestNominalAppHeight, largestNominalAppWidth,
    //    largestNominalAppHeight, logicalWidth, logicalHeight (8×i32).
    r.skip_i32s(8)?;

    // 6. displayCutout: ParcelableWrapper. writeCutoutToParcel writes -1
    //    (null), 0 (NO_CUTOUT), or 1 + body. Body: safeInsets (typed
    //    Rect), bounds (typed Rect array), waterfallInsets (typed Rect),
    //    4×i32 cutout path parser dims, density f32, cutoutSpec String16,
    //    rotation i32, scale f32, physicalPixelDisplaySizeRatio f32.
    let cutout_marker = r.read_i32()?;
    if cutout_marker == 1 {
        r.skip_typed_rect()?; // mSafeInsets
        let bounds = r.read_i32()?; // mBounds Rect[]
        if bounds >= 0 {
            for _ in 0..bounds {
                r.skip_typed_rect()?;
            }
        }
        r.skip_typed_rect()?; // mWaterfallInsets
        r.skip_i32s(4)?; // cutout path parser info (display/phys dims)
        r.read_float()?; // density
        r.skip_string16()?; // cutoutSpec
        r.read_i32()?; // rotation
        r.read_float()?; // scale
        r.read_float()?; // physicalPixelDisplaySizeRatio
    }

    // 7. rotation (i32), modeId (i32), renderFrameRate (f32),
    //    defaultModeId (i32), then nModes (i32).
    r.read_i32()?; // rotation
    let active_mode_id = r.read_i32()?;
    let render_frame_rate = {
        let v = r.read_float()?;
        if v.is_finite() && v >= 0.0 { v } else { 0.0 }
    };
    r.read_i32()?; // defaultModeId
    let n_modes = r.read_i32()?.max(0) as usize;

    // 8. supportedModes: each Display.Mode = { modeId, width, height,
    //    refreshRate (×4 primitive) + alternativeRefreshRates float[],
    //    supportedHdrTypes int[] }.
    let mut vsync_rate: f32 = 0.0;
    let mut peak_refresh_rate: f32 = 0.0;
    for _ in 0..n_modes {
        let mode_id = r.read_i32()?;
        r.read_i32()?; // width
        r.read_i32()?; // height
        let refresh_rate = {
            let v = r.read_float()?;
            // collapse NaN/±Inf/negative like the FPS path; otherwise a
            // hostile peer could leak +Inf into the public DisplayInfo.
            if v.is_finite() && v >= 0.0 { v } else { 0.0 }
        };
        // alternativeRefreshRates: float[] (count + values)
        let alt = r.read_i32()?.max(0) as usize;
        for _ in 0..alt {
            let _f = r.read_float()?;
        }
        // supportedHdrTypes: int[]
        let hdr = r.read_i32()?.max(0) as usize;
        for _ in 0..hdr {
            r.read_i32()?;
        }
        peak_refresh_rate = peak_refresh_rate.max(refresh_rate);
        if mode_id == active_mode_id {
            vsync_rate = refresh_rate.max(vsync_rate);
        }
    }

    Ok(ParsedDisplayInfo {
        active_mode_id,
        vsync_rate,
        peak_refresh_rate,
        render_frame_rate,
    })
}
// ── DisplayManager ─────────────────────────────────────────────────

const DISPLAY_SERVICE: &[u8] = b"display\0";
const DISPLAY_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManager\0";
const CALLBACK_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManagerCallback\0";
const POWER_SERVICE: &[u8] = b"power\0";
const POWER_DESCRIPTOR: &[u8] = b"android.os.IPowerManager\0";

const TX_DISPLAY_REGISTER_CALLBACK: u32 = 4;

// Core owns the callback eventfd; the consumer gets a dup and may close it
// freely. Same lifetime discipline as the ActivityManager observer (C2).
static DISP_EVENTFD: Mutex<Option<OwnedFd>> = Mutex::new(None);

fn disp_eventfd_guard() -> std::sync::MutexGuard<'static, Option<OwnedFd>> {
    DISP_EVENTFD.lock().unwrap_or_else(|p| p.into_inner())
}

unsafe extern "C" fn disp_cb_on_create(_: *mut c_void) -> *mut c_void {
    std::ptr::null_mut()
}
unsafe extern "C" fn disp_cb_on_destroy(_: *mut c_void) {}
unsafe extern "C" fn disp_cb_on_transact(
    _: *mut AIBinder,
    code: u32,
    _: *const AParcel,
    _: *mut AParcel,
) -> BinderStatus {
    if code == 1 {
        if let Some(fd) = disp_eventfd_guard().as_ref() {
            let val: u64 = 1;
            unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
        }
    }
    STATUS_OK
}
// Client-only classes: the NDK requires a class on ANY binder that
// participates in a transaction (AIBinder_prepareTransaction fails with
// STATUS_INVALID_OPERATION when getClass() == null) and uses the class
// descriptor to write the interface token. These stubs are never invoked
// for a client (remote) binder — onCreate/onTransact only fire on local
// AIBinder_new instances — so they mirror the ActivityManager client
// pattern exactly.
unsafe extern "C" fn disp_client_on_create(_: *mut c_void) -> *mut c_void {
    std::ptr::null_mut()
}
unsafe extern "C" fn disp_client_on_destroy(_: *mut c_void) {}
unsafe extern "C" fn disp_client_on_transact(
    _: *mut AIBinder,
    _: u32,
    _: *const AParcel,
    _: *mut AParcel,
) -> BinderStatus {
    STATUS_UNKNOWN_TRANSACTION
}
/// Snapshot of the active display state used for FPS normalization.
///
/// Populated from `IDisplayManager.getDisplayInfo` (tx resolved from the
/// installed ROM's framework.jar). The parcel layout is Android-version
/// specific; this parse targets Android 14 and must be validated on-device
/// before consumers rely on the values for fps/R normalization.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct DisplayInfo {
    /// Active display mode id (`DisplayInfo.modeId`).
    pub active_mode_id: i32,
    /// Refresh rate of the active mode, in Hz.
    pub vsync_rate: f32,
    /// Highest refresh rate across supported modes, in Hz.
    pub peak_refresh_rate: f32,
    /// `DisplayInfo.renderFrameRate` (Android 13+), in Hz.
    pub render_frame_rate: f32,
    /// Interactive state reported by the power service.
    pub is_interactive: bool,
}

pub struct DisplayManager {
    _lib: DlHandle,
    vt: Vtable,
    display: Option<OwnedBinder>,
    display_info_tx: Option<u32>,
    power: Option<OwnedBinder>,
    is_interactive_tx: u32,
}
unsafe impl Send for DisplayManager {}

impl DisplayManager {
    pub fn open_with_callback() -> Result<(Self, crate::fd::Fd), 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)?;

        // Blocking eventfd (no EFD_NONBLOCK) — callback writes, caller's
        // read_u64_blocking() waits. The core owns it for the callback's
        // lifetime; the consumer receives a dup below (C2).
        let owned = unsafe {
            let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
            if raw < 0 {
                return Err(CoreError::sys(*libc::__errno(), "eventfd"));
            }
            OwnedFd::from_raw_fd(raw)
        };

        // Get display service. A client-only binder still needs a class
        // (AIBinder_prepareTransaction requires getClass() != null and
        // writes the interface token from the class descriptor).
        let raw_display = unsafe { (vt.get_service)(DISPLAY_SERVICE.as_ptr() as *const c_char) };
        if raw_display.is_null() {
            return Err(CoreError::binder(-1, "AServiceManager_getService:display"));
        }
        let display_class = unsafe {
            (vt.class_define)(
                DISPLAY_DESCRIPTOR.as_ptr() as *const c_char,
                disp_client_on_create,
                disp_client_on_destroy,
                disp_client_on_transact,
            )
        };
        if display_class.is_null() {
            return Err(CoreError::binder(
                -1,
                "AIBinder_Class_define:IDisplayManager",
            ));
        }
        unsafe { (vt.associate_class)(raw_display, display_class) };
        let display = OwnedBinder {
            ptr: raw_display,
            dec_strong: vt.dec_strong,
        };

        // Define IDisplayManagerCallback (we're the server receiving callbacks)
        let cb_class = unsafe {
            (vt.class_define)(
                CALLBACK_DESCRIPTOR.as_ptr() as *const c_char,
                disp_cb_on_create,
                disp_cb_on_destroy,
                disp_cb_on_transact,
            )
        };
        if cb_class.is_null() {
            return Err(CoreError::binder(
                -1,
                "AIBinder_Class_define:DisplayCallback",
            ));
        }

        let cb_binder = unsafe { (vt.new_binder)(cb_class, std::ptr::null_mut()) };
        if cb_binder.is_null() {
            return Err(CoreError::binder(-1, "AIBinder_new:DisplayCallback"));
        }

        // registerCallback(callback) — tx 4
        let _ = transact_write(&vt, display.ptr, TX_DISPLAY_REGISTER_CALLBACK, |w| {
            w.write_strong_binder(cb_binder)
        })?;

        // Optional: grab power service for is_interactive(). Same client
        // class requirement as the display binder above.
        let power_class = unsafe {
            (vt.class_define)(
                POWER_DESCRIPTOR.as_ptr() as *const c_char,
                disp_client_on_create,
                disp_client_on_destroy,
                disp_client_on_transact,
            )
        };
        let power = if power_class.is_null() {
            None
        } else {
            let raw = unsafe { (vt.get_service)(POWER_SERVICE.as_ptr() as *const c_char) };
            if raw.is_null() {
                None
            } else {
                unsafe { (vt.associate_class)(raw, power_class) };
                Some(OwnedBinder {
                    ptr: raw,
                    dec_strong: vt.dec_strong,
                })
            }
        };

        // Resolve isInteractive tx code from DEX at open time
        let is_interactive_tx = crate::android::dex::resolve_is_interactive_tx()
            .ok_or_else(|| CoreError::binder(-1, "dex:TRANSACTION_isInteractive not found"))?;

        // Resolve getDisplayInfo tx from DEX (best-effort, optional). ROMs
        // that drop the field skip the info() API rather than failing open.
        let display_info_tx = crate::android::dex::resolve_display_info_tx();

        // Consumer dup — made before publishing, so an error path drops the
        // owned fd without ever leaving a stale handle for the callback.
        let efd_owned = owned
            .try_clone()
            .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:display"))
            .and_then(|dup| unsafe {
                crate::fd::Fd::from_owned_raw_fd(dup.into_raw_fd(), "display.efd")
                    .map_err(|_| CoreError::binder(-1, "Fd::from_owned_raw_fd:display.efd"))
            })?;

        // Publish the core-owned eventfd for the callback
        *disp_eventfd_guard() = Some(owned);

        // Join binder thread pool so callbacks can fire
        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,
                display: Some(display),
                display_info_tx,
                power,
                is_interactive_tx,
            },
            efd_owned,
        ))
    }

    pub fn is_interactive(&self) -> Result<bool, CoreError> {
        let power = self
            .power
            .as_ref()
            .ok_or_else(|| CoreError::binder(-1, "power:unavailable"))?;
        let out = transact_write(&self.vt, power.ptr, self.is_interactive_tx, |_| Ok(()))?;
        let r = ParcelReader::owned(&self.vt, &out);
        let ex = r.read_i32()?;
        if ex != EX_NONE {
            return Err(CoreError::binder(ex, "isInteractive:exception"));
        }
        r.read_bool()
    }

    /// Query the display's current render state for FPS normalization.
    ///
    /// Transacts `IDisplayManager.getDisplayInfo(display_id)` and decodes
    /// the reply parcel (Android 14 `android.view.DisplayInfo` layout):
    /// active mode id, active-mode refresh rate, peak refresh rate across
    /// supported modes, and `renderFrameRate`, plus the interactive state
    /// from the power service.
    ///
    /// # Best-effort / on-device caveat
    /// The parcel walk below follows the Android 14 field ordering. ROMs or
    /// future Android releases that reorder `DisplayInfo` fields will fail
    /// the walk with a `CoreError` (never garbage) — tune
    /// `parse_display_info` against the installed framework.jar, which is
    /// why the tx code is resolved at open time via DEX.
    pub fn info(&self, display_id: i32) -> Result<DisplayInfo, CoreError> {
        let display = self
            .display
            .as_ref()
            .ok_or_else(|| CoreError::binder(-1, "display:unavailable"))?;
        let tx = self
            .display_info_tx
            .ok_or_else(|| CoreError::binder(-1, "display_info:tx unavailable"))?;
        let out = transact_write(&self.vt, display.ptr, tx, |w| w.write_i32(display_id))?;
        let r = ParcelReader::owned(&self.vt, &out);

        let ex = r.read_i32()?;
        if ex != EX_NONE {
            return Err(CoreError::binder(ex, "getDisplayInfo:exception"));
        }
        // The reply parcel carries the parcelable off the framework's own
        // writeToParcel, which writes no class-name header for a top-level
        // reply value in native code — it is the raw DisplayInfo fields.
        // The first field read must be layerStack. If a future framework
        // writes a class name first, the first read becomes a string length
        // (small positive int) and `layer_stack` is a plausible-looking
        // wrong value; guard by sanity-checking later fields instead.
        let parsed = parse_display_info(&r)?;

        let interactive = self.is_interactive().unwrap_or(false);
        Ok(DisplayInfo {
            active_mode_id: parsed.active_mode_id,
            vsync_rate: parsed.vsync_rate,
            peak_refresh_rate: parsed.peak_refresh_rate,
            render_frame_rate: parsed.render_frame_rate,
            is_interactive: interactive,
        })
    }
}