Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(process-monitor): Include information about namespaces #207

Merged
merged 1 commit into from Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 28 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/bpf-filtering/Cargo.toml
Expand Up @@ -23,6 +23,8 @@ bpf-common = { path = "../bpf-common" }
pulsar-core = { path = "../pulsar-core" }
which = { version = "4.2.5", optional = true }
cgroups-rs = { version = "0.3.2", optional = true }
regex = "1.9"
lazy_static = "1.4"

[build-dependencies]
bpf-builder = { path = "../bpf-builder" }
2 changes: 2 additions & 0 deletions crates/bpf-filtering/src/initializer.rs
Expand Up @@ -74,12 +74,14 @@ pub async fn setup_events_filter(
ppid: process.parent,
pid: process.pid,
timestamp: Timestamp::from(0),
namespaces: process.namespaces,
});
process_tracker.update(TrackerUpdate::Exec {
pid: process.pid,
image: process.image.to_string(),
timestamp: Timestamp::from(0),
argv: Vec::new(),
namespaces: process.namespaces,
});
}

Expand Down
81 changes: 78 additions & 3 deletions crates/bpf-filtering/src/process_tree.rs
@@ -1,11 +1,18 @@
use std::collections::HashMap;
use std::{collections::HashMap, fs, path::Path};

use bpf_common::{
parsing::procfs::{self, ProcfsError},
Pid,
};
use lazy_static::lazy_static;
use pulsar_core::event::Namespaces;
use regex::Regex;
use thiserror::Error;

lazy_static! {
static ref NAMESPACE_RE: Regex = Regex::new(r"(\d+)").unwrap();
}

