| // |
| // Copyright (C) 2025 The Android Open-Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| use anyhow::{bail, Context, Result}; |
| use bitflags::bitflags; |
| use libc::{ |
| dl_iterate_phdr, dl_phdr_info, mallopt, mprotect, size_t, PROT_EXEC, PROT_READ, PT_LOAD, |
| }; |
| use native_activity_thread_bindgen::{tzset, PF_X}; |
| use nix::{ |
| errno::Errno, |
| sys::resource::{getrlimit, setrlimit, Resource}, |
| unistd::{getuid, sysconf, SysconfVar}, |
| }; |
| use rustutils::android::process::android_mallopt; |
| use rustutils::android::process::MalloptOpcode; |
| use rustutils::android::system_properties; |
| |
| use std::ffi::{c_int, c_ulong, c_void}; |
| use std::io::Error; |
| use std::slice; |
| |
| const AID_APP_START: u32 = 10000; |
| |
| static PROP_ZYGOTE_CORE_DUMP: &str = "persist.zygote.core_dump"; |
| static PROP_DEBUGGABLE: &str = "ro.debuggable"; |
| |
| /// A safe wrapper around tzset() |
| pub fn reset_time_zone() { |
| // Refresh Bionic's timezone information. |
| // SAFETY: Not passing any function parameter. |
| unsafe { tzset() }; |
| } |
| |
| /// Since Native Zygote only forks isolated process, which cannot read the `ro.debuggable` property, |
| /// we should call this function and cache the value when the Native Zygote server is initialized. |
| pub fn get_or_init_debuggable() -> bool { |
| static DEBUGGABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new(); |
| *DEBUGGABLE |
| .get_or_init(|| system_properties::read_bool(PROP_DEBUGGABLE, false).unwrap_or(false)) |
| } |
| |
| fn get_page_size() -> usize { |
| match sysconf(SysconfVar::PAGE_SIZE) { |
| Ok(Some(size)) => size as usize, |
| Ok(None) => panic!("sysconf(PAGE_SIZE) returned None"), |
| Err(e) => panic!("sysconf(PAGE_SIZE) returned an error: {e}"), |
| } |
| } |
| |
| #[allow(non_camel_case_types)] |
| #[cfg(target_pointer_width = "64")] |
| type ElfW_Phdr = libc::Elf64_Phdr; |
| #[allow(non_camel_case_types)] |
| #[cfg(target_pointer_width = "32")] |
| type ElfW_Phdr = libc::Elf32_Phdr; |
| |
| /// A callback function for `dl_iterate_phdr` which changes XO regions to RX. |
| /// |
| /// # Safety |
| /// Callers must ensure that `info` is a valid pointer to `dl_phdr_info` which outlives this |
| /// function and contains a shared object information. |
| unsafe extern "C" fn disable_execute_only( |
| info: *mut dl_phdr_info, |
| _size: usize, |
| _data: *mut c_void, |
| ) -> c_int { |
| if info.is_null() { |
| return 0; |
| } |
| |
| // SAFETY: `info` is a valid pointer which outlives this function and is aligned properly. |
| let info_ref = unsafe { &*info }; |
| |
| let phdr_ptr = info_ref.dlpi_phdr; |
| let phdr_count = info_ref.dlpi_phnum as usize; |
| |
| // SAFETY: `phdr_ptr` points to a valid ElfW_Phdr array of `phdr_count` elements. |
| let program_headers: &[ElfW_Phdr] = unsafe { slice::from_raw_parts(phdr_ptr, phdr_count) }; |
| |
| let page_mask = !(get_page_size() - 1); |
| |
| // Search for any execute-only segments and mark them read+execute. |
| // This operation only affects RWX flags because of the implementation |
| // of mprotect, so other architectural flags (like PROT_BTI) will not be cleared. |
| for phdr in program_headers { |
| if phdr.p_type == PT_LOAD && phdr.p_flags == PF_X { |
| let addr = (info_ref.dlpi_addr + phdr.p_vaddr) as usize; |
| let aligned_addr = (addr & page_mask) as *mut c_void; |
| let len = phdr.p_memsz as size_t; |
| let adjusted_len = addr + len - aligned_addr as usize; |
| |
| // SAFETY: `aligned_addr` is aligned to a page boundary and `adjusted_len` is a valid |
| // size of the region including the XO segment. |
| let ret = unsafe { mprotect(aligned_addr, adjusted_len, PROT_READ | PROT_EXEC) }; |
| if ret != 0 { |
| log::warn!("Failed to mprotect(): {}", Error::last_os_error()); |
| } |
| } |
| } |
| |
| 0 |
| } |
| |
| bitflags! { |
| /// Runtime flag constants. |
| /// Must match values in com.android.internal.os.Zygote. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub struct RuntimeFlags: u32 { |
| const DEBUG_ENABLE_JDWP = 1; |
| const DEBUG_ENABLE_CHECKJNI = 1 << 1; |
| const DEBUG_ENABLE_ASSERT = 1 << 2; |
| const DEBUG_ENABLE_SAFEMODE = 1 << 3; |
| const DEBUG_ENABLE_JNI_LOGGING = 1 << 4; |
| const DEBUG_GENERATE_DEBUG_INFO = 1 << 5; |
| const DEBUG_ALWAYS_JIT = 1 << 6; |
| const DEBUG_NATIVE_DEBUGGABLE = 1 << 7; |
| const DEBUG_JAVA_DEBUGGABLE = 1 << 8; |
| const DISABLE_VERIFIER = 1 << 9; |
| const ONLY_USE_SYSTEM_OAT_FILES = 1 << 10; |
| const DEBUG_GENERATE_MINI_DEBUG_INFO = 1 << 11; |
| const API_ENFORCEMENT_POLICY_MASK = (1 << 12) | (1 << 13); |
| const PROFILE_SYSTEM_SERVER = 1 << 14; |
| const PROFILE_FROM_SHELL = 1 << 15; |
| const USE_APP_IMAGE_STARTUP_CACHE = 1 << 16; |
| const DEBUG_IGNORE_APP_SIGNAL_HANDLER = 1 << 17; |
| const DISABLE_TEST_API_ENFORCEMENT_POLICY = 1 << 18; |
| const MEMORY_TAG_LEVEL_MASK = (1 << 19) | (1 << 20); |
| const MEMORY_TAG_LEVEL_TBI = 1 << 19; |
| const MEMORY_TAG_LEVEL_ASYNC = 2 << 19; |
| const MEMORY_TAG_LEVEL_SYNC = 3 << 19; |
| const GWP_ASAN_LEVEL_MASK = (1 << 21) | (1 << 22); |
| const GWP_ASAN_LEVEL_NEVER = 0 << 21; |
| const GWP_ASAN_LEVEL_LOTTERY = 1 << 21; |
| const GWP_ASAN_LEVEL_ALWAYS = 2 << 21; |
| const GWP_ASAN_LEVEL_DEFAULT = 3 << 21; |
| const NATIVE_HEAP_ZERO_INIT_ENABLED = 1 << 23; |
| const PROFILEABLE = 1 << 24; |
| const DEBUG_ENABLE_PTRACE = 1 << 25; |
| const ENABLE_PAGE_SIZE_APP_COMPAT = 1 << 26; |
| const ENABLE_EXECUTE_ONLY_MEMORY = 1 << 27; |
| } |
| } |
| |
| impl RuntimeFlags { |
| pub fn get_heap_tagging_level(&self) -> c_int { |
| match self.intersection(Self::MEMORY_TAG_LEVEL_MASK) { |
| Self::MEMORY_TAG_LEVEL_TBI => libc::M_HEAP_TAGGING_LEVEL_TBI, |
| Self::MEMORY_TAG_LEVEL_ASYNC => libc::M_HEAP_TAGGING_LEVEL_ASYNC, |
| Self::MEMORY_TAG_LEVEL_SYNC => libc::M_HEAP_TAGGING_LEVEL_SYNC, |
| _ => libc::M_HEAP_TAGGING_LEVEL_NONE, |
| } |
| } |
| |
| pub fn is_native_heap_zero_init_enabled(&self) -> bool { |
| self.contains(Self::NATIVE_HEAP_ZERO_INIT_ENABLED) |
| } |
| |
| pub fn is_ptrace_enabled(&self) -> bool { |
| self.contains(Self::DEBUG_ENABLE_PTRACE) |
| } |
| |
| pub fn is_profileable_from_shell(&self) -> bool { |
| self.contains(Self::PROFILE_FROM_SHELL) |
| } |
| |
| pub fn is_profileable(&self) -> bool { |
| self.contains(Self::PROFILEABLE) |
| } |
| |
| pub fn is_execute_only_memory_enabled(&self) -> bool { |
| self.contains(Self::ENABLE_EXECUTE_ONLY_MEMORY) |
| } |
| } |
| |
| pub fn apply_runtime_flags(runtime_flags: u32) -> Result<()> { |
| let Some(flags) = RuntimeFlags::from_bits(runtime_flags) else { |
| bail!("runtime_flags doesn't have a valid representation: {:#x}", runtime_flags); |
| }; |
| |
| // Set process properties to enable debugging if required. |
| if flags.is_ptrace_enabled() { |
| if let Err(e) = enable_debugger() { |
| log::warn!("Failed to enable debbuger: {e}"); |
| } |
| } |
| |
| if flags.is_profileable_from_shell() { |
| // simpleperf needs the process to be dumpable to profile it. |
| prctl_set_dumpable(Dumpable::ByUser).expect("prctl(PR_SET_DUMPABLE) failed"); |
| } |
| |
| if get_or_init_debuggable() || flags.is_profileable() { |
| // SAFETY: This opcode takes no arguments so a nullptr is passed |
| // instead. |
| let ret = unsafe { |
| android_mallopt(MalloptOpcode::InitZygoteChildProfiling, std::ptr::null_mut(), 0) |
| }; |
| if ret.is_err() { |
| log::error!( |
| "Failed to android_mallopt(M_INIT_ZYGOTE_CHILD_HEAP_PROFILING): {}", |
| Error::last_os_error() |
| ); |
| } |
| } |
| |
| if !flags.is_native_heap_zero_init_enabled() { |
| // SAFETY: Setting configuration with valid argument (0 = disable). |
| let ret = unsafe { mallopt(libc::M_BIONIC_ZERO_INIT, 0) }; |
| if ret == 0 { |
| log::warn!("Failed to mallopt(M_BIONIC_ZERO_INIT): {}", Error::last_os_error()); |
| } |
| } |
| |
| // SAFETY: Setting configuration with valid argument (as defined in get_heap_tagging_level). |
| let ret = |
| unsafe { mallopt(libc::M_BIONIC_SET_HEAP_TAGGING_LEVEL, flags.get_heap_tagging_level()) }; |
| if ret == 0 { |
| log::warn!( |
| "Failed to mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL): {}", |
| Error::last_os_error() |
| ); |
| } |
| |
| // If the app does not support execute-only memory, then iterate through |
| // the shared objects and mark them readable. |
| if cfg!(feature = "build_execute_only_memory") && !flags.is_execute_only_memory_enabled() { |
| // SAFETY: Passes a callback function which has the expected function signature. The |
| // callback doesn't use `data`. The libc runtime guarantees to meet the safety requirements |
| // of the callback. |
| unsafe { dl_iterate_phdr(Some(disable_execute_only), std::ptr::null_mut()) }; |
| } |
| |
| Ok(()) |
| } |
| |
| // Enum corresponding to SUID_DUMP_* defined in linux/sched/coredump.h. |
| #[derive(PartialEq)] |
| enum Dumpable { |
| Disable, |
| ByUser, |
| ByRoot, |
| } |
| |
| impl TryFrom<i32> for Dumpable { |
| type Error = (); |
| |
| fn try_from(value: i32) -> Result<Self, Self::Error> { |
| let dumpable = match value { |
| 0 => Dumpable::Disable, |
| 1 => Dumpable::ByUser, |
| 2 => Dumpable::ByRoot, |
| _ => return Err(()), |
| }; |
| Ok(dumpable) |
| } |
| } |
| |
| impl From<Dumpable> for i32 { |
| fn from(value: Dumpable) -> Self { |
| match value { |
| Dumpable::Disable => 0, |
| Dumpable::ByUser => 1, |
| Dumpable::ByRoot => 2, |
| } |
| } |
| } |
| |
| // TODO(b/450462991): Use nix::sys::prctl::get_dumpable once `prctl` module is available to android |
| // and this issue is fixed: https://github.com/nix-rust/nix/issues/2684. |
| fn prctl_get_dumpable() -> nix::Result<Dumpable> { |
| // SAFETY: Trivially safe. See `man PR_GET_DUMPABLE`. |
| let res = unsafe { libc::prctl(libc::PR_GET_DUMPABLE) }; |
| if res == -1 { |
| return Err(Errno::last()); |
| } |
| Ok(Dumpable::try_from(res) |
| .unwrap_or_else(|_| panic!("prctl(PR_GET_DUMPABLE) returned an unexpected value: {res}"))) |
| } |
| |
| // TODO(b/450462991): Use nix::sys::prctl::set_dumpable once `prctl` module is available to |
| // android. |
| fn prctl_set_dumpable(dumpable: Dumpable) -> nix::Result<()> { |
| let val: i32 = dumpable.into(); |
| // SAFETY: Trivially safe. See `man PR_SET_DUMPABLE`. |
| let res = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, val) }; |
| if res == -1 { |
| return Err(Errno::last()); |
| } |
| Ok(()) |
| } |
| |
| fn prctl_set_ptracer(pid: c_ulong) -> nix::Result<()> { |
| // SAFETY: Trivially safe. See `man PR_SET_PTRACER`. |
| let res = unsafe { libc::prctl(libc::PR_SET_PTRACER, pid) }; |
| if res == -1 { |
| return Err(Errno::last()); |
| } |
| Ok(()) |
| } |
| |
| pub fn setup_process_dumpability() -> Result<()> { |
| if prctl_get_dumpable().context("prctl(PR_GET_DUMPABLE) failed")? == Dumpable::ByRoot |
| && getuid().as_raw() >= AID_APP_START |
| { |
| prctl_set_dumpable(Dumpable::Disable).context("prctl(PR_SET_DUMPABLE) failed")?; |
| } |
| Ok(()) |
| } |
| |
| fn enable_debugger() -> Result<()> { |
| // To let a non-privileged gdbserver attach to this process, |
| // we must set our dumpable flag. |
| prctl_set_dumpable(Dumpable::ByUser).context("prctl(PR_SET_DUMPABLE) failed")?; |
| |
| // A non-privileged native debugger should be able to attach to the debuggable app, |
| // even if Yama is enabled (see kernel/Documentation/security/Yama.txt). |
| if let Err(errno) = prctl_set_ptracer(libc::PR_SET_PTRACER_ANY) { |
| // if Yama is off prctl(PR_SET_PTRACER) returns EINVAL - |
| // don't log in this case since it's expected behaviour. |
| if errno != Errno::EINVAL { |
| bail!("prctl(PR_SET_TRACER, PR_SET_PTRACER_ANY) failed: {errno}"); |
| } |
| } |
| |
| // Set the core dump size to zero unless wanted |
| // (see also coredump_setup in build/envsetup.sh). |
| if system_properties::read_bool(PROP_ZYGOTE_CORE_DUMP, false) |
| .with_context(|| format!("Failed to read {PROP_ZYGOTE_CORE_DUMP}"))? |
| { |
| // Set the soft limit on core dump size to 0 without changing the hard limit. |
| let (_, hard_limit) = |
| getrlimit(Resource::RLIMIT_CORE).context("getrlimit(RLIMIT_CORE) failed")?; |
| setrlimit(Resource::RLIMIT_CORE, 0, hard_limit).context("setrlimit(RLIMIT_CORE) failed")?; |
| } |
| |
| Ok(()) |
| } |