1use std::ffi::OsStr;
12use std::os::unix::ffi::OsStrExt;
13use std::path::Component;
14use std::path::Path;
15use std::path::PathBuf;
16
17use reverie::Errno;
18use reverie::Error;
19use reverie::Guest;
20use reverie::Stack;
21use reverie::syscalls;
22use reverie::syscalls::AddrMut;
23use reverie::syscalls::MemoryAccess;
24use reverie::syscalls::PathPtr;
25use reverie::syscalls::ReadAddr;
26use reverie::syscalls::Syscall;
27
28use super::deterministic_stdio_inode;
29use super::deterministic_stdio_inode_for_resource;
30use crate::record_or_replay::RecordOrReplay;
31use crate::tool_global::determinize_inode;
32use crate::tool_local::Detcore;
33use crate::types::DetInode;
34use crate::types::RawInode;
35
36fn is_proc_id(component: &OsStr) -> bool {
39 component == "self"
40 || component == "thread-self"
41 || component.to_str().is_some_and(|value| {
42 !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
43 })
44}
45
46fn canonical_namespace_name(name: &OsStr) -> Option<&'static [u8]> {
49 match name.to_str()? {
50 "cgroup" => Some(b"cgroup:[4026531835]"),
51 "ipc" => Some(b"ipc:[4026531839]"),
52 "mnt" => Some(b"mnt:[4026531841]"),
53 "net" => Some(b"net:[4026531840]"),
54 "pid" | "pid_for_children" => Some(b"pid:[4026531836]"),
55 "time" | "time_for_children" => Some(b"time:[4026531834]"),
56 "user" => Some(b"user:[4026531837]"),
57 "uts" => Some(b"uts:[4026531838]"),
58 _ => None,
59 }
60}
61
62fn canonical_namespace_target(path: &Path) -> Option<&'static [u8]> {
65 if !path.is_absolute() {
66 return None;
67 }
68
69 let mut parts = Vec::new();
70 for component in path.components() {
71 match component {
72 Component::RootDir => {}
73 Component::Normal(part) => parts.push(part),
74 Component::CurDir | Component::ParentDir | Component::Prefix(_) => return None,
75 }
76 }
77
78 let namespace = match parts.as_slice() {
79 [proc, subject, ns, namespace] if *proc == "proc" && is_proc_id(subject) && *ns == "ns" => {
80 namespace
81 }
82 [proc, subject, task, tid, ns, namespace]
83 if *proc == "proc"
84 && is_proc_id(subject)
85 && *task == "task"
86 && is_proc_id(tid)
87 && *ns == "ns" =>
88 {
89 namespace
90 }
91 _ => return None,
92 };
93 canonical_namespace_name(namespace)
94}
95
96fn normalized_absolute_parts(path: &Path) -> Option<Vec<&OsStr>> {
99 if !path.is_absolute() {
100 return None;
101 }
102
103 let mut parts = Vec::new();
104 for component in path.components() {
105 match component {
106 Component::RootDir | Component::CurDir => {}
107 Component::Normal(part) => parts.push(part),
108 Component::ParentDir => {
109 parts.pop()?;
110 }
111 Component::Prefix(_) => return None,
112 }
113 }
114 Some(parts)
115}
116
117fn decimal_u32(component: &OsStr) -> Option<u32> {
118 let value = component.to_str()?;
119 (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
120 .then(|| value.parse().ok())?
121}
122
123fn decimal_fd(component: &OsStr) -> Option<i32> {
124 let value = component.to_str()?;
125 (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
126 .then(|| value.parse().ok())?
127}
128
129fn proc_fd_target(path: &Path) -> Option<(Option<u32>, i32)> {
131 let parts = normalized_absolute_parts(path)?;
132 match parts.as_slice() {
133 [dev, fd_dir, fd] if *dev == "dev" && *fd_dir == "fd" => Some((None, decimal_fd(fd)?)),
134 [proc, subject, fd_dir, fd] if *proc == "proc" && *fd_dir == "fd" => {
135 let subject = match subject.to_str()? {
136 "self" | "thread-self" => None,
137 _ => Some(decimal_u32(subject)?),
138 };
139 Some((subject, decimal_fd(fd)?))
140 }
141 _ => None,
142 }
143}
144
145fn host_self_proc_fd_alias(path: &Path, current_pid: i64) -> Option<PathBuf> {
147 let (Some(subject), fd) = proc_fd_target(path)? else {
148 return None;
149 };
150 (i64::from(subject) == current_pid).then(|| PathBuf::from(format!("/proc/self/fd/{fd}")))
151}
152
153#[derive(Debug, Eq, PartialEq)]
154struct AnonymousProcFdIdentity {
155 kind: &'static str,
156 raw_inode: RawInode,
157}
158
159const ANONYMOUS_PROC_FD_TARGET_CAPACITY: usize = 32;
161
162fn needs_anonymous_proc_fd_scratch(buffer_present: bool, buffer_len: usize) -> bool {
163 buffer_present && buffer_len != 0 && buffer_len < ANONYMOUS_PROC_FD_TARGET_CAPACITY
164}
165
166fn anonymous_proc_fd_identity(target: &[u8]) -> Option<AnonymousProcFdIdentity> {
168 for (kind, prefix) in [
169 ("pipe", b"pipe:[".as_slice()),
170 ("socket", b"socket:[".as_slice()),
171 ] {
172 let Some(digits) = target
173 .strip_prefix(prefix)
174 .and_then(|rest| rest.strip_suffix(b"]"))
175 else {
176 continue;
177 };
178 if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
179 continue;
180 }
181 let raw_inode = std::str::from_utf8(digits).ok()?.parse().ok()?;
182 return Some(AnonymousProcFdIdentity { kind, raw_inode });
183 }
184 None
185}
186
187fn deterministic_stdio_inode_for_raw(
193 raw_inode: RawInode,
194 stdio_raw_inodes: &[Option<RawInode>; 3],
195) -> Option<DetInode> {
196 let mut matched = None;
197 for (fd, cached) in stdio_raw_inodes.iter().enumerate() {
198 if *cached == Some(raw_inode) {
199 matched = deterministic_stdio_inode(fd as i32);
200 }
201 }
202 matched
203}
204
205fn canonical_anonymous_proc_fd_target(
206 identity: &AnonymousProcFdIdentity,
207 inode: DetInode,
208 buffer_len: usize,
209) -> Vec<u8> {
210 let mut target = format!("{}:[{}]", identity.kind, inode.as_raw()).into_bytes();
211 target.truncate(buffer_len);
212 target
213}
214
215impl<T: RecordOrReplay> Detcore<T> {
216 async fn canonicalize_other_proc_fd_target<G>(
217 &self,
218 guest: &mut G,
219 raw_target: &[u8],
220 buffer: Option<AddrMut<'_, libc::c_char>>,
221 buffer_len: usize,
222 ) -> Result<Option<i64>, Error>
223 where
224 G: Guest<Self>,
225 {
226 let Some(identity) = anonymous_proc_fd_identity(raw_target) else {
227 return Ok(None);
228 };
229 let mut stdio_raw_inodes = [None; 3];
230 for fd in libc::STDIN_FILENO..=libc::STDERR_FILENO {
231 stdio_raw_inodes[fd as usize] = guest
232 .thread_state()
233 .with_detfd(fd, |detfd| {
234 deterministic_stdio_inode_for_resource(fd, detfd.resource())?;
235 detfd.stat().map(|stat| stat.inode)
236 })
237 .ok()
238 .flatten();
239 }
240 let inode = match deterministic_stdio_inode_for_raw(identity.raw_inode, &stdio_raw_inodes) {
241 Some(inode) => inode,
242 None => determinize_inode(guest, identity.raw_inode).await.0,
243 };
244 let target = canonical_anonymous_proc_fd_target(&identity, inode, buffer_len);
245 let buffer = buffer.ok_or(Errno::EFAULT)?;
246 guest.memory().write_exact(buffer.cast(), &target)?;
247 Ok(Some(target.len() as i64))
248 }
249
250 async fn canonicalize_other_proc_fd_readlink<G>(
254 &self,
255 guest: &mut G,
256 buffer: Option<AddrMut<'_, libc::c_char>>,
257 buffer_len: usize,
258 result: i64,
259 ) -> Result<i64, Error>
260 where
261 G: Guest<Self>,
262 {
263 let buffer = buffer.expect("a successful readlink requires a non-null buffer");
264 let observed_len = usize::try_from(result)
265 .expect("a positive readlink result must fit usize")
266 .min(buffer_len);
267 let mut observed = vec![0; observed_len];
268 guest.memory().read_exact(buffer.cast(), &mut observed)?;
269 Ok(self
270 .canonicalize_other_proc_fd_target(guest, &observed, Some(buffer), buffer_len)
271 .await?
272 .unwrap_or(result))
273 }
274
275 async fn canonicalize_namespace_readlink_result<G>(
276 &self,
277 guest: &mut G,
278 path: PathBuf,
279 buffer: Option<AddrMut<'_, libc::c_char>>,
280 buffer_len: usize,
281 result: i64,
282 ) -> Result<i64, Error>
283 where
284 G: Guest<Self>,
285 {
286 if result <= 0 {
287 return Ok(result);
288 }
289
290 let target = if let Some(target) = canonical_namespace_target(&path) {
291 target.to_vec()
292 } else if let Some((subject, fd)) = proc_fd_target(&path) {
293 if let Some(subject) = subject {
294 let current_pid = guest.inject(syscalls::Getpid::new()).await?;
295 if current_pid != i64::from(subject) {
296 return self
297 .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
298 .await;
299 }
300 }
301
302 let stat = self.inject_fstat(guest, fd).await?;
303 let kind = match stat.st_mode & libc::S_IFMT {
304 libc::S_IFIFO => "pipe",
305 libc::S_IFSOCK => "socket",
306 _ => return Ok(result),
307 };
308 let inode_override = guest
309 .thread_state()
310 .with_detfd(fd, |detfd| {
311 deterministic_stdio_inode_for_resource(fd, detfd.resource())
312 })
313 .ok()
314 .flatten();
315 let inode = match inode_override {
316 Some(inode) => inode,
317 None => determinize_inode(guest, stat.st_ino).await.0,
318 };
319 format!("{kind}:[{inode}]").into_bytes()
320 } else {
321 return Ok(result);
322 };
323
324 let written = target.len().min(buffer_len);
325 let buffer = buffer.expect("a successful readlink requires a non-null buffer");
326 guest
327 .memory()
328 .write_exact(buffer.cast(), &target[..written])?;
329 Ok(written as i64)
330 }
331 async fn write_other_proc_fd_target<G>(
332 &self,
333 guest: &mut G,
334 raw_target: &[u8],
335 buffer: Option<AddrMut<'_, libc::c_char>>,
336 buffer_len: usize,
337 ) -> Result<i64, Error>
338 where
339 G: Guest<Self>,
340 {
341 if let Some(result) = self
342 .canonicalize_other_proc_fd_target(guest, raw_target, buffer, buffer_len)
343 .await?
344 {
345 return Ok(result);
346 }
347 let written = raw_target.len().min(buffer_len);
348 let buffer = buffer.ok_or(Errno::EFAULT)?;
349 guest
350 .memory()
351 .write_exact(buffer.cast(), &raw_target[..written])?;
352 Ok(written as i64)
353 }
354
355 async fn finish_other_proc_fd_readlink<G>(
356 &self,
357 guest: &mut G,
358 call: syscalls::Readlink,
359 ) -> Result<i64, Error>
360 where
361 G: Guest<Self>,
362 {
363 let buffer = call.buf();
364 let buffer_len = call.bufsize();
365 if !needs_anonymous_proc_fd_scratch(buffer.is_some(), buffer_len) {
366 let result = self.record_or_replay(guest, call).await?;
367 if result <= 0 {
368 return Ok(result);
369 }
370 return self
371 .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
372 .await;
373 }
374
375 let mut stack = guest.stack().await;
376 let scratch = stack
377 .reserve::<[u8; ANONYMOUS_PROC_FD_TARGET_CAPACITY]>()
378 .cast::<libc::c_char>();
379 let guard = stack.commit()?;
380 let physical_call = call
381 .with_buf(Some(scratch))
382 .with_bufsize(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
383 let result = self.record_or_replay(guest, physical_call).await?;
384 let length = usize::try_from(result)
385 .expect("a successful readlink result must fit usize")
386 .min(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
387 let mut raw_target = vec![0; length];
388 guest.memory().read_exact(scratch.cast(), &mut raw_target)?;
389 drop(guard);
390 self.write_other_proc_fd_target(guest, &raw_target, buffer, buffer_len)
391 .await
392 }
393
394 async fn finish_other_proc_fd_readlinkat<G>(
395 &self,
396 guest: &mut G,
397 call: syscalls::Readlinkat,
398 ) -> Result<i64, Error>
399 where
400 G: Guest<Self>,
401 {
402 let buffer = call.buf();
403 let buffer_len = call.buf_len();
404 if !needs_anonymous_proc_fd_scratch(buffer.is_some(), buffer_len) {
405 let result = self.record_or_replay(guest, call).await?;
406 if result <= 0 {
407 return Ok(result);
408 }
409 return self
410 .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
411 .await;
412 }
413
414 let mut stack = guest.stack().await;
415 let scratch = stack
416 .reserve::<[u8; ANONYMOUS_PROC_FD_TARGET_CAPACITY]>()
417 .cast::<libc::c_char>();
418 let guard = stack.commit()?;
419 let physical_call = call
420 .with_buf(Some(scratch))
421 .with_buf_len(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
422 let result = self.record_or_replay(guest, physical_call).await?;
423 let length = usize::try_from(result)
424 .expect("a successful readlinkat result must fit usize")
425 .min(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
426 let mut raw_target = vec![0; length];
427 guest.memory().read_exact(scratch.cast(), &mut raw_target)?;
428 drop(guard);
429 self.write_other_proc_fd_target(guest, &raw_target, buffer, buffer_len)
430 .await
431 }
432
433 async fn finish_namespace_readlink<G, S>(
436 &self,
437 guest: &mut G,
438 path: PathBuf,
439 buffer: Option<AddrMut<'_, libc::c_char>>,
440 buffer_len: usize,
441 syscall: S,
442 ) -> Result<i64, Error>
443 where
444 G: Guest<Self>,
445 S: Into<Syscall>,
446 {
447 let result = self.record_or_replay(guest, syscall).await?;
448 self.canonicalize_namespace_readlink_result(guest, path, buffer, buffer_len, result)
449 .await
450 }
451
452 pub async fn handle_readlink<G: Guest<Self>>(
456 &self,
457 guest: &mut G,
458 call: syscalls::Readlink,
459 ) -> Result<i64, Error> {
460 let path: PathBuf = call.path().ok_or(Errno::EFAULT)?.read(&guest.memory())?;
461 let (host_path, other_proc_fd) = if let Some((Some(subject), _)) = proc_fd_target(&path) {
462 let current_pid = guest.inject(syscalls::Getpid::new()).await?;
463 (
464 host_self_proc_fd_alias(&path, current_pid),
465 current_pid != i64::from(subject),
466 )
467 } else {
468 (None, false)
469 };
470 if let Some(host_path) = host_path {
471 let bytes = host_path.as_os_str().as_bytes();
472 let mut path_buffer = [0_u8; 64];
473 path_buffer[..bytes.len()].copy_from_slice(bytes);
474 let mut stack = guest.stack().await;
475 let path_address = stack.push(path_buffer).cast::<libc::c_char>();
476 let stack_guard = stack.commit()?;
477 let physical_call = call.with_path(PathPtr::from_ptr(
478 path_address.as_raw() as *const libc::c_char
479 ));
480 let result = self.record_or_replay(guest, physical_call).await?;
481 drop(stack_guard);
482 return self
483 .canonicalize_namespace_readlink_result(
484 guest,
485 path,
486 call.buf(),
487 call.bufsize(),
488 result,
489 )
490 .await;
491 }
492 if other_proc_fd {
493 return self.finish_other_proc_fd_readlink(guest, call).await;
494 }
495 self.finish_namespace_readlink(guest, path, call.buf(), call.bufsize(), call)
496 .await
497 }
498
499 pub async fn handle_readlinkat<G: Guest<Self>>(
503 &self,
504 guest: &mut G,
505 call: syscalls::Readlinkat,
506 ) -> Result<i64, Error> {
507 let path: PathBuf = call.path().ok_or(Errno::EFAULT)?.read(&guest.memory())?;
508 let observed_path = if path.is_absolute() || call.dirfd() == libc::AT_FDCWD {
509 path
510 } else {
511 guest
512 .thread_state()
513 .with_detfd(call.dirfd(), |detfd| detfd.path())?
514 .map_or(path.clone(), |directory| directory.join(path))
515 };
516 if let Some((Some(subject), _)) = proc_fd_target(&observed_path) {
517 let current_pid = guest.inject(syscalls::Getpid::new()).await?;
518 if current_pid != i64::from(subject) {
519 return self.finish_other_proc_fd_readlinkat(guest, call).await;
520 }
521 }
522 self.finish_namespace_readlink(guest, observed_path, call.buf(), call.buf_len(), call)
523 .await
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn recognizes_process_and_thread_namespace_links() {
533 assert_eq!(
534 canonical_namespace_target(Path::new("/proc/self/ns/mnt")),
535 Some(b"mnt:[4026531841]".as_slice())
536 );
537 assert_eq!(
538 canonical_namespace_target(Path::new("/proc/123/task/456/ns/user")),
539 Some(b"user:[4026531837]".as_slice())
540 );
541 assert_eq!(
542 canonical_namespace_target(Path::new("/proc/thread-self/ns/pid_for_children")),
543 Some(b"pid:[4026531836]".as_slice())
544 );
545 }
546
547 #[test]
548 fn leaves_non_namespace_and_relative_links_untouched() {
549 assert_eq!(
550 canonical_namespace_target(Path::new("/proc/self/exe")),
551 None
552 );
553 assert_eq!(canonical_namespace_target(Path::new("/tmp/ns/mnt")), None);
554 assert_eq!(
555 canonical_namespace_target(Path::new("proc/self/ns/mnt")),
556 None
557 );
558 assert_eq!(
559 canonical_namespace_target(Path::new("/proc/self/ns/unknown")),
560 None
561 );
562 }
563
564 #[test]
565 fn recognizes_proc_fd_aliases_and_lexical_normalization() {
566 for (path, expected) in [
567 ("/proc/self/fd/1", (None, 1)),
568 ("/proc/thread-self/fd/20", (None, 20)),
569 ("/proc/123/fd/7", (Some(123), 7)),
570 ("/proc/self/fd/../fd/9", (None, 9)),
571 ("/dev/fd/3", (None, 3)),
572 ] {
573 assert_eq!(proc_fd_target(Path::new(path)), Some(expected), "{path}");
574 }
575 }
576
577 #[test]
578 fn rewrites_only_numeric_virtual_self_proc_fd_aliases() {
579 assert_eq!(
580 host_self_proc_fd_alias(Path::new("/proc/123/fd/7"), 123),
581 Some(PathBuf::from("/proc/self/fd/7"))
582 );
583 assert_eq!(
584 host_self_proc_fd_alias(Path::new("/proc/124/fd/7"), 123),
585 None
586 );
587 assert_eq!(
588 host_self_proc_fd_alias(Path::new("/proc/self/fd/7"), 123),
589 None
590 );
591 }
592
593 #[test]
594 fn rejects_non_proc_fd_targets() {
595 for path in [
596 "/proc/self/fd/",
597 "/proc/self/fd/stdout",
598 "/proc/self/fd/1/status",
599 "/proc/not-a-pid/fd/1",
600 "/dev/fd/-1",
601 "proc/self/fd/1",
602 ] {
603 assert_eq!(proc_fd_target(Path::new(path)), None, "{path}");
604 }
605 }
606
607 #[test]
608 fn recognizes_only_anonymous_pipe_and_socket_targets() {
609 assert_eq!(
610 anonymous_proc_fd_identity(b"pipe:[987654321]"),
611 Some(AnonymousProcFdIdentity {
612 kind: "pipe",
613 raw_inode: 987_654_321,
614 })
615 );
616 assert_eq!(
617 anonymous_proc_fd_identity(b"socket:[42]"),
618 Some(AnonymousProcFdIdentity {
619 kind: "socket",
620 raw_inode: 42,
621 })
622 );
623
624 for target in [
625 b"pip".as_slice(),
626 b"socket:[12345".as_slice(),
627 b"/tmp/regular-file".as_slice(),
628 b"anon_inode:[eventpoll]".as_slice(),
629 b"pipe:[]".as_slice(),
630 b"pipe:[12]suffix".as_slice(),
631 b"socket:[not-a-number]".as_slice(),
632 ] {
633 assert_eq!(anonymous_proc_fd_identity(target), None, "{target:?}");
634 }
635 }
636
637 #[test]
638 fn short_buffers_use_scratch_large_enough_for_every_anonymous_target() {
639 assert!(!needs_anonymous_proc_fd_scratch(false, 1));
640 assert!(!needs_anonymous_proc_fd_scratch(true, 0));
641 assert!(needs_anonymous_proc_fd_scratch(true, 1));
642 assert!(needs_anonymous_proc_fd_scratch(true, 31));
643 assert!(!needs_anonymous_proc_fd_scratch(true, 32));
644
645 let maximum = format!("socket:[{}]", RawInode::MAX);
646 assert!(maximum.len() < ANONYMOUS_PROC_FD_TARGET_CAPACITY);
647 assert_eq!(
648 anonymous_proc_fd_identity(maximum.as_bytes()),
649 Some(AnonymousProcFdIdentity {
650 kind: "socket",
651 raw_inode: RawInode::MAX,
652 })
653 );
654 }
655
656 #[test]
657 fn stdio_identity_requires_a_raw_inode_match_and_preserves_alias_precedence() {
658 let stdio = [Some(11), Some(22), Some(33)];
659 assert_eq!(
660 deterministic_stdio_inode_for_raw(22, &stdio),
661 Some(DetInode::mint(1001))
662 );
663 assert_eq!(deterministic_stdio_inode_for_raw(44, &stdio), None);
664
665 let aliased = [None, Some(55), Some(55)];
666 assert_eq!(
667 deterministic_stdio_inode_for_raw(55, &aliased),
668 Some(DetInode::mint(1002))
669 );
670 }
671
672 #[test]
673 fn anonymous_target_rewrite_ignores_raw_inode_width_and_truncates_to_buffer() {
674 let short_raw = anonymous_proc_fd_identity(b"pipe:[42]").unwrap();
675 let long_raw = anonymous_proc_fd_identity(b"pipe:[987654321]").unwrap();
676 let short_rewrite =
677 canonical_anonymous_proc_fd_target(&short_raw, DetInode::mint(1001), usize::MAX);
678 let long_rewrite =
679 canonical_anonymous_proc_fd_target(&long_raw, DetInode::mint(1001), usize::MAX);
680 assert_eq!(short_rewrite, b"pipe:[1001]");
681 assert_eq!(long_rewrite, short_rewrite);
682
683 assert_eq!(
684 canonical_anonymous_proc_fd_target(&long_raw, DetInode::mint(1001), 8),
685 b"pipe:[10"
686 );
687 }
688}