rsbinder 0.11.0

Pure-Rust Binder IPC for Linux, Android, and macOS — kernel binder plus a libbinder-compatible RPC (binder-over-socket) transport.
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
// Copyright 2022 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0

//! RPC transport (binder-over-socket) — a **separate stack** from the
//! kernel binder path.
//!
//! This module is the rsbinder equivalent of Android's `Rpc*` code
//! (`RpcServer`/`RpcSession`/`RpcState`). It shares only the high-level
//! data model (`IBinder`/`Parcel`/AIDL stubs) with the kernel path; it
//! never touches `ProcessState`, `ThreadState`, `/dev/binder`, ioctl or
//! mmap.
//!
//! # Endianness
//!
//! The wire is little-endian, header and body alike, on whatever host
//! either end runs — so an rsbinder peer and an AOSP libbinder peer
//! exchange byte-identical parcels, and a big-endian host is a peer like
//! any other rather than a silent corruption. See the crate docs'
//! *Wire byte order* for the three-layer split.
//!
//! # Security
//!
//! **RPC is _not_ a drop-in for kernel binder's security model.** The
//! kernel gives `getCallingUid()`/SELinux for free; RPC does not. Each
//! [`RpcTransport`](crate::rpc::transport::RpcTransport)
//! implementation *defines its own trust boundary* and reports a
//! [`PeerIdentity`](crate::rpc::transport::PeerIdentity). A transport
//! that returns
//! [`PeerIdentity::Anonymous`](crate::rpc::transport::PeerIdentity::Anonymous)
//! gives the RPC layer **no basis for access control** — this is
//! logged explicitly and must be treated as untrusted. Plaintext
//! network transport is never appropriate for production (use the
//! `tls` backend).
//!
//! # Example (Unix-domain server + client)
//!
//! ```no_run
//! # #[cfg(feature = "rpc")] {
//! use rsbinder::rpc::{RpcServer, RpcSession};
//!
//! // Server: bind, publish a root binder, accept in the background.
//! let server = RpcServer::setup_unix_server("/tmp/demo.sock").unwrap();
//! # let root: rsbinder::SIBinder = unimplemented!();
//! server.set_root(root).unwrap();
//! let _bg = server.run_background();
//!
//! // Client: connect, (optionally) negotiate, fetch the root object.
//! let client = RpcSession::setup_unix_client("/tmp/demo.sock").unwrap();
//! let _negotiated = client.negotiate(4).unwrap();
//! let root = client.get_root().unwrap();
//! // Drive `root` with the **same generated stub** as the kernel path
//! // — the AIDL generator emits `as_remote().ok_or(BadType)?`, so one
//! // `Bp*` resolves either stack:
//! //
//! //     let foo: Strong<dyn IFoo> =
//! //         <dyn IFoo as FromIBinder>::try_from(root)?;
//! # let _ = root;
//! # }
//! ```
//!
//! A complete, runnable Unix-domain client/server pair driving a
//! generated AIDL stub is in
//! [`example-hello`](https://github.com/hiking90/rsbinder/tree/master/example-hello)
//! (`cargo run -p example-hello --features rpc --bin rpc_hello_service`
//! / `--bin rpc_hello_client`). A full pair incl. nested callbacks,
//! oneway, timeout and shared-session concurrency is exercised by
//! `rsbinder/tests/rpc_server.rs`.
//!
//! # Async
//!
//! The RPC stack's I/O is **blocking** (thread-per-connection, matching
//! android-12 r34's blocking-thread model). There is deliberately *no*
//! non-blocking `RpcTransport` / async reactor serve loop.
//!
//! What *is* supported (and verified — `tests/rpc_async.rs`) is the
//! same `spawn_blocking` adapter the kernel async path uses, over RPC:
//!
//! * **Async client** — the generated `…Async<P>` stub
//!   (`Strong::into_async::<rsbinder::Tokio>()`) runs each blocking
//!   `client_transact` on `tokio::task::spawn_blocking`; the reply
//!   parse is the async continuation. Concurrent calls on one shared
//!   session stay correctly serialized by the per-connection driver
//!   lock, now under genuine async concurrency.
//! * **Async service** — `Bn*::new_async_binder(impl …AsyncService,
//!   TokioRuntime(handle))` drives an `async fn` handler via
//!   `rt.block_on` from the blocking serve worker.
//!
//! Note this needs no kernel binder: the `Tokio` pool's
//! "am-I-in-a-kernel-transaction?" guard short-circuits via
//! `ProcessState::is_initialized()` so a pure-RPC process (e.g. on
//! macOS) no longer panics on an uninitialized `ProcessState`.

