cloudfox-coreshift-core 2.29.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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
// 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/

//! Host-runnable byte model of the NDK parcel wire format used by the handoff
//! `sendBinder` bundle.
//!
//! Compiled on **all** targets so the byte math is exercised by `cargo test`
//! on every host commit, not only on Android where `AParcel` exists. The
//! length the write path emits is *derived* here by running the same write
//! sequence against a byte counter, so the constant can never drift from what
//! is actually written (finding 28).
//!
//! Wire facts (verified against AOSP android-14.0.0_r1):
//! - `AParcel_writeString` (`system/libbinder_ndk/parcel.cpp:318`) writes
//!   `writeInt32(len16)` (UTF-16 code units) then
//!   `writeInplace((len16 + 1) * sizeof(char16_t))` — chars plus the NUL
//!   terminator, padded to 4 by `Parcel::writeInplace` (`pad_size`). So
//!   `String16 "binder"` (6 units) = 4 + pad(14) = 4 + 16 = 20, and
//!   `String16 "exec"` (4 units) = 4 + pad(10) = 4 + 12 = 16. A null string
//!   is a single `writeInt32(-1)`.
//! - `writeStrongBinder` = `flattenBinder` = 24-byte `flat_binder_object`
//!   (`writeObject`) + 4-byte stability int32 (`finishFlattenBinder`) = 28.
//! - `BaseBundle.writeToParcelInner` (`writeInt(length)` + `BUNDLE_MAGIC` +
//!   body): `startPos` is captured after the magic, so `length = endPos -
//!   startPos` counts the map body only (N + key/value entries) — not the
//!   length int itself nor the magic.

use crate::CoreError;
use std::os::raw::c_void;

/// `BaseBundle.BUNDLE_MAGIC` — the int `Parcel.writeBundle` writes before the
/// array-map payload. The handoff walk validates it while skipping a bundle.
pub(crate) const BUNDLE_MAGIC: i32 = 0x4C444E42;
/// Java exception code for "no exception". The leading i32 of every binder
/// reply: `EX_NONE` (0) means the transaction succeeded, any other value is
/// the framework's exception code. `sys::EX_NONE` re-exports this constant so
/// the on-device reply check and the host synthetic-parcel test share it.
pub(crate) const EX_NONE: i32 = 0;
/// `Parcel.writeValue` tag for an `IBinder` (`VAL_IBINDER`). 15, not 22.
pub(crate) const VAL_IBINDER: i32 = 15;
/// Bundle key under which the app reads the handed service binder.
pub(crate) const BUNDLE_KEY: &str = "binder";
/// Bundle key under which the app reads the handed exec binder (A1 delivery:
/// `{"binder": watch, "exec": exec}`). Optional — its absence must never
/// invalidate `"binder"`, so an old manager that only knows `"binder"` still
/// receives the watch service.
pub(crate) const EXEC_KEY: &str = "exec";
/// Bundle key under which the app reads the daemon's per-boot epoch (A1
/// delivery with the exec binder: `{"binder": watch, "exec": exec,
/// "epoch": byte[16]}`). The epoch is the consistency boundary for exec
/// frames (server plan §4.1): the manager echoes it in every request, and a
/// stale request after a daemon restart is rejected by identity, never by
/// numeric collision. Optional like `"exec"` — its absence must never
/// invalidate `"binder"`.
pub(crate) const EPOCH_KEY: &str = "epoch";
/// `Parcel.writeValue` tag for a `byte[]` (`VAL_BYTEARRAY`). 13 — pinned to
/// the AOSP value (`Parcel.java`: `VAL_BYTEARRAY=13`). The epoch bundle entry
/// is a 16-byte array (`writeInt(16)` + 16 raw bytes padded to 4).
pub(crate) const VAL_BYTEARRAY: i32 = 13;
/// `Parcel.writeValue` tag for an `Integer` (`VAL_INTEGER`). 1 — pinned to
/// the AOSP value (`Parcel.java`: `VAL_NULL=-1, VAL_STRING=0, VAL_INTEGER=1`).
/// The handoff-status reply bundle's value is written by the manager's
/// `Bundle.putInt`, whose tag the daemon must match byte-for-byte.
pub(crate) const VAL_INTEGER: i32 = 1;
/// Bundle key under which the manager's `call()` reply reports the handoff
/// outcome (`{"status": int}`). The daemon reads it so a rejected handoff is
/// surfaced instead of silently pinned (finding 4 mirror at the mint boundary).
pub(crate) const HANDOFF_STATUS_KEY: &str = "status";
/// The handoff-accepted status. A manager reply bundle whose `status` is any
/// other value means the handoff was rejected; the daemon must not pin the
/// uid for a capability the app never received.
pub(crate) const HANDOFF_STATUS_OK: i32 = 0;
/// Rejected: the calling uid is not a privileged daemon uid
/// (`CoreShiftProvider`'s uid gate). Distinct codes let the daemon log the
/// reason; Core only tests `!= HANDOFF_STATUS_OK`.
#[cfg(test)]
pub(crate) const HANDOFF_STATUS_REJECTED_UID: i32 = 1;
/// Rejected: the handed binder failed the descriptor shape check
/// (`CoreShiftProvider`'s `isWatchService`/`isExecService` gate).
#[cfg(test)]
pub(crate) const HANDOFF_STATUS_REJECTED_DESCRIPTOR: i32 = 2;

