rsbinder 0.12.0

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

//! Shared-memory IPC: AOSP `IMemory` / `IMemoryHeap` for rsbinder (plan 4-7a).
//!
//! AOSP `IMemory.h`
//! ([source](https://cs.android.com/android/platform/superproject/+/android-16.0.0_r4:frameworks/native/libs/binder/include/binder/IMemory.h))
//! describes **handwritten** C++ binders, not AIDL. This module mirrors
//! that surface:
//!
//! * [`IMemoryHeap`](crate::shared_memory::IMemoryHeap) / [`IMemory`](crate::shared_memory::IMemory) — the trait surface.
//! * [`MemoryHeapBase`](crate::shared_memory::MemoryHeapBase) — owner-side heap (`memfd_create` on
//!   Linux/Android, `shm_open` on macOS). [`MappedHeap`](crate::shared_memory::MappedHeap) — the
//!   receiver-side mapping of an fd that arrived through a parcel.
//!   See [`heap`](crate::shared_memory::heap) for the backing-store table and the aliasing
//!   contract.
//! * [`MemoryDealer`](crate::shared_memory::MemoryDealer) — AOSP's chunk
//!   allocator: one heap, many `IMemory` windows, with
//!   [`HeapCache`](crate::shared_memory::HeapCache) on the receiving side
//!   so the heap is mapped once. See [`dealer`](crate::shared_memory::dealer).
//!
//! A shared region travels over any transport that can carry an fd:
//! the kernel binder (`BINDER_TYPE_FD`) and Unix-socket RPC with
//! `rpc::FileDescriptorTransportMode::Unix` (the `rpc` feature)
//! negotiated on both ends. TCP / vsock / TLS sessions cannot carry
//! fds at all; writing a heap fd into such a parcel fails with
//! `StatusCode::FdsNotAllowed` exactly as a plain
//! [`ParcelFileDescriptor`] does.
//!
//! Targets without a backing store (anything other than Linux,
//! Android, macOS) compile the whole trait surface, but every
//! `MemoryHeapBase` constructor returns
//! `Err(StatusCode::InvalidOperation)`; the receiver side
//! (`MappedHeap::from_fd`) still maps a received fd. Check
//! [`is_supported`](crate::shared_memory::is_supported) to branch at runtime.
//!
//! # Mixed-size atomics
//!
//! The crate-private `Region` view splits a mapped shared region into its `usize`-aligned
//! words and the `< WORD` trailing bytes. Every access is word-sized on the word part (partial
//! words go through a CAS) and byte-sized on the tail: Rust's memory model makes overlapping
//! atomic accesses of *different* sizes a data race when one is a write, so a byte-granular
//! fast path would race the word path of another thread writing the same window.

pub mod dealer;
pub mod heap;
pub mod shared;
pub mod wire;

use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};

use crate::error::{Result, StatusCode};

pub use dealer::{Allocation, MemoryDealer, ALLOCATION_ALIGNMENT};
pub use heap::{
    is_supported, MappedHeap, MemoryHeapBase, FLAG_DONT_MAP_LOCALLY, FLAG_FORCE_MEMFD,
    FLAG_MEMFD_ALLOW_SEALING, FLAG_NO_CACHING, SEAL_FUTURE_WRITE, SEAL_GROW, SEAL_SEAL,
    SEAL_SHRINK, SEAL_WRITE,
};
pub use shared::{region_size, SharedMemory};
pub use wire::{
    export_heap, BnMemory, BnMemoryHeap, BpMemory, BpMemoryHeap, HeapCache, MemoryBase, GET_MEMORY,
    HEAP_ID, IMEMORY_DESCRIPTOR, IMEMORY_HEAP_DESCRIPTOR,
};