pub mod address;
pub mod end;
pub mod fd_mode;
pub(crate) mod lifecycle;
pub mod proxy;
pub mod server;
pub mod session;
// Internal RPC machinery: the wire-codec layer and per-session refcount/async
// state. Not part of the public API — the codec is selected internally (no user
// injection point) and `RpcState` is private session bookkeeping. Keeping them
// `pub(crate)` lets the protocol evolve without semver-breaking releases.
pub(crate) mod state;
pub mod transport;
// The wire modules implement the complete AOSP codec surface (both directions
// of every message, all negotiated versions), fully exercised by their own
// hermetic `mod tests`. Some encode/decode entry points are validated there but
// not reached by the current live dispatch path; `dead_code` fired on them only
// after the demotion from `pub`. Keep the complete, tested surface.
#[allow(dead_code)]
pub(crate) mod wire;
/// Fuzz entrypoints (`fuzz/fuzz_targets/rpc_{wire,address}_decode.rs`,
/// `rpc_session_handshake.rs`); not part of the supported API.
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub use wire::{__fuzz_decode_address, __fuzz_decode_wire, __fuzz_session_handshake};
#[allow(dead_code)]
pub(crate) mod wire_android13;

pub use address::{AddressSpace, RpcAddress, SpecialTransaction, RPC_SESSION_ID_NEW};
pub use end::{EndReason, EndedBy, SessionEnd, StreamState};
pub use fd_mode::FileDescriptorTransportMode;
pub use proxy::RpcProxy;
pub use server::RpcServer;
pub use session::{RpcSession, RpcUnixClientConfig};
pub use transport::{CertId, PeerIdentity, RpcTransport};

/// Re-export of the exact `rustls` the `tls` backend links, so callers
/// build `ClientConfig`/`ServerConfig` against a matching version
/// (key/cert management stays caller-side).
#[cfg(feature = "rpc-tls")]
pub use rustls;

use std::fmt;

/// Reject a remote binder at registration time.
///
/// An RPC server / session can only publish objects this process owns. A proxy
/// put here would either be handed back to its own peer or refused later, when
/// the parcel is written (AOSP `RpcState::onBinderLeaving`); catching it at
/// registration turns a confusing late failure into an immediate one.
pub(crate) fn refuse_remote(binder: &crate::SIBinder, what: &str) -> crate::Result<()> {
    if (**binder).is_remote() {
        log::error!("{what}: refusing a remote binder; wrap it in a local Bn* (gateway) instead");
        return Err(crate::StatusCode::InvalidOperation);
    }
    Ok(())
}

/// Result type for the RPC transport / protocol layer.
///
/// The transport layer surfaces a *rich* [`RpcError`] (so callers can
/// distinguish a clean peer close from a truncated frame, etc.). Public
/// RPC APIs project this onto
/// `rsbinder::Result` (`StatusCode`) or AIDL-facing `Status` at the
/// boundary — see [`StatusCode::RpcError`](crate::StatusCode::RpcError)
/// and `From<RpcError> for StatusCode`.
pub type RpcResult<T> = std::result::Result<T, RpcError>;

