From a87a190be270774ea0c6c5fad4982f21e5829a74 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Sat, 30 Dec 2023 15:34:20 +0900 Subject: [PATCH] agent: Support composite events CRYPTO_AUDITING_*_DATA defines a single USDT probe point for each invocation. This causes multiple context switches to happen if they are writen in series. This adds a new macro, CRYPTO_AUDITING_DATA and CRYPTO_AUDITING_DATAV, which take an array of events to limit the number of context switches. Signed-off-by: Daiki Ueno --- agent/src/bpf/audit.bpf.c | 169 +++++++++++++++++++++++++++++++++++++- agent/src/main.rs | 18 ++++ agent/tests/composite.rs | 169 ++++++++++++++++++++++++++++++++++++++ dist/audit.h | 48 +++++++++++ 4 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 agent/tests/composite.rs diff --git a/agent/src/bpf/audit.bpf.c b/agent/src/bpf/audit.bpf.c index afff438..f66ea16 100644 --- a/agent/src/bpf/audit.bpf.c +++ b/agent/src/bpf/audit.bpf.c @@ -10,6 +10,16 @@ bpf_trace_printk ("%s: " format, sizeof("%s: " format), \ __PRETTY_FUNCTION__, __VA_ARGS__) +#define MAX_EVENTS 16 +#define DATA_TYPE_WORD (unsigned long)-2 +#define DATA_TYPE_STRING (unsigned long)-1 + +struct crypto_auditing_data { + char *key_ptr; + void *value_ptr; + unsigned long value_size; +}; + struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 4096 /* one page */); @@ -231,18 +241,152 @@ record_blob_data (struct pt_regs *ctx, long context, const char *key_ptr) return err; } +/* This is similar to record_blob_data but always assumes VALUE_PTR is + * present. + */ +static __always_inline int +record_blob_data_explicit (struct pt_regs *ctx, long context, const char *key_ptr, + const void *value_ptr, unsigned long value_size) +{ + int err; + + if (value_size > VALUE_SIZE) + { + DEBUG ("value size out of range: %lu\n", value_size); + return -EINVAL; + } + + struct audit_blob_data_event_st *event = + bpf_ringbuf_reserve (&ringbuf, + sizeof(struct audit_blob_data_event_st), + 0); + if (!event) + { + DEBUG ("unable to allocate ringbuf entry: %ld\n", -ENOMEM); + return -ENOMEM; + } + + populate_event_header (&event->base.header, + AUDIT_EVENT_DATA, + sizeof(*event), + context); + + event->base.type = AUDIT_DATA_BLOB; + err = bpf_probe_read_user_str (event->base.key, KEY_SIZE, (void *)key_ptr); + if (err < 0) + { + DEBUG ("unable to read event key: %ld\n", err); + goto error; + } + + if (value_size > 0) + { + err = bpf_probe_read_user (event->value, value_size, (void *)value_ptr); + if (err < 0) + { + DEBUG ("unable to read event data: %ld\n", err); + goto error; + } + } + + event->size = value_size; + + bpf_ringbuf_submit (event, 0); + return 0; + + error: + bpf_ringbuf_discard (event, 0); + return err; +} + +static __always_inline int +record_data (struct pt_regs *ctx, + long context, + const struct crypto_auditing_data *array_ptr, + unsigned long array_size) +{ + struct crypto_auditing_data events[MAX_EVENTS]; + int err; + + if (array_size > MAX_EVENTS) + { + DEBUG ("value size out of range: %lu\n", array_size); + return -EINVAL; + } + array_size &= MAX_EVENTS - 1; + + err = bpf_probe_read_user (events, + array_size * sizeof (struct crypto_auditing_data), + array_ptr); + if (err < 0) + { + DEBUG ("unable to read from data array: %ld\n", err); + return err; + } + + for (unsigned long i = 0; i < array_size; i++) + { + switch (events[i].value_size) + { + case DATA_TYPE_WORD: + err = record_word_data (ctx, context, + events[i].key_ptr, + (long) events[i].value_ptr); + if (err < 0) + DEBUG ("unable to process word data: %ld\n", err); + break; + + case DATA_TYPE_STRING: + err = record_string_data (ctx, context, + events[i].key_ptr, + (const char *) events[i].value_ptr); + if (err < 0) + DEBUG ("unable to process string data: %ld\n", err); + break; + + default: + err = record_blob_data_explicit (ctx, context, + events[i].key_ptr, + events[i].value_ptr, + events[i].value_size); + if (err < 0) + DEBUG ("unable to process blob data: %ld\n", err); + break; + } + + if (err < 0) + break; + } + + return err; +} + +static __always_inline int +record_new_context_with_data (struct pt_regs *ctx, long context, long parent, + struct crypto_auditing_data *array_ptr, + unsigned long array_size) +{ + int err; + + err = record_new_context (ctx, context, parent); + if (err < 0) + return err; + + return record_data (ctx, context, array_ptr, array_size); +} + SEC("usdt") int BPF_USDT(new_context, long context, long parent) { - return record_new_context(ctx, context, parent); + return record_new_context (ctx, context, parent); } SEC("usdt") int BPF_USDT(word_data, long context, const char *key_ptr, long value) { - return record_word_data(ctx, context, key_ptr, value); + return record_word_data (ctx, context, key_ptr, value); } SEC("usdt") @@ -250,14 +394,31 @@ int BPF_USDT(string_data, long context, const char *key_ptr, const char *value_ptr) { - return record_string_data(ctx, context, key_ptr, value_ptr); + return record_string_data (ctx, context, key_ptr, value_ptr); } SEC("usdt") int BPF_USDT(blob_data, long context, const char *key_ptr) { - return record_blob_data(ctx, context, key_ptr); + return record_blob_data (ctx, context, key_ptr); +} + +SEC("usdt") +int +BPF_USDT(data, long context, const struct crypto_auditing_data *array_ptr, + unsigned long array_size) +{ + return record_data (ctx, context, array_ptr, array_size); +} + +SEC("usdt") +int +BPF_USDT(new_context_with_data, long context, long parent, + void *array_ptr, unsigned long array_size) +{ + return record_new_context_with_data (ctx, context, parent, array_ptr, + array_size); } char LICENSE[] SEC("license") = "GPL"; diff --git a/agent/src/main.rs b/agent/src/main.rs index 85c46c6..4618c77 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -146,6 +146,24 @@ fn main() -> Result<(), Box> { ) { links.push(link); } + let prog = progs.data(); + if let Ok(link) = prog.attach_usdt( + -1, // any process + library, + "crypto_auditing", + "data", + ) { + links.push(link); + } + let prog = progs.new_context_with_data(); + if let Ok(link) = prog.attach_usdt( + -1, // any process + library, + "crypto_auditing", + "new_context_with_data", + ) { + links.push(link); + } } let cipher = Cipher::aes_128_ecb(); diff --git a/agent/tests/composite.rs b/agent/tests/composite.rs new file mode 100644 index 0000000..3377704 --- /dev/null +++ b/agent/tests/composite.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023 The crypto-auditing developers. + +extern crate agenttest; +use agenttest::*; + +use crypto_auditing::types::{Event, EventGroup}; +use libc::{c_long, c_uchar, c_void}; +use probe::probe; +use serde_cbor::de::Deserializer; +use std::env; +use std::path::PathBuf; +use std::process::{Child, Command}; +use std::thread; +use std::time::Duration; +use tempfile::tempdir; + +fn target_dir() -> PathBuf { + env::current_exe() + .ok() + .map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + .unwrap() +} + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("fixtures") +} + +struct AgentProcess(Child); + +impl Drop for AgentProcess { + fn drop(&mut self) { + self.0.kill().expect("unable to kill event-broker"); + } +} + +#[repr(C)] +struct EventData { + key_ptr: *const c_uchar, + value_ptr: *const c_void, + value_size: c_long, +} + +#[test] +fn test_probe_composite() { + bump_memlock_rlimit().expect("unable to bump memlock rlimit"); + + let agent_path = target_dir().join("crypto-auditing-agent"); + let log_dir = tempdir().expect("unable to create temporary directory"); + let log_path = log_dir.path().join("agent.log"); + let process = Command::new(&agent_path) + .arg("-c") + .arg(fixture_dir().join("conf").join("agent.conf")) + .arg("--log-file") + .arg(&log_path) + .arg("--library") + .arg(&env::current_exe().unwrap()) + .spawn() + .expect("unable to spawn agent"); + + // Make sure the agent process will be killed at exit + let process = AgentProcess(process); + + // Wait until the agent starts up + for _ in 0..5 { + if log_path.exists() { + break; + } + thread::sleep(Duration::from_millis(100)); + } + assert!(log_path.exists()); + + let foo = String::from("foo\0"); + let bar = String::from("bar\0"); + let baz = String::from("baz\0"); + let events = vec![ + EventData { + key_ptr: foo.as_ptr(), + value_ptr: 3 as *const c_void, + value_size: -2, + }, + EventData { + key_ptr: bar.as_ptr(), + value_ptr: bar.as_ptr() as *const c_void, + value_size: -1, + }, + EventData { + key_ptr: baz.as_ptr(), + value_ptr: baz.as_ptr() as *const c_void, + value_size: baz.len() as i64, + }, + ]; + + let (_link, object) = + attach_bpf(&process.0, &agent_path).expect("unable to attach agent.bpf.o"); + let map = object.map("ringbuf").expect("unable to get ringbuf map"); + + let timeout = Duration::from_secs(10); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 1, 2); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, data, 1, events.as_ptr(), events.len()); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 4, 5); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let log_file = std::fs::File::open(&log_path) + .expect(&format!("unable to read file `{}`", log_path.display())); + + let groups: Result, _> = Deserializer::from_reader(&log_file) + .into_iter::() + .collect(); + let groups = groups.expect("error deserializing"); + assert_eq!(groups.len(), 5); + assert_eq!(groups[0].events().len(), 1); + assert!(matches!(groups[0].events()[0], Event::NewContext { .. })); + assert_eq!(groups[1].events().len(), 1); + if let Event::Data { key, .. } = &groups[1].events()[0] { + assert_eq!(key, "foo"); + } else { + unreachable!(); + } + assert_eq!(groups[2].events().len(), 1); + if let Event::Data { key, .. } = &groups[2].events()[0] { + assert_eq!(key, "bar"); + } else { + unreachable!(); + } + assert_eq!(groups[3].events().len(), 1); + if let Event::Data { key, .. } = &groups[3].events()[0] { + assert_eq!(key, "baz"); + } else { + unreachable!(); + } + assert_eq!(groups[4].events().len(), 1); + assert!(matches!(groups[4].events()[0], Event::NewContext { .. })); +} diff --git a/dist/audit.h b/dist/audit.h index 1906c2c..314282f 100644 --- a/dist/audit.h +++ b/dist/audit.h @@ -33,11 +33,59 @@ # define CRYPTO_AUDITING_BLOB_DATA(context, key_ptr, value_ptr, value_size) \ DTRACE_PROBE4(crypto_auditing, blob_data, context, key_ptr, value_ptr, value_size) +struct crypto_auditing_data { + char *key_ptr; + void *value_ptr; + unsigned long value_size; +}; + +# define CRYPTO_AUDITING_WORD(key_ptr, value_ptr) \ + { (char *)(key_ptr), (void *)(intptr_t)(value_ptr), (unsigned long)-2 } +# define CRYPTO_AUDITING_STRING(key_ptr, value_ptr) \ + { (char *)(key_ptr), (void *)(value_ptr), (unsigned long)-1 } +# define CRYPTO_AUDITING_BLOB(key_ptr, value_ptr, value_size) \ + { (char *)(key_ptr), (void *)(value_ptr), value_size } + +/* Assert multiple events (16 at maxiumum) at once as a typed + * array. The VALUE_SIZE field of each element indicates the type of + * event: -2 means a word, -1 means a NUL-terminated string, and any + * other value means a blob with the length of VALUE_SIZE. + */ +# define CRYPTO_AUDITING_DATA(context, array_ptr, array_size) \ + DTRACE_PROBE3(crypto_auditing, data, context, array_ptr, array_size) + +# define CRYPTO_AUDITING_DATAV(context, ...) ({ \ + struct crypto_auditing_data __crypto_auditing_events[] = { __VA_ARGS__ }; \ + CRYPTO_AUDITING_DATA(context, \ + __crypto_auditing_events, \ + sizeof (__crypto_auditing_events) / sizeof (__crypto_auditing_events[0])); \ +}) + +/* Introduce a new context CONTEXT, derived from PARENT, as well as + * assert multiple events. + */ +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, array_ptr, array_size) \ + DTRACE_PROBE4(crypto_auditing, new_context_with_data, context, parent, array_ptr, array_size) + +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATAV(context, parent, ...) ({ \ + struct crypto_auditing_data __crypto_auditing_events[] = { __VA_ARGS__ }; \ + CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, \ + __crypto_auditing_events, \ + sizeof (__crypto_auditing_events) / sizeof (__crypto_auditing_events[0])); \ +}) + #else # define CRYPTO_AUDITING_NEW_CONTEXT(context, parent) # define CRYPTO_AUDITING_WORD_DATA(context, key_ptr, value_ptr) # define CRYPTO_AUDITING_STRING_DATA(context, key_ptr, value_ptr) # define CRYPTO_AUDITING_BLOB_DATA(context, key_ptr, value_ptr, value_size) +# define CRYPTO_AUDITING_WORD(key_ptr, value_ptr) +# define CRYPTO_AUDITING_STRING(key_ptr, value_ptr) +# define CRYPTO_AUDITING_BLOB(key_ptr, value_ptr, value_size) +# define CRYPTO_AUDITING_DATA(context, array_ptr, array_size) +# define CRYPTO_AUDITING_DATAV(context, ...) +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, array_ptr, array_size) +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATAV(context, parent, ...) #endif /* ENABLE_CRYPTO_AUDITING */