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}