1use crate::sys::{errno, strerror};
25use std::ffi::CString;
26
27#[derive(Clone, Debug)]
29pub struct Access {
30 pub ptrace: bool,
32 pub userns: Option<UserNs>,
34 pub trap_mode: bool,
37}
38
39#[derive(Clone, Debug)]
42pub struct UserNs {
43 uid_map: CString,
45 gid_map: CString,
47}
48
49static MARKER: u64 = 0x7072_6f6f_745f_6e78;
52
53impl UserNs {
54 pub fn identity() -> UserNs {
59 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
60 UserNs {
61 uid_map: CString::new(format!("{uid} {uid} 1")).unwrap(),
62 gid_map: CString::new(format!("{gid} {gid} 1")).unwrap(),
63 }
64 }
65
66 pub fn enter(&self) -> Result<(), i32> {
75 if unsafe { libc::unshare(libc::CLONE_NEWUSER) } < 0 {
76 return Err(errno());
77 }
78 write_file(c"/proc/self/setgroups", c"deny")?;
79 write_file(c"/proc/self/uid_map", &self.uid_map)?;
80 write_file(c"/proc/self/gid_map", &self.gid_map)
81 }
82}
83
84fn write_file(path: &std::ffi::CStr, text: &std::ffi::CStr) -> Result<(), i32> {
93 let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
94 if fd < 0 {
95 return Err(errno());
96 }
97 let bytes = text.to_bytes();
98 let n = unsafe { libc::write(fd, bytes.as_ptr() as *const _, bytes.len()) };
99 let e = errno();
100 unsafe { libc::close(fd) };
101 if n == bytes.len() as isize { Ok(()) } else { Err(e) }
102}
103
104struct Trial {
106 memory: Result<(), i32>,
108 ptrace: Result<(), i32>,
110}
111
112pub fn probe(ptrace_wanted: bool, stub: Option<bool>) -> Result<Access, String> {
133 let stub = stub.filter(|_| cfg!(any(target_arch = "x86_64", target_arch = "aarch64")));
134 let trapped = Access {
135 ptrace: false,
136 userns: None,
137 trap_mode: true,
138 };
139 if stub == Some(true) {
140 return Ok(trapped);
141 }
142 let forced = std::env::var_os("PROOT_USERNS").map(|v| v != "0");
143 let userns = UserNs::identity();
144 let mut plans: Vec<(bool, bool)> = Vec::new();
145 for ptrace in [ptrace_wanted, false] {
146 for ns in [false, true] {
147 if forced.is_some_and(|f| f != ns) || plans.contains(&(ptrace, ns)) {
148 continue;
149 }
150 plans.push((ptrace, ns));
151 }
152 }
153 let mut plain: Option<Trial> = None;
154 let mut namespaced: Option<Trial> = None;
155 for (ptrace, ns) in plans {
156 let slot = if ns { &mut namespaced } else { &mut plain };
157 if slot.is_none() {
158 *slot = Some(trial(ns.then_some(&userns), ptrace_wanted));
159 }
160 let t = slot.as_ref().unwrap();
161 if t.memory.is_ok() && (!ptrace || t.ptrace.is_ok()) {
162 return Ok(Access {
163 ptrace,
164 userns: ns.then_some(userns),
165 trap_mode: false,
166 });
167 }
168 }
169 if stub.is_some() {
170 return Ok(trapped);
171 }
172 Err(refusal(plain.as_ref(), namespaced.as_ref()))
173}
174
175fn trial(userns: Option<&UserNs>, try_ptrace: bool) -> Trial {
186 let mut ready = [0i32; 2];
187 let mut go = [0i32; 2];
188 if unsafe { libc::pipe2(ready.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
189 let e = errno();
190 return Trial {
191 memory: Err(e),
192 ptrace: Err(e),
193 };
194 }
195 if unsafe { libc::pipe2(go.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
196 let e = errno();
197 unsafe {
198 libc::close(ready[0]);
199 libc::close(ready[1]);
200 }
201 return Trial {
202 memory: Err(e),
203 ptrace: Err(e),
204 };
205 }
206 let pid = unsafe { libc::fork() };
207 if pid == 0 {
208 unsafe {
209 libc::close(ready[0]);
210 libc::close(go[1]);
211 let status: i32 = match userns.map(UserNs::enter) {
212 Some(Err(e)) => e,
213 _ => 0,
214 };
215 libc::write(ready[1], &status as *const i32 as *const _, 4);
216 let mut byte = 0u8;
217 libc::read(go[0], &mut byte as *mut u8 as *mut _, 1);
218 libc::_exit(0);
219 }
220 }
221 unsafe {
222 libc::close(ready[1]);
223 libc::close(go[0]);
224 }
225 let result = if pid < 0 {
226 let e = errno();
227 Trial {
228 memory: Err(e),
229 ptrace: Err(e),
230 }
231 } else {
232 let mut status = 0i32;
233 let n = unsafe { libc::read(ready[0], &mut status as *mut i32 as *mut _, 4) };
234 if n != 4 || status != 0 {
235 let e = if n == 4 { status } else { libc::ECHILD };
236 Trial {
237 memory: Err(e),
238 ptrace: Err(e),
239 }
240 } else {
241 Trial {
242 memory: read_marker(pid),
243 ptrace: if try_ptrace { seize(pid) } else { Err(libc::ENOSYS) },
244 }
245 }
246 };
247 unsafe {
248 libc::close(ready[0]);
249 libc::close(go[1]);
250 if pid > 0 {
251 libc::kill(pid, libc::SIGKILL);
252 let mut st = 0;
253 while libc::waitpid(pid, &mut st, libc::__WALL) < 0 && errno() == libc::EINTR {}
254 }
255 }
256 result
257}
258
259fn read_marker(pid: i32) -> Result<(), i32> {
268 let mut value = 0u64;
269 let addr = &MARKER as *const u64 as u64;
270 let local = libc::iovec {
271 iov_base: &mut value as *mut u64 as *mut _,
272 iov_len: 8,
273 };
274 let remote = libc::iovec {
275 iov_base: addr as *mut _,
276 iov_len: 8,
277 };
278 if unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) } == 8 && value == MARKER {
279 return Ok(());
280 }
281 let path = CString::new(format!("/proc/{pid}/mem")).unwrap();
282 let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
283 if fd < 0 {
284 return Err(errno());
285 }
286 value = 0;
287 let n = unsafe { libc::pread64(fd, &mut value as *mut u64 as *mut _, 8, addr as i64) };
288 let e = errno();
289 unsafe { libc::close(fd) };
290 if n == 8 && value == MARKER { Ok(()) } else { Err(e) }
291}
292
293fn seize(pid: i32) -> Result<(), i32> {
301 if unsafe { libc::ptrace(libc::PTRACE_SEIZE, pid, 0usize, 0usize) } < 0 {
302 Err(errno())
303 } else {
304 Ok(())
305 }
306}
307
308fn refusal(plain: Option<&Trial>, namespaced: Option<&Trial>) -> String {
317 let scope = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
318 .map(|s| format!("kernel.yama.ptrace_scope is {}", s.trim()))
319 .unwrap_or_else(|_| "Yama is not present".into());
320 let mut msg = String::from("cannot reach the guest's memory: ");
321 if let Some(t) = plain {
322 let e = t.memory.err().unwrap_or(libc::EPERM);
323 msg.push_str(&format!(
324 "process_vm_readv and /proc/<pid>/mem are refused for a child ({})",
325 strerror(e)
326 ));
327 }
328 match namespaced {
329 Some(t) => {
330 let e = t.memory.err().unwrap_or(libc::EPERM);
331 msg.push_str(&format!(
332 "{}a user namespace did not help ({})",
333 if plain.is_some() { ", and " } else { "" },
334 strerror(e)
335 ));
336 }
337 None if std::env::var_os("PROOT_USERNS").is_some_and(|v| v == "0") => {
338 msg.push_str(", and PROOT_USERNS=0 rules out a user namespace")
339 }
340 None => {}
341 }
342 msg.push_str(&format!(
343 "; {scope}, and proot-next needs either CAP_SYS_PTRACE, a ptrace_scope of 0 or 1, or unprivileged user namespaces"
344 ));
345 if cfg!(any(target_arch = "x86_64", target_arch = "aarch64")) {
346 msg.push_str(" (PROOT_GUEST_STUB=0 rules out the guest-stub mode, which needs none of them)");
347 }
348 msg
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn probe_reaches_a_child_or_explains_why_not() {
357 match probe(true, None) {
358 Ok(a) => {
359 assert!(!a.trap_mode);
360 if a.userns.is_none() {
361 assert!(read_marker(std::process::id() as i32).is_ok());
362 }
363 }
364 Err(msg) => assert!(msg.contains("ptrace_scope") || msg.contains("Yama")),
365 }
366 }
367
368 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
371 #[test]
372 fn the_guest_stub_is_the_last_resort_when_asked_for() {
373 let a = probe(true, Some(false)).expect("the guest stub needs no access");
374 assert_eq!(a.trap_mode, probe(true, None).is_err());
375 assert!(probe(true, Some(true)).unwrap().trap_mode);
376 }
377
378 #[test]
379 fn identity_mapping_names_the_callers_ids() {
380 let ns = UserNs::identity();
381 let uid = unsafe { libc::getuid() };
382 assert_eq!(ns.uid_map.to_bytes(), format!("{uid} {uid} 1").as_bytes());
383 }
384}