/// ProcessTree contains information about all running processes
pub(crate) struct ProcessTree {
processes: Vec<ProcessData>,
Expand All @@ -16,6 +23,7 @@ pub(crate) struct ProcessData {
pub(crate) pid: Pid,
pub(crate) image: String,
pub(crate) parent: Pid,
pub(crate) namespaces: Namespaces,
}

#[derive(Debug, Error)]
Expand All @@ -24,16 +32,70 @@ pub(crate) enum Error {
ProcessNotFound { pid: Pid },
#[error("loading process {pid}: parent image {ppid} not found")]
ParentNotFound { pid: Pid, ppid: Pid },
#[error(transparent)]
Procfs(#[from] ProcfsError),
#[error("failed to get the {ns_type} namespace for process {pid}")]
Namespace { pid: Pid, ns_type: String },
}

pub(crate) const PID_0: Pid = Pid::from_raw(0);

fn get_process_namespace(pid: Pid, ns_type: &str) -> Result<u32, Error> {
let path = Path::new("/proc")
.join(pid.to_string())
.join("ns")
.join(ns_type);

let link_target = fs::read_link(path).map_err(|_| Error::Namespace {
pid,
ns_type: ns_type.to_owned(),
})?;
let link_target = link_target.to_string_lossy();
let ns: u32 = NAMESPACE_RE
.captures(&link_target)
.and_then(|cap| cap.get(1))
.and_then(|m| m.as_str().parse().ok())
.ok_or(Error::Namespace {
pid,
ns_type: ns_type.to_owned(),
})?;

Ok(ns)
}

fn get_process_namespace_or_log(pid: Pid, namespace_type: &str) -> u32 {
get_process_namespace(pid, namespace_type).map_or_else(
|e| {
log::warn!(
"Failed to determine {} namespace for process {:?}: {}",
namespace_type,
pid,
e
);
u32::default()
},
|v| v,
)
}

fn get_process_namespaces(pid: Pid) -> Namespaces {
Namespaces {
uts: get_process_namespace_or_log(pid, "uts"),
ipc: get_process_namespace_or_log(pid, "ipc"),
mnt: get_process_namespace_or_log(pid, "mnt"),
net: get_process_namespace_or_log(pid, "net"),
pid: get_process_namespace_or_log(pid, "pid"),
time: get_process_namespace_or_log(pid, "time"),
cgroup: get_process_namespace_or_log(pid, "cgroup"),
}
}

impl ProcessTree {
/// Construct the `ProcessTree` by reading from `procfs`:
/// - process list
/// - parent pid
/// - image
pub(crate) fn load_from_procfs() -> Result<Self, ProcfsError> {
pub(crate) fn load_from_procfs() -> Result<Self, Error> {
let mut processes: HashMap<Pid, ProcessData> = HashMap::new();
let mut children: HashMap<Pid, Vec<Pid>> = HashMap::new();
let mut sorted_processes: Vec<ProcessData> = Vec::new();
Expand All @@ -50,18 +112,29 @@ impl ProcessTree {
log::debug!("Error getting parent pid of {pid}: {}", err);
Pid::from_raw(1)
});
processes.insert(pid, ProcessData { pid, image, parent });
let namespaces = get_process_namespaces(pid);
processes.insert(
pid,
ProcessData {
pid,
image,
parent,
namespaces,
},
);
children.entry(parent).or_default().push(pid);
}

// Make sure to add PID 0 (which is part of kernel) to map_interest to avoid
// warnings about missing entries.
let namespaces = get_process_namespaces(PID_0);
processes.insert(
PID_0,
ProcessData {
pid: PID_0,
image: String::from("kernel"),
parent: PID_0,
namespaces,
},
);

Expand Down Expand Up @@ -97,10 +170,12 @@ impl ProcessTree {
match parent {
Some(parent) => {
let image = parent.image.to_string();
let namespaces = get_process_namespaces(pid);
self.processes.push(ProcessData {
pid,
image,
parent: ppid,
namespaces,
});
Ok(self.processes.last().unwrap())
}
Expand Down
28 changes: 28 additions & 0 deletions crates/modules/process-monitor/probes.bpf.c
Expand Up @@ -25,14 +25,26 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL";

#define MAX_PENDING_DEAD_PARENTS 30

struct namespaces {
unsigned int uts;
unsigned int ipc;
unsigned int mnt;
unsigned int pid;
unsigned int net;
unsigned int time;
unsigned int cgroup;
};

struct fork_event {
pid_t ppid;
struct namespaces namespaces;
};

struct exec_event {
struct buffer_index filename;
int argc;
struct buffer_index argv;
struct namespaces namespaces;
};

struct exit_event {
Expand Down Expand Up @@ -108,6 +120,13 @@ int BPF_PROG(sched_process_fork, struct task_struct *parent,
return 0;

event->fork.ppid = parent_tgid;
event->fork.namespaces.uts = BPF_CORE_READ(child, nsproxy, uts_ns, ns.inum);
event->fork.namespaces.ipc = BPF_CORE_READ(child, nsproxy, ipc_ns, ns.inum);
event->fork.namespaces.mnt = BPF_CORE_READ(child, nsproxy, mnt_ns, ns.inum);
event->fork.namespaces.pid = BPF_CORE_READ(child, nsproxy, pid_ns_for_children, ns.inum);
event->fork.namespaces.net = BPF_CORE_READ(child, nsproxy, net_ns, ns.inum);
event->fork.namespaces.time = BPF_CORE_READ(child, nsproxy, time_ns, ns.inum);
event->fork.namespaces.cgroup = BPF_CORE_READ(child, nsproxy, cgroup_ns, ns.inum);

output_process_event(ctx, event);
return 0;
Expand All @@ -122,6 +141,15 @@ int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid,
if (!event)
return 0;
event->exec.argc = BPF_CORE_READ(bprm, argc);

event->exec.namespaces.uts = BPF_CORE_READ(p, nsproxy, uts_ns, ns.inum);
event->exec.namespaces.ipc = BPF_CORE_READ(p, nsproxy, ipc_ns, ns.inum);
event->exec.namespaces.mnt = BPF_CORE_READ(p, nsproxy, mnt_ns, ns.inum);
event->exec.namespaces.pid = BPF_CORE_READ(p, nsproxy, pid_ns_for_children, ns.inum);
event->exec.namespaces.net = BPF_CORE_READ(p, nsproxy, net_ns, ns.inum);
event->exec.namespaces.time = BPF_CORE_READ(p, nsproxy, time_ns, ns.inum);
event->exec.namespaces.cgroup = BPF_CORE_READ(p, nsproxy, cgroup_ns, ns.inum);

// This is needed because the first MAX_IMAGE_LEN bytes of buffer will
// be used as a lookup key for the target and whitelist maps and garbage
// would make the search fail.
Expand Down
13 changes: 11 additions & 2 deletions crates/modules/process-monitor/src/lib.rs
Expand Up @@ -3,6 +3,7 @@ use bpf_common::{
ebpf_program, parsing::BufferIndex, program::BpfContext, BpfSender, Pid, Program,
ProgramBuilder, ProgramError,
};
use pulsar_core::event::Namespaces;

const MODULE_NAME: &str = "process-monitor";

Expand Down Expand Up @@ -35,11 +36,13 @@ pub async fn program(
pub enum ProcessEvent {
Fork {
ppid: Pid,
namespaces: Namespaces,
},
Exec {
filename: BufferIndex<str>,
argc: u32,
argv: BufferIndex<str>, // 0 separated strings
namespaces: Namespaces,
},
Exit {
exit_code: u32,
Expand Down Expand Up @@ -108,15 +111,17 @@ pub mod pulsar {
// We do this by wrapping the pulsar sender and calling this closure on every event.
BpfSenderWrapper::new(ctx.get_sender(), move |event: &BpfEvent<ProcessEvent>| {
let _ = tx_processes.send(match event.payload {
ProcessEvent::Fork { ppid } => TrackerUpdate::Fork {
ProcessEvent::Fork { ppid, namespaces } => TrackerUpdate::Fork {
pid: event.pid,
ppid,
timestamp: event.timestamp,
namespaces,
},
ProcessEvent::Exec {
ref filename,
argc,
ref argv,
namespaces,
} => {
let argv =
extract_parameters(argv.bytes(&event.buffer).unwrap_or_else(|err| {
Expand All @@ -137,6 +142,7 @@ pub mod pulsar {
image: filename.string(&event.buffer).unwrap_or_default(),
timestamp: event.timestamp,
argv,
namespaces,
}
}
ProcessEvent::Exit { .. } => TrackerUpdate::Exit {
Expand Down Expand Up @@ -180,17 +186,20 @@ pub mod pulsar {
payload, buffer, ..
} = event;
Ok(match payload {
ProcessEvent::Fork { ppid } => Payload::Fork {
ProcessEvent::Fork { ppid, namespaces } => Payload::Fork {
ppid: ppid.as_raw(),
namespaces,
},
ProcessEvent::Exec {
filename,
argc,
argv,
namespaces,
} => Payload::Exec {
filename: filename.string(&buffer)?,
argc: argc as usize,
argv: extract_parameters(argv.bytes(&buffer)?).into(),
namespaces,
},
ProcessEvent::Exit { exit_code } => Payload::Exit { exit_code },
ProcessEvent::ChangeParent { ppid } => Payload::ChangeParent {
Expand Down