cloudfox-coreshift-core 2.30.0

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
// 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 parcel allocator primitives (compiled on Android — where
//! `sys` uses them — and in `cargo test` on host, so the AOSP allocator
//! length convention is verified on every host commit; mirrors the `wire`
//! pattern).
//!
//! ## AOSP allocator-length convention (D1, Phase 1)
//!
//! `AParcel_readString` calls the allocator with
//! `len8 = utf16_to_utf8_length(str16, len16) + 1` — the NUL terminator is
//! **included** (an empty string is length `1`). `StringBuf` records that
//! allocator length and `finish()` drops **only** the trailing terminator,
//! preserving embedded NULs so callers (spawn argv, daemon `NulInArg`) can
//! reject them instead of silently truncating a NUL-bearing arg into a
//! different valid argv.

use std::os::raw::{c_char, c_void};

/// Ceiling on a single parcel string regardless of the length the peer
/// advertises. Component names are at most a few hundred bytes; this bounds
/// the allocation so a malformed advertised length cannot drive a giant
/// `reserve_exact` (which would abort on OOM).
pub(super) const MAX_BINDER_STRING_LEN: usize = 1024 * 1024;

// `StringAllocator`/`ByteArrayAllocator` are the FFI callback types consumed
// by the android-only `sys::Vtable` fields; on host they only appear in tests,
// so the dead-code lint is silenced for this module's public surface.
#[allow(dead_code)]
pub(super) type StringAllocator = unsafe extern "C" fn(*mut c_void, i32, *mut *mut c_char) -> bool;

/// `AParcel_byteArrayAllocator`: `bool (*)(void* arrayData, int32_t length,
/// int8_t** outBuffer)`.
#[allow(dead_code)]
pub(super) type ByteArrayAllocator = unsafe extern "C" fn(*mut c_void, i32, *mut *mut i8) -> bool;

pub(super) unsafe extern "C" fn string_alloc(
    cookie: *mut c_void,
    length: i32,
    buffer: *mut *mut c_char,
) -> bool {
    // length == -1 is the Java null-string marker: AParcel_readString
    // calls the allocator with -1 (and a null buffer) when the parcel
    // holds a null string, so returning true there decodes it to None
    // instead of a hard STATUS_UNEXPECTED_NULL failure. Any other
    // negative length is corruption and an allocation failure: returning
    // true with no usable buffer would hand the reader a dangling pointer,
    // and an oversized reserve_exact would abort on OOM.
    let s = unsafe { &mut *(cookie as *mut StringBuf) };
    if length == -1 {
        s.is_null = true;
        return true;
    }
    if length < 0 {
        return false;
    }
    let len = length as usize;
    if len > MAX_BINDER_STRING_LEN {
        return false;
    }
    s.alloc_len = len;
    s.s.reserve_exact(len + 1);
    unsafe { s.s.as_mut_vec().resize(len + 1, 0) };
    unsafe { *buffer = s.s.as_mut_ptr() as *mut c_char };
    true
}

pub(super) struct StringBuf {
    s: String,
    /// `true` iff the `-1` null-string marker was seen. Distinct from an
    /// empty string (`Some("")`), which the parcel reader can produce for a
    /// real zero-length `writeString("")`.
    is_null: bool,
    /// The length AOSP passed the allocator: `utf16_to_utf8_length() + 1`
    /// (terminator included; empty string -> 1). Recorded so `finish()` can
    /// drop only the terminator instead of scanning for the first NUL.
    alloc_len: usize,
}
impl StringBuf {
    pub(super) fn new() -> Self {
        Self {
            s: String::new(),
            is_null: false,
            alloc_len: 0,
        }
    }
    pub(super) fn finish(mut self) -> Option<String> {
        if self.is_null {
            return None;
        }
        // Drop only the trailing terminator AOSP wrote at `alloc_len - 1`.
        // Embedded NULs are preserved and are the caller's to reject (Core
        // spawn `CString::new` EINVAL, daemon `NulInArg`).
        let data_len = self.alloc_len.saturating_sub(1);
        unsafe { self.s.as_mut_vec().truncate(data_len) };
        Some(self.s)
    }
}

/// Byte-array allocator mirroring `string_alloc`: `-1` (Java `null` array
/// marker) decodes to `None`, `<0` is corruption, `> 1 MiB` is capped, and a
/// zero-length array still yields a non-null buffer (AOSP never writes to it
/// — `ReadAndValidateArraySize` returns OK for `length <= 0` before the
/// `memcpy` — but the contract asks for a valid pointer).
pub(super) unsafe extern "C" fn byte_alloc(
    cookie: *mut c_void,
    length: i32,
    buffer: *mut *mut i8,
) -> bool {
    let s = unsafe { &mut *(cookie as *mut ByteBuf) };
    if length == -1 {
        s.is_null = true;
        return true;
    }
    if length < 0 {
        return false;
    }
    let len = length as usize;
    if len > MAX_BINDER_STRING_LEN {
        return false;
    }
    s.v.resize(len, 0);
    unsafe { *buffer = s.v.as_mut_ptr() as *mut i8 };
    true
}

