use crate::consts::*;
use crate::error::{Error, Result};
#[inline]
fn read_u32(buf: &[u8], off: usize) -> u32 {
let arr: [u8; 4] = buf[off..off + 4].try_into().unwrap();
u32::from_ne_bytes(arr)
}
#[inline]
fn read_u16(buf: &[u8], off: usize) -> u16 {
let arr: [u8; 2] = buf[off..off + 2].try_into().unwrap();
u16::from_ne_bytes(arr)
}
#[inline]
fn read_i32(buf: &[u8], off: usize) -> i32 {
let arr: [u8; 4] = buf[off..off + 4].try_into().unwrap();
i32::from_ne_bytes(arr)
}
#[inline]
fn read_u64(buf: &[u8], off: usize) -> u64 {
let arr: [u8; 8] = buf[off..off + 8].try_into().unwrap();
u64::from_ne_bytes(arr)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProcEvent {
Exec {
pid: u32,
tgid: u32,
},
Fork {
parent_pid: u32,
parent_tgid: u32,
child_pid: u32,
child_tgid: u32,
},
Exit {
pid: u32,
tgid: u32,
exit_code: u32,
exit_signal: u32,
},
Uid {
pid: u32,
tgid: u32,
ruid: u32,
euid: u32,
},
Gid {
pid: u32,
tgid: u32,
rgid: u32,
egid: u32,
},
Sid {
pid: u32,
tgid: u32,
},
Ptrace {
pid: u32,
tgid: u32,
tracer_pid: u32,
tracer_tgid: u32,
},
Comm {
pid: u32,
tgid: u32,
comm: [u8; 16],
},
Coredump {
pid: u32,
tgid: u32,
},
Unknown {
what: u32,
raw_data: Vec<u8>,
},
}
pub fn parse_netlink_message(payload: &[u8], len: usize) -> Result<Option<ProcEvent>> {
let payload = &payload[..len];
if payload.len() < SIZE_NLMSGHDR {
return Err(Error::Truncated);
}
let nlmsg_type = read_u16(payload, 4);
let nlmsg_len = read_u32(payload, 0) as usize;
if nlmsg_len > payload.len() {
return Err(Error::Truncated);
}
match nlmsg_type {
NLMSG_NOOP | NLMSG_DONE => Ok(None),
NLMSG_ERROR => {
let errno = read_i32(payload, SIZE_NLMSGHDR);
if errno == 0 {
return Ok(None);
}
let pos_errno = errno.checked_neg().unwrap_or(errno);
Err(Error::Os(std::io::Error::from_raw_os_error(pos_errno)))
}
NLMSG_OVERRUN => Err(Error::Overrun),
_ => {
let cn_offset = nlmsg_hdrlen();
if nlmsg_len < cn_offset {
return Err(Error::Truncated);
}
let cn_payload = &payload[cn_offset..nlmsg_len];
parse_cn_msg(cn_payload).map(Some)
}
}
}
fn parse_cn_msg(buf: &[u8]) -> Result<ProcEvent> {
if buf.len() < SIZE_CN_MSG {
return Err(Error::Truncated);
}
let idx = read_u32(buf, 0);
let val = read_u32(buf, 4);
if idx != CN_IDX_PROC || val != CN_VAL_PROC {
return Err(Error::Truncated);
}
let data_len = read_u16(buf, 16) as usize;
let proc_off = SIZE_CN_MSG;
let proc_data = if buf.len() >= proc_off + data_len {
&buf[proc_off..proc_off + data_len]
} else {
return Err(Error::Truncated);
};
parse_proc_event(proc_data)
}
fn parse_proc_event(buf: &[u8]) -> Result<ProcEvent> {
if buf.len() < PROC_EVENT_HEADER_SIZE {
return Err(Error::Truncated);
}
let what = read_u32(buf, 0);
let _cpu = read_u32(buf, 4);
let _timestamp_ns = read_u64(buf, 8);
let data = &buf[PROC_EVENT_HEADER_SIZE..];
match what {
PROC_EVENT_EXEC => {
if data.len() < SIZE_EXEC_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Exec {
pid: read_i32(data, EXEC_PID) as u32,
tgid: read_i32(data, EXEC_TGID) as u32,
})
}
PROC_EVENT_FORK => {
if data.len() < SIZE_FORK_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Fork {
parent_pid: read_i32(data, FORK_PARENT_PID) as u32,
parent_tgid: read_i32(data, FORK_PARENT_TGID) as u32,
child_pid: read_i32(data, FORK_CHILD_PID) as u32,
child_tgid: read_i32(data, FORK_CHILD_TGID) as u32,
})
}
PROC_EVENT_EXIT => {
if data.len() < SIZE_EXIT_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Exit {
pid: read_i32(data, EXIT_PID) as u32,
tgid: read_i32(data, EXIT_TGID) as u32,
exit_code: read_u32(data, EXIT_CODE),
exit_signal: read_u32(data, EXIT_SIGNAL),
})
}
PROC_EVENT_UID => {
if data.len() < SIZE_ID_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Uid {
pid: read_i32(data, ID_PID) as u32,
tgid: read_i32(data, ID_TGID) as u32,
ruid: read_u32(data, ID_RUID_RGID),
euid: read_u32(data, ID_EUID_EGID),
})
}
PROC_EVENT_GID => {
if data.len() < SIZE_ID_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Gid {
pid: read_i32(data, ID_PID) as u32,
tgid: read_i32(data, ID_TGID) as u32,
rgid: read_u32(data, ID_RUID_RGID),
egid: read_u32(data, ID_EUID_EGID),
})
}
PROC_EVENT_SID => {
if data.len() < SIZE_SID_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Sid {
pid: read_i32(data, SID_PID) as u32,
tgid: read_i32(data, SID_TGID) as u32,
})
}
PROC_EVENT_PTRACE => {
if data.len() < SIZE_PTRACE_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Ptrace {
pid: read_i32(data, PTRACE_PID) as u32,
tgid: read_i32(data, PTRACE_TGID) as u32,
tracer_pid: read_i32(data, PTRACE_TRACER_PID) as u32,
tracer_tgid: read_i32(data, PTRACE_TRACER_TGID) as u32,
})
}
PROC_EVENT_COMM => {
if data.len() < SIZE_COMM_EVENT {
return Err(Error::Truncated);
}
let mut comm = [0u8; 16];
comm.copy_from_slice(&data[COMM_DATA..COMM_DATA + 16]);
Ok(ProcEvent::Comm {
pid: read_i32(data, COMM_PID) as u32,
tgid: read_i32(data, COMM_TGID) as u32,
comm,
})
}
PROC_EVENT_COREDUMP => {
if data.len() < SIZE_COREDUMP_EVENT {
return Err(Error::Truncated);
}
Ok(ProcEvent::Coredump {
pid: read_i32(data, COREDUMP_PID) as u32,
tgid: read_i32(data, COREDUMP_TGID) as u32,
})
}
_ => {
Ok(ProcEvent::Unknown {
what,
raw_data: data.to_vec(),
})
}
}
}
impl std::fmt::Display for ProcEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProcEvent::Exec { pid, tgid } => write!(f, "EXEC pid={pid} tgid={tgid}"),
ProcEvent::Fork {
parent_pid,
parent_tgid,
child_pid,
child_tgid,
} => write!(
f,
"FORK parent=({parent_pid},{parent_tgid}) child=({child_pid},{child_tgid})"
),
ProcEvent::Exit {
pid,
tgid,
exit_code,
exit_signal,
} => write!(
f,
"EXIT pid={pid} tgid={tgid} code={exit_code} signal={exit_signal}"
),
ProcEvent::Uid {
pid, tgid, ruid, euid,
} => write!(f, "UID pid={pid} tgid={tgid} ruid={ruid} euid={euid}"),
ProcEvent::Gid {
pid, tgid, rgid, egid,
} => write!(f, "GID pid={pid} tgid={tgid} rgid={rgid} egid={egid}"),
ProcEvent::Sid { pid, tgid } => write!(f, "SID pid={pid} tgid={tgid}"),
ProcEvent::Ptrace {
pid,
tgid,
tracer_pid,
tracer_tgid,
} => write!(
f,
"PTRACE pid={pid} tgid={tgid} tracer=({tracer_pid},{tracer_tgid})"
),
ProcEvent::Comm {
pid,
tgid,
comm,
} => {
let end = comm.iter().position(|&b| b == 0).unwrap_or(16);
let name = std::str::from_utf8(&comm[..end]).unwrap_or("<invalid>");
write!(f, "COMM pid={pid} tgid={tgid} name=\"{name}\"")
}
ProcEvent::Coredump { pid, tgid } => {
write!(f, "COREDUMP pid={pid} tgid={tgid}")
}
ProcEvent::Unknown { what, raw_data } => {
write!(f, "UNKNOWN what=0x{what:08x} len={}", raw_data.len())
}
}
}
}
pub struct NetlinkMessageIter<'a> {
buf: &'a [u8],
pos: usize,
len: usize,
}
impl<'a> NetlinkMessageIter<'a> {
pub fn new(buf: &'a [u8], len: usize) -> Self {
NetlinkMessageIter {
buf,
pos: 0,
len,
}
}
}
impl<'a> Iterator for NetlinkMessageIter<'a> {
type Item = Result<Option<ProcEvent>>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.len {
return None;
}
let remaining = self.len - self.pos;
if remaining < SIZE_NLMSGHDR {
return Some(Err(Error::Truncated));
}
let nlmsg_len = read_u32(&self.buf[self.pos..], 0) as usize;
if nlmsg_len < SIZE_NLMSGHDR || nlmsg_len > remaining {
return Some(Err(Error::Truncated));
}
let msg_slice = &self.buf[self.pos..self.pos + nlmsg_len];
let nlmsg_type = read_u16(msg_slice, 4);
if nlmsg_type == NLMSG_DONE {
self.pos = self.len; return None; }
let result = parse_netlink_message(msg_slice, nlmsg_len);
self.pos += nlmsg_align(nlmsg_len);
Some(result)
}
}
use crate::socket::ProcConnector;
impl ProcConnector {
pub fn recv(&self, buf: &mut [u8]) -> Result<ProcEvent> {
self.recv_impl(buf)
}
pub fn recv_timeout(&self, buf: &mut [u8], timeout: std::time::Duration) -> Result<Option<ProcEvent>> {
if buf.len() < SIZE_NLMSGHDR {
return Err(Error::BufferTooSmall { needed: SIZE_NLMSGHDR });
}
loop {
let n = match self.recv_raw_timeout(buf, timeout) {
Ok(Some(n)) => n,
Ok(None) => return Ok(None),
Err(Error::WouldBlock) => {
return Ok(None);
}
Err(e) => return Err(e),
};
let iter = NetlinkMessageIter::new(buf, n);
for msg in iter {
match msg? {
Some(event) => return Ok(Some(event)),
None => continue, }
}
}
}
fn recv_impl(&self, buf: &mut [u8]) -> Result<ProcEvent> {
if buf.len() < SIZE_NLMSGHDR {
return Err(Error::BufferTooSmall {
needed: SIZE_NLMSGHDR,
});
}
loop {
let n = match self.recv_raw(buf) {
Ok(n) => n,
Err(Error::WouldBlock) => {
return Err(Error::Os(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"socket is non-blocking, use AsyncFd to wait for readiness",
)));
}
Err(e) => return Err(e),
};
let iter = NetlinkMessageIter::new(buf, n);
for msg in iter {
match msg? {
Some(event) => return Ok(event),
None => continue, }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_proc_event_payload(what: u32, event_data: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(PROC_EVENT_HEADER_SIZE + event_data.len());
buf.extend_from_slice(&what.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&0u64.to_ne_bytes()); buf.extend_from_slice(event_data);
buf
}
fn make_cn_msg(data: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(SIZE_CN_MSG + data.len());
buf.extend_from_slice(&CN_IDX_PROC.to_ne_bytes()); buf.extend_from_slice(&CN_VAL_PROC.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&(data.len() as u16).to_ne_bytes()); buf.extend_from_slice(&0u16.to_ne_bytes()); buf.extend_from_slice(data);
buf
}
fn make_netlink_message(nlmsg_type: u16, payload: &[u8]) -> Vec<u8> {
let hdr_len = nlmsg_hdrlen();
let total_len = nlmsg_length(payload.len());
let mut buf = vec![0u8; total_len];
buf[0..4].copy_from_slice(&(total_len as u32).to_ne_bytes()); buf[4..6].copy_from_slice(&nlmsg_type.to_ne_bytes()); buf[6..8].copy_from_slice(&0u16.to_ne_bytes()); buf[8..12].copy_from_slice(&0u32.to_ne_bytes()); buf[12..16].copy_from_slice(&0u32.to_ne_bytes()); buf[hdr_len..hdr_len + payload.len()].copy_from_slice(payload);
buf
}
fn make_full_message(what: u32, event_data: &[u8]) -> Vec<u8> {
let proc_payload = make_proc_event_payload(what, event_data);
let cn_payload = make_cn_msg(&proc_payload);
make_netlink_message(NLMSG_MIN_TYPE, &cn_payload)
}
#[test]
fn parse_exec() {
let data = [
42i32.to_ne_bytes(), 100i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_EXEC, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Exec {
pid: 42,
tgid: 100,
}
);
}
#[test]
fn parse_fork() {
let data = [
10i32.to_ne_bytes(), 10i32.to_ne_bytes(), 20i32.to_ne_bytes(), 20i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_FORK, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Fork {
parent_pid: 10,
parent_tgid: 10,
child_pid: 20,
child_tgid: 20,
}
);
}
#[test]
fn parse_exit() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), 0u32.to_ne_bytes(), 17u32.to_ne_bytes(), 0i32.to_ne_bytes(), 0i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_EXIT, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Exit {
pid: 1,
tgid: 1,
exit_code: 0,
exit_signal: 17,
}
);
}
#[test]
fn parse_uid() {
let data = [
5i32.to_ne_bytes(), 5i32.to_ne_bytes(), 1000u32.to_ne_bytes(), 0u32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_UID, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Uid {
pid: 5,
tgid: 5,
ruid: 1000,
euid: 0,
}
);
}
#[test]
fn parse_gid() {
let data = [
5i32.to_ne_bytes(), 5i32.to_ne_bytes(), 100u32.to_ne_bytes(), 200u32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_GID, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Gid {
pid: 5,
tgid: 5,
rgid: 100,
egid: 200,
}
);
}
#[test]
fn parse_sid() {
let data = [
7i32.to_ne_bytes(), 7i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_SID, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Sid { pid: 7, tgid: 7 }
);
}
#[test]
fn parse_ptrace() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), 999i32.to_ne_bytes(), 999i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_PTRACE, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Ptrace {
pid: 1,
tgid: 1,
tracer_pid: 999,
tracer_tgid: 999,
}
);
}
#[test]
fn parse_comm() {
let data = [
42i32.to_ne_bytes(), 42i32.to_ne_bytes(), ]
.concat();
let mut comm_event = data;
let mut comm = [0u8; 16];
comm[..7].copy_from_slice(b"bash\0\0\0");
comm_event.extend_from_slice(&comm);
let buf = make_full_message(PROC_EVENT_COMM, &comm_event);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Comm {
pid: 42,
tgid: 42,
comm,
}
);
}
#[test]
fn parse_coredump() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), 0i32.to_ne_bytes(), 0i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_COREDUMP, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Coredump { pid: 1, tgid: 1 }
);
}
#[test]
fn parse_unknown_skipped() {
let data = [1u8, 2, 3, 4];
let buf = make_full_message(0xDEAD, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
match event {
ProcEvent::Unknown { what, raw_data } => {
assert_eq!(what, 0xDEAD);
assert_eq!(raw_data, data);
}
_ => panic!("expected Unknown event"),
}
}
#[test]
fn parse_nlmsg_noop() {
let buf = make_netlink_message(NLMSG_NOOP, &[]);
let result = parse_netlink_message(&buf, buf.len()).unwrap();
assert!(result.is_none());
}
#[test]
fn parse_nlmsg_done() {
let buf = make_netlink_message(NLMSG_DONE, &[]);
let result = parse_netlink_message(&buf, buf.len()).unwrap();
assert!(result.is_none());
}
#[test]
fn parse_nlmsg_error() {
let errno = -libc::EPERM;
let mut payload = vec![0u8; SIZE_NLMSGHDR + 20]; payload[0..4].copy_from_slice(&errno.to_ne_bytes());
let buf = make_netlink_message(NLMSG_ERROR, &payload);
let result = parse_netlink_message(&buf, buf.len());
assert!(result.is_err());
match result {
Err(Error::Os(e)) => assert_eq!(e.raw_os_error(), Some(libc::EPERM)),
_ => panic!("expected Os error"),
}
}
#[test]
fn parse_nlmsg_overrun() {
let buf = make_netlink_message(NLMSG_OVERRUN, &[]);
let result = parse_netlink_message(&buf, buf.len());
match result {
Err(Error::Overrun) => {} _ => panic!("expected Overrun error"),
}
}
#[test]
fn truncated_message() {
let buf = vec![0u8; 4]; let result = parse_netlink_message(&buf, buf.len());
match result {
Err(Error::Truncated) => {} _ => panic!("expected Truncated error"),
}
}
#[test]
fn display_exec() {
let event = ProcEvent::Exec {
pid: 42,
tgid: 100,
};
assert_eq!(format!("{event}"), "EXEC pid=42 tgid=100");
}
#[test]
fn display_comm() {
let mut comm = [0u8; 16];
comm[..4].copy_from_slice(b"bash");
let event = ProcEvent::Comm {
pid: 1,
tgid: 1,
comm,
};
assert_eq!(format!("{event}"), "COMM pid=1 tgid=1 name=\"bash\"");
}
#[test]
fn multi_part_message_iteration() {
let exec_data = [42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat();
let msg1 = make_full_message(PROC_EVENT_EXEC, &exec_data);
let exec_data2 = [43i32.to_ne_bytes(), 101i32.to_ne_bytes()].concat();
let msg2 = make_full_message(PROC_EVENT_EXEC, &exec_data2);
let mut combined = Vec::new();
combined.extend_from_slice(&msg1);
combined.extend_from_slice(&msg2);
let iter = NetlinkMessageIter::new(&combined, combined.len());
let events: Vec<_> = iter.filter_map(|r| r.ok().flatten()).collect();
assert_eq!(events.len(), 2);
assert_eq!(events[0], ProcEvent::Exec { pid: 42, tgid: 100 });
assert_eq!(events[1], ProcEvent::Exec { pid: 43, tgid: 101 });
}
#[test]
fn test_nlmsg_hdrlen() {
assert_eq!(nlmsg_hdrlen(), 16);
}
#[test]
fn test_nlmsg_align() {
assert_eq!(nlmsg_align(0), 0);
assert_eq!(nlmsg_align(1), 4);
assert_eq!(nlmsg_align(4), 4);
assert_eq!(nlmsg_align(5), 8);
assert_eq!(nlmsg_align(16), 16);
}
#[test]
fn test_nlmsg_length() {
assert_eq!(nlmsg_length(0), 16);
assert_eq!(nlmsg_length(20), 36);
}
#[test]
fn recv_buffer_too_small() {
let err = Error::BufferTooSmall {
needed: SIZE_NLMSGHDR,
};
assert_eq!(
format!("{err}"),
"buffer too small, need at least 16 bytes"
);
}
#[test]
fn parse_exec_truncated() {
let data = [42i32.to_ne_bytes()].concat(); let buf = make_full_message(PROC_EVENT_EXEC, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_fork_truncated() {
let data = [
10i32.to_ne_bytes(), 10i32.to_ne_bytes(), 20i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_FORK, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_exit_truncated() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), 0u32.to_ne_bytes(), 17u32.to_ne_bytes(), 0i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_EXIT, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_uid_truncated() {
let data = [
5i32.to_ne_bytes(), 5i32.to_ne_bytes(), 1000u32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_UID, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_gid_truncated() {
let data = [
5i32.to_ne_bytes(), 5i32.to_ne_bytes(), 100u32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_GID, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_sid_truncated() {
let data = [7i32.to_ne_bytes()].concat(); let buf = make_full_message(PROC_EVENT_SID, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_ptrace_truncated() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), 999i32.to_ne_bytes(), ]
.concat();
let buf = make_full_message(PROC_EVENT_PTRACE, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_comm_truncated_missing_comm() {
let data = [42i32.to_ne_bytes(), 42i32.to_ne_bytes()].concat(); let buf = make_full_message(PROC_EVENT_COMM, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_coredump_truncated() {
let data = [1i32.to_ne_bytes()].concat(); let buf = make_full_message(PROC_EVENT_COREDUMP, &data);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_nlmsg_len_too_small() {
let mut buf = make_netlink_message(NLMSG_MIN_TYPE, &[0u8; 20]);
buf[0..4].copy_from_slice(&10u32.to_ne_bytes()); let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_nlmsg_len_exceeds_buffer() {
let mut buf = make_netlink_message(NLMSG_MIN_TYPE, &[0u8; 20]);
buf[0..4].copy_from_slice(&1000u32.to_ne_bytes()); let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_cn_msg_wrong_idx() {
let proc_payload = make_proc_event_payload(PROC_EVENT_EXEC, &[42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat());
let mut cn_payload = Vec::with_capacity(SIZE_CN_MSG + proc_payload.len());
cn_payload.extend_from_slice(&999u32.to_ne_bytes()); cn_payload.extend_from_slice(&CN_VAL_PROC.to_ne_bytes());
cn_payload.extend_from_slice(&0u32.to_ne_bytes());
cn_payload.extend_from_slice(&0u32.to_ne_bytes());
cn_payload.extend_from_slice(&(proc_payload.len() as u16).to_ne_bytes());
cn_payload.extend_from_slice(&0u16.to_ne_bytes());
cn_payload.extend_from_slice(&proc_payload);
let buf = make_netlink_message(NLMSG_MIN_TYPE, &cn_payload);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_cn_msg_truncated_header() {
let buf = make_netlink_message(NLMSG_MIN_TYPE, &[0u8; 10]);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_nlmsg_error_ack() {
let mut payload = vec![0u8; 20];
payload[0..4].copy_from_slice(&0i32.to_ne_bytes()); let buf = make_netlink_message(NLMSG_ERROR, &payload);
let result = parse_netlink_message(&buf, buf.len()).unwrap();
assert!(result.is_none());
}
#[test]
fn parse_exec_negative_pid() {
let data = [(-1i32).to_ne_bytes(), (-1i32).to_ne_bytes()].concat();
let buf = make_full_message(PROC_EVENT_EXEC, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Exec {
pid: u32::MAX,
tgid: u32::MAX,
}
);
}
#[test]
fn parse_comm_no_nul() {
let data = [
42i32.to_ne_bytes(), 42i32.to_ne_bytes(), ]
.concat();
let mut comm_event = data;
let comm: [u8; 16] = [
b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h',
b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p',
];
comm_event.extend_from_slice(&comm);
let buf = make_full_message(PROC_EVENT_COMM, &comm_event);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(
event,
ProcEvent::Comm {
pid: 42,
tgid: 42,
comm,
}
);
let s = format!("{event}");
assert_eq!(s, "COMM pid=42 tgid=42 name=\"abcdefghijklmnop\"");
}
#[test]
fn parse_comm_invalid_utf8() {
let data = [
1i32.to_ne_bytes(), 1i32.to_ne_bytes(), ]
.concat();
let mut comm_event = data;
let mut comm = [0u8; 16];
comm[0] = 0xFF; comm[1] = 0xFE;
comm[2] = 0;
comm_event.extend_from_slice(&comm);
let buf = make_full_message(PROC_EVENT_COMM, &comm_event);
let event = format!(
"{}",
parse_netlink_message(&buf, buf.len()).unwrap().unwrap()
);
assert!(event.contains("<invalid>"));
}
#[test]
fn parse_unknown_large_data_skipped() {
let data = vec![0xABu8; 1024];
let buf = make_full_message(0xFFFFFFFF, &data);
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
match event {
ProcEvent::Unknown { what, raw_data } => {
assert_eq!(what, 0xFFFFFFFF);
assert_eq!(raw_data.len(), 1024);
assert_eq!(raw_data[0], 0xAB);
assert_eq!(raw_data[1023], 0xAB);
}
_ => panic!("expected Unknown"),
}
}
#[test]
fn parse_zero_length_proc_event_data() {
let proc_payload = make_proc_event_payload(PROC_EVENT_EXEC, &[]);
let cn_payload = make_cn_msg(&proc_payload);
let buf = make_netlink_message(NLMSG_MIN_TYPE, &cn_payload);
let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn display_fork() {
let event = ProcEvent::Fork {
parent_pid: 100,
parent_tgid: 100,
child_pid: 200,
child_tgid: 200,
};
assert_eq!(
format!("{event}"),
"FORK parent=(100,100) child=(200,200)"
);
}
#[test]
fn display_exit() {
let event = ProcEvent::Exit {
pid: 42,
tgid: 42,
exit_code: 0,
exit_signal: 17,
};
assert_eq!(format!("{event}"), "EXIT pid=42 tgid=42 code=0 signal=17");
}
#[test]
fn display_uid() {
let event = ProcEvent::Uid {
pid: 1,
tgid: 1,
ruid: 1000,
euid: 0,
};
assert_eq!(format!("{event}"), "UID pid=1 tgid=1 ruid=1000 euid=0");
}
#[test]
fn display_gid() {
let event = ProcEvent::Gid {
pid: 2,
tgid: 2,
rgid: 100,
egid: 200,
};
assert_eq!(format!("{event}"), "GID pid=2 tgid=2 rgid=100 egid=200");
}
#[test]
fn display_sid() {
let event = ProcEvent::Sid { pid: 3, tgid: 3 };
assert_eq!(format!("{event}"), "SID pid=3 tgid=3");
}
#[test]
fn display_ptrace() {
let event = ProcEvent::Ptrace {
pid: 10,
tgid: 10,
tracer_pid: 99,
tracer_tgid: 99,
};
assert_eq!(
format!("{event}"),
"PTRACE pid=10 tgid=10 tracer=(99,99)"
);
}
#[test]
fn display_coredump() {
let event = ProcEvent::Coredump { pid: 7, tgid: 7 };
assert_eq!(format!("{event}"), "COREDUMP pid=7 tgid=7");
}
#[test]
fn iter_empty_buffer() {
let iter = NetlinkMessageIter::new(&[], 0);
assert_eq!(iter.count(), 0);
}
#[test]
fn iter_single_done_message() {
let buf = make_netlink_message(NLMSG_DONE, &[]);
let iter = NetlinkMessageIter::new(&buf, buf.len());
let results: Vec<_> = iter.collect();
assert_eq!(results.len(), 0);
}
#[test]
fn iter_done_terminates_early() {
let exec_data = [42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat();
let msg_exec = make_full_message(PROC_EVENT_EXEC, &exec_data);
let msg_done = make_netlink_message(NLMSG_DONE, &[]);
let mut combined = Vec::new();
combined.extend_from_slice(&msg_exec);
combined.extend_from_slice(&msg_done);
let iter = NetlinkMessageIter::new(&combined, combined.len());
let events: Vec<_> = iter.filter_map(|r| r.ok().flatten()).collect();
assert_eq!(events.len(), 1);
assert_eq!(events[0], ProcEvent::Exec { pid: 42, tgid: 100 });
}
#[test]
fn iter_interleaved_control_messages() {
let exec_data = [42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat();
let fork_data = [
10i32.to_ne_bytes(),
10i32.to_ne_bytes(),
20i32.to_ne_bytes(),
20i32.to_ne_bytes(),
]
.concat();
let msg_noop = make_netlink_message(NLMSG_NOOP, &[]);
let msg_exec = make_full_message(PROC_EVENT_EXEC, &exec_data);
let msg_fork = make_full_message(PROC_EVENT_FORK, &fork_data);
let msg_done = make_netlink_message(NLMSG_DONE, &[]);
let mut combined = Vec::new();
combined.extend_from_slice(&msg_noop);
combined.extend_from_slice(&msg_exec);
combined.extend_from_slice(&msg_noop);
combined.extend_from_slice(&msg_fork);
combined.extend_from_slice(&msg_done);
let iter = NetlinkMessageIter::new(&combined, combined.len());
let events: Vec<_> = iter.filter_map(|r| r.ok().flatten()).collect();
assert_eq!(events.len(), 2);
assert_eq!(events[0], ProcEvent::Exec { pid: 42, tgid: 100 });
assert_eq!(
events[1],
ProcEvent::Fork {
parent_pid: 10,
parent_tgid: 10,
child_pid: 20,
child_tgid: 20,
}
);
}
#[test]
fn iter_malformed_zero_length() {
let mut buf = vec![0u8; 16];
buf[0..4].copy_from_slice(&0u32.to_ne_bytes()); let mut iter = NetlinkMessageIter::new(&buf, buf.len());
let result = iter.next().unwrap();
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn iter_remaining_too_small_for_header() {
let buf = vec![0u8; 4];
let mut iter = NetlinkMessageIter::new(&buf, 4);
let result = iter.next().unwrap();
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn iter_no_valid_msgs_returns_none_on_second_call() {
let buf = make_netlink_message(NLMSG_DONE, &[]);
let mut iter = NetlinkMessageIter::new(&buf, buf.len());
assert!(iter.next().is_none());
assert!(iter.next().is_none());
}
#[test]
fn test_nlmsg_align_max() {
assert_eq!(nlmsg_align(65535), 65536); assert_eq!(nlmsg_align(65536), 65536);
assert_eq!(nlmsg_align(65537), 65540);
}
#[test]
fn test_nlmsg_align_neg_like() {
assert_eq!(nlmsg_align(2), 4);
assert_eq!(nlmsg_align(3), 4);
assert_eq!(nlmsg_align(6), 8);
assert_eq!(nlmsg_align(7), 8);
assert_eq!(nlmsg_align(8), 8);
assert_eq!(nlmsg_align(9), 12);
}
#[test]
fn parse_cn_msg_truncated_no_data() {
let result = parse_cn_msg(&[0u8; 15]);
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn parse_cn_msg_data_len_mismatch() {
let mut buf = vec![0u8; SIZE_CN_MSG + 4];
buf[0..4].copy_from_slice(&CN_IDX_PROC.to_ne_bytes());
buf[4..8].copy_from_slice(&CN_VAL_PROC.to_ne_bytes());
buf[16..18].copy_from_slice(&100u16.to_ne_bytes()); let result = parse_cn_msg(&buf);
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn iter_many_messages() {
let exec_data = [42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat();
let mut combined = Vec::new();
for _ in 0..100 {
let msg = make_full_message(PROC_EVENT_EXEC, &exec_data);
combined.extend_from_slice(&msg);
}
let iter = NetlinkMessageIter::new(&combined, combined.len());
let count = iter.filter_map(|r| r.ok().flatten()).count();
assert_eq!(count, 100);
}
#[test]
fn error_display_all_variants() {
assert_eq!(
format!("{}", Error::Os(std::io::Error::from_raw_os_error(1))),
"system call error: Operation not permitted (os error 1)"
);
assert_eq!(format!("{}", Error::Truncated), "truncated message");
assert_eq!(
format!("{}", Error::BufferTooSmall { needed: 64 }),
"buffer too small, need at least 64 bytes"
);
assert_eq!(
format!("{}", Error::Interrupted),
"interrupted by signal"
);
assert_eq!(
format!("{}", Error::ConnectionClosed),
"connection closed"
);
assert_eq!(
format!("{}", Error::Overrun),
"message overrun, events may have been dropped"
);
}
#[test]
fn error_source() {
use std::error::Error as _;
assert!(Error::Os(std::io::Error::from_raw_os_error(1)).source().is_some());
assert!(Error::Truncated.source().is_none());
assert!(Error::BufferTooSmall { needed: 16 }.source().is_none());
assert!(Error::Interrupted.source().is_none());
assert!(Error::ConnectionClosed.source().is_none());
assert!(Error::Overrun.source().is_none());
}
#[test]
fn parse_nlmsg_len_inconsistent() {
let mut buf = make_netlink_message(NLMSG_MIN_TYPE, &[0u8; 100]);
buf[0..4].copy_from_slice(&20u32.to_ne_bytes()); let result = parse_netlink_message(&buf, buf.len());
assert!(matches!(result, Err(Error::Truncated)));
}
#[test]
fn proc_event_clone_and_eq() {
let e1 = ProcEvent::Exec {
pid: 42,
tgid: 100,
};
let e2 = e1.clone();
assert_eq!(e1, e2);
let e3 = ProcEvent::Exec {
pid: 43,
tgid: 100,
};
assert_ne!(e1, e3);
}
#[test]
fn roundtrip_all_event_types() {
use std::error::Error as _;
let _ = Error::Truncated.source(); }
#[test]
fn parse_with_nlm_f_request_flag() {
let exec_data = [42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat();
let proc_payload = make_proc_event_payload(PROC_EVENT_EXEC, &exec_data);
let cn_payload = make_cn_msg(&proc_payload);
let mut buf = make_netlink_message(NLMSG_MIN_TYPE, &cn_payload);
buf[6..8].copy_from_slice(&NLM_F_REQUEST.to_ne_bytes());
let event = parse_netlink_message(&buf, buf.len()).unwrap().unwrap();
assert_eq!(event, ProcEvent::Exec { pid: 42, tgid: 100 });
}
#[test]
fn iter_alignment_correct() {
let msg1 = make_netlink_message(NLMSG_NOOP, &[]); let msg2 = make_full_message(PROC_EVENT_EXEC, &[42i32.to_ne_bytes(), 100i32.to_ne_bytes()].concat());
let msg3 = make_netlink_message(NLMSG_DONE, &[]);
let mut combined = Vec::new();
combined.extend_from_slice(&msg1);
combined.extend_from_slice(&msg2);
combined.extend_from_slice(&msg3);
let mut pos = 0;
let len1 = u32::from_ne_bytes(msg1[0..4].try_into().unwrap()) as usize;
assert_eq!(len1, 16);
pos += nlmsg_align(len1);
let len2 = u32::from_ne_bytes(msg2[0..4].try_into().unwrap()) as usize;
assert!(len2 > 16);
assert_eq!(pos, 16); pos += nlmsg_align(len2);
let len3 = u32::from_ne_bytes(msg3[0..4].try_into().unwrap()) as usize;
assert_eq!(len3, 16);
assert_eq!(pos, 16 + nlmsg_align(len2));
let iter = NetlinkMessageIter::new(&combined, combined.len());
let events: Vec<_> = iter.filter_map(|r| r.ok().flatten()).collect();
assert_eq!(events.len(), 1);
assert_eq!(events[0], ProcEvent::Exec { pid: 42, tgid: 100 });
}
}