/// A transport-/protocol-level RPC error.
///
/// Kept separate from [`StatusCode`](crate::StatusCode) because
/// `StatusCode` is `Copy`/`Ord`/`Hash` and cannot carry a rich payload.
/// `#[non_exhaustive]` so the wire-decode, session-handshake, and TLS
/// layers can add variants without a breaking change.
#[non_exhaustive]
#[derive(Debug)]
pub enum RpcError {
    /// The stream ended: EOF with no frame pending, or a disconnect kind
    /// (`BrokenPipe` / `ConnectionReset` / `ConnectionAborted`) from a
    /// read or a write. It says nothing about
    /// **who** ended the stream or whether the end was **clean**: this
    /// end's own write failing after its own
    /// [`shutdown`](transport::RpcTransport::shutdown) folds here just as
    /// a peer's EOF does, and so does a reset. Whether this end decided
    /// is [`SessionEnd::by`]; whether the transport's close signal
    /// arrived is [`UncleanEndOfStream`](Self::UncleanEndOfStream). The
    /// frame-boundary guarantee is **read-side only**: a read that
    /// disconnects part-way through a frame is
    /// [`Truncated`](Self::Truncated), never this. A *send* that
    /// disconnects past its first byte is this variant with the position
    /// lost — no writer can report how much of the frame went out — which
    /// is why the session's send-failure rule retires a slot on it.
    /// Projects to [`StatusCode::DeadObject`](crate::StatusCode).
    EndOfStream,
    /// The stream ended without the transport's own close signal — a TLS
    /// session whose TCP stream ended with no `close_notify`. It arrives at
    /// a frame boundary or part-way through one, and the position is not
    /// assumed intact either way (the r34 framing readers promote it to
    /// [`Truncated`](Self::Truncated) mid-frame; the android-13+ ones leave
    /// it as itself). Kept apart from [`EndOfStream`](Self::EndOfStream)
    /// because on the one backend built for untrusted networks this is
    /// exactly what a truncation attack looks like; a plain socket has no
    /// close signal to miss and never reports it. The transport's own
    /// [`shutdown`](transport::RpcTransport::shutdown) refuses every send
    /// from that point, lets the one already in flight finish, and only
    /// then sends the signal and cuts the socket — so a deliberate close
    /// on this end is `EndOfStream` on the other, and every frame a sender
    /// was told went out is one the peer reads before it. The wait for
    /// that send is bounded so teardown stays finite: a peer that has
    /// stopped reading holds its sender past it, and reads the unclean end
    /// it was heading for anyway. Projects to
    /// [`StatusCode::DeadObject`](crate::StatusCode).
    UncleanEndOfStream,
    /// A frame was cut short: the length header itself arrived incomplete,
    /// or the header arrived in full and the body did not (peer closed
    /// mid-body, or declared more than it sent).
    Truncated,
    /// A read deadline elapsed part-way through a frame. The stream
    /// position is as lost as after [`Truncated`](Self::Truncated). Whose
    /// deadline it was is not knowable here: a read deadline of this end's
    /// (a reply deadline, a server's idle timeout) and the kernel's own
    /// `ETIMEDOUT` from a peer whose host went away arrive as the same
    /// io error kind and are not told apart. Attribution is
    /// [`SessionEnd::by`], which is decided with the knowledge of whether
    /// a deadline of this end's was armed at all.
    /// Projects to [`StatusCode::TimedOut`](crate::StatusCode).
    DeadlineMidFrame,
    /// A declared frame length exceeds [`transport::MAX_FRAME_LEN`].
    /// Rejected *before* any allocation (anti-OOM).
    FrameTooLarge {
        /// The length the peer (or caller) declared.
        declared: usize,
        /// The configured maximum ([`transport::MAX_FRAME_LEN`]).
        max: usize,
    },
    /// An underlying transport I/O error that is not a clean close.
    Io(std::io::Error),
    /// A protocol-level violation (used by the wire codec).
    Protocol(&'static str),
    /// A wait deadline elapsed with no frame boundary crossed: a read that
    /// consumed nothing (a reply or negotiation deadline), or a send that
    /// put nothing on the wire. The stream stays frame-synchronized either
    /// way, which is what lets the connection keep serving. Whose deadline
    /// it was is not knowable here — one of this end's and the kernel's own
    /// `ETIMEDOUT` arrive as the same io error kind; attribution is
    /// [`SessionEnd::by`], decided with the knowledge of whether a deadline
    /// of this end's was armed at all.
    /// Projects to [`StatusCode::TimedOut`](crate::StatusCode).
    Timeout,
}

impl fmt::Display for RpcError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RpcError::EndOfStream => write!(f, "RPC stream ended"),
            RpcError::UncleanEndOfStream => {
                write!(f, "RPC stream ended without a close signal (truncated?)")
            }
            RpcError::Truncated => write!(f, "RPC frame truncated (incomplete body)"),
            RpcError::DeadlineMidFrame => {
                write!(f, "RPC read deadline elapsed part-way through a frame")
            }
            RpcError::FrameTooLarge { declared, max } => {
                write!(
                    f,
                    "RPC frame too large: declared {declared} bytes, max {max}"
                )
            }
            RpcError::Io(e) => write!(f, "RPC transport I/O error: {e}"),
            RpcError::Protocol(why) => write!(f, "RPC protocol violation: {why}"),
            RpcError::Timeout => write!(f, "RPC wait deadline elapsed"),
        }
    }
}