/// The parcel-read surface a reply bundle is read through. Implemented by the
/// real `ParcelReader` on Android and by the host `ByteCursor` in tests, so
/// the exact same read sequence runs against both.
pub(crate) trait BundleSource {
    fn read_i32(&self) -> Result<i32, CoreError>;
    fn read_string(&self) -> Result<Option<String>, CoreError>;
}

/// Read the handoff `call()` reply's status.
///
/// Reply shape (`ContentProviderNative.onTransact`, `CALL_TRANSACTION`):
/// `writeNoException()` writes the leading `EX_NONE` i32 (checked by the
/// caller), then `writeBundle(reply)` writes `writeInt(length)` +
/// `BUNDLE_MAGIC` + body. A `null` reply bundle is a single `writeInt(-1)` —
/// the legacy manager shape, which carried no status (finding 4: a rejection
/// was indistinguishable from success). A present bundle must be exactly
/// `{"status": int}`: `N=1`, key `HANDOFF_STATUS_KEY`, tag `VAL_INTEGER`.
/// Anything else fails closed (`Err`), never silently accepted.
pub(crate) fn read_handoff_status(r: &impl BundleSource) -> Result<i32, CoreError> {
    let length = r.read_i32()?;
    if length == -1 {
        // Legacy manager: null reply, no status observable. Preserve the
        // pre-status behavior (accept) so an old manager keeps working.
        return Ok(HANDOFF_STATUS_OK);
    }
    let magic = r.read_i32()?;
    if magic != BUNDLE_MAGIC {
        return Err(CoreError::binder(magic, "call:reply_bad_bundle_magic"));
    }
    let n = r.read_i32()?;
    if n != 1 {
        return Err(CoreError::binder(n, "call:reply_unexpected_entry_count"));
    }
    let key = r.read_string()?;
    if key.as_deref() != Some(HANDOFF_STATUS_KEY) {
        return Err(CoreError::binder(-1, "call:reply_missing_status_key"));
    }
    let tag = r.read_i32()?;
    if tag != VAL_INTEGER {
        return Err(CoreError::binder(tag, "call:reply_status_not_integer"));
    }
    r.read_i32()
}

/// The parcel-write surface the bundle body is written through. Implemented
/// by the real `ParcelWriter` on Android and by the host `ByteCounter` in
/// tests, so the exact same write sequence runs against both.
pub(crate) trait BundleSink {
    fn write_i32(&self, v: i32) -> Result<(), CoreError>;
    fn write_string(&self, s: Option<&str>) -> Result<(), CoreError>;
    fn write_strong_binder(&self, b: *mut c_void) -> Result<(), CoreError>;
    /// `Parcel.writeByteArray`: `writeInt(len)` + len raw bytes padded to 4.
    /// The epoch bundle entry is a fixed 16-byte array (`VAL_BYTEARRAY`).
    fn write_byte_array(&self, b: &[u8]) -> Result<(), CoreError>;
}

