1use 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(); 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
231pub enum CgroupInfo {
232 V2 { path: String },
234 V1Only,
236 NotCollected,
238 Error(CgroupError),
240}
241
242pub 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
254pub 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 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 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
287pub 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 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 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
609pub 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 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 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 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 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 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 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 pub fn need_env_argument_separator(&self, filename: &OutputMsg) -> bool {
864 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 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 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 let interp2 = dir.path().join("interp2");
1322 File::create(&interp2).unwrap();
1323 fs::set_permissions(&interp2, fs::Permissions::from_mode(0o755)).unwrap();
1324
1325 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 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 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 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}