impl std::error::Error for RpcError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            RpcError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl RpcError {
    /// Whether a read that failed with this error is documented to have
    /// left the stream at a frame boundary: [`EndOfStream`](Self::EndOfStream)
    /// ("no frame pending") and [`Timeout`](Self::Timeout) ("no frame
    /// boundary crossed"). Every other read failure lacks
    /// that guarantee — including ones that in fact consumed nothing — and
    /// is treated as a lost position. Two callers ask: the android-13+
    /// reader, which promotes a mid-frame case to `Truncated` /
    /// `DeadlineMidFrame`, and `client_transact`'s reply wait, which decides
    /// whether an abandoned nested call left its `REPLY` inbound. The r34
    /// framing readers and the serve loop reimplement the same split inline
    /// — against the io error kind and the `RpcError` variants
    /// respectively — so a change to the set here has to be made there too.
    pub(crate) fn leaves_frame_boundary_intact(&self) -> bool {
        matches!(self, RpcError::EndOfStream | RpcError::Timeout)
    }
}

impl From<std::io::Error> for RpcError {
    /// Map a clean disconnect to [`RpcError::EndOfStream`]; everything
    /// else stays [`RpcError::Io`]. A truncated *body* is classified by
    /// the framing reader, not here. An `RpcError` that travelled as the
    /// `io::Error`'s payload (how the `Read` adapters carry
    /// [`RpcError::UncleanEndOfStream`] through the framing readers)
    /// comes back as itself rather than folded by kind.
    fn from(e: std::io::Error) -> Self {
        use std::io::ErrorKind::*;
        let e = match e.downcast::<RpcError>() {
            Ok(rpc) => return rpc,
            Err(e) => e,
        };
        match e.kind() {
            UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {
                RpcError::EndOfStream
            }
            _ => RpcError::Io(e),
        }
    }
}

