cloudfox-coreshift-core 2.19.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
// 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";
/// `Parcel.writeValue` tag for an `Integer` (`VAL_INTEGER`). 2.
pub(crate) const VAL_INTEGER: i32 = 2;
/// 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`.
pub(crate) const HANDOFF_STATUS_REJECTED_UID: i32 = 1;
/// Rejected: the handed binder failed the descriptor shape check
/// (`CoreShiftProvider`'s `isWatchService`/`isExecService` gate).
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>;
}

/// 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.
pub(crate) fn write_bundle_body(
    w: &impl BundleSink,
    has_exec: bool,
    service_binder: *mut c_void,
    exec_binder: *mut c_void,
) -> Result<(), CoreError> {
    w.write_i32(if has_exec { 2 } else { 1 })?;
    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)?;
    }
    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.
pub(crate) fn bundle_length(has_exec: bool) -> i32 {
    let c = ByteCounter::default();
    // ByteCounter writes never fail; the pointers are ignored.
    write_bundle_body(&c, has_exec, 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(())
    }
}

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);
    }

    #[test]
    fn single_bundle_body_is_56() {
        assert_eq!(bundle_length(false), 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), 104);
    }

    #[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)).unwrap();
        c.write_i32(BUNDLE_MAGIC).unwrap();
        write_bundle_body(&c, true, 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) as usize);
    }

    #[test]
    fn single_bundle_write_path_is_byte_identical_shape() {
        let c = ByteCounter::default();
        c.write_i32(bundle_length(false)).unwrap();
        c.write_i32(BUNDLE_MAGIC).unwrap();
        write_bundle_body(&c, false, 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
        );
    }
}