nucleus/security/
seccomp_trace.rs1use crate::error::{NucleusError, Result};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::io::{BufRead, BufReader, Write};
16use std::path::{Path, PathBuf};
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::Arc;
19use std::thread::JoinHandle;
20use tracing::{debug, info, warn};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TraceRecord {
25 pub syscall: i64,
27 pub name: Option<String>,
29 pub count: u64,
31}
32
33pub struct SeccompTraceReader {
35 pid: u32,
36 output_path: PathBuf,
37 stop: Arc<AtomicBool>,
38 handle: Option<JoinHandle<()>>,
39}
40
41impl SeccompTraceReader {
42 pub fn new(pid: u32, output_path: &Path) -> Self {
44 Self {
45 pid,
46 output_path: output_path.to_path_buf(),
47 stop: Arc::new(AtomicBool::new(false)),
48 handle: None,
49 }
50 }
51
52 pub fn start_recording(&mut self) -> Result<()> {
57 let pid = self.pid;
58 let output_path = self.output_path.clone();
59 let stop = self.stop.clone();
60
61 let handle = std::thread::spawn(move || {
62 if let Err(e) = record_loop(pid, &output_path, &stop) {
63 warn!("Seccomp trace reader error: {}", e);
64 }
65 });
66
67 self.handle = Some(handle);
68 info!("Seccomp trace reader started for PID {}", self.pid);
69 Ok(())
70 }
71
72 pub fn stop_and_flush(mut self) {
74 self.stop.store(true, Ordering::Release);
75 if let Some(handle) = self.handle.take() {
76 let _ = handle.join();
77 }
78 info!(
79 "Seccomp trace reader stopped, output at {:?}",
80 self.output_path
81 );
82 }
83}
84
85fn record_loop(pid: u32, output_path: &Path, stop: &AtomicBool) -> Result<()> {
87 let mut syscalls: BTreeMap<i64, u64> = BTreeMap::new();
88
89 let kmsg_path = std::path::Path::new("/dev/kmsg");
91 if let Ok(meta) = std::fs::symlink_metadata(kmsg_path) {
92 if meta.file_type().is_symlink() {
93 warn!("/dev/kmsg is a symlink — refusing to open for seccomp tracing");
94 write_trace_file(output_path, &syscalls)?;
95 return Ok(());
96 }
97 }
98
99 let file = match std::fs::File::open(kmsg_path) {
101 Ok(f) => f,
102 Err(e) => {
103 warn!(
104 "Cannot open /dev/kmsg for seccomp tracing: {} \
105 (requires root or CAP_SYSLOG). Falling back to no-trace mode.",
106 e
107 );
108 write_trace_file(output_path, &syscalls)?;
110 return Ok(());
111 }
112 };
113
114 use std::os::unix::io::AsRawFd;
119 let fd = file.as_raw_fd();
120 unsafe {
121 let flags = libc::fcntl(fd, libc::F_GETFL);
122 if flags >= 0 {
123 libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
124 }
125 }
126
127 let reader = BufReader::new(file);
128 let pid_pattern = format!("pid={}", pid);
129
130 for line in reader.lines() {
131 if stop.load(Ordering::Acquire) {
132 break;
133 }
134
135 let line = match line {
136 Ok(l) => l,
137 Err(e) => {
138 if e.kind() == std::io::ErrorKind::WouldBlock {
139 let mut pfd = libc::pollfd {
141 fd,
142 events: libc::POLLIN,
143 revents: 0,
144 };
145 unsafe { libc::poll(&mut pfd, 1, 2000) };
146 continue;
147 }
148 debug!("kmsg read error: {}", e);
149 continue;
150 }
151 };
152
153 if line.contains("type=1326") && line.contains(&pid_pattern) {
156 if let Some(nr) = extract_syscall_nr(&line) {
157 *syscalls.entry(nr).or_insert(0) += 1;
158 }
159 }
160 }
161
162 write_trace_file(output_path, &syscalls)?;
163 info!("Seccomp trace: recorded {} unique syscalls", syscalls.len());
164 Ok(())
165}
166
167fn extract_syscall_nr(line: &str) -> Option<i64> {
169 line.split_whitespace()
171 .find(|s| s.starts_with("syscall="))
172 .and_then(|s| s.strip_prefix("syscall="))
173 .and_then(|s| s.parse().ok())
174}
175
176fn write_trace_file(path: &Path, syscalls: &BTreeMap<i64, u64>) -> Result<()> {
178 let mut file = std::fs::File::create(path).map_err(|e| {
179 NucleusError::ConfigError(format!("Failed to create trace file {:?}: {}", path, e))
180 })?;
181
182 for (&nr, &count) in syscalls {
183 let record = TraceRecord {
184 syscall: nr,
185 name: super::seccomp_generate::syscall_number_to_name(nr).map(String::from),
186 count,
187 };
188 let line = serde_json::to_string(&record)
189 .unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e));
190 writeln!(file, "{}", line).map_err(|e| {
191 NucleusError::ConfigError(format!("Failed to write trace record: {}", e))
192 })?;
193 }
194
195 Ok(())
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_extract_syscall_nr() {
204 let line = "6,1234,5678,-;audit: type=1326 audit(123:456): auid=0 uid=0 gid=0 ses=1 pid=42 comm=\"test\" exe=\"/bin/test\" sig=0 arch=c000003e syscall=257 compat=0 ip=0x7f action=0x7fff0000";
205 assert_eq!(extract_syscall_nr(line), Some(257));
206 }
207
208 #[test]
209 fn test_extract_syscall_nr_missing() {
210 assert_eq!(extract_syscall_nr("no syscall here"), None);
211 }
212
213 fn extract_fn_body<'a>(source: &'a str, fn_signature: &str) -> &'a str {
216 let fn_start = source.find(fn_signature)
217 .unwrap_or_else(|| panic!("function '{}' not found in source", fn_signature));
218 let after = &source[fn_start..];
219 let open = after.find('{')
220 .unwrap_or_else(|| panic!("no opening brace found for '{}'", fn_signature));
221 let mut depth = 0u32;
222 let mut end = open;
223 for (i, ch) in after[open..].char_indices() {
224 match ch {
225 '{' => depth += 1,
226 '}' => {
227 depth -= 1;
228 if depth == 0 { end = open + i + 1; break; }
229 }
230 _ => {}
231 }
232 }
233 &after[..end]
234 }
235
236 #[test]
237 fn test_reader_uses_nonblocking_io() {
238 let source = include_str!("seccomp_trace.rs");
242 let fn_body = extract_fn_body(source, "fn record_loop");
243 assert!(
244 fn_body.contains("O_NONBLOCK"),
245 "record_loop must use O_NONBLOCK for non-blocking reads on /dev/kmsg"
246 );
247 assert!(
248 fn_body.contains("libc::poll"),
249 "record_loop must use poll() for timed waits on /dev/kmsg"
250 );
251 let setsockopt_lines: Vec<&str> = fn_body
253 .lines()
254 .filter(|l| !l.trim().starts_with("//"))
255 .filter(|l| l.contains("setsockopt"))
256 .collect();
257 assert!(
258 setsockopt_lines.is_empty(),
259 "record_loop must not call setsockopt on /dev/kmsg"
260 );
261 }
262
263 #[test]
264 fn test_trace_record_serialization() {
265 let record = TraceRecord {
266 syscall: 0,
267 name: Some("read".to_string()),
268 count: 42,
269 };
270 let json = serde_json::to_string(&record).unwrap();
271 assert!(json.contains("\"syscall\":0"));
272 assert!(json.contains("\"name\":\"read\""));
273 assert!(json.contains("\"count\":42"));
274 }
275}