impl From<RpcError> for std::io::Error {
    /// Boundary projection back to `std::io::Error` for callers that
    /// hold an `io::Result<_>` accumulator. Its consumers are the
    /// adapters that bridge an [`transport::RpcTransport`] to a
    /// `std::io` `Read`/`Write` — `RawTransportIo`
    /// (`rpc::wire_android13`, the android-13+ raw framing) and `RawIo`
    /// (`rpc::transport::tls`, R34 framing over TLS).
    ///
    /// `RpcError::Io` hands its payload back as-is, but the reverse
    /// direction folds the disconnect kinds, so an `Io` carrying one of
    /// them returns as `EndOfStream`.
    /// [`RpcError::EndOfStream`] projects onto a single representative
    /// disconnect kind (`BrokenPipe`): the four kinds `From<io::Error>`
    /// folds into `EndOfStream` are not distinguished, and a
    /// `EndOfStream` raised from a 0-byte read never had one. That kind
    /// folds back into `EndOfStream`, so a write-side disconnect on an
    /// android-13+ session stays `StatusCode::DeadObject` rather than
    /// degrading to an unclassified `Io(Other)` (`StatusCode::Unknown`)
    /// — the status a caller's dead-peer check keys on.
    /// [`RpcError::Timeout`] projects onto `TimedOut`
    /// — kind-preserving rather than variant-preserving, since
    /// `From<io::Error>` folds that kind into `Io(TimedOut)`.
    /// [`RpcError::UncleanEndOfStream`] must survive the round trip —
    /// folding it by kind would turn it back into `EndOfStream`, the very
    /// thing it exists to be told apart from — so it goes out as an
    /// `UnexpectedEof` carrying itself as the payload, which
    /// `From<io::Error>` recovers first. The remaining variants have no
    /// `io::ErrorKind` that means what they mean, so they stay `Other`.
    fn from(e: RpcError) -> Self {
        match e {
            RpcError::Io(io) => io,
            RpcError::EndOfStream => std::io::ErrorKind::BrokenPipe.into(),
            RpcError::Timeout => std::io::ErrorKind::TimedOut.into(),
            e @ RpcError::UncleanEndOfStream => {
                std::io::Error::new(std::io::ErrorKind::UnexpectedEof, e)
            }
            // Same carriage, so the variant is at least recoverable on the
            // far side: by kind alone it would come back as a boundary
            // `Timeout`, the opposite of what it means. No reader takes it
            // yet — each checks `is_timeout` by kind before the downcast —
            // and no producer sends this variant across this boundary.
            e @ RpcError::DeadlineMidFrame => std::io::Error::new(std::io::ErrorKind::TimedOut, e),
            other => std::io::Error::other(format!("{other}")),
        }
    }
}

impl From<RpcError> for crate::StatusCode {
    /// Boundary projection used when an RPC failure must surface through
    /// `rsbinder::Result`. Specific, actionable mappings where they
    /// help a caller; the catch-all [`StatusCode::RpcError`] otherwise.
    ///
    /// [`StatusCode::RpcError`]: crate::StatusCode::RpcError
    fn from(e: RpcError) -> Self {
        match e {
            RpcError::EndOfStream => crate::StatusCode::DeadObject,
            RpcError::UncleanEndOfStream => crate::StatusCode::DeadObject,
            RpcError::Truncated => crate::StatusCode::NotEnoughData,
            RpcError::DeadlineMidFrame => crate::StatusCode::TimedOut,
            RpcError::FrameTooLarge { .. } => crate::StatusCode::BadValue,
            RpcError::Io(io) => crate::StatusCode::from(io),
            RpcError::Protocol(_) => crate::StatusCode::RpcError,
            RpcError::Timeout => crate::StatusCode::TimedOut,
        }
    }
}

