#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/types.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "process.h"
#include "process.skel.h"
static struct env {
bool verbose;
const char *config;
pid_t agent_pid;
unsigned long long agent_label;
} env = {0};
const char *argp_program_version = "actplane-taint 2.0";
const char argp_program_doc[] =
"ActPlane in-kernel taint enforcer.\n"
"\n"
"Loads a compiled policy and reports only taint-rule violations.\n"
"USAGE: ./process --config policy.bin [--agent-pid PID --agent-label BIT]\n";
static const struct argp_option opts[] = {
{ "config", 'c', "FILE", 0, "Compiled policy (struct taint_config blob)" },
{ "agent-pid", 1000, "PID", 0, "Seed this pid as the ActPlane AGENT root" },
{ "agent-label", 1001, "BIT", 0, "Label bit to apply to --agent-pid" },
{ "verbose", 'v', NULL, 0, "Verbose libbpf debug output" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'v': env.verbose = true; break;
case 'c': env.config = arg; break;
case 1000: env.agent_pid = (pid_t)strtol(arg, NULL, 10); break;
case 1001: env.agent_label = strtoull(arg, NULL, 0); break;
case ARGP_KEY_ARG: argp_usage(state); break;
default: return ARGP_ERR_UNKNOWN;
}
return 0;
}
static const struct argp argp = { .options = opts, .parser = parse_arg, .doc = argp_program_doc };
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
}
static volatile bool exiting = false;
static void sig_handler(int sig) { (void)sig; exiting = true; }
static bool bpf_lsm_active(void)
{
FILE *f = fopen("/sys/kernel/security/lsm", "r");
char buf[512];
bool active = false;
if (!f)
return false;
if (fgets(buf, sizeof(buf), f))
active = strstr(buf, "bpf") != NULL;
fclose(f);
return active;
}
static const char *effect_name(unsigned int effect)
{
switch (effect) {
case TEFFECT_AUDIT: return "audit";
case TEFFECT_KILL: return "kill";
case TEFFECT_BLOCK: return "block";
default: return "unknown";
}
}
static bool config_has_effect(const struct taint_config *cfg, unsigned int effect)
{
for (unsigned int i = 0; i < cfg->n_rules && i < MAX_TAINT_RULES; i++) {
if (cfg->rules[i].effect == effect)
return true;
}
return false;
}
static int handle_event(void *ctx, void *data, size_t sz)
{
const struct event *e = data;
char target[MAX_FILENAME_LEN];
(void)ctx; (void)sz;
if (e->type != EVENT_TYPE_TAINT_VIOLATION)
return 0;
if (e->conn_ip) {
unsigned int ip = e->conn_ip;
snprintf(target, sizeof(target), "%u.%u.%u.%u",
ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff, (ip >> 24) & 0xff);
} else {
snprintf(target, sizeof(target), "%s", e->filename);
}
printf("{\"timestamp\":%llu,\"event\":\"TAINT_VIOLATION\",\"effect\":\"%s\","
"\"blocked\":%s,\"killed\":%s,\"comm\":\"%s\",\"pid\":%d,\"ppid\":%d,"
"\"target\":\"%s\",\"rule_id\":%u,\"taint_label\":%llu}\n",
e->timestamp_ns, effect_name(e->effect), e->blocked ? "true" : "false",
e->killed ? "true" : "false", e->comm, e->pid, e->ppid,
target, e->taint_rule_id, e->taint_label);
fflush(stdout);
return 0;
}
static int load_config(const char *path, struct taint_config *cfg)
{
FILE *f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "cannot open config '%s': %s\n", path, strerror(errno));
return -1;
}
memset(cfg, 0, sizeof(*cfg));
size_t n = fread(cfg, 1, sizeof(*cfg), f);
fclose(f);
if (n != sizeof(*cfg)) {
fprintf(stderr, "config size mismatch: read %zu, expected %zu\n",
n, sizeof(*cfg));
return -1;
}
return 0;
}
struct proc_state_seed {
unsigned long long labels;
unsigned long long lin_gates;
};
static int seed_agent_root(struct process_bpf *skel)
{
struct proc_state_seed state = {};
pid_t pid = env.agent_pid;
if (!env.agent_pid && !env.agent_label)
return 0;
if (env.agent_pid <= 0 || env.agent_label == 0) {
fprintf(stderr, "--agent-pid and --agent-label must be provided together\n");
return -1;
}
state.labels = env.agent_label;
if (bpf_map_update_elem(bpf_map__fd(skel->maps.ts_proc), &pid, &state, BPF_ANY) < 0) {
fprintf(stderr, "failed to seed AGENT pid %d in ts_proc: %s\n", pid, strerror(errno));
return -1;
}
if (bpf_map_update_elem(bpf_map__fd(skel->maps.ts_root), &pid, &pid, BPF_ANY) < 0) {
fprintf(stderr, "failed to seed AGENT pid %d in ts_root: %s\n", pid, strerror(errno));
return -1;
}
fprintf(stderr, "ActPlane: seeded AGENT pid %d label 0x%llx\n", pid, env.agent_label);
return 0;
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct process_bpf *skel;
struct taint_config cfg;
bool enforce;
int err;
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
if (!env.config) {
fprintf(stderr, "missing --config <policy.bin>\n");
return 1;
}
if (load_config(env.config, &cfg))
return 1;
enforce = bpf_lsm_active();
libbpf_set_print(libbpf_print_fn);
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
skel = process_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
if (!enforce) {
bpf_program__set_autoload(skel->progs.enforce_bprm_check_security, false);
bpf_program__set_autoload(skel->progs.enforce_file_open, false);
bpf_program__set_autoload(skel->progs.enforce_file_permission, false);
bpf_program__set_autoload(skel->progs.enforce_file_truncate, false);
bpf_program__set_autoload(skel->progs.enforce_path_truncate, false);
bpf_program__set_autoload(skel->progs.enforce_path_unlink, false);
bpf_program__set_autoload(skel->progs.enforce_path_rename, false);
bpf_program__set_autoload(skel->progs.enforce_socket_connect, false);
}
skel->rodata->enforce_mode = enforce ? 1 : 0;
skel->rodata->n_sources = cfg.n_sources;
skel->rodata->n_rules = cfg.n_rules;
skel->rodata->n_xforms = cfg.n_xforms;
skel->rodata->n_gates = cfg.n_gates;
skel->rodata->n_invals = cfg.n_invals;
memcpy((void *)skel->rodata->taint_sources, cfg.sources, sizeof(cfg.sources));
memcpy((void *)skel->rodata->taint_rules, cfg.rules, sizeof(cfg.rules));
memcpy((void *)skel->rodata->taint_xforms, cfg.xforms, sizeof(cfg.xforms));
memcpy((void *)skel->rodata->taint_gates, cfg.gates, sizeof(cfg.gates));
memcpy((void *)skel->rodata->taint_invals, cfg.invals, sizeof(cfg.invals));
fprintf(stderr, "ActPlane: %u sources, %u rules, %u xforms, %u gates, %u invals\n",
cfg.n_sources, cfg.n_rules, cfg.n_xforms, cfg.n_gates, cfg.n_invals);
fprintf(stderr, "ActPlane: %s mode (%s)\n",
enforce ? "enforce" : "tracepoint",
enforce ? "BPF LSM is active" :
"BPF LSM is not active; block effects are unsupported, audit reports, kill terminates");
if (!enforce && config_has_effect(&cfg, TEFFECT_BLOCK))
fprintf(stderr,
"ActPlane: warning: effect block requires BPF LSM; block rules will not fire in tracepoint mode\n");
err = process_bpf__load(skel);
if (err) { fprintf(stderr, "Failed to load BPF skeleton\n"); goto cleanup; }
{
__u32 ks[5] = {0, 1, 2, 3, 4};
__u32 vs[5] = { cfg.n_rules, cfg.n_sources, cfg.n_xforms,
cfg.n_gates, cfg.n_invals };
int cfd = bpf_map__fd(skel->maps.ts_counts);
for (int i = 0; i < 5; i++) {
if (bpf_map_update_elem(cfd, &ks[i], &vs[i], BPF_ANY) < 0) {
fprintf(stderr, "failed to set loop count %d: %s\n", i, strerror(errno));
err = -1; goto cleanup;
}
}
}
err = process_bpf__attach(skel);
if (err) { fprintf(stderr, "Failed to attach BPF skeleton\n"); goto cleanup; }
if (seed_agent_root(skel)) { err = -1; goto cleanup; }
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) { err = -1; fprintf(stderr, "ring buffer failed\n"); goto cleanup; }
fprintf(stderr, "ActPlane: ready\n");
while (!exiting) {
err = ring_buffer__poll(rb, 100);
if (err == -EINTR) { err = 0; break; }
if (err < 0) { fprintf(stderr, "poll error: %d\n", err); break; }
}
cleanup:
ring_buffer__free(rb);
process_bpf__destroy(skel);
return err < 0 ? -err : 0;
}