Skip to main content

proot/tracee/
mem.rs

1//! Reading and writing tracee memory from the supervisor.
2//!
3//! `process_vm_readv`/`process_vm_writev` are used when the container allows
4//! them; otherwise `/proc/<pid>/mem` is opened per access (the descriptor is
5//! not cached because it would silently read the old address space after an
6//! `execve`).
7//!
8//! In the guest-stub restricted mode the supervisor cannot reach a guest's
9//! memory at all; the stub copies what a syscall uses into the mailbox (see
10//! `guest_stub`), whose guest addresses are then served from the
11//! supervisor's own mapping of it, and every other address is `EFAULT`.
12
13use crate::sys::{PATH_MAX, Result, errno};
14
15/// The kernel's limit on one argument or environment string of an exec
16/// (`MAX_ARG_STRLEN`, 32 pages): a longer one fails the exec with `E2BIG`
17/// before any supervisor sees it.
18pub const MAX_ARG_STRLEN: usize = 32 * 4096;
19use std::ffi::CString;
20use std::sync::OnceLock;
21use std::sync::atomic::{AtomicU8, Ordering};
22
23const MODE_UNPROBED: u8 = 0;
24#[allow(dead_code)]
25const MODE_PROCESS_VM: u8 = 1;
26const MODE_PROC_MEM: u8 = 2;
27
28/// Tracee memory accessor.  Cheap to copy; the probe result is global.
29#[derive(Clone, Copy, Debug, Default)]
30pub struct Mem;
31
32static MODE: AtomicU8 = AtomicU8::new(MODE_UNPROBED);
33
34/// The mailbox of the guest-stub restricted mode as each ABI's guests map
35/// it: its guest address, its length, and the address of the supervisor's
36/// mapping of it.
37static WINDOWS: OnceLock<Vec<(u64, usize, usize)>> = OnceLock::new();
38
39/// Serves every later access from the mailbox mapped at `local`.
40///
41/// # Arguments
42/// * `guests` - Addresses of the mailbox in the guests, one per ABI.
43/// * `len` - Its length.
44/// * `local` - The supervisor's mapping of it.
45pub fn set_window(guests: &[u64], len: usize, local: *mut u8) {
46    let _ = WINDOWS.set(guests.iter().map(|&g| (g, len, local as usize)).collect());
47}
48
49/// Where `len` bytes at guest address `addr` lie in the supervisor's mapping
50/// of the mailbox, when it is in use.
51///
52/// # Arguments
53/// * `addr` - Guest address.
54/// * `len` - Bytes from there.
55///
56/// # Returns
57/// `None` outside the guest-stub mode; otherwise the local address and how
58/// many of the bytes lie inside the mailbox.
59///
60/// # Errors
61/// `EFAULT` when `addr` is not in the mailbox.
62fn in_window(addr: u64, len: usize) -> Option<Result<(*mut u8, usize)>> {
63    let windows = WINDOWS.get()?;
64    let Some(&(guest, size, local)) = windows
65        .iter()
66        .find(|&&(g, size, _)| addr >= g && addr < g + size as u64)
67    else {
68        return Some(Err(libc::EFAULT));
69    };
70    let off = (addr - guest) as usize;
71    Some(Ok(((local + off) as *mut u8, len.min(size - off))))
72}
73
74impl Mem {
75    /// Reads `buf.len()` bytes at `addr` in `pid`'s address space.
76    ///
77    /// # Arguments
78    /// * `pid` - Tracee thread id.
79    /// * `addr` - Virtual address in the tracee.
80    /// * `buf` - Destination; a short read returns fewer bytes.
81    ///
82    /// # Returns
83    /// Bytes read.
84    ///
85    /// # Errors
86    /// `EFAULT` for unmapped memory, `ESRCH` when the tracee is gone.
87    pub fn read(&self, pid: u32, addr: u64, buf: &mut [u8]) -> Result<usize> {
88        if let Some(w) = in_window(addr, buf.len()) {
89            let (at, n) = w?;
90            unsafe { std::ptr::copy_nonoverlapping(at, buf.as_mut_ptr(), n) };
91            return Ok(n);
92        }
93        match self.mode() {
94            MODE_PROC_MEM => {
95                let fd = open_mem(pid)?;
96                let n = unsafe { libc::pread64(fd, buf.as_mut_ptr() as *mut _, buf.len(), addr as i64) };
97                unsafe { libc::close(fd) };
98                if n < 0 { Err(errno()) } else { Ok(n as usize) }
99            }
100            _ => {
101                let local = libc::iovec {
102                    iov_base: buf.as_mut_ptr() as *mut _,
103                    iov_len: buf.len(),
104                };
105                let remote = libc::iovec {
106                    iov_base: addr as *mut _,
107                    iov_len: buf.len(),
108                };
109                let n = unsafe { libc::process_vm_readv(pid as i32, &local, 1, &remote, 1, 0) };
110                if n < 0 {
111                    let e = errno();
112                    if (e == libc::EPERM || e == libc::ENOSYS) && self.demote() {
113                        return self.read(pid, addr, buf);
114                    }
115                    Err(e)
116                } else {
117                    Ok(n as usize)
118                }
119            }
120        }
121    }
122
123    /// Writes `buf` at `addr` in `pid`'s address space.
124    ///
125    /// # Arguments
126    /// * `pid` - Tracee thread id.
127    /// * `addr` - Virtual address in the tracee.
128    /// * `buf` - Bytes to write; a short write is an error.
129    pub fn write(&self, pid: u32, addr: u64, buf: &[u8]) -> Result<()> {
130        if let Some(w) = in_window(addr, buf.len()) {
131            return write_window(w, buf);
132        }
133        let n = match self.mode() {
134            MODE_PROC_MEM => {
135                let fd = open_mem(pid)?;
136                let n = unsafe { libc::pwrite64(fd, buf.as_ptr() as *const _, buf.len(), addr as i64) };
137                unsafe { libc::close(fd) };
138                n
139            }
140            _ => {
141                let local = libc::iovec {
142                    iov_base: buf.as_ptr() as *mut _,
143                    iov_len: buf.len(),
144                };
145                let remote = libc::iovec {
146                    iov_base: addr as *mut _,
147                    iov_len: buf.len(),
148                };
149                let n = unsafe { libc::process_vm_writev(pid as i32, &local, 1, &remote, 1, 0) };
150                if n < 0 {
151                    let e = errno();
152                    if (e == libc::EPERM || e == libc::ENOSYS) && self.demote() {
153                        return self.write(pid, addr, buf);
154                    }
155                }
156                n
157            }
158        };
159        if n < 0 {
160            Err(errno())
161        } else if n as usize != buf.len() {
162            Err(libc::EFAULT)
163        } else {
164            Ok(())
165        }
166    }
167
168    /// Writes `buf` at `addr` through `/proc/<pid>/mem`, whatever the page's protection.
169    ///
170    /// `process_vm_writev` honours the mapping's permissions, so a string in
171    /// `.rodata` cannot be changed that way; `/proc/<pid>/mem` writes behave
172    /// like a debugger's and copy the page privately.  Used for the in-place
173    /// argument rewrites of the no-ptrace mode.
174    ///
175    /// # Arguments
176    /// * `pid` - Tracee thread id.
177    /// * `addr` - Virtual address in the tracee.
178    /// * `buf` - Bytes to write; a short write is an error.
179    pub fn write_force(&self, pid: u32, addr: u64, buf: &[u8]) -> Result<()> {
180        if let Some(w) = in_window(addr, buf.len()) {
181            return write_window(w, buf);
182        }
183        let fd = open_mem(pid)?;
184        let n = unsafe { libc::pwrite64(fd, buf.as_ptr() as *const _, buf.len(), addr as i64) };
185        unsafe { libc::close(fd) };
186        if n < 0 {
187            Err(errno())
188        } else if n as usize != buf.len() {
189            Err(libc::EFAULT)
190        } else {
191            Ok(())
192        }
193    }
194
195    /// Reads a NUL-terminated path of at most `PATH_MAX - 1` bytes.
196    ///
197    /// # Arguments
198    /// * `pid` - Tracee thread id.
199    /// * `addr` - Address of the first byte.
200    ///
201    /// # Returns
202    /// The bytes before the NUL.
203    ///
204    /// # Errors
205    /// `ENAMETOOLONG` when no NUL is found within `PATH_MAX`, `EFAULT` otherwise.
206    pub fn read_cstr(&self, pid: u32, addr: u64) -> Result<Vec<u8>> {
207        self.read_string(pid, addr, PATH_MAX)
208    }
209
210    /// Reads a NUL-terminated string of fewer than `max` bytes.
211    ///
212    /// Reads stop at page boundaries so a short string next to an unmapped
213    /// page does not fault.  An argument or environment entry of an exec is
214    /// read with [`MAX_ARG_STRLEN`], the kernel's own limit for one such
215    /// string; a path with `PATH_MAX`.
216    ///
217    /// # Arguments
218    /// * `pid` - Tracee thread id.
219    /// * `addr` - Address of the first byte.
220    /// * `max` - Length at which the read gives up.
221    ///
222    /// # Returns
223    /// The bytes before the NUL.
224    ///
225    /// # Errors
226    /// `ENAMETOOLONG` when no NUL is found within `max` bytes, `EFAULT` otherwise.
227    pub fn read_string(&self, pid: u32, addr: u64, max: usize) -> Result<Vec<u8>> {
228        let mut out = Vec::with_capacity(256);
229        let mut cur = addr;
230        let mut chunk = [0u8; 4096];
231        while out.len() < max {
232            let to_page = (4096 - (cur & 4095) as usize).min(max - out.len());
233            let n = self.read(pid, cur, &mut chunk[..to_page])?;
234            if n == 0 {
235                return Err(libc::EFAULT);
236            }
237            if let Some(z) = chunk[..n].iter().position(|&b| b == 0) {
238                out.extend_from_slice(&chunk[..z]);
239                return Ok(out);
240            }
241            out.extend_from_slice(&chunk[..n]);
242            cur += n as u64;
243        }
244        Err(libc::ENAMETOOLONG)
245    }
246
247    /// Reads a NUL-terminated array of 8-byte pointers (argv/envp style).
248    #[allow(dead_code)]
249    ///
250    /// # Arguments
251    /// * `pid` - Tracee thread id.
252    /// * `addr` - Address of the first pointer.
253    /// * `max` - Give up (with `E2BIG`) after this many entries.
254    pub fn read_ptr_array(&self, pid: u32, addr: u64, max: usize) -> Result<Vec<u64>> {
255        self.read_ptr_array_sized(pid, addr, max, 8)
256    }
257
258    /// Reads a NUL-terminated pointer array whose entries are `width` bytes (4 or 8).
259    ///
260    /// # Arguments
261    /// * `pid` - Tracee thread id.
262    /// * `addr` - Address of the first pointer.
263    /// * `max` - Give up (with `E2BIG`) after this many entries.
264    /// * `width` - Pointer size of the tracee's ABI.
265    pub fn read_ptr_array_sized(&self, pid: u32, addr: u64, max: usize, width: usize) -> Result<Vec<u64>> {
266        let mut out = Vec::new();
267        let mut cur = addr;
268        let mut buf = [0u8; 8 * 64];
269        loop {
270            let n = self.read(pid, cur, &mut buf)?;
271            if n < width {
272                return Err(libc::EFAULT);
273            }
274            let whole = n - n % width;
275            for w in buf[..whole].chunks_exact(width) {
276                let p = if width == 8 {
277                    u64::from_ne_bytes(w.try_into().unwrap())
278                } else {
279                    u32::from_ne_bytes(w.try_into().unwrap()) as u64
280                };
281                if p == 0 {
282                    return Ok(out);
283                }
284                out.push(p);
285                if out.len() > max {
286                    return Err(libc::E2BIG);
287                }
288            }
289            cur += whole as u64;
290        }
291    }
292
293    fn mode(&self) -> u8 {
294        MODE.load(Ordering::Relaxed)
295    }
296
297    /// Switches to `/proc/<pid>/mem` once; returns false if already there.
298    fn demote(&self) -> bool {
299        MODE.swap(MODE_PROC_MEM, Ordering::Relaxed) != MODE_PROC_MEM
300    }
301}
302
303/// Copies `buf` into the mailbox at a place [`in_window`] found.
304///
305/// # Arguments
306/// * `w` - The lookup's result.
307/// * `buf` - The bytes to write.
308///
309/// # Errors
310/// `EFAULT` when `buf` runs past the end of the mailbox, or the error of
311/// the lookup.
312fn write_window(w: Result<(*mut u8, usize)>, buf: &[u8]) -> Result<()> {
313    let (at, n) = w?;
314    if n != buf.len() {
315        return Err(libc::EFAULT);
316    }
317    unsafe { std::ptr::copy_nonoverlapping(buf.as_ptr(), at, n) };
318    Ok(())
319}
320
321fn open_mem(pid: u32) -> Result<i32> {
322    let p = CString::new(format!("/proc/{pid}/mem")).unwrap();
323    let fd = unsafe { libc::open(p.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
324    if fd < 0 { Err(errno()) } else { Ok(fd) }
325}