/// Write surface for the uid-observer registration body
/// ([`write_uid_observer_body`]). Inherits the bundle write surface and adds
/// the int-array write. Implemented by `ParcelWriter` on Android and by the
/// host `ByteCounter` in tests, so the exact write sequence — including the
/// conditional `uids` array (finding 24) — runs against both.
pub(crate) trait RegisterBodySink: BundleSink {
    fn write_int32_array(&self, v: &[i32]) -> Result<(), CoreError>;
}

/// Write the `extras` map body of the `sendBinder` bundle (the bytes
/// `BaseBundle.writeToParcelInner` counts after the magic): `N` entries of
/// `writeString(key)` + `writeValue(binder)`.
///
/// This is the single source of truth for the bundle's wire shape — both the
/// on-device transaction and the host byte test run it, so the emitted length
/// (from [`bundle_length`]) and the emitted bytes cannot disagree.
///
/// `epoch` (16 bytes) rides with the exec binder: `{"binder", "exec",
/// "epoch"}`. It is optional like `"exec"` — an old manager that only reads
/// `"binder"` still receives the watch service byte-identically, and a watch
/// -only handoff (no exec) never writes it.
pub(crate) fn write_bundle_body(
    w: &impl BundleSink,
    has_exec: bool,
    epoch: Option<&[u8; 16]>,
    service_binder: *mut c_void,
    exec_binder: *mut c_void,
) -> Result<(), CoreError> {
    let n = if has_exec { 2 } else { 1 } + if epoch.is_some() { 1 } else { 0 };
    w.write_i32(n)?;
    w.write_string(Some(BUNDLE_KEY))?;
    w.write_i32(VAL_IBINDER)?;
    w.write_strong_binder(service_binder)?;
    if has_exec {
        w.write_string(Some(EXEC_KEY))?;
        w.write_i32(VAL_IBINDER)?;
        w.write_strong_binder(exec_binder)?;
    }
    if let Some(epoch) = epoch {
        w.write_string(Some(EPOCH_KEY))?;
        w.write_i32(VAL_BYTEARRAY)?;
        w.write_byte_array(epoch)?;
    }
    Ok(())
}

/// Byte length of the `sendBinder` extras bundle body — derived by running
/// [`write_bundle_body`] through the AOSP-verified byte counter, so it can
/// never disagree with what the write path actually emits. `56` for the
/// single-entry `{"binder": IBinder}` bundle, `104` for the two-entry
/// `{"binder": …, "exec": …}` bundle, `144` with the `"epoch"` entry.
pub(crate) fn bundle_length(has_exec: bool, epoch: Option<&[u8; 16]>) -> i32 {
    let c = ByteCounter::default();
    // ByteCounter writes never fail; the pointers are ignored.
    write_bundle_body(
        &c,
        has_exec,
        epoch,
        std::ptr::null_mut(),
        std::ptr::null_mut(),
    )
    .unwrap();
    c.bytes.get() as i32
}

/// The AIDL reply convention: the reply's leading i32 is the Java exception
/// code (`EX_NONE` = 0). A registration that the framework rejects returns a
/// non-`EX_NONE` code as that first field — the caller that reads it can
/// surface the failure instead of running with a dead observer (finding 25).
/// This pure decision is shared by the on-device `transact_write_checked` and
/// the host synthetic-parcel tests.
pub(crate) fn check_reply_exception(ex: i32) -> Result<(), CoreError> {
    if ex == EX_NONE {
        Ok(())
    } else {
        Err(CoreError::binder(ex, "register:reply_exception"))
    }
}

/// The `registerUidObserverForUids`/`registerUidObserver` argument body.
///
/// AIDL shape: `(IUidObserver observer, int which, int cutpoint, String
/// callingPackage[, int[] uids])`. The trailing `uids` array exists **only** on
/// the `ForUids` variant; the 4-arg `registerUidObserver` fallback must not
/// write it — the server ignores the trailing array and registers **unfiltered**
/// across every uid (finding 24). The write is branched here so the on-device
/// path and the host byte test share the exact sequence.
pub(crate) fn write_uid_observer_body(
    w: &impl RegisterBodySink,
    which: i32,
    cutpoint: i32,
    calling_package: Option<&str>,
    has_for_uids: bool,
    watched_uid: i32,
) -> Result<(), CoreError> {
    w.write_i32(which)?;
    w.write_i32(cutpoint)?;
    w.write_string(calling_package)?;
    if has_for_uids {
        w.write_int32_array(&[watched_uid])?;
    }
    Ok(())
}

