Skip to main content

tracexec_core/
proc.rs

1//! This module provides utilities about processing process information(e.g. comm, argv, envp).
2
3use core::fmt;
4use std::{
5  borrow::Cow,
6  collections::{
7    BTreeMap,
8    BTreeSet,
9    HashSet,
10  },
11  ffi::{
12    CString,
13    OsString,
14  },
15  fmt::{
16    Display,
17    Formatter,
18  },
19  fs,
20  io::{
21    self,
22    BufRead,
23    BufReader,
24    Read,
25  },
26  os::raw::c_int,
27  path::{
28    Path,
29    PathBuf,
30  },
31};
32
33use filedescriptor::AsRawFileDescriptor;
34use nix::{
35  fcntl::OFlag,
36  libc::{
37    AT_FDCWD,
38    gid_t,
39  },
40  unistd::{
41    Pid,
42    getpid,
43  },
44};
45use owo_colors::OwoColorize;
46use serde::{
47  Serialize,
48  Serializer,
49  ser::SerializeSeq,
50};
51use snafu::Snafu;
52use tracing::warn;
53
54use crate::{
55  cache::{
56    ArcStr,
57    StringCache,
58  },
59  event::{
60    FriendlyError,
61    OutputMsg,
62  },
63  pty::UnixSlavePty,
64};
65
66#[allow(unused)]
67pub fn read_argv(pid: Pid) -> color_eyre::Result<Vec<CString>> {
68  let filename = format!("/proc/{pid}/cmdline");
69  let buf = std::fs::read(filename)?;
70  Ok(
71    buf
72      .split(|&c| c == 0)
73      .map(CString::new)
74      .collect::<Result<Vec<_>, _>>()?,
75  )
76}
77
78pub fn read_comm(pid: Pid) -> color_eyre::Result<ArcStr> {
79  let filename = format!("/proc/{pid}/comm");
80  let mut buf = std::fs::read(filename)?;
81  buf.pop(); // remove trailing newline
82  let utf8 = String::from_utf8_lossy(&buf);
83  Ok(CACHE.get_or_insert(&utf8))
84}
85
86pub fn read_cwd(pid: Pid) -> std::io::Result<ArcStr> {
87  let filename = format!("/proc/{pid}/cwd");
88  let buf = std::fs::read_link(filename)?;
89  Ok(cached_str(&buf.to_string_lossy()))
90}
91
92pub fn read_exe(pid: Pid) -> std::io::Result<ArcStr> {
93  let filename = format!("/proc/{pid}/exe");
94  let buf = std::fs::read_link(filename)?;
95  Ok(cached_str(&buf.to_string_lossy()))
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct ProcStatus {
100  pub cred: Cred,
101}
102
103#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
104pub struct Cred {
105  pub groups: Vec<gid_t>,
106  pub uid_real: u32,
107  pub uid_effective: u32,
108  pub uid_saved_set: u32,
109  pub uid_fs: u32,
110  pub gid_real: u32,
111  pub gid_effective: u32,
112  pub gid_saved_set: u32,
113  pub gid_fs: u32,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
117pub enum CredInspectError {
118  #[snafu(display("Failed to read credential info: {kind}"))]
119  Io { kind: std::io::ErrorKind },
120  #[snafu(display("Failed to inspect credential info from kernel"))]
121  Inspect,
122}
123
124pub fn read_status(pid: Pid) -> std::io::Result<ProcStatus> {
125  let filename = format!("/proc/{pid}/status");
126  let contents = fs::read_to_string(filename)?;
127  parse_status_contents(&contents)
128}
129
130fn parse_status_contents(contents: &str) -> std::io::Result<ProcStatus> {
131  let mut uid = None;
132  let mut gid = None;
133  let mut groups = None;
134
135  fn parse_ids(s: &str) -> std::io::Result<[u32; 4]> {
136    let mut iter = s.trim_ascii().split_ascii_whitespace().take(4).map(|v| {
137      v.parse()
138        .map_err(|_| std::io::Error::new(io::ErrorKind::InvalidData, "non numeric uid/gid"))
139    });
140    Ok([
141      iter
142        .next()
143        .transpose()?
144        .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
145      iter
146        .next()
147        .transpose()?
148        .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
149      iter
150        .next()
151        .transpose()?
152        .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
153      iter
154        .next()
155        .transpose()?
156        .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
157    ])
158  }
159
160  for line in contents.lines() {
161    if let Some(rest) = line.strip_prefix("Uid:") {
162      uid = Some(parse_ids(rest)?);
163    } else if let Some(rest) = line.strip_prefix("Gid:") {
164      gid = Some(parse_ids(rest)?);
165    } else if let Some(rest) = line.strip_prefix("Groups:") {
166      let r: Result<Vec<_>, _> = rest
167        .trim_ascii()
168        .split_ascii_whitespace()
169        .map(|v| {
170          v.parse()
171            .map_err(|_| std::io::Error::new(io::ErrorKind::InvalidData, "non numeric group id"))
172        })
173        .collect();
174      groups = Some(r?);
175    }
176
177    if uid.is_some() && gid.is_some() && groups.is_some() {
178      break;
179    }
180  }
181
182  let Some([uid_real, uid_effective, uid_saved_set, uid_fs]) = uid else {
183    return Err(std::io::Error::new(
184      io::ErrorKind::InvalidData,
185      "status output does not contain uids",
186    ));
187  };
188  let Some([gid_real, gid_effective, gid_saved_set, gid_fs]) = gid else {
189    return Err(std::io::Error::new(
190      io::ErrorKind::InvalidData,
191      "status output does not contain gids",
192    ));
193  };
194  let Some(groups) = groups else {
195    return Err(std::io::Error::new(
196      io::ErrorKind::InvalidData,
197      "status output does not contain groups",
198    ));
199  };
200
201  Ok(ProcStatus {
202    cred: Cred {
203      groups,
204      uid_real,
205      uid_effective,
206      uid_saved_set,
207      uid_fs,
208      gid_real,
209      gid_effective,
210      gid_saved_set,
211      gid_fs,
212    },
213  })
214}
215
216/// Error variants for cgroup resolution.
217#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
218pub enum CgroupError {
219  #[snafu(display("Failed to read /proc/<pid>/cgroup: {kind}"))]
220  ReadProcCgroup { kind: std::io::ErrorKind },
221  #[snafu(display("cgroupv2 filesystem not mounted"))]
222  CgroupFsNotMounted,
223  #[snafu(display("Failed to read cgroup directory"))]
224  ReadCgroupDir,
225  #[snafu(display("cgroup ID not found"))]
226  CgroupIdNotFound,
227}
228
229/// Information about the cgroup of a process at the time of exec.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub enum CgroupInfo {
232  /// cgroupv2 path (e.g. "/user.slice/user-1000.slice/session-1.scope")
233  V2 { path: String },
234  /// Only cgroupv1 hierarchies were found, cgroupv2 is not available
235  V1Only,
236  /// Cgroup collection was not requested
237  NotCollected,
238  /// Failed to read or resolve cgroup information
239  Error(CgroupError),
240}
241
242/// Read the cgroup path from `/proc/{pid}/cgroup`.
243/// Returns `CgroupInfo::V2` if a cgroupv2 entry is found,
244/// `CgroupInfo::V1Only` if only v1 entries exist.
245pub fn read_cgroup(pid: Pid) -> CgroupInfo {
246  let filename = format!("/proc/{pid}/cgroup");
247  let contents = match fs::read_to_string(&filename) {
248    Ok(c) => c,
249    Err(e) => return CgroupInfo::Error(CgroupError::ReadProcCgroup { kind: e.kind() }),
250  };
251  parse_proc_cgroup(&contents)
252}
253
254/// Parse the contents of `/proc/pid/cgroup`.
255/// For cgroupv2: the line is `0::<path>`.
256/// For cgroupv1: hierarchy-ID is non-zero and controller-list is non-empty.
257pub fn parse_proc_cgroup(contents: &str) -> CgroupInfo {
258  for line in contents.lines() {
259    let line = line.trim();
260    if line.is_empty() {
261      continue;
262    }
263    // Format: hierarchy-ID:controller-list:cgroup-path
264    let mut parts = line.splitn(3, ':');
265    let hierarchy_id = match parts.next() {
266      Some(id) => id,
267      None => continue,
268    };
269    let controller_list = match parts.next() {
270      Some(cl) => cl,
271      None => continue,
272    };
273    let cgroup_path = match parts.next() {
274      Some(p) => p,
275      None => continue,
276    };
277    // cgroupv2: hierarchy-ID is "0" and controller-list is empty
278    if hierarchy_id == "0" && controller_list.is_empty() {
279      return CgroupInfo::V2 {
280        path: cgroup_path.to_string(),
281      };
282    }
283  }
284  CgroupInfo::V1Only
285}
286
287/// Resolve a cgroupv2 ID (inode number) to its path by walking `/sys/fs/cgroup`.
288/// Returns `CgroupInfo::V2` if the cgroup is found, `CgroupInfo::Error` otherwise.
289pub fn resolve_cgroup_id(cgroup_id: u64) -> CgroupInfo {
290  use std::os::unix::fs::MetadataExt;
291
292  let cgroup_root = Path::new("/sys/fs/cgroup");
293  if !cgroup_root.exists() {
294    return CgroupInfo::Error(CgroupError::CgroupFsNotMounted);
295  }
296
297  // Check if the root itself matches
298  if let Ok(meta) = fs::metadata(cgroup_root)
299    && meta.ino() == cgroup_id
300  {
301    return CgroupInfo::V2 {
302      path: "/".to_string(),
303    };
304  }
305
306  resolve_cgroup_id_in_dir(cgroup_id, cgroup_root, cgroup_root)
307}
308
309fn resolve_cgroup_id_in_dir(cgroup_id: u64, dir: &Path, root: &Path) -> CgroupInfo {
310  use std::os::unix::fs::MetadataExt;
311
312  let entries = match fs::read_dir(dir) {
313    Ok(e) => e,
314    Err(_) => return CgroupInfo::Error(CgroupError::ReadCgroupDir),
315  };
316
317  for entry in entries {
318    let entry = match entry {
319      Ok(e) => e,
320      Err(_) => continue,
321    };
322    let path = entry.path();
323    let meta = match fs::metadata(&path) {
324      Ok(m) => m,
325      Err(_) => continue,
326    };
327    if meta.is_dir() {
328      if meta.ino() == cgroup_id {
329        let relative = path
330          .strip_prefix(root)
331          .map(|p| format!("/{}", p.display()))
332          .unwrap_or_else(|_| path.display().to_string());
333        return CgroupInfo::V2 { path: relative };
334      }
335      // Recurse into subdirectories
336      let result = resolve_cgroup_id_in_dir(cgroup_id, &path, root);
337      if matches!(result, CgroupInfo::V2 { .. }) {
338        return result;
339      }
340    }
341  }
342
343  CgroupInfo::Error(CgroupError::CgroupIdNotFound)
344}
345
346#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
347pub struct FileDescriptorInfoCollection {
348  #[serde(flatten)]
349  pub fdinfo: BTreeMap<c_int, FileDescriptorInfo>,
350  #[serde(
351    skip_serializing_if = "Option::is_none",
352    serialize_with = "serialize_optional_friendly_error"
353  )]
354  pub error: Option<FriendlyError>,
355}
356
357impl FileDescriptorInfoCollection {
358  pub fn mark_error(&mut self, error: FriendlyError) {
359    self.error = Some(error);
360  }
361
362  pub fn is_reliable(&self) -> bool {
363    self.error.is_none()
364  }
365
366  pub fn stdin(&self) -> Option<&FileDescriptorInfo> {
367    self.fdinfo.get(&0)
368  }
369
370  pub fn stdout(&self) -> Option<&FileDescriptorInfo> {
371    self.fdinfo.get(&1)
372  }
373
374  pub fn stderr(&self) -> Option<&FileDescriptorInfo> {
375    self.fdinfo.get(&2)
376  }
377
378  pub fn get(&self, fd: c_int) -> Option<&FileDescriptorInfo> {
379    self.fdinfo.get(&fd)
380  }
381
382  pub fn stdio(&self) -> impl Iterator<Item = (c_int, &FileDescriptorInfo)> {
383    self.fdinfo.range(0..=2).map(|(&fd, info)| (fd, info))
384  }
385
386  pub fn new_baseline() -> color_eyre::Result<Self> {
387    let mut fdinfo = BTreeMap::new();
388    let pid = getpid();
389    fdinfo.insert(0, read_fdinfo(pid, 0)?);
390    fdinfo.insert(1, read_fdinfo(pid, 1)?);
391    fdinfo.insert(2, read_fdinfo(pid, 2)?);
392
393    Ok(Self {
394      fdinfo,
395      error: None,
396    })
397  }
398
399  pub fn with_pts(pts: &UnixSlavePty) -> color_eyre::Result<Self> {
400    let mut result = Self::default();
401    let ptyfd = &pts.fd;
402    let raw_fd = ptyfd.as_raw_file_descriptor();
403    let mut info = read_fdinfo(getpid(), raw_fd)?;
404    for fd in 0..3 {
405      info.fd = fd;
406      result.fdinfo.insert(fd, read_fdinfo(getpid(), raw_fd)?);
407    }
408    Ok(result)
409  }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
413pub struct FileDescriptorInfo {
414  pub fd: c_int,
415  pub path: OutputMsg,
416  pub pos: Fallible<usize>,
417  pub flags: Fallible<FdFlags>,
418  pub mnt_id: Fallible<c_int>,
419  pub ino: Fallible<u64>,
420  pub mnt: ArcStr,
421  pub extra: Vec<ArcStr>,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub struct FdFlags(OFlag);
426
427impl FdFlags {
428  pub fn contains(self, flag: OFlag) -> bool {
429    self.0.contains(flag)
430  }
431
432  pub fn intersects(self, flag: OFlag) -> bool {
433    self.0.intersects(flag)
434  }
435
436  pub fn iter(self) -> impl Iterator<Item = OFlag> {
437    self.0.iter()
438  }
439
440  pub fn as_oflag(self) -> OFlag {
441    self.0
442  }
443}
444
445impl From<OFlag> for FdFlags {
446  fn from(value: OFlag) -> Self {
447    Self(value)
448  }
449}
450
451impl Default for FdFlags {
452  fn default() -> Self {
453    Self(OFlag::empty())
454  }
455}
456
457impl From<OFlag> for Fallible<FdFlags> {
458  fn from(value: OFlag) -> Self {
459    Self::Ok(value.into())
460  }
461}
462
463impl Fallible<FdFlags> {
464  pub fn contains(&self, flag: OFlag) -> bool {
465    self.ok().is_some_and(|flags| flags.contains(flag))
466  }
467
468  pub fn intersects(&self, flag: OFlag) -> bool {
469    self.ok().is_some_and(|flags| flags.intersects(flag))
470  }
471
472  pub fn iter(&self) -> impl Iterator<Item = OFlag> + '_ {
473    self.ok().into_iter().flat_map(|flags| flags.iter())
474  }
475}
476
477impl Display for FdFlags {
478  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
479    bitflags::parser::to_writer(&self.0, f).map_err(|_| fmt::Error)
480  }
481}
482
483impl Serialize for FdFlags {
484  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
485  where
486    S: Serializer,
487  {
488    serialize_oflags(&self.0, serializer)
489  }
490}
491
492#[derive(Debug, Clone, PartialEq, Eq)]
493pub enum Fallible<T> {
494  Ok(T),
495  Err(FriendlyError),
496}
497
498impl<T> Fallible<T> {
499  pub fn ok(&self) -> Option<&T> {
500    match self {
501      Self::Ok(value) => Some(value),
502      Self::Err(_) => None,
503    }
504  }
505}
506
507impl<T> From<T> for Fallible<T> {
508  fn from(value: T) -> Self {
509    Self::Ok(value)
510  }
511}
512
513impl<T: Default> Default for Fallible<T> {
514  fn default() -> Self {
515    Self::Ok(T::default())
516  }
517}
518
519impl<T: Display> Display for Fallible<T> {
520  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
521    match self {
522      Self::Ok(value) => Display::fmt(value, f),
523      Self::Err(error) => Display::fmt(error, f),
524    }
525  }
526}
527
528impl<T: Serialize> Serialize for Fallible<T> {
529  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530  where
531    S: Serializer,
532  {
533    match self {
534      Self::Ok(value) => value.serialize(serializer),
535      Self::Err(error) => <&'static str>::from(error).serialize(serializer),
536    }
537  }
538}
539
540impl FileDescriptorInfo {
541  pub fn not_same_file_as(&self, other: &Self) -> bool {
542    !self.same_file_as(other)
543  }
544
545  pub fn same_file_as(&self, other: &Self) -> bool {
546    self
547      .ino
548      .ok()
549      .zip(other.ino.ok())
550      .is_some_and(|(a, b)| a == b)
551      && self
552        .mnt_id
553        .ok()
554        .zip(other.mnt_id.ok())
555        .is_some_and(|(a, b)| a == b)
556  }
557}
558
559fn serialize_oflags<S>(oflag: &OFlag, serializer: S) -> Result<S::Ok, S::Error>
560where
561  S: Serializer,
562{
563  let mut seq = serializer.serialize_seq(None)?;
564  let mut flag_display = String::with_capacity(16);
565  for f in oflag.iter() {
566    flag_display.clear();
567    bitflags::parser::to_writer(&f, &mut flag_display).map_err(serde::ser::Error::custom)?;
568    seq.serialize_element(&flag_display)?;
569  }
570  seq.end()
571}
572
573fn serialize_optional_friendly_error<S>(
574  error: &Option<FriendlyError>,
575  serializer: S,
576) -> Result<S::Ok, S::Error>
577where
578  S: Serializer,
579{
580  error
581    .as_ref()
582    .map(<&'static str>::from)
583    .serialize(serializer)
584}
585
586impl Default for FileDescriptorInfo {
587  fn default() -> Self {
588    Self {
589      fd: Default::default(),
590      path: OutputMsg::Ok(ArcStr::default()),
591      pos: Default::default(),
592      flags: OFlag::empty().into(),
593      mnt_id: Default::default(),
594      ino: Default::default(),
595      mnt: Default::default(),
596      extra: Default::default(),
597    }
598  }
599}
600
601pub fn read_fd(pid: Pid, fd: i32) -> std::io::Result<ArcStr> {
602  if fd == AT_FDCWD {
603    return read_cwd(pid);
604  }
605  let filename = format!("/proc/{pid}/fd/{fd}");
606  Ok(cached_str(&std::fs::read_link(filename)?.to_string_lossy()))
607}
608
609/// Read /proc/{pid}/fdinfo/{fd} to get more information about the file descriptor.
610pub fn read_fdinfo(pid: Pid, fd: i32) -> color_eyre::Result<FileDescriptorInfo> {
611  let filename = format!("/proc/{pid}/fdinfo/{fd}");
612  let file = std::fs::File::open(filename)?;
613  let reader = BufReader::new(file);
614  let mut info = FileDescriptorInfo::default();
615  for line in reader.lines() {
616    let line = line?;
617    let mut parts = line.split_ascii_whitespace();
618    let key = parts.next().unwrap_or("");
619    let value = parts.next().unwrap_or("");
620    match key {
621      "pos:" => info.pos = value.parse::<usize>()?.into(),
622      "flags:" => info.flags = OFlag::from_bits_truncate(c_int::from_str_radix(value, 8)?).into(),
623      "mnt_id:" => info.mnt_id = value.parse::<c_int>()?.into(),
624      "ino:" => info.ino = value.parse::<u64>()?.into(),
625      _ => {
626        let line = CACHE.get_or_insert_owned(line);
627        info.extra.push(line)
628      }
629    }
630  }
631  if let Some(mnt_id) = info.mnt_id.ok() {
632    info.mnt = get_mountinfo_by_mnt_id(pid, *mnt_id)?;
633  }
634  info.path = read_fd(pid, fd).map(OutputMsg::Ok)?;
635  Ok(info)
636}
637
638pub fn read_fds(pid: Pid) -> color_eyre::Result<FileDescriptorInfoCollection> {
639  let mut collection = FileDescriptorInfoCollection::default();
640  let filename = format!("/proc/{pid}/fdinfo");
641  for entry in std::fs::read_dir(filename)? {
642    let entry = entry?;
643    let fd = entry.file_name().to_string_lossy().parse()?;
644    collection.fdinfo.insert(fd, read_fdinfo(pid, fd)?);
645  }
646  Ok(collection)
647}
648
649fn get_mountinfo_by_mnt_id(pid: Pid, mnt_id: c_int) -> color_eyre::Result<ArcStr> {
650  let filename = format!("/proc/{pid}/mountinfo");
651  let file = std::fs::File::open(filename)?;
652  let reader = BufReader::new(file);
653  for line in reader.lines() {
654    let line = line?;
655    let parts = line.split_once(' ');
656    if parts.map(|(mount_id, _)| mount_id.parse()) == Some(Ok(mnt_id)) {
657      return Ok(CACHE.get_or_insert_owned(line));
658    }
659  }
660  Ok(CACHE.get_or_insert("Not found. This is probably a pipe or something else."))
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
664#[serde(tag = "what", content = "value", rename_all = "kebab-case")]
665pub enum Interpreter {
666  None,
667  Shebang(ArcStr),
668  ExecutableInaccessible,
669  Error(ArcStr),
670}
671
672impl Display for Interpreter {
673  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
674    match self {
675      Self::None => write!(f, "{}", "none".bold()),
676      Self::Shebang(s) => write!(f, "{s:?}"),
677      Self::ExecutableInaccessible => {
678        write!(f, "{}", "executable inaccessible".red().bold())
679      }
680      Self::Error(e) => write!(f, "({}: {})", "err".red().bold(), e.red().bold()),
681    }
682  }
683}
684
685pub fn read_interpreter_recursive(exe: impl AsRef<Path>) -> Vec<Interpreter> {
686  let mut exe = Cow::Borrowed(exe.as_ref());
687  let mut interpreters = Vec::new();
688  loop {
689    match read_interpreter(exe.as_ref()) {
690      Interpreter::Shebang(shebang) => {
691        exe = Cow::Owned(PathBuf::from(
692          shebang.split_ascii_whitespace().next().unwrap_or(""),
693        ));
694        interpreters.push(Interpreter::Shebang(shebang));
695      }
696      Interpreter::None => break,
697      err => {
698        interpreters.push(err);
699        break;
700      }
701    };
702  }
703  interpreters
704}
705
706pub fn read_interpreter(exe: &Path) -> Interpreter {
707  fn err_to_interpreter(e: io::Error) -> Interpreter {
708    if e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::NotFound {
709      Interpreter::ExecutableInaccessible
710    } else {
711      let e = CACHE.get_or_insert_owned(e.to_string());
712      Interpreter::Error(e)
713    }
714  }
715  let file = match std::fs::File::open(exe) {
716    Ok(file) => file,
717    Err(e) => return err_to_interpreter(e),
718  };
719  let mut reader = BufReader::new(file);
720  // First, check if it's a shebang script
721  let mut buf = [0u8; 2];
722
723  if let Err(e) = reader.read_exact(&mut buf) {
724    if e.kind() == std::io::ErrorKind::UnexpectedEof {
725      // File is too short to contain a shebang
726      return Interpreter::None;
727    }
728    let e = CACHE.get_or_insert_owned(e.to_string());
729    return Interpreter::Error(e);
730  };
731  if &buf != b"#!" {
732    return Interpreter::None;
733  }
734  // Read the rest of the line
735  let mut buf = Vec::new();
736
737  if let Err(e) = reader.read_until(b'\n', &mut buf) {
738    let e = CACHE.get_or_insert_owned(e.to_string());
739    return Interpreter::Error(e);
740  };
741  // Get trimmed shebang line [start, end) indices
742  // If the shebang line is empty, we don't care
743  let start = buf
744    .iter()
745    .position(|&c| !c.is_ascii_whitespace())
746    .unwrap_or(0);
747  let end = buf
748    .iter()
749    .rposition(|&c| !c.is_ascii_whitespace())
750    .map(|x| x + 1)
751    .unwrap_or(buf.len());
752  let shebang = String::from_utf8_lossy(&buf[start..end]);
753  let shebang = CACHE.get_or_insert(&shebang);
754  Interpreter::Shebang(shebang)
755}
756
757pub fn parse_env_entry(item: &str) -> (&str, &str) {
758  // trace!("Parsing envp entry: {:?}", item);
759  let Some(mut sep_loc) = item.as_bytes().iter().position(|&x| x == b'=') else {
760    warn!(
761      "Invalid envp entry: {:?}, assuming value to empty string!",
762      item
763    );
764    return (item, "");
765  };
766  if sep_loc == 0 {
767    // Find the next equal sign
768    sep_loc = item
769      .as_bytes()
770      .iter()
771      .skip(1)
772      .position(|&x| x == b'=')
773      .unwrap_or_else(|| {
774        warn!(
775          "Invalid envp entry starting with '=': {:?}, assuming value to empty string!",
776          item
777        );
778        item.len()
779      });
780  }
781  let (head, tail) = item.split_at(sep_loc);
782  (head, { if tail.is_empty() { "" } else { &tail[1..] } })
783}
784
785pub fn parse_failiable_envp(envp: Vec<OutputMsg>) -> (BTreeMap<OutputMsg, OutputMsg>, bool) {
786  let mut has_dash_var = false;
787  (
788    envp
789      .into_iter()
790      .map(|entry| {
791        if let OutputMsg::Ok(s) | OutputMsg::PartialOk(s) = entry {
792          let (key, value) = parse_env_entry(&s);
793          if key.starts_with('-') {
794            has_dash_var = true;
795          }
796          (
797            OutputMsg::Ok(CACHE.get_or_insert(key)),
798            OutputMsg::Ok(CACHE.get_or_insert(value)),
799          )
800        } else {
801          (entry.clone(), entry)
802        }
803      })
804      .collect(),
805    has_dash_var,
806  )
807}
808
809pub fn cached_str(s: &str) -> ArcStr {
810  CACHE.get_or_insert(s)
811}
812
813pub fn cached_string(s: String) -> ArcStr {
814  CACHE.get_or_insert_owned(s)
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
818pub struct EnvDiff {
819  has_added_or_modified_keys_starting_with_dash: bool,
820  pub added: BTreeMap<OutputMsg, OutputMsg>,
821  pub removed: BTreeSet<OutputMsg>,
822  pub modified: BTreeMap<OutputMsg, OutputMsg>,
823}
824
825impl EnvDiff {
826  #[cfg(test)]
827  pub(crate) fn empty() -> Self {
828    Self {
829      has_added_or_modified_keys_starting_with_dash: Default::default(),
830      added: Default::default(),
831      removed: Default::default(),
832      modified: Default::default(),
833    }
834  }
835
836  pub fn is_modified_or_removed(&self, key: &OutputMsg) -> bool {
837    self.modified.contains_key(key) || self.removed.contains(key)
838  }
839
840  #[allow(clippy::unwrap_used)]
841  pub fn removed_with_values<'a>(
842    &'a self,
843    original: &'a BTreeMap<OutputMsg, OutputMsg>,
844  ) -> impl Iterator<Item = (&'a OutputMsg, &'a OutputMsg)> {
845    self
846      .removed
847      .iter()
848      .map(|key| original.get_key_value(key).unwrap())
849  }
850
851  #[allow(clippy::unwrap_used)]
852  pub fn modified_with_values<'a>(
853    &'a self,
854    original: &'a BTreeMap<OutputMsg, OutputMsg>,
855  ) -> impl Iterator<Item = (&'a OutputMsg, &'a OutputMsg, &'a OutputMsg)> {
856    self
857      .modified
858      .iter()
859      .map(|(key, value)| (key, original.get(key).unwrap(), value))
860  }
861
862  /// Whether we need to use `--` to prevent argument injection
863  pub fn need_env_argument_separator(&self, filename: &OutputMsg) -> bool {
864    // When env keys contain dash, we need the separator.
865    // When env is empty (No diff or using raw env and there's none),
866    // we need the separator only when the filename starts with dash
867    self.has_added_or_modified_keys_starting_with_dash
868      || (self.added.is_empty() && self.modified.is_empty() && filename.as_ref().starts_with('-'))
869  }
870}
871
872pub fn diff_env(
873  original: &BTreeMap<OutputMsg, OutputMsg>,
874  envp: &BTreeMap<OutputMsg, OutputMsg>,
875) -> EnvDiff {
876  let mut added = BTreeMap::new();
877  let mut modified = BTreeMap::<OutputMsg, OutputMsg>::new();
878  // Use str to avoid cloning all env vars
879  let mut removed: HashSet<OutputMsg> = original.keys().cloned().collect();
880  let mut has_added_or_modified_keys_starting_with_dash = false;
881  for (key, value) in envp.iter() {
882    // Too bad that we still don't have if- and while-let-chains
883    // https://github.com/rust-lang/rust/issues/53667
884    if let Some(orig_v) = original.get(key) {
885      if orig_v != value {
886        modified.insert(key.clone(), value.clone());
887        if key.as_ref().starts_with('-') {
888          has_added_or_modified_keys_starting_with_dash = true;
889        }
890      }
891      removed.remove(key);
892    } else {
893      added.insert(key.clone(), value.clone());
894      if key.as_ref().starts_with('-') {
895        has_added_or_modified_keys_starting_with_dash = true;
896      }
897    }
898  }
899  EnvDiff {
900    has_added_or_modified_keys_starting_with_dash,
901    added,
902    removed: removed.into_iter().collect(),
903    modified,
904  }
905}
906
907#[derive(Debug, Clone, Serialize)]
908pub struct BaselineInfo {
909  pub cwd: OutputMsg,
910  pub env: BTreeMap<OutputMsg, OutputMsg>,
911  pub fdinfo: FileDescriptorInfoCollection,
912}
913
914impl BaselineInfo {
915  fn env_from_vars_os(
916    vars: impl IntoIterator<Item = (OsString, OsString)>,
917  ) -> BTreeMap<OutputMsg, OutputMsg> {
918    vars
919      .into_iter()
920      .map(|(k, v)| {
921        (
922          CACHE
923            .get_or_insert_owned(k.to_string_lossy().into_owned())
924            .into(),
925          CACHE
926            .get_or_insert_owned(v.to_string_lossy().into_owned())
927            .into(),
928        )
929      })
930      .collect()
931  }
932
933  fn env_from_override(env: Option<&[(OsString, OsString)]>) -> BTreeMap<OutputMsg, OutputMsg> {
934    match env {
935      Some(env) => Self::env_from_vars_os(env.iter().cloned()),
936      None => std::env::vars()
937        .map(|(k, v)| {
938          (
939            CACHE.get_or_insert_owned(k).into(),
940            CACHE.get_or_insert_owned(v).into(),
941          )
942        })
943        .collect(),
944    }
945  }
946
947  pub fn new() -> color_eyre::Result<Self> {
948    Self::new_with_env(None)
949  }
950
951  pub fn new_with_env(env: Option<&[(OsString, OsString)]>) -> color_eyre::Result<Self> {
952    let cwd = cached_str(&std::env::current_dir()?.to_string_lossy()).into();
953    let env = Self::env_from_override(env);
954    let fdinfo = FileDescriptorInfoCollection::new_baseline()?;
955    Ok(Self { cwd, env, fdinfo })
956  }
957
958  pub fn with_pts(pts: &UnixSlavePty) -> color_eyre::Result<Self> {
959    Self::with_pts_and_env(pts, None)
960  }
961
962  pub fn with_pts_and_env(
963    pts: &UnixSlavePty,
964    env: Option<&[(OsString, OsString)]>,
965  ) -> color_eyre::Result<Self> {
966    let cwd = cached_str(&std::env::current_dir()?.to_string_lossy()).into();
967    let env = Self::env_from_override(env);
968    let fdinfo = FileDescriptorInfoCollection::with_pts(pts)?;
969    Ok(Self { cwd, env, fdinfo })
970  }
971}
972
973static CACHE: StringCache = StringCache;
974
975#[cfg(test)]
976mod proc_status_tests {
977
978  use super::*;
979
980  #[test]
981  fn test_parse_status_contents_valid() {
982    let sample = "\
983Name:\ttestproc
984State:\tR (running)
985Uid:\t1000\t1001\t1002\t1003
986Gid:\t2000\t2001\t2002\t2003
987Threads:\t1
988Groups:\t0\t1\t2
989";
990
991    let status = parse_status_contents(sample).unwrap();
992    assert_eq!(
993      status,
994      ProcStatus {
995        cred: Cred {
996          groups: vec![0, 1, 2],
997          uid_real: 1000,
998          uid_effective: 1001,
999          uid_saved_set: 1002,
1000          uid_fs: 1003,
1001          gid_real: 2000,
1002          gid_effective: 2001,
1003          gid_saved_set: 2002,
1004          gid_fs: 2003,
1005        }
1006      }
1007    );
1008  }
1009
1010  #[test]
1011  fn test_parse_status_contents_missing_gid() {
1012    let sample = "Uid:\t1\t2\t3\t4\nGroups:\t0\n";
1013    let e = parse_status_contents(sample).unwrap_err();
1014    assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
1015  }
1016
1017  #[test]
1018  fn test_parse_status_contents_missing_groups() {
1019    let sample = "Uid:\t1\t2\t3\t4\nGid:\t0\t1\t2\t3\n";
1020    let e = parse_status_contents(sample).unwrap_err();
1021    assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
1022  }
1023
1024  #[test]
1025  fn test_parse_status_contents_non_numeric_uid() {
1026    let sample = "\
1027Uid:\ta\t2\t3\t4
1028Gid:\t1\t2\t3\t4
1029Groups:\t0
1030";
1031    let err = parse_status_contents(sample).unwrap_err();
1032    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1033  }
1034
1035  #[test]
1036  fn test_parse_status_contents_not_enough_uids() {
1037    let sample = "\
1038Uid:\t1\t2
1039Gid:\t1\t2\t3\t4
1040Groups:\t0
1041";
1042    let err = parse_status_contents(sample).unwrap_err();
1043    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1044  }
1045}
1046
1047#[cfg(test)]
1048mod env_tests {
1049
1050  use super::*;
1051  use crate::event::FriendlyError;
1052
1053  #[test]
1054  fn test_parse_env_entry_normal() {
1055    let (k, v) = parse_env_entry("KEY=value");
1056    assert_eq!(k, "KEY");
1057    assert_eq!(v, "value");
1058  }
1059
1060  #[test]
1061  fn test_parse_env_entry_missing_equal() {
1062    let (k, v) = parse_env_entry("KEY");
1063    assert_eq!(k, "KEY");
1064    assert_eq!(v, "");
1065  }
1066
1067  #[test]
1068  fn test_parse_env_entry_leading_equal() {
1069    let (k, v) = parse_env_entry("=value");
1070    assert_eq!(k, "=value");
1071    assert_eq!(v, "");
1072  }
1073
1074  #[test]
1075  fn test_parse_env_entry_multiple_equals() {
1076    let (k, v) = parse_env_entry("A=B=C");
1077    assert_eq!(k, "A");
1078    assert_eq!(v, "B=C");
1079  }
1080
1081  #[test]
1082  fn test_parse_failiable_envp_basic() {
1083    let envp = vec![OutputMsg::Ok("A=1".into()), OutputMsg::Ok("B=2".into())];
1084
1085    let (map, has_dash) = parse_failiable_envp(envp);
1086
1087    assert!(!has_dash);
1088    assert_eq!(map.len(), 2);
1089    assert_eq!(
1090      map.get(&OutputMsg::Ok("A".into())).unwrap(),
1091      &OutputMsg::Ok("1".into())
1092    );
1093  }
1094
1095  #[test]
1096  fn test_parse_failiable_envp_dash_key() {
1097    let envp = vec![OutputMsg::Ok("-X=1".into())];
1098
1099    let (_map, has_dash) = parse_failiable_envp(envp);
1100
1101    assert!(has_dash);
1102  }
1103
1104  #[test]
1105  fn test_parse_failiable_envp_error_passthrough() {
1106    let envp = vec![OutputMsg::Err(FriendlyError::InspectError(
1107      nix::errno::Errno::EAGAIN,
1108    ))];
1109
1110    let (map, _) = parse_failiable_envp(envp);
1111
1112    assert!(matches!(
1113      map.values().next().unwrap(),
1114      OutputMsg::Err(FriendlyError::InspectError(nix::errno::Errno::EAGAIN))
1115    ));
1116  }
1117}
1118
1119#[cfg(test)]
1120mod env_diff_tests {
1121  use std::collections::BTreeMap;
1122
1123  use crate::{
1124    event::OutputMsg,
1125    proc::diff_env,
1126  };
1127
1128  #[test]
1129  fn test_env_diff_added_removed_modified() {
1130    let orig = BTreeMap::from([
1131      (OutputMsg::Ok("A".into()), OutputMsg::Ok("1".into())),
1132      (OutputMsg::Ok("B".into()), OutputMsg::Ok("2".into())),
1133    ]);
1134
1135    let new = BTreeMap::from([
1136      (OutputMsg::Ok("A".into()), OutputMsg::Ok("10".into())),
1137      (OutputMsg::Ok("C".into()), OutputMsg::Ok("3".into())),
1138    ]);
1139
1140    let diff = diff_env(&orig, &new);
1141
1142    assert_eq!(diff.modified.len(), 1);
1143    assert_eq!(diff.added.len(), 1);
1144    assert_eq!(diff.removed.len(), 1);
1145
1146    assert!(diff.modified.contains_key(&OutputMsg::Ok("A".into())));
1147    assert!(diff.added.contains_key(&OutputMsg::Ok("C".into())));
1148    assert!(diff.removed.contains(&OutputMsg::Ok("B".into())));
1149  }
1150
1151  #[test]
1152  fn test_env_diff_dash_key_requires_separator() {
1153    let orig = BTreeMap::new();
1154    let new = BTreeMap::from([(
1155      OutputMsg::Ok("-LD_PRELOAD".into()),
1156      OutputMsg::Ok("evil.so".into()),
1157    )]);
1158
1159    let diff = diff_env(&orig, &new);
1160
1161    assert!(diff.need_env_argument_separator(&OutputMsg::Ok("program".into())));
1162  }
1163
1164  #[test]
1165  fn test_env_diff_without_added_or_modified_vars_requires_separator() {
1166    let unchanged = BTreeMap::from([(
1167      OutputMsg::Ok("UNCHANGED".into()),
1168      OutputMsg::Ok("value".into()),
1169    )]);
1170
1171    let diff = diff_env(&unchanged, &unchanged);
1172
1173    assert!(diff.need_env_argument_separator(&OutputMsg::Ok("--ignore-signal".into())));
1174    assert!(!diff.need_env_argument_separator(&OutputMsg::Ok("program".into())));
1175  }
1176}
1177
1178#[cfg(test)]
1179mod fdinfo_tests {
1180
1181  use crate::proc::FileDescriptorInfo;
1182
1183  #[test]
1184  fn test_fdinfo_same_file() {
1185    let a = FileDescriptorInfo {
1186      ino: 1.into(),
1187      mnt_id: 2.into(),
1188      ..Default::default()
1189    };
1190
1191    let b = FileDescriptorInfo {
1192      ino: 1.into(),
1193      mnt_id: 2.into(),
1194      ..Default::default()
1195    };
1196
1197    assert!(a.same_file_as(&b));
1198    assert!(!a.not_same_file_as(&b));
1199  }
1200
1201  #[test]
1202  fn test_fdinfo_not_same_file() {
1203    let a = FileDescriptorInfo {
1204      ino: 1.into(),
1205      mnt_id: 2.into(),
1206      ..Default::default()
1207    };
1208
1209    let b = FileDescriptorInfo {
1210      ino: 3.into(),
1211      mnt_id: 2.into(),
1212      ..Default::default()
1213    };
1214
1215    assert!(a.not_same_file_as(&b));
1216  }
1217}
1218
1219#[cfg(test)]
1220mod interpreter_test {
1221  use std::{
1222    fs::{
1223      self,
1224      File,
1225    },
1226    io::Write,
1227    os::unix::fs::PermissionsExt,
1228  };
1229
1230  use tempfile::tempdir;
1231  use test_that::prelude::*;
1232
1233  use crate::proc::{
1234    Interpreter,
1235    cached_str,
1236    read_interpreter,
1237    read_interpreter_recursive,
1238  };
1239
1240  #[test]
1241  fn test_interpreter_display() {
1242    let none = Interpreter::None;
1243    assert_that!(none.to_string(), contains_substring("none"));
1244
1245    let err = Interpreter::Error(cached_str("boom"));
1246    assert_that!(err.to_string(), contains_substring("err"));
1247  }
1248
1249  #[test]
1250  fn test_read_interpreter_none() {
1251    let dir = tempdir().unwrap();
1252    let exe = dir.path().join("binary");
1253    File::create(&exe).unwrap();
1254
1255    let result = read_interpreter(&exe);
1256    assert_eq!(result, Interpreter::None);
1257    dir.close().unwrap();
1258  }
1259
1260  #[test]
1261  fn test_read_interpreter_shebang() {
1262    let dir = tempdir().unwrap();
1263
1264    let target = dir.path().join("target");
1265    File::create(&target).unwrap();
1266    fs::set_permissions(&target, fs::Permissions::from_mode(0o755)).unwrap();
1267
1268    let script = dir.path().join("script");
1269    let mut f = File::create(&script).unwrap();
1270    writeln!(f, "#!{}", target.display()).unwrap();
1271
1272    let result = read_interpreter(&script);
1273    match result {
1274      Interpreter::Shebang(s) => assert_that!(s.as_ref(), ends_with("target")),
1275      other => panic!("unexpected result: {other:?}"),
1276    }
1277    dir.close().unwrap();
1278  }
1279
1280  #[test]
1281  fn test_read_interpreter_inaccessible() {
1282    let dir = tempdir().unwrap();
1283    let exe = dir.path().join("noaccess");
1284    File::create(&exe).unwrap();
1285    fs::set_permissions(&exe, fs::Permissions::from_mode(0o000)).unwrap();
1286
1287    let result = read_interpreter(&exe);
1288    assert_eq!(result, Interpreter::ExecutableInaccessible);
1289    dir.close().unwrap();
1290  }
1291
1292  #[test]
1293  fn test_read_interpreter_empty_file() {
1294    let dir = tempdir().unwrap();
1295    let exe = dir.path().join("empty");
1296    File::create(&exe).unwrap();
1297
1298    let result = read_interpreter(&exe);
1299    assert_eq!(result, Interpreter::None);
1300    dir.close().unwrap();
1301  }
1302
1303  #[test]
1304  fn test_read_interpreter_recursive_shebang_chain() {
1305    use std::{
1306      fs::{
1307        self,
1308        File,
1309      },
1310      io::Write,
1311    };
1312
1313    use tempfile::tempdir;
1314
1315    use super::read_interpreter_recursive;
1316
1317    let dir = tempdir().unwrap();
1318
1319    // interpreter2: real binary (no shebang)
1320    // Note: an edge case that the file length does not permit it to contain shebang thus EOF.
1321    let interp2 = dir.path().join("interp2");
1322    File::create(&interp2).unwrap();
1323    fs::set_permissions(&interp2, fs::Permissions::from_mode(0o755)).unwrap();
1324
1325    // interpreter1: shebang -> interpreter2
1326    let interp1 = dir.path().join("interp1");
1327    {
1328      let mut f = File::create(&interp1).unwrap();
1329      writeln!(f, "#!{}", interp2.display()).unwrap();
1330      f.flush().unwrap();
1331    }
1332    fs::set_permissions(&interp1, fs::Permissions::from_mode(0o755)).unwrap();
1333
1334    // script: shebang -> interpreter1
1335    let script = dir.path().join("script");
1336    {
1337      let mut f = File::create(&script).unwrap();
1338      writeln!(f, "#!{}", interp1.display()).unwrap();
1339      f.flush().unwrap();
1340    }
1341    fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
1342
1343    let result = read_interpreter_recursive(&script);
1344
1345    assert_eq!(result.len(), 2);
1346
1347    match &result[0] {
1348      Interpreter::Shebang(s) => {
1349        assert_that!(s.as_ref(), ends_with("interp1"));
1350      }
1351      other => panic!("unexpected interpreter: {other:?}"),
1352    }
1353
1354    match &result[1] {
1355      Interpreter::Shebang(s) => {
1356        assert_that!(s.as_ref(), ends_with("interp2"));
1357      }
1358      other => panic!("unexpected interpreter: {other:?}"),
1359    }
1360
1361    dir.close().unwrap();
1362  }
1363
1364  #[test]
1365  fn test_read_interpreter_recursive_no_shebang() {
1366    use std::fs::File;
1367
1368    use tempfile::tempdir;
1369
1370    let dir = tempdir().unwrap();
1371    let exe = dir.path().join("binary");
1372    File::create(&exe).unwrap();
1373
1374    let result = read_interpreter_recursive(&exe);
1375    assert_that!(result, empty());
1376    dir.close().unwrap();
1377  }
1378}
1379
1380#[cfg(test)]
1381mod cgroup_tests {
1382  use super::{
1383    CgroupInfo,
1384    parse_proc_cgroup,
1385    resolve_cgroup_id_in_dir,
1386  };
1387
1388  #[test]
1389  fn parse_cgroupv2_only() {
1390    let contents = "0::/user.slice/user-1000.slice/session-1.scope\n";
1391    let result = parse_proc_cgroup(contents);
1392    assert_eq!(
1393      result,
1394      CgroupInfo::V2 {
1395        path: "/user.slice/user-1000.slice/session-1.scope".to_string()
1396      }
1397    );
1398  }
1399
1400  #[test]
1401  fn parse_cgroupv2_root() {
1402    let contents = "0::/\n";
1403    let result = parse_proc_cgroup(contents);
1404    assert_eq!(
1405      result,
1406      CgroupInfo::V2 {
1407        path: "/".to_string()
1408      }
1409    );
1410  }
1411
1412  #[test]
1413  fn parse_cgroupv1_only() {
1414    let contents = "5:cpuacct,cpu,cpuset:/daemons\n3:memory:/system.slice\n";
1415    let result = parse_proc_cgroup(contents);
1416    assert_eq!(result, CgroupInfo::V1Only);
1417  }
1418
1419  #[test]
1420  fn parse_mixed_v1_and_v2() {
1421    // cgroupv2 line should be found even with v1 lines present
1422    let contents = "12:pids:/user.slice\n5:cpuacct,cpu:/daemons\n0::/user.slice/user-1000.slice\n";
1423    let result = parse_proc_cgroup(contents);
1424    assert_eq!(
1425      result,
1426      CgroupInfo::V2 {
1427        path: "/user.slice/user-1000.slice".to_string()
1428      }
1429    );
1430  }
1431
1432  #[test]
1433  fn parse_empty_contents() {
1434    let result = parse_proc_cgroup("");
1435    assert_eq!(result, CgroupInfo::V1Only);
1436  }
1437
1438  #[test]
1439  fn parse_malformed_line() {
1440    // Missing fields should just be skipped
1441    let contents = "badline\n0::/good\n";
1442    let result = parse_proc_cgroup(contents);
1443    assert_eq!(
1444      result,
1445      CgroupInfo::V2 {
1446        path: "/good".to_string()
1447      }
1448    );
1449  }
1450
1451  #[test]
1452  fn parse_only_blank_lines() {
1453    let contents = "\n\n\n";
1454    let result = parse_proc_cgroup(contents);
1455    assert_eq!(result, CgroupInfo::V1Only);
1456  }
1457
1458  #[test]
1459  fn resolve_cgroup_id_finds_directory() {
1460    use std::os::unix::fs::MetadataExt;
1461    let dir = tempfile::tempdir().unwrap();
1462    let sub = dir.path().join("child");
1463    std::fs::create_dir(&sub).unwrap();
1464    let ino = std::fs::metadata(&sub).unwrap().ino();
1465
1466    let result = resolve_cgroup_id_in_dir(ino, dir.path(), dir.path());
1467    assert_eq!(
1468      result,
1469      CgroupInfo::V2 {
1470        path: "/child".to_string()
1471      }
1472    );
1473  }
1474
1475  #[test]
1476  fn resolve_cgroup_id_nested() {
1477    use std::os::unix::fs::MetadataExt;
1478    let dir = tempfile::tempdir().unwrap();
1479    let parent = dir.path().join("a");
1480    std::fs::create_dir(&parent).unwrap();
1481    let child = parent.join("b");
1482    std::fs::create_dir(&child).unwrap();
1483    let ino = std::fs::metadata(&child).unwrap().ino();
1484
1485    let result = resolve_cgroup_id_in_dir(ino, dir.path(), dir.path());
1486    assert_eq!(
1487      result,
1488      CgroupInfo::V2 {
1489        path: "/a/b".to_string()
1490      }
1491    );
1492  }
1493
1494  #[test]
1495  fn resolve_cgroup_id_not_found() {
1496    let dir = tempfile::tempdir().unwrap();
1497    let result = resolve_cgroup_id_in_dir(999999999, dir.path(), dir.path());
1498    assert!(matches!(result, CgroupInfo::Error(_)));
1499  }
1500}