Skip to main content

lurk_cli/
syscall_info.rs

1use crate::arch::parse_args;
2use crate::style::StyleConfig;
3use libc::{c_ulonglong, user_regs_struct};
4use libc::{
5    MAP_ANONYMOUS, MAP_FIXED, MAP_LOCKED, MAP_NONBLOCK, MAP_NORESERVE, MAP_POPULATE, MAP_PRIVATE,
6    MAP_SHARED, MAP_STACK, PROT_EXEC, PROT_NONE, PROT_READ, PROT_WRITE,
7};
8use nix::unistd::Pid;
9use serde::ser::{SerializeMap, SerializeSeq};
10use serde::Serialize;
11use serde_json::Value;
12use std::borrow::Cow::{self, Borrowed, Owned};
13use std::fmt::{Debug, Display};
14use std::io;
15use std::io::Write;
16use std::time::Duration;
17use syscalls::Sysno;
18
19#[derive(Debug)]
20pub struct SyscallInfo {
21    pub typ: &'static str,
22    pub pid: Pid,
23    pub syscall: Sysno,
24    pub args: SyscallArgs,
25    pub result: RetCode,
26    pub duration: Duration,
27}
28
29impl SyscallInfo {
30    pub fn new(
31        pid: Pid,
32        syscall: Sysno,
33        ret_code: RetCode,
34        registers: user_regs_struct,
35        duration: Duration,
36    ) -> Self {
37        Self {
38            typ: "SYSCALL",
39            pid,
40            syscall,
41            args: parse_args(pid, syscall, registers),
42            result: ret_code,
43            duration,
44        }
45    }
46
47    pub fn write_syscall(
48        &self,
49        style: StyleConfig,
50        string_limit: Option<usize>,
51        show_syscall_num: bool,
52        show_duration: bool,
53        output: &mut dyn Write,
54    ) -> anyhow::Result<()> {
55        if style.use_colors {
56            write!(output, "[{}] ", style.pid.apply_to(&self.pid.to_string()))?;
57        } else {
58            write!(output, "[{}] ", &self.pid)?;
59        }
60        if show_syscall_num {
61            write!(output, "{:>3} ", self.syscall.id())?;
62        }
63        if style.use_colors {
64            let styled = style.syscall.apply_to(self.syscall.to_string());
65            write!(output, "{styled}(")
66        } else {
67            write!(output, "{}(", &self.syscall)
68        }?;
69        for (idx, arg) in self.args.0.iter().enumerate() {
70            if idx > 0 {
71                write!(output, ", ")?;
72            }
73            // Special-case a few syscalls for more readable output
74            if self.syscall == Sysno::execve || self.syscall == Sysno::execveat {
75                // execve(filename, argv, envp)
76                match (idx, arg) {
77                    // argv: show array (possibly truncated by `string_limit` via write)
78                    (1, SyscallArg::StrVec(_, _)) => arg.write(output, string_limit)?,
79                    // envp: summarize like strace: print original pointer and count
80                    (2, SyscallArg::StrVec(vs, maybe_addr)) => {
81                        if let Some(addr) = maybe_addr {
82                            let count = if !vs.is_empty() {
83                                vs.len()
84                            } else {
85                                // try a best-effort pointer-only count if the strings couldn't be read
86                                crate::arch::read_string_array_count(self.pid, *addr as c_ulonglong)
87                            };
88                            write!(output, "{:#x} /* {} vars */", *addr, count)?
89                        } else {
90                            // fall back to printing the array
91                            arg.write(output, string_limit)?;
92                        }
93                    }
94                    // default
95                    _ => arg.write(output, string_limit)?,
96                }
97            } else if self.syscall == Sysno::mmap {
98                // mmap(addr, len, prot, flags, fd, offset)
99                // produce symbolic prot and flags
100                let parts = format_mmap_args(&self.args.0, string_limit);
101                write!(
102                    output,
103                    "{}",
104                    parts.get(idx).map(|s| s.as_str()).unwrap_or("")
105                )?;
106            } else {
107                arg.write(output, string_limit)?;
108            }
109        }
110        write!(output, ") = ")?;
111        if self.syscall == Sysno::exit || self.syscall == Sysno::exit_group {
112            write!(output, "?")?;
113        } else {
114            if style.use_colors {
115                let style = style.from_ret_code(self.result);
116                // TODO: it would be great if we can force termcolor to write
117                //       the styling prefix and suffix into the formatter.
118                //       This would allow us to use the same code for both cases,
119                //       and avoid additional string alloc
120                write!(output, "{}", style.apply_to(self.result.to_string()))
121            } else {
122                write!(output, "{}", self.result)
123            }?;
124            if show_duration {
125                // TODO: add an option to control each syscall duration scaling, e.g. ms, us, ns
126                write!(output, " <{:.6}ns>", self.duration.as_nanos())?;
127            }
128        }
129        Ok(writeln!(output)?)
130    }
131}
132
133impl Serialize for SyscallInfo {
134    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
135        let mut map = serializer.serialize_map(Some(7))?;
136        map.serialize_entry("type", &self.typ)?;
137        map.serialize_entry("pid", &self.pid.as_raw())?;
138        map.serialize_entry("num", &self.syscall)?;
139        map.serialize_entry("syscall", &self.syscall.to_string())?;
140        map.serialize_entry("args", &self.args)?;
141        match self.result {
142            RetCode::Ok(value) => map.serialize_entry("success", &value)?,
143            RetCode::Err(value) => map.serialize_entry("error", &value)?,
144            RetCode::Address(value) => map.serialize_entry("result", &value)?,
145        }
146        map.serialize_entry("duration", &self.duration.as_secs_f64())?;
147        map.end()
148    }
149}
150
151#[derive(Debug)]
152pub struct SyscallArgs(pub Vec<SyscallArg>);
153
154impl Serialize for SyscallArgs {
155    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
156        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
157        for arg in &self.0 {
158            let value = match arg {
159                SyscallArg::Int(v) => serde_json::to_value(v).unwrap(),
160                SyscallArg::Str(v) => serde_json::to_value(v).unwrap(),
161                SyscallArg::Addr(v) => Value::String(format!("{v:#x}")),
162                SyscallArg::StrVec(vs, _addr) => serde_json::to_value(vs).unwrap(),
163            };
164            seq.serialize_element(&value)?;
165        }
166        seq.end()
167    }
168}
169
170#[derive(Debug, Copy, Clone)]
171pub enum RetCode {
172    Ok(i32),
173    Err(i32),
174    Address(usize),
175}
176
177impl RetCode {
178    pub fn from_raw(ret_code: c_ulonglong) -> Self {
179        let ret_i32 = ret_code as isize;
180        // TODO: is this > or >= ?  Add a link to the docs.
181        if ret_i32.abs() > 0x8000 {
182            Self::Address(ret_code as usize)
183        } else if ret_i32 < 0 {
184            Self::Err(ret_i32 as i32)
185        } else {
186            Self::Ok(ret_i32 as i32)
187        }
188    }
189}
190
191impl Display for RetCode {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Self::Ok(v) | Self::Err(v) => Display::fmt(v, f),
195            Self::Address(v) => write!(f, "{v:#X}"),
196        }
197    }
198}
199
200#[derive(Debug, Serialize)]
201pub enum SyscallArg {
202    Int(i64),
203    Str(String),
204    // store optional original pointer address for arrays so we can summarize like strace
205    StrVec(Vec<String>, Option<usize>),
206    Addr(usize),
207}
208
209impl SyscallArg {
210    pub fn write(&self, f: &mut dyn Write, string_limit: Option<usize>) -> io::Result<()> {
211        match self {
212            Self::Int(v) => write!(f, "{v}"),
213            Self::Str(v) => {
214                let value: Value = match string_limit {
215                    Some(width) => trim_str(v, width),
216                    None => Borrowed(v.as_ref()),
217                }
218                .into();
219                write!(f, "{value}")
220            }
221            Self::StrVec(vs, _addr) => {
222                // format vector as JSON-like array, applying trimming to each element
223                let mut parts = Vec::with_capacity(vs.len());
224                for s in vs {
225                    let trimmed = match string_limit {
226                        Some(width) => trim_str(s, width).into_owned(),
227                        None => s.clone(),
228                    };
229                    parts.push(serde_json::to_string(&trimmed).unwrap());
230                }
231                write!(f, "[{}]", parts.join(", "))
232            }
233            Self::Addr(v) => write!(f, "{v:#X}"),
234        }
235    }
236}
237
238fn trim_str(string: &str, limit: usize) -> Cow<'_, str> {
239    match string.chars().as_str().get(..limit) {
240        None => Borrowed(string),
241        Some(s) => Owned(format!("{s}...")),
242    }
243}
244
245fn format_prot(flags: i64) -> String {
246    let mut parts = Vec::new();
247    if flags & (PROT_READ as i64) != 0 {
248        parts.push("PROT_READ");
249    }
250    if flags & (PROT_WRITE as i64) != 0 {
251        parts.push("PROT_WRITE");
252    }
253    if flags & (PROT_EXEC as i64) != 0 {
254        parts.push("PROT_EXEC");
255    }
256    if flags == (PROT_NONE as i64) {
257        parts.push("PROT_NONE");
258    }
259    if parts.is_empty() {
260        format!("{flags}")
261    } else {
262        parts.join("|")
263    }
264}
265
266fn format_map_flags(flags: i64) -> String {
267    let mut parts = Vec::new();
268    if flags & (MAP_SHARED as i64) != 0 {
269        parts.push("MAP_SHARED");
270    }
271    if flags & (MAP_PRIVATE as i64) != 0 {
272        parts.push("MAP_PRIVATE");
273    }
274    if flags & (MAP_ANONYMOUS as i64) != 0 {
275        parts.push("MAP_ANONYMOUS");
276    }
277    if flags & (MAP_FIXED as i64) != 0 {
278        parts.push("MAP_FIXED");
279    }
280    if flags & (MAP_STACK as i64) != 0 {
281        parts.push("MAP_STACK");
282    }
283    if flags & (MAP_NORESERVE as i64) != 0 {
284        parts.push("MAP_NORESERVE");
285    }
286    if flags & (MAP_LOCKED as i64) != 0 {
287        parts.push("MAP_LOCKED");
288    }
289    if flags & (MAP_POPULATE as i64) != 0 {
290        parts.push("MAP_POPULATE");
291    }
292    if flags & (MAP_NONBLOCK as i64) != 0 {
293        parts.push("MAP_NONBLOCK");
294    }
295    if parts.is_empty() {
296        format!("{flags}")
297    } else {
298        parts.join("|")
299    }
300}
301
302fn format_mmap_args(args: &[SyscallArg], string_limit: Option<usize>) -> Vec<String> {
303    // Expect 6 args: addr, len, prot, flags, fd, offset
304    let mut out = vec![String::new(); args.len()];
305    for (i, a) in args.iter().enumerate() {
306        match (i, a) {
307            (0, SyscallArg::Addr(addr)) => {
308                if *addr == 0 {
309                    out[i] = "NULL".to_string();
310                } else {
311                    out[i] = format!("{:#X}", addr);
312                }
313            }
314            (1, SyscallArg::Int(len)) => out[i] = format!("{}", len),
315            (2, SyscallArg::Int(prot)) => out[i] = format_prot(*prot),
316            (3, SyscallArg::Int(flags)) => out[i] = format_map_flags(*flags),
317            (4, SyscallArg::Int(fd)) => out[i] = format!("{}", fd),
318            (5, SyscallArg::Int(off)) => out[i] = format!("{}", off),
319            // fall back to default write for other or unexpected types
320            (_, other) => {
321                let mut buf = Vec::new();
322                other.write(&mut buf, string_limit).ok();
323                out[i] = String::from_utf8_lossy(&buf).to_string();
324            }
325        }
326    }
327    out
328}