/// Decode-only entrypoint for the `rpc_parcel_rpc_mode` fuzz target.
/// Arbitrary bytes are interpreted as an **RPC-mode**
/// `Parcel` body and run through the deserializers a real RPC
/// transaction reaches: scalars, `String` and the generic `Vec<T>` array
/// path. (The binder reads below hit a stub `RpcParcelOps` that returns
/// before touching the address bytes — `RpcAddress` decoding is covered
/// by the `rpc_address_decode` target, not here.)
/// Property: no panic / OOM / UB / unbounded pre-allocation on *any*
/// input — every length is bounded by the bytes actually present.
/// Not part of the supported API surface.
#[cfg(any(test, feature = "fuzzing"))]
#[doc(hidden)]
pub fn __fuzz_decode_rpc_parcel(input: &[u8]) {
    use crate::binder::SIBinder;
    use crate::error::{Result, StatusCode};
    use crate::parcel::{Parcel, RpcParcelOps};
    use std::sync::Arc;

    // Binder hook with no live session: lets `read::<SIBinder>` be
    // driven without a connection. It does not decode the address.
    struct NullOps;
    impl RpcParcelOps for NullOps {
        fn write_binder(&self, _b: Option<&SIBinder>, _p: &mut Parcel) -> Result<()> {
            Err(StatusCode::DeadObject)
        }
        fn read_binder(&self, _p: &mut Parcel) -> Result<Option<SIBinder>> {
            Err(StatusCode::DeadObject)
        }
    }

    fn fresh(input: &[u8]) -> Parcel {
        let mut p = Parcel::from_vec(input.to_vec());
        p.set_for_rpc(true);
        p.attach_rpc_ops(Arc::new(NullOps));
        p.set_data_position(0);
        p
    }

    let _ = fresh(input).read::<i32>();
    let _ = fresh(input).read::<i64>();
    let _ = fresh(input).read::<String>();
    let _ = fresh(input).read::<Vec<i32>>();
    let _ = fresh(input).read::<Vec<i64>>();
    let _ = fresh(input).read::<Vec<String>>();
    let _ = fresh(input).read::<Option<SIBinder>>();
    let _ = fresh(input).read::<SIBinder>();
    // NOTE: `resize_out_vec`/`resize_nullable_out_vec` are intentionally
    // *not* fuzzed here — an out-vec length is not backed by parcel
    // bytes (the callee fills it), so they are unbounded by design
    // (upstream Android is identical); feeding an arbitrary length
    // would just OOM the fuzzer without modelling a real wire input.
    // The bounded `in`-array path (`deserialize_array`) above is the
    // surface this target covers.
}

#[cfg(test)]
mod strong_session_tests;

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

    #[test]
    fn io_clean_close_maps_to_peer_closed() {
        for kind in [
            std::io::ErrorKind::UnexpectedEof,
            std::io::ErrorKind::BrokenPipe,
            std::io::ErrorKind::ConnectionReset,
            std::io::ErrorKind::ConnectionAborted,
        ] {
            let e: RpcError = std::io::Error::from(kind).into();
            assert!(matches!(e, RpcError::EndOfStream), "{kind:?} -> {e:?}");
        }
        // A non-disconnect I/O error stays Io(..).
        let other: RpcError = std::io::Error::from(std::io::ErrorKind::PermissionDenied).into();
        assert!(matches!(other, RpcError::Io(_)));
    }

    #[test]
    fn rpc_error_projects_onto_status_code() {
        assert_eq!(
            StatusCode::from(RpcError::EndOfStream),
            StatusCode::DeadObject
        );
        assert_eq!(
            StatusCode::from(RpcError::Truncated),
            StatusCode::NotEnoughData
        );
        assert_eq!(
            StatusCode::from(RpcError::FrameTooLarge {
                declared: 1 << 30,
                max: 1
            }),
            StatusCode::BadValue
        );
        assert_eq!(
            StatusCode::from(RpcError::Protocol("bad")),
            StatusCode::RpcError
        );
    }

    /// `RpcError -> io::Error` must preserve the `io::ErrorKind` for the
    /// two variants that have one meaning the same thing.
    /// `RawTransportIo` crosses that boundary on every android-13+
    /// handshake and on every read and write of a non-fd session, so a
    /// variant that degrades to `Other` there comes back
    /// as `Io(Other)`: a peer that closes before our write has to report
    /// `StatusCode::DeadObject`, not `Unknown`. Which side of an
    /// exchange notices a disconnect first is a host- and timing-
    /// dependent race, so both directions have to classify alike.
    /// `EndOfStream` returns as the same variant; `Timeout` is only
    /// kind-preserving — it comes back as `Io(TimedOut)`.
    ///
    /// **Mutant gate**: restoring `other => io::Error::other(...)` for
    /// `EndOfStream` fails the round trip below (and the timeout arm
    /// guards `read_exact_into`'s `is_timeout` path the same way).
    #[test]
    fn peer_closed_and_timeout_round_trip_through_io_error() {
        let io = std::io::Error::from(RpcError::EndOfStream);
        assert_eq!(io.kind(), std::io::ErrorKind::BrokenPipe);
        assert!(matches!(RpcError::from(io), RpcError::EndOfStream));
        assert_eq!(
            StatusCode::from(RpcError::from(std::io::Error::from(RpcError::EndOfStream))),
            StatusCode::DeadObject,
            "a write-side disconnect must stay DeadObject, not Unknown"
        );

        let io = std::io::Error::from(RpcError::Timeout);
        assert_eq!(io.kind(), std::io::ErrorKind::TimedOut);
        assert!(
            transport::is_timeout(&io),
            "read_exact_into's deadline arm keys on this kind"
        );
        // Reverse: `From<io::Error>` folds only the disconnect kinds, so
        // a timeout comes back as `Io(TimedOut)`, not `Timeout` itself.
        assert!(
            matches!(RpcError::from(io), RpcError::Io(ref e) if transport::is_timeout(e)),
            "a timeout stays recognizable as one across the round trip"
        );

        // An `Io` payload still round-trips unchanged, and a variant
        // with no matching kind still degrades to `Other`.
        let io = std::io::Error::from(RpcError::Io(std::io::Error::from(
            std::io::ErrorKind::PermissionDenied,
        )));
        assert_eq!(io.kind(), std::io::ErrorKind::PermissionDenied);
        assert_eq!(
            std::io::Error::from(RpcError::Protocol("bad")).kind(),
            std::io::ErrorKind::Other
        );
    }

    /// The cfg-gated `StatusCode::RpcError` must round-trip through the
    /// three hand-written exhaustive matches (Display / i32 both ways)
    /// without colliding with another code.
    #[test]
    fn status_code_rpc_error_roundtrips() {
        let v: i32 = StatusCode::RpcError.into();
        assert_eq!(StatusCode::from(v), StatusCode::RpcError);
        assert_eq!(format!("{}", StatusCode::RpcError), "RpcError");
        // Distinct from its neighbours in the UNKNOWN_ERROR + n block.
        assert_ne!(v, StatusCode::UnexpectedNull.into());
        assert_ne!(v, StatusCode::FailedTransaction.into());
    }

    /// A hostile array length in an RPC-mode parcel must fail
    /// gracefully (bounded pre-allocation + `Err`), never pre-allocate
    /// gigabytes. Deterministic regression — reverting the
    /// `min(len, data_avail())` / `len > data_avail()` guards turns each
    /// of these into a multi-GB allocation that aborts the test
    /// process. The `rpc_parcel_rpc_mode` fuzz target is the soak
    /// supplement.
    #[test]
    fn rpc_parcel_hostile_array_len_is_bounded_not_oom() {
        use crate::parcel::Parcel;

        // Generic `Vec<T>` array path, body empty after the length.
        let mut p = Parcel::new();
        p.set_for_rpc(true);
        p.write(&i32::MAX).unwrap();
        p.set_data_position(0);
        assert!(
            p.read::<Vec<i32>>().is_err(),
            "hostile Vec<i32> len must error, not OOM"
        );

        // A little data present (data_avail() > 0 but << len).
        let mut p = Parcel::new();
        p.set_for_rpc(true);
        p.write(&i32::MAX).unwrap();
        p.write(&7i32).unwrap();
        p.write(&8i32).unwrap();
        p.set_data_position(0);
        assert!(p.read::<Vec<i64>>().is_err());

        // (`resize_out_vec`/`resize_nullable_out_vec` are deliberately
        // not asserted here: an out-vec length is not backed by parcel
        // data — the callee fills it — so they are unbounded by design,
        // exactly like Android libbinder's `resizeOutVector`. Bounding
        // them by `data_avail()` regressed the live kernel out-array
        // path; see the note in `parcel.rs`.)

        // The fuzz entrypoint must never panic on adversarial bytes.
        for pat in [
            vec![],
            vec![0xFFu8; 4],
            vec![0xFF, 0xFF, 0xFF, 0x7F, 0, 0, 0, 0],
            (0..64u8).collect::<Vec<u8>>(),
        ] {
            __fuzz_decode_rpc_parcel(&pat);
        }
    }
}