pub(super) struct ByteBuf {
    v: Vec<u8>,
    is_null: bool,
}
impl ByteBuf {
    pub(super) fn new() -> Self {
        Self {
            v: Vec::new(),
            is_null: false,
        }
    }
    pub(super) fn finish(self) -> Option<Vec<u8>> {
        if self.is_null {
            return None;
        }
        Some(self.v)
    }
}

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

    fn alloc(length: i32) -> bool {
        let mut buf = StringBuf::new();
        let mut out: *mut c_char = std::ptr::null_mut();
        unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, length, &mut out) }
    }

    #[test]
    fn string_alloc_rejects_oversized() {
        assert!(!alloc(MAX_BINDER_STRING_LEN as i32 + 1));
    }

    #[test]
    fn string_alloc_accepts_null_marker_rejects_other_negative() {
        assert!(alloc(-1));
        assert!(!alloc(-2));
    }

    #[test]
    fn string_alloc_null_marker_finishes_to_none() {
        let mut buf = StringBuf::new();
        let mut out: *mut c_char = std::ptr::null_mut();
        assert!(unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, -1, &mut out) });
        assert_eq!(buf.finish(), None);
    }

    #[test]
    fn string_alloc_empty_string_len_1_finishes_to_some_empty() {
        // AOSP: an empty string is allocator length 1 (utf8_len 0 + 1), NOT
        // 0. The old test encoded the false `len == data-bytes` convention.
        let mut buf = StringBuf::new();
        let mut out: *mut c_char = std::ptr::null_mut();
        assert!(unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, 1, &mut out) });
        assert!(!out.is_null());
        assert_eq!(buf.finish().as_deref(), Some(""));
    }

    #[test]
    fn string_alloc_accepts_valid_len_and_nul_terminates() {
        // "ABCD" is 4 UTF-8 bytes; AOSP passes allocator length 4 + 1 = 5.
        let mut buf = StringBuf::new();
        let mut out: *mut c_char = std::ptr::null_mut();
        let ok = unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, 5, &mut out) };
        assert!(ok);
        assert!(!out.is_null());
        {
            let vec = unsafe { buf.s.as_mut_vec() };
            b"ABCD".iter().enumerate().for_each(|(i, &b)| vec[i] = b);
            vec[4] = 0;
        }
        assert_eq!(buf.finish().as_deref(), Some("ABCD"));
    }

    #[test]
    fn string_alloc_preserves_embedded_nul() {
        // "A\0B" is 3 UTF-8 bytes; AOSP passes length 3 + 1 = 4 and writes
        // the data plus a trailing terminator at index 3. A scan-based finish
        // would truncate at the embedded NUL (index 1) -> "A".
        let mut buf = StringBuf::new();
        let mut out: *mut c_char = std::ptr::null_mut();
        let ok = unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, 4, &mut out) };
        assert!(ok);
        assert!(!out.is_null());
        {
            let vec = unsafe { buf.s.as_mut_vec() };
            b"A\0B".iter().enumerate().for_each(|(i, &b)| vec[i] = b);
            vec[3] = 0;
        }
        assert_eq!(buf.finish().as_deref(), Some("A\0B"));
    }

    fn balloc(length: i32) -> bool {
        let mut buf = ByteBuf::new();
        let mut out: *mut i8 = std::ptr::null_mut();
        unsafe { byte_alloc(&mut buf as *mut ByteBuf as *mut c_void, length, &mut out) }
    }

    #[test]
    fn byte_alloc_rejects_oversized() {
        assert!(!balloc(MAX_BINDER_STRING_LEN as i32 + 1));
    }

    #[test]
    fn byte_alloc_accepts_null_marker_rejects_other_negative() {
        assert!(balloc(-1));
        assert!(!balloc(-2));
    }

    #[test]
    fn byte_alloc_null_marker_finishes_to_none() {
        let mut buf = ByteBuf::new();
        let mut out: *mut i8 = std::ptr::null_mut();
        assert!(unsafe { byte_alloc(&mut buf as *mut ByteBuf as *mut c_void, -1, &mut out) });
        assert_eq!(buf.finish(), None);
    }

    #[test]
    fn byte_alloc_zero_len_finishes_to_some_empty() {
        // AOSP: `writeByteArray(new byte[0])` yields allocator length 0 with
        // a valid (non-null) buffer; the reader returns OK without writing.
        let mut buf = ByteBuf::new();
        let mut out: *mut i8 = std::ptr::null_mut();
        assert!(unsafe { byte_alloc(&mut buf as *mut ByteBuf as *mut c_void, 0, &mut out) });
        assert!(!out.is_null());
        assert_eq!(buf.finish(), Some(vec![]));
    }

    #[test]
    fn byte_alloc_round_trips_bytes_including_nul() {
        // Byte arrays are raw: interior NULs and 0x80+ bytes must survive.
        let data: &[u8] = &[0xff, 0x00, 0xfe, 0x80, 0x41];
        let mut buf = ByteBuf::new();
        let mut out: *mut i8 = std::ptr::null_mut();
        let ok = unsafe {
            byte_alloc(
                &mut buf as *mut ByteBuf as *mut c_void,
                data.len() as i32,
                &mut out,
            )
        };
        assert!(ok);
        assert!(!out.is_null());
        {
            let v = buf.v.as_mut_slice();
            v.copy_from_slice(data);
        }
        assert_eq!(buf.finish(), Some(data.to_vec()));
    }
}