/// Byte-accounting `BundleSink` that reproduces the AOSP parcel wire format
/// exactly (see the module docs for the derivation).
#[derive(Default)]
struct ByteCounter {
    bytes: std::cell::Cell<usize>,
}

fn pad4(n: usize) -> usize {
    (n + 3) & !3
}

/// `AParcel_writeString` byte size for an ASCII key: 4-byte UTF-16 length int
/// + `(len16 + 1) * 2` bytes (chars + NUL) padded to 4.
///
/// The bundle keys are ASCII, so UTF-8 byte length == UTF-16 code-unit count.
fn string16_size(s: &str) -> usize {
    4 + pad4((s.len() + 1) * 2)
}

impl BundleSink for ByteCounter {
    fn write_i32(&self, _v: i32) -> Result<(), CoreError> {
        self.bytes.set(self.bytes.get() + 4);
        Ok(())
    }
    fn write_string(&self, s: Option<&str>) -> Result<(), CoreError> {
        let n = match s {
            None => 4, // writeInt32(-1)
            Some(s) => string16_size(s),
        };
        self.bytes.set(self.bytes.get() + n);
        Ok(())
    }
    fn write_strong_binder(&self, _b: *mut c_void) -> Result<(), CoreError> {
        // 24-byte flat_binder_object + 4-byte stability int32.
        self.bytes.set(self.bytes.get() + 28);
        Ok(())
    }
    fn write_byte_array(&self, b: &[u8]) -> Result<(), CoreError> {
        // `AParcel_writeByteArray`: writeInt(len) + len raw bytes padded to 4.
        self.bytes.set(self.bytes.get() + 4 + pad4(b.len()));
        Ok(())
    }
}