/// AOSP `IMemoryHeap::READ_ONLY` flag
/// ([IMemory.h:37-39](https://cs.android.com/android/platform/superproject/+/android-16.0.0_r4:frameworks/native/libs/binder/include/binder/IMemory.h;l=37)).
/// Receivers map `PROT_READ` only. On Linux/Android the owner also
/// applies `F_SEAL_FUTURE_WRITE` so the kernel refuses a writable
/// mapping from a peer that ignores the flag (Linux 5.1+; an older
/// kernel lacks that seal, which `seals()` reports); the owner's own
/// mapping, created before the seal, stays writable.
pub const FLAG_READ_ONLY: u32 = 0x0000_0001;

pub(crate) const WORD: usize = std::mem::size_of::<usize>();

/// Mapped region as `usize` words + a `< WORD` tail; see module doc "Mixed-size atomics".
#[derive(Clone, Copy)]
pub(crate) struct Region<'a> {
    words: &'a [AtomicUsize],
    tail: &'a [AtomicU8],
}

impl<'a> Region<'a> {
    pub(crate) fn new(words: &'a [AtomicUsize], tail: &'a [AtomicU8]) -> Self {
        Self { words, tail }
    }

    pub(crate) fn len(&self) -> usize {
        self.words.len() * WORD + self.tail.len()
    }

    fn check(&self, off: usize, len: usize) -> Result<()> {
        let end = off.checked_add(len).ok_or(StatusCode::BadValue)?;
        if end > self.len() {
            return Err(StatusCode::BadValue);
        }
        Ok(())
    }

    /// Copy `dst.len()` bytes out, starting at `off` (relaxed loads).
    pub(crate) fn load(&self, off: usize, dst: &mut [u8]) -> Result<()> {
        self.check(off, dst.len())?;
        let word_bytes = self.words.len() * WORD;
        let mut done = 0;
        while done < dst.len() && off + done < word_bytes {
            let at = off + done;
            let shift = at % WORD;
            let n = (WORD - shift).min(dst.len() - done);
            let w = self.words[at / WORD].load(Ordering::Relaxed).to_ne_bytes();
            dst[done..done + n].copy_from_slice(&w[shift..shift + n]);
            done += n;
        }
        let tail_at = (off + done).saturating_sub(word_bytes);
        for (d, s) in dst[done..].iter_mut().zip(&self.tail[tail_at..]) {
            *d = s.load(Ordering::Relaxed);
        }
        Ok(())
    }

    /// Copy `src` in at `off` (relaxed stores; a partial word is CAS-merged to keep neighbours).
    pub(crate) fn store(&self, off: usize, src: &[u8]) -> Result<()> {
        self.check(off, src.len())?;
        let word_bytes = self.words.len() * WORD;
        let mut done = 0;
        while done < src.len() && off + done < word_bytes {
            let at = off + done;
            let shift = at % WORD;
            let n = (WORD - shift).min(src.len() - done);
            let w = &self.words[at / WORD];
            let chunk = &src[done..done + n];
            if n == WORD {
                let full = usize::from_ne_bytes(chunk.try_into().expect("one word"));
                w.store(full, Ordering::Relaxed);
            } else {
                let mut cur = w.load(Ordering::Relaxed);
                loop {
                    let mut bytes = cur.to_ne_bytes();
                    bytes[shift..shift + n].copy_from_slice(chunk);
                    let next = usize::from_ne_bytes(bytes);
                    match w.compare_exchange_weak(cur, next, Ordering::Relaxed, Ordering::Relaxed) {
                        Ok(_) => break,
                        Err(seen) => cur = seen,
                    }
                }
            }
            done += n;
        }
        let tail_at = (off + done).saturating_sub(word_bytes);
        for (d, s) in self.tail[tail_at..].iter().zip(&src[done..]) {
            d.store(*s, Ordering::Relaxed);
        }
        Ok(())
    }
}

