Skip to main content

sys_rs/
syscall.rs

1use core::ffi::c_void;
2use nix::{errno::Errno, sys::ptrace, unistd::Pid};
3use serde_derive::Deserialize;
4use std::{collections::HashMap, fmt};
5
6use crate::{diag::Result, hwaccess::Registers};
7
8#[derive(Debug, Deserialize, PartialEq)]
9/// Representation of a syscall argument or return value type.
10///
11/// This enum describes how a syscall argument or return value should be
12/// interpreted when pretty-printing traced syscalls (signed integer, pointer,
13/// string, or unsigned integer).
14pub enum Type {
15    /// Signed integer value (printed as i64).
16    Int,
17    /// Pointer value (printed as `NULL` or hex address).
18    Ptr,
19    /// NUL-terminated string located in the traced process memory.
20    Str,
21    /// Unsigned integer value.
22    Uint,
23}
24
25#[derive(Deserialize)]
26/// A single syscall argument description.
27///
28/// `Arg` holds the argument name and its `Type` (how it should be formatted).
29/// Instances are deserialized from the `syscall.json` metadata used to
30/// pretty-print syscall invocations.
31pub struct Arg {
32    name: String,
33    arg_type: Type,
34}
35
36impl Arg {
37    #[must_use]
38    /// Return the argument name as defined in the syscall metadata.
39    ///
40    /// # Returns
41    ///
42    /// A reference to the argument name string.
43    pub fn name(&self) -> &String {
44        &self.name
45    }
46
47    #[must_use]
48    /// Return the argument's `Type` which determines how the value is formatted.
49    ///
50    /// # Returns
51    ///
52    /// A reference to the `Type` for this argument.
53    pub fn arg_type(&self) -> &Type {
54        &self.arg_type
55    }
56}
57
58#[derive(Deserialize)]
59/// Metadata describing a single syscall entry.
60///
61/// Contains the syscall name, return type and an optional vector of
62/// `Arg` descriptions for the syscall's parameters. Entries are populated
63/// from `syscall.json` and used by `Repr::build` to format syscall output.
64pub struct Entry {
65    name: String,
66    ret_type: Type,
67    args: Option<Vec<Arg>>,
68}
69
70impl Entry {
71    #[must_use]
72    /// Return the syscall name (for example "write").
73    ///
74    /// # Returns
75    ///
76    /// A reference to the syscall name string.
77    pub fn name(&self) -> &String {
78        &self.name
79    }
80
81    #[must_use]
82    /// Return the return `Type` for this syscall.
83    ///
84    /// # Returns
85    ///
86    /// A reference to the return `Type` describing how to format the return value.
87    pub fn ret_type(&self) -> &Type {
88        &self.ret_type
89    }
90
91    #[must_use]
92    /// Return the optional argument descriptions for this syscall.
93    ///
94    /// # Returns
95    ///
96    /// `Some(Vec<Arg>)` when the syscall has parameters, otherwise `None`.
97    pub fn args(&self) -> &Option<Vec<Arg>> {
98        &self.args
99    }
100}
101
102impl Default for Entry {
103    fn default() -> Self {
104        Self {
105            name: "unknown".to_string(),
106            ret_type: Type::Int,
107            args: None,
108        }
109    }
110}
111
112/// Lookup table for syscall entries indexed by syscall number.
113///
114/// `Entries` wraps an internal `HashMap<u64, Entry>` populated from the
115/// embedded `syscall.json` metadata. Use `Entries::new()` to construct an
116/// instance and `Entries::get(id)` to retrieve the corresponding `Entry`.
117pub struct Entries {
118    map: HashMap<u64, Entry>,
119    default: Entry,
120}
121
122impl Entries {
123    /// Creates a new instance of `Entries`.
124    ///
125    /// This function reads the contents of the `syscall.json` file and parses it into a `HashMap<u64, Entry>`.
126    /// If the parsing fails, an `Err` is returned.
127    ///
128    /// # Errors
129    ///
130    /// This function will return an `Err` if it fails to parse the `syscall.json` file.
131    ///
132    /// # Returns
133    ///
134    /// Returns a `Result` containing the newly created `Entries` instance if successful, or an `Err` if parsing fails.
135    pub fn new() -> Result<Self> {
136        let json = include_str!("data/syscall.json");
137        let map = serde_json::from_str(json)?;
138        Ok(Self {
139            map,
140            default: Entry::default(),
141        })
142    }
143
144    #[must_use]
145    /// Lookup the `Entry` for the provided syscall number.
146    ///
147    /// # Arguments
148    ///
149    /// * `id` - Syscall number to look up.
150    ///
151    /// # Returns
152    ///
153    /// A reference to the matching `Entry` or the default `Entry` when no
154    /// entry exists for `id`.
155    pub fn get(&self, id: u64) -> &Entry {
156        self.map.get(&id).unwrap_or(&self.default)
157    }
158}
159
160/// Formatted representation of a syscall invocation for printing.
161///
162/// `Repr` holds the syscall name, a comma-separated argument string and the
163/// formatted return value. Construct one with `Repr::build` using the
164/// traced process registers and the `Entries` metadata.
165pub struct Repr<'a> {
166    name: &'a str,
167    args: String,
168    ret_val: String,
169}
170
171fn trace_str(addr: u64, pid: Pid) -> Result<String> {
172    let mut ret = String::new();
173    let mut offset = 0;
174    loop {
175        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
176        let c = char::from(ptrace::read(pid, (addr + offset) as *mut c_void)? as u8);
177        if c == '\0' {
178            break;
179        }
180        if c == '\n' {
181            ret.push('\\');
182            ret.push('n');
183            offset += 1;
184            continue;
185        }
186        ret.push(c);
187        offset += 1;
188    }
189
190    Ok(format!("\"{ret}\""))
191}
192
193fn parse_value(type_repr: &Type, val: u64, pid: Pid) -> Result<String> {
194    match type_repr {
195        #[allow(clippy::cast_possible_wrap)]
196        Type::Int => Ok(format!("{}", val as i64)),
197        Type::Ptr => {
198            let ptr_str = if val == 0x0 {
199                "NULL".to_string()
200            } else {
201                format!("{val:#x}")
202            };
203            Ok(ptr_str)
204        }
205        Type::Str => Ok(if val == 0x0 {
206            "?".to_string()
207        } else {
208            trace_str(val, pid)?
209        }),
210        Type::Uint => Ok(format!("{val}")),
211    }
212}
213
214impl<'a> Repr<'a> {
215    /// Builds a `Repr` struct from the given process ID (`pid`) and a reference to `Entries`.
216    ///
217    /// # Arguments
218    ///
219    /// * `pid` - The process ID for which to build the `Repr` struct.
220    /// * `infos` - A reference to `Entries` containing the syscall information.
221    ///
222    /// # Errors
223    ///
224    /// Returns an `Err` if `ptrace::getregs()` or `ptrace::read()` fails.
225    ///
226    /// # Returns
227    ///
228    /// Returns a `Result` containing the constructed `Repr` struct on success.
229    pub fn build(pid: Pid, infos: &'a Entries) -> Result<Self> {
230        let regs = Registers::read(pid)?;
231        let info = infos.get(regs.orig_rax());
232        let name = info.name();
233
234        let reg_vals = regs.function_params();
235        let args = if let Some(args) = info.args() {
236            args.iter()
237                .enumerate()
238                .map(|(i, arg)| {
239                    parse_value(arg.arg_type(), reg_vals[i], pid)
240                        .map(|val| format!("{}={}", arg.name(), val))
241                })
242                .collect::<Result<Vec<_>>>()
243                .map(|v| v.join(", "))
244        } else {
245            Ok(String::new())
246        }?;
247
248        #[allow(clippy::cast_possible_wrap)]
249        let ret_val = if regs.rax() as i64 == -(Errno::ENOSYS as i64) {
250            "?".to_string()
251        } else {
252            parse_value(info.ret_type(), regs.rax(), pid)?
253        };
254
255        Ok(Self {
256            name,
257            args,
258            ret_val,
259        })
260    }
261
262    #[must_use]
263    /// Return true when this `Repr` represents an exit-style syscall.
264    ///
265    /// # Returns
266    ///
267    /// `true` when the syscall name contains the substring "exit".
268    pub fn is_exit(&self) -> bool {
269        self.name.contains("exit")
270    }
271}
272
273impl fmt::Display for Repr<'_> {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(f, "{}({}) = {}", self.name, self.args, self.ret_val)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_entry_default() {
285        let entry = Entry::default();
286        assert_eq!(entry.name(), "unknown");
287        assert_eq!(entry.ret_type(), &Type::Int);
288        assert!(entry.args().is_none());
289    }
290
291    #[test]
292    fn test_entries_new() {
293        let entries = Entries::new();
294        assert!(entries.is_ok());
295    }
296
297    #[test]
298    fn test_entries_get() {
299        let entries = Entries::new().expect("Failed to create Entries");
300        let entry = entries.get(1);
301        assert_eq!(entry.name(), "write");
302    }
303
304    #[test]
305    fn test_arg_methods() {
306        let arg = Arg {
307            name: "arg1".to_string(),
308            arg_type: Type::Int,
309        };
310        assert_eq!(arg.name(), "arg1");
311        assert_eq!(arg.arg_type(), &Type::Int);
312    }
313
314    #[test]
315    fn test_entry_methods() {
316        let entry = Entry {
317            name: "test".to_string(),
318            ret_type: Type::Uint,
319            args: Some(vec![Arg {
320                name: "arg1".to_string(),
321                arg_type: Type::Str,
322            }]),
323        };
324        assert_eq!(entry.name(), "test");
325        assert_eq!(entry.ret_type(), &Type::Uint);
326        assert!(entry.args().is_some());
327    }
328
329    #[test]
330    fn test_parse_value() {
331        let pid = Pid::from_raw(1);
332        assert_eq!(
333            parse_value(&Type::Int, 42, pid).expect("Failed to parse Int value"),
334            "42"
335        );
336        assert_eq!(
337            parse_value(&Type::Uint, 42, pid).expect("Failed to parse Uint value"),
338            "42"
339        );
340        assert_eq!(
341            parse_value(&Type::Ptr, 0, pid).expect("Failed to parse Ptr value"),
342            "NULL"
343        );
344        assert_eq!(
345            parse_value(&Type::Ptr, 42, pid).expect("Failed to parse Ptr value"),
346            "0x2a"
347        );
348        assert_eq!(
349            parse_value(&Type::Str, 0, pid).expect("Failed to parse Str value"),
350            "?"
351        );
352    }
353
354    #[test]
355    fn test_repr_is_exit() {
356        let repr = Repr {
357            name: "exit",
358            args: String::new(),
359            ret_val: String::new(),
360        };
361        assert!(repr.is_exit());
362
363        let repr = Repr {
364            name: "open",
365            args: String::new(),
366            ret_val: String::new(),
367        };
368        assert!(!repr.is_exit());
369    }
370
371    #[test]
372    fn test_repr_display() {
373        let repr = Repr {
374            name: "open",
375            args: "arg1=42".to_string(),
376            ret_val: "0".to_string(),
377        };
378        assert_eq!(format!("{}", repr), "open(arg1=42) = 0");
379    }
380
381    #[test]
382    fn test_entries_get_unknown() {
383        let entries = Entries::new().expect("Failed to create Entries");
384        let entry = entries.get(999_999);
385        assert_eq!(entry.name(), "unknown");
386    }
387}