impl RegisterBodySink for ByteCounter {
    fn write_int32_array(&self, v: &[i32]) -> Result<(), CoreError> {
        // `AParcel_writeInt32Array`: writeInt32(length) then length ints.
        self.bytes.set(self.bytes.get() + 4 + 4 * v.len());
        Ok(())
    }
}

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

    #[test]
    fn string16_sizes_match_aosp_write_path() {
        // "binder": 6 UTF-16 units → 4 + pad((6+1)*2)=pad(14)=16 → 20.
        assert_eq!(string16_size("binder"), 20);
        // "exec": 4 UTF-16 units → 4 + pad((4+1)*2)=pad(10)=12 → 16.
        // (The pre-test constant undercounted this as 12 — it dropped the NUL
        // terminator and the padding. Finding: BUNDLE_LENGTH_TWO was 100, and
        // the write path requires 104.)
        assert_eq!(string16_size("exec"), 16);
    }

    #[test]
    fn null_string_is_four_bytes() {
        let c = ByteCounter::default();
        c.write_string(None).unwrap();
        assert_eq!(c.bytes.get(), 4);
    }

    /// The `Parcel.writeValue` tags are pinned to their AOSP literals
    /// (`Parcel.java`): a wrong constant here used to round-trip through the
    /// byte tests (they build replies with the same constant) and only failed
    /// on device, where the manager's `Bundle.putInt` wrote the real AOSP tag
    /// `1` and the daemon rejected the handoff reply (`reply_status_not_integer`,
    /// uid never pinned, exec CREATE always denied). `VAL_IBINDER` is 15 (not
    /// 22); `VAL_INTEGER` is 1 (not 2).
    #[test]
    fn write_value_tags_pin_aosp_literals() {
        assert_eq!(VAL_IBINDER, 15);
        assert_eq!(VAL_INTEGER, 1);
        assert_eq!(EX_NONE, 0);
    }

    #[test]
    fn single_bundle_body_is_56() {
        assert_eq!(bundle_length(false, None), 56);
    }

    #[test]
    fn two_bundle_body_is_104() {
        // N=2 (4) + "binder" entry (20 + 4 + 28 = 52) + "exec" entry
        // (16 + 4 + 28 = 48) = 104.
        assert_eq!(bundle_length(true, None), 104);
    }

    #[test]
    fn three_bundle_body_with_epoch_is_144() {
        // N=3 (4) + "binder" entry (52) + "exec" entry (48) + "epoch" entry:
        // "epoch" string16 (4 + pad4(6*2)=12 = 16) + VAL_BYTEARRAY (4) +
        // byte[16] (4 + pad4(16)=16 = 20) = 40. Total 4 + 52 + 48 + 40 = 144.
        let epoch = [0xAB; 16];
        assert_eq!(bundle_length(true, Some(&epoch)), 144);
        // The epoch entry is optional: watch-only handoffs stay byte-identical.
        assert_eq!(bundle_length(false, Some(&epoch)), 96);
    }

    #[test]
    fn write_path_emits_exactly_the_advertised_length() {
        // Construct the full bundle as the transaction writes it (length int,
        // magic, body) and assert the length field equals the body bytes.
        let c = ByteCounter::default();
        c.write_i32(bundle_length(true, None)).unwrap();
        c.write_i32(BUNDLE_MAGIC).unwrap();
        write_bundle_body(&c, true, None, std::ptr::null_mut(), std::ptr::null_mut()).unwrap();
        let total = c.bytes.get();
        // 4 (length int) + 4 (magic) + 104 (body).
        assert_eq!(total, 4 + 4 + 104);
        // And the body alone is exactly what the length field advertised.
        assert_eq!(c.bytes.get() - 8, bundle_length(true, None) as usize);
    }

    #[test]
    fn epoch_write_path_emits_exactly_the_advertised_length() {
        // The epoch-carrying bundle's length field matches its real bytes.
        let epoch = [0x11; 16];
        let c = ByteCounter::default();
        c.write_i32(bundle_length(true, Some(&epoch))).unwrap();
        c.write_i32(BUNDLE_MAGIC).unwrap();
        write_bundle_body(
            &c,
            true,
            Some(&epoch),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
        )
        .unwrap();
        assert_eq!(c.bytes.get(), 4 + 4 + 144);
        assert_eq!(
            c.bytes.get() - 8,
            bundle_length(true, Some(&epoch)) as usize
        );
    }

    #[test]
    fn single_bundle_write_path_is_byte_identical_shape() {
        let c = ByteCounter::default();
        c.write_i32(bundle_length(false, None)).unwrap();
        c.write_i32(BUNDLE_MAGIC).unwrap();
        write_bundle_body(&c, false, None, std::ptr::null_mut(), std::ptr::null_mut()).unwrap();
        assert_eq!(c.bytes.get(), 4 + 4 + 56);
    }

    // ── reply exception check (finding 25) ────────────────────────────────

    #[test]
    fn reply_ex_none_is_ok() {
        assert!(check_reply_exception(EX_NONE).is_ok());
    }

    #[test]
    fn reply_non_ex_none_is_err() {
        // A rejected registration returns the framework exception code as the
        // reply's leading i32; the caller must surface it, not run with a
        // dead observer.
        assert!(check_reply_exception(1).is_err());
        assert!(check_reply_exception(-1).is_err());
        assert!(check_reply_exception(32).is_err());
    }

    #[test]
    fn synthetic_parcel_leading_i32_is_exception_code() {
        // Host model of the reply's first field: little-endian i32. The
        // on-device reader does the same `read_i32` → `check_reply_exception`
        // sequence; this pins the data shape without a device.
        fn parse_leading_i32(buf: &[u8]) -> Result<(), CoreError> {
            let ex = i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
            check_reply_exception(ex)
        }
        assert!(parse_leading_i32(&0i32.to_le_bytes()).is_ok());
        assert!(parse_leading_i32(&1i32.to_le_bytes()).is_err());
        assert!(parse_leading_i32(&(-2i32).to_le_bytes()).is_err());
    }

    // ── handoff status reply (finding 4 mint-boundary mirror) ─────────────

    /// Host cursor over a synthetic reply parcel (little-endian i32; String16
    /// as `writeString` emits: len16 then `(len16+1)*2` bytes). Implements the
    /// same `BundleSource` surface `ParcelReader` implements on-device, so the
    /// exact read sequence runs against both. Position advances through
    /// interior mutability because the read surface borrows `&self`.
    struct ByteCursor<'a> {
        buf: &'a [u8],
        pos: std::cell::Cell<usize>,
    }

    impl<'a> ByteCursor<'a> {
        fn take(&self, n: usize) -> Result<&'a [u8], CoreError> {
            let pos = self.pos.get();
            if pos + n > self.buf.len() {
                return Err(CoreError::binder(-1, "cursor:past_end"));
            }
            self.pos.set(pos + n);
            Ok(&self.buf[pos..pos + n])
        }
    }

    impl<'a> BundleSource for ByteCursor<'a> {
        fn read_i32(&self) -> Result<i32, CoreError> {
            let b: &[u8] = self.take(4)?;
            Ok(i32::from_le_bytes(b.try_into().unwrap()))
        }
        fn read_string(&self) -> Result<Option<String>, CoreError> {
            let len16 = i32::from_le_bytes(self.take(4)?.try_into().unwrap());
            if len16 == -1 {
                return Ok(None);
            }
            if len16 < 0 {
                return Err(CoreError::binder(len16, "cursor:bad_string_len"));
            }
            // `writeString` writes `(len16+1)*2` bytes (chars + NUL), padded
            // to 4 by `Parcel::writeInplace` — consume the padded extent.
            let padded = ((len16 as usize + 1) * 2).next_multiple_of(4);
            let bytes = self.take(padded)?;
            let mut u16s = Vec::with_capacity(len16 as usize);
            for pair in bytes[..len16 as usize * 2].chunks_exact(2) {
                u16s.push(u16::from_le_bytes([pair[0], pair[1]]));
            }
            Ok(Some(String::from_utf16_lossy(&u16s)))
        }
    }

    /// `writeString` wire bytes for an ASCII key: len16 + `(len+1)*2` chars
    /// (NUL-terminated), padded to 4 — the exact shape `AParcel_writeString`
    /// emits.
    fn string16(buf: &mut Vec<u8>, s: &str) {
        buf.extend((s.len() as i32).to_le_bytes());
        let payload = (s.len() + 1) * 2;
        for c in s.chars() {
            buf.extend((c as u16).to_le_bytes());
        }
        buf.extend(0u16.to_le_bytes()); // NUL terminator
        buf.extend(std::iter::repeat_n(
            0u8,
            payload.next_multiple_of(4) - payload,
        ));
    }

    /// Build the exact reply bytes a manager's `call()` produces for a status
    /// bundle: `writeNoException()` → `[EX_NONE]`, then `writeBundle` →
    /// `[length][BUNDLE_MAGIC][N][key][VAL_INTEGER][status]`.
    fn status_reply(status: i32) -> Vec<u8> {
        let mut b: Vec<u8> = Vec::new();
        b.extend(EX_NONE.to_le_bytes());
        // Bundle body: N=1 + "status" key + VAL_INTEGER tag + status.
        let body = {
            let mut m: Vec<u8> = Vec::new();
            m.extend(1i32.to_le_bytes());
            string16(&mut m, HANDOFF_STATUS_KEY);
            m.extend(VAL_INTEGER.to_le_bytes());
            m.extend(status.to_le_bytes());
            m
        };
        b.extend((body.len() as i32).to_le_bytes());
        b.extend(BUNDLE_MAGIC.to_le_bytes());
        b.extend(body);
        b
    }

    /// A legacy manager returned `null`: `writeBundle(null)` → `writeInt(-1)`.
    fn null_reply() -> Vec<u8> {
        let mut b: Vec<u8> = Vec::new();
        b.extend(EX_NONE.to_le_bytes());
        b.extend((-1i32).to_le_bytes());
        b
    }

    #[test]
    fn status_reply_shape_is_pinned() {
        // EX_NONE(4) + length(4) + magic(4) + [N(4) + "status" String16
        // (4 + pad((6+1)*2)=16 → 20) + VAL_INTEGER(4) + status(4)] = 44.
        assert_eq!(
            status_reply(HANDOFF_STATUS_OK).len(),
            4 + 4 + 4 + 4 + 20 + 4 + 4
        );
    }

    #[test]
    fn read_handoff_status_ok() {
        let c = ByteCursor {
            buf: &status_reply(HANDOFF_STATUS_OK),
            pos: std::cell::Cell::new(4),
        };
        assert_eq!(read_handoff_status(&c).unwrap(), HANDOFF_STATUS_OK);
    }

    #[test]
    fn read_handoff_status_rejected_uid() {
        let c = ByteCursor {
            buf: &status_reply(HANDOFF_STATUS_REJECTED_UID),
            pos: std::cell::Cell::new(4),
        };
        assert_eq!(
            read_handoff_status(&c).unwrap(),
            HANDOFF_STATUS_REJECTED_UID
        );
    }

    #[test]
    fn read_handoff_status_rejected_descriptor() {
        let c = ByteCursor {
            buf: &status_reply(HANDOFF_STATUS_REJECTED_DESCRIPTOR),
            pos: std::cell::Cell::new(4),
        };
        assert_eq!(
            read_handoff_status(&c).unwrap(),
            HANDOFF_STATUS_REJECTED_DESCRIPTOR
        );
    }

    #[test]
    fn read_handoff_status_legacy_null_reply_is_ok() {
        // An old manager that never carried a status returns null on success;
        // the reader must accept it, not reject the handoff.
        let c = ByteCursor {
            buf: &null_reply(),
            pos: std::cell::Cell::new(4),
        };
        assert_eq!(read_handoff_status(&c).unwrap(), HANDOFF_STATUS_OK);
    }

    #[test]
    fn read_handoff_status_bad_magic_fails_closed() {
        let mut r = status_reply(HANDOFF_STATUS_OK);
        r[8..12].copy_from_slice(&0xBAD0u32.to_le_bytes());
        let c = ByteCursor {
            buf: &r,
            pos: std::cell::Cell::new(4),
        };
        assert!(read_handoff_status(&c).is_err());
    }

    #[test]
    fn read_handoff_status_unknown_key_fails_closed() {
        // A manager reply bundle with a different/extra shape must not be
        // silently accepted as OK.
        let mut b: Vec<u8> = Vec::new();
        b.extend(EX_NONE.to_le_bytes());
        let body = {
            let mut m: Vec<u8> = Vec::new();
            m.extend(1i32.to_le_bytes());
            string16(&mut m, "other");
            m.extend(VAL_INTEGER.to_le_bytes());
            m.extend(0i32.to_le_bytes());
            m
        };
        b.extend((body.len() as i32).to_le_bytes());
        b.extend(BUNDLE_MAGIC.to_le_bytes());
        b.extend(body);
        let c = ByteCursor {
            buf: &b,
            pos: std::cell::Cell::new(4),
        };
        assert!(read_handoff_status(&c).is_err());
    }

    // ── uid-observer register body (finding 24) ───────────────────────────

    fn uid_observer_body_len(has_for_uids: bool) -> usize {
        let c = ByteCounter::default();
        write_uid_observer_body(&c, 0x1E, -1, None, has_for_uids, 10123).unwrap();
        c.bytes.get()
    }

    #[test]
    fn uid_observer_for_uids_writes_trailing_uids_array() {
        // 5-arg form: which(4) + cutpoint(4) + null package(4) +
        // int32 array len(4) + one uid(4) = 20.
        assert_eq!(uid_observer_body_len(true), 20);
    }

    #[test]
    fn uid_observer_fallback_omits_trailing_uids_array() {
        // 4-arg fallback: which(4) + cutpoint(4) + null package(4) = 12 —
        // the trailing `int[] uids` must NOT be written, or the server
        // registers unfiltered (finding 24).
        assert_eq!(uid_observer_body_len(false), 12);
    }

    #[test]
    fn uid_observer_register_bodies_differ_exactly_by_uids_array() {
        // The two variants differ by exactly the array: length int + uid.
        assert_eq!(
            uid_observer_body_len(true) - uid_observer_body_len(false),
            8
        );
    }
}