Skip to content

Commit

Permalink
feat(process-monitor): Include information about namespaces
Browse files Browse the repository at this point in the history
This is a preparatory change for supporting container monitoring
in Pulsar. Containers are just set of namespaces, so the easiest
way to recognize that a process belongs to a particular container
from information available from BPF programs, is to do that based
on namespaces.

For now, we simply add information about all namespaces for each
executed or forked process.
  • Loading branch information
vadorovsky committed Oct 16, 2023
1 parent b4304c6 commit dde7040
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 15 deletions.
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

0 comments on commit dde7040

Please sign in to comment.