/// Read-only view of a mapped shared-memory window — what
/// [`IMemoryHeap::base`] returns.
///
/// The window aliases memory another process (or, through `write_at`,
/// another thread) writes at any moment, so the view hands out neither a
/// `&[u8]` — which would promise an immutability shared memory cannot keep
/// — nor a way to store: reads copy through relaxed atomics, and writes go
/// through the concrete heap's `write_at`, which also honours the mapping's
/// protection (a [`FLAG_READ_ONLY`] mapping is `PROT_READ`; a store into it
/// would fault). Any cross-process ordering is the surrounding protocol's
/// business, as with AOSP `unsecurePointer()`
/// ([IMemory.h:78-91](https://cs.android.com/android/platform/superproject/+/android-16.0.0_r4:frameworks/native/libs/binder/include/binder/IMemory.h;l=78)).
#[derive(Clone, Copy)]
pub struct SharedBytes<'a> {
    region: Region<'a>,
    off: usize,
    len: usize,
}

impl<'a> SharedBytes<'a> {
    pub(crate) fn whole(region: Region<'a>) -> Self {
        Self {
            region,
            off: 0,
            len: region.len(),
        }
    }

    /// Length of the window in bytes.
    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// The byte at `i`, or `None` past the end.
    pub fn load(&self, i: usize) -> Option<u8> {
        let mut b = [0u8; 1];
        self.copy_to(i, &mut b).ok().map(|()| b[0])
    }

    /// Copy `dst.len()` bytes starting at `off` (relative to the window).
    /// `BadValue` if the range is out of bounds.
    pub fn copy_to(&self, off: usize, dst: &mut [u8]) -> Result<()> {
        let end = off.checked_add(dst.len()).ok_or(StatusCode::BadValue)?;
        if end > self.len {
            return Err(StatusCode::BadValue);
        }
        self.region.load(self.off + off, dst)
    }

    /// The sub-window `[off, off + len)`, or `None` if out of bounds.
    pub fn slice(&self, off: usize, len: usize) -> Option<SharedBytes<'a>> {
        let end = off.checked_add(len)?;
        if end > self.len {
            return None;
        }
        Some(Self {
            region: self.region,
            off: self.off + off,
            len,
        })
    }

    /// Snapshot of the whole window.
    pub fn to_vec(&self) -> Vec<u8> {
        let mut v = vec![0u8; self.len];
        self.region
            .load(self.off, &mut v)
            .expect("window is within the region");
        v
    }
}

impl std::fmt::Debug for SharedBytes<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SharedBytes")
            .field("len", &self.len)
            .finish()
    }
}

/// Server-side representation of a heap. AOSP `IMemoryHeap` is keyed by
/// the heap fd; this trait exposes it as a [`BorrowedFd`] rather than an
/// owned [`std::os::fd::OwnedFd`] so the transaction marshalling can write
/// it into the reply as a bare fd object (AOSP `writeFileDescriptor`, which
/// dups) without taking ownership away from the heap object.
///
/// The fd is a [`BorrowedFd`], not a raw number, because a peer's
/// `HEAP_ID` request makes [`BnMemoryHeap`] dup it into the reply: the
/// borrow is what guarantees the fd is open and belongs to the heap for as
/// long as `&self` lives. A raw number from a safe trait method could name
/// any fd of the process (stdin, or one already closed), and a remote
/// request would then send it to the peer.
///
/// Heap geometry is immutable for the lifetime of the heap: the size and
/// offset are captured at heap construction time and never mutate.
/// Mutation surface is intentionally absent — heap resize is not in AOSP `IMemoryHeap` either
/// ([IMemory.h:41-45](https://cs.android.com/android/platform/superproject/+/android-16.0.0_r4:frameworks/native/libs/binder/include/binder/IMemory.h;l=41)).
pub trait IMemoryHeap: Send + Sync {
    /// The heap fd, as AOSP `getHeapID()` names it; `None` while the heap
    /// has none (an unmapped [`BpMemoryHeap`]).
    ///
    /// [`BnMemoryHeap`] answers `HEAP_ID` with a dup of this fd, written as
    /// a bare fd object (AOSP `writeFileDescriptor`), and with `BadValue`
    /// when it is `None`.
    fn heap_fd(&self) -> Option<BorrowedFd<'_>>;
    /// AOSP `getHeapID()`: the number of [`heap_fd`](Self::heap_fd), or
    /// `-1` without one. Informational only: the `HEAP_ID` reply is built
    /// from [`heap_fd`](Self::heap_fd), never from this number.
    fn heap_id(&self) -> i32 {
        self.heap_fd().map_or(-1, |fd| fd.as_raw_fd())
    }
    /// AOSP `getSize()`. Total byte length of the heap.
    fn size(&self) -> usize;
    /// AOSP `getFlags()`. Bitmask of `FLAG_READ_ONLY` etc.
    fn flags(&self) -> u32;
    /// AOSP `getOffset()`. Offset within the underlying fd at which
    /// this heap begins; `0` for a freshly-allocated heap.
    fn offset(&self) -> usize;
    /// AOSP `getBase()`. Returns a read-only view of the local mapping if
    /// the heap is currently mapped into this process, else `None`. See
    /// [`SharedBytes`] for why it is neither a `&[u8]` nor writable;
    /// writes go through the concrete heap's `write_at`.
    fn base(&self) -> Option<SharedBytes<'_>>;
}

/// Sub-region of an [`IMemoryHeap`]. AOSP
/// [`IMemory`](https://cs.android.com/android/platform/superproject/+/android-16.0.0_r4:frameworks/native/libs/binder/include/binder/IMemory.h;l=69)
/// equivalent. An `IMemory` references a heap plus an `(offset, size)`
/// pair so that one large heap can host many small allocations (the
/// AOSP `MemoryDealer` pattern).
pub trait IMemory: Send + Sync {
    /// AOSP `getMemory(offset*, size*)`: the backing heap; the window is
    /// [`offset`](Self::offset)/[`size`](Self::size). `&self` borrow keeps
    /// the heap alive for the duration of the returned reference.
    fn memory(&self) -> &dyn IMemoryHeap;
    /// AOSP `offset()`. Offset within the backing heap.
    fn offset(&self) -> usize;
    /// AOSP `size()`. Byte length of this slice. May be smaller than
    /// the backing heap.
    fn size(&self) -> usize;
}

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

    /// `FLAG_READ_ONLY` matches AOSP `IMemoryHeap::READ_ONLY = 0x01`, the rest `MemoryHeapBase.h`.
    #[test]
    fn flags_match_aosp_constants() {
        assert_eq!(FLAG_READ_ONLY, 0x0000_0001);
        assert_eq!(FLAG_DONT_MAP_LOCALLY, 0x0000_0100);
        assert_eq!(FLAG_NO_CACHING, 0x0000_0200);
        assert_eq!(FLAG_FORCE_MEMFD, 0x0000_0400);
        assert_eq!(FLAG_MEMFD_ALLOW_SEALING, 0x0000_0800);
    }

    /// Unsupported targets return `InvalidOperation` instead of panicking, so callers can opt out.
    #[test]
    fn constructor_matches_is_supported() {
        let r = MemoryHeapBase::new(4096, FLAG_READ_ONLY);
        if is_supported() {
            assert!(r.is_ok());
        } else {
            assert_eq!(r.unwrap_err(), crate::StatusCode::InvalidOperation);
        }
    }

    /// `IMemoryHeap` is object-safe (`&dyn` compiles); `heap_id` derives from `heap_fd`.
    #[test]
    fn imemoryheap_is_object_safe() {
        struct Stub(Option<std::fs::File>);
        impl IMemoryHeap for Stub {
            fn heap_fd(&self) -> Option<BorrowedFd<'_>> {
                self.0.as_ref().map(std::os::fd::AsFd::as_fd)
            }
            fn size(&self) -> usize {
                0
            }
            fn flags(&self) -> u32 {
                0
            }
            fn offset(&self) -> usize {
                0
            }
            fn base(&self) -> Option<SharedBytes<'_>> {
                None
            }
        }
        let file = std::fs::File::open("/dev/null").unwrap();
        let raw = file.as_raw_fd();
        let h: &dyn IMemoryHeap = &Stub(Some(file));
        assert_eq!(h.heap_id(), raw);
        assert_eq!(Stub(None).heap_id(), -1);
    }
}