Skip to content

Commit cfe353c

Browse files
committed
merge: regorus-bpf-lsm file-access backend + #22-27 (bpf-lsm-file)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents be2a566 + 36a9771 commit cfe353c

14 files changed

Lines changed: 2007 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"xtask",
66
"regorus-lift",
77
"regorus-bpf",
8+
"regorus-bpf-lsm",
89
]
910

1011
[package]

regorus-bpf-lsm/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "regorus-bpf-lsm"
3+
description = "Lower a regorus-lift EnforcerConfig into BPF maps + a fixed LSM file_open access enforcer (MVP)"
4+
version = "0.0.0"
5+
edition = "2021"
6+
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
7+
repository = "https://github.com/microsoft/regorus"
8+
publish = false
9+
10+
[lib]
11+
doctest = false
12+
13+
[dependencies]
14+
regorus-lift = { path = "../regorus-lift" }
15+
regorus = { path = "..", default-features = false, features = ["std", "arc", "rvm", "explanations", "net"] }
16+
serde = { version = "1.0", features = ["derive"] }
17+
serde_json = "1.0"
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Fixed, hand-audited lsm/file_open file-access enforcer.
5+
//
6+
// This is the kernel half of `regorus-bpf-lsm`. Like its egress sibling, it is
7+
// intentionally a *fixed* program: policy is data, not code. The user-space
8+
// exporter (`regorus_bpf_lsm::export`) compiles a `regorus-lift` EnforcerConfig
9+
// into the rule table consumed here; this program merely scans that table.
10+
//
11+
// It is INTENDED to mirror the user-space reference enforcer
12+
// `regorus_bpf_lsm::enforce`:
13+
//
14+
// * extract the path and op from the opened `struct file`,
15+
// * scan up to MAX_RULES rule rows,
16+
// * ALLOW iff some rule's full conjunction matches (wildcards match
17+
// anything); otherwise the request is UNDECIDED, which collapses to DENY.
18+
//
19+
// For the LSM hook, the boundary verdicts map to return values:
20+
// ALLOW -> 0 (permit the open), DENY/UNDECIDED -> -EPERM (block).
21+
//
22+
// ---------------------------------------------------------------------------
23+
// Phase-2 SOUNDNESS CAVEAT (documented, intentional).
24+
//
25+
// The kernel cannot trivially do arbitrary-length string matching, and
26+
// `bpf_d_path` returns a path that is NOT the canonicalized string the policy
27+
// was written against (no symlink/`..`/mount-namespace normalization), and is
28+
// read into a BOUNDED buffer (MAX_PATH_PREFIX bytes). This program is therefore
29+
// BEST-EFFORT:
30+
//
31+
// * It compares only the first MAX_PATH_PREFIX bytes.
32+
// * EXACT matches additionally require the kernel path length to equal the
33+
// pattern length (so a bounded read cannot let a longer path masquerade as
34+
// a shorter exact pattern).
35+
// * A pattern longer than MAX_PATH_PREFIX is treated as a NON-match
36+
// (fail-closed), never a truncated/over-permissive match.
37+
//
38+
// Canonicalization stays in USER SPACE. The conformance reference for the
39+
// lowered IR is the user-space enforcer (`src/enforcer.rs`), NOT this program.
40+
//
41+
// ---------------------------------------------------------------------------
42+
// Kernel-version assumptions:
43+
// * BTF + CO-RE (a kernel with /sys/kernel/btf/vmlinux).
44+
// * BPF LSM support (CONFIG_BPF_LSM, `lsm.s`/`lsm` hooks) and the
45+
// `bpf_d_path` helper, both available on modern (>= 5.10) kernels.
46+
// We cannot LOAD this here (uid 1000, no CAP_BPF); the gated compile test only
47+
// checks that it COMPILES against the live BTF. If `struct file`/`struct path`
48+
// are absent from BTF we still produce a compilable object via the guards below.
49+
50+
#include "vmlinux.h"
51+
#include "regorus_bpf_compat.h"
52+
53+
#define MAX_RULES 64
54+
// Must match regorus_bpf_lsm::abi::MAX_PATH_PREFIX.
55+
#define MAX_PATH_PREFIX 256
56+
57+
// Path match kinds (must match regorus_bpf_lsm::plan::PathMatch ordering intent).
58+
#define PATH_ANY 0
59+
#define PATH_EXACT 1
60+
#define PATH_PREFIX 2
61+
62+
// Op match kinds (must match regorus_bpf_lsm::plan::OpMatch).
63+
#define OP_MATCH_ANY 0
64+
#define OP_MATCH_EXACT 1
65+
66+
// File operations (must match regorus_bpf_lsm::abi::FileOp).
67+
#define OP_READ 0
68+
#define OP_WRITE 1
69+
#define OP_EXEC 2
70+
71+
// Verdict ABI (must match regorus_bpf_lsm::abi::Verdict).
72+
#define VERDICT_DENY 0
73+
#define VERDICT_ALLOW 1
74+
#define VERDICT_UNDECIDED 2
75+
76+
// FMODE_* bits from include/linux/fs.h (stable UAPI-adjacent constants).
77+
#define FMODE_READ_BIT 0x1
78+
#define FMODE_WRITE_BIT 0x2
79+
#define FMODE_EXEC_BIT 0x20
80+
81+
#ifndef EPERM
82+
#define EPERM 1
83+
#endif
84+
85+
// One rule row: a full conjunction over the two observable fields. `pattern`
86+
// holds the path bytes (exact or prefix), `pattern_len` its length in bytes
87+
// (<= MAX_PATH_PREFIX; a longer pattern is rejected by user space before it
88+
// reaches the kernel, and additionally guarded here).
89+
struct file_rule {
90+
__u8 path_kind; // PATH_ANY | PATH_EXACT | PATH_PREFIX
91+
__u8 op_kind; // OP_MATCH_ANY | OP_MATCH_EXACT
92+
__u8 op_value; // OP_READ | OP_WRITE | OP_EXEC
93+
__u8 _pad;
94+
__u32 pattern_len;
95+
char pattern[MAX_PATH_PREFIX];
96+
};
97+
98+
// The rule table, populated by user space from the exported FilePlan.
99+
struct {
100+
__uint(type, BPF_MAP_TYPE_ARRAY);
101+
__uint(max_entries, MAX_RULES);
102+
__type(key, __u32);
103+
__type(value, struct file_rule);
104+
} file_rules SEC(".maps");
105+
106+
// Number of populated rule rows (index 0). A request matches only rows
107+
// [0, rule_count); the rest of the fixed-size table is ignored.
108+
struct {
109+
__uint(type, BPF_MAP_TYPE_ARRAY);
110+
__uint(max_entries, 1);
111+
__type(key, __u32);
112+
__type(value, __u32);
113+
} file_rule_count SEC(".maps");
114+
115+
// A single-entry control map: when value != 0, enforcement is active. (When the
116+
// table is empty / unconfigured this lets the loader choose fail-open during
117+
// rollout; the default compiled-in behaviour is fail-closed.)
118+
struct {
119+
__uint(type, BPF_MAP_TYPE_ARRAY);
120+
__uint(max_entries, 1);
121+
__type(key, __u32);
122+
__type(value, __u32);
123+
} file_enabled SEC(".maps");
124+
125+
// A scratch percpu buffer to read the path into (the stack is too small for
126+
// MAX_PATH_PREFIX).
127+
struct {
128+
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
129+
__uint(max_entries, 1);
130+
__type(key, __u32);
131+
__type(value, char[MAX_PATH_PREFIX]);
132+
} file_path_scratch SEC(".maps");
133+
134+
// bpf_d_path (helper ID 147): copy a `struct path` into `buf`, returning the
135+
// length (including NUL) on success or a negative errno. Declared here because
136+
// the minimal compat shim only provides bpf_map_lookup_elem. When the real
137+
// libbpf headers are present they declare it for us, so only add it in shim
138+
// mode.
139+
#if !__has_include(<bpf/bpf_helpers.h>)
140+
static long (*bpf_d_path)(struct path *path, char *buf, __u32 sz) = (void *)147;
141+
#endif
142+
143+
static __always_inline __u8 op_from_fmode(unsigned int f_mode)
144+
{
145+
// Exec is the most restrictive intent; classify it first. Then write,
146+
// then default to read.
147+
if (f_mode & FMODE_EXEC_BIT)
148+
return OP_EXEC;
149+
if (f_mode & FMODE_WRITE_BIT)
150+
return OP_WRITE;
151+
return OP_READ;
152+
}
153+
154+
// Bounded comparison of the first `n` bytes of `a` and `b`.
155+
static __always_inline bool bytes_equal(const char *a, const char *b, __u32 n)
156+
{
157+
for (__u32 i = 0; i < MAX_PATH_PREFIX; i++) {
158+
if (i >= n)
159+
break;
160+
if (a[i] != b[i])
161+
return false;
162+
}
163+
return true;
164+
}
165+
166+
static __always_inline bool path_matches(const struct file_rule *r,
167+
const char *path, __u32 path_len)
168+
{
169+
__u32 plen = r->pattern_len;
170+
171+
if (r->path_kind == PATH_ANY)
172+
return true;
173+
174+
// A pattern we could not fully store cannot be verified soundly.
175+
if (plen == 0 || plen > MAX_PATH_PREFIX)
176+
return false;
177+
178+
if (r->path_kind == PATH_EXACT) {
179+
// Exact requires equal length AND equal bytes; otherwise a longer
180+
// path sharing a prefix could masquerade as the exact pattern.
181+
if (path_len != plen)
182+
return false;
183+
return bytes_equal(path, r->pattern, plen);
184+
}
185+
186+
if (r->path_kind == PATH_PREFIX) {
187+
// The path must be at least as long as the prefix and share it.
188+
if (path_len < plen)
189+
return false;
190+
return bytes_equal(path, r->pattern, plen);
191+
}
192+
193+
return false;
194+
}
195+
196+
static __always_inline bool op_matches(const struct file_rule *r, __u8 op)
197+
{
198+
if (r->op_kind == OP_MATCH_ANY)
199+
return true;
200+
return r->op_kind == OP_MATCH_EXACT && op == r->op_value;
201+
}
202+
203+
SEC("lsm/file_open")
204+
int regorus_file_open(unsigned long long *ctx)
205+
{
206+
// LSM/BTF programs receive a pointer to an array of u64 arguments. For
207+
// `file_open(struct file *file)` the file pointer is ctx[0]. (This is the
208+
// manual equivalent of libbpf's BPF_PROG() unwrapping, which the minimal
209+
// compat shim does not provide.)
210+
struct file *file = (struct file *)ctx[0];
211+
212+
__u32 zero = 0;
213+
214+
__u32 *enabled = bpf_map_lookup_elem(&file_enabled, &zero);
215+
if (enabled && *enabled == 0) {
216+
// Enforcement disabled: allow (fail-open is an explicit opt-in).
217+
return 0;
218+
}
219+
220+
char *buf = bpf_map_lookup_elem(&file_path_scratch, &zero);
221+
if (!buf)
222+
return -EPERM; // no scratch -> cannot evaluate -> fail closed
223+
224+
// Best-effort: read a bounded, NON-canonical path string. Direct field
225+
// access (`&file->f_path`) is CO-RE-relocated by clang against the BTF.
226+
long ret = bpf_d_path(&file->f_path, buf, MAX_PATH_PREFIX);
227+
if (ret <= 0)
228+
return -EPERM; // could not extract the path -> fail closed
229+
230+
// `ret` includes the trailing NUL; the byte length is ret - 1.
231+
__u32 path_len = (__u32)ret;
232+
if (path_len > 0)
233+
path_len -= 1;
234+
if (path_len > MAX_PATH_PREFIX)
235+
path_len = MAX_PATH_PREFIX;
236+
237+
__u8 op = op_from_fmode(file->f_mode);
238+
239+
__u32 *count_p = bpf_map_lookup_elem(&file_rule_count, &zero);
240+
__u32 count = count_p ? *count_p : 0;
241+
if (count > MAX_RULES)
242+
count = MAX_RULES;
243+
244+
int verdict = VERDICT_UNDECIDED;
245+
246+
for (__u32 i = 0; i < MAX_RULES; i++) {
247+
if (i >= count)
248+
break;
249+
struct file_rule *r = bpf_map_lookup_elem(&file_rules, &i);
250+
if (!r)
251+
continue;
252+
if (path_matches(r, buf, path_len) && op_matches(r, op)) {
253+
verdict = VERDICT_ALLOW;
254+
break;
255+
}
256+
}
257+
258+
// Boundary collapse: only ALLOW returns 0 (permit); UNDECIDED/DENY map to
259+
// -EPERM (block the open).
260+
return verdict == VERDICT_ALLOW ? 0 : -EPERM;
261+
}
262+
263+
char LICENSE[] SEC("license") = "GPL";
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Minimal, self-authored BPF compatibility shim.
5+
//
6+
// This provides just enough of the libbpf CO-RE conveniences (the SEC macro,
7+
// the BTF-defined map field macros, and the handful of helper prototypes /
8+
// byte-order builtins) for `egress.bpf.c` to compile to a valid BPF object with
9+
// nothing more than `clang -target bpf` and a `vmlinux.h` generated from the
10+
// running kernel's BTF.
11+
//
12+
// When the real libbpf headers are available (`<bpf/bpf_helpers.h>`), prefer
13+
// them — they are the canonical, fully-featured definitions. This shim exists
14+
// only so the deliverable is verifiable in minimal environments.
15+
16+
#ifndef REGORUS_BPF_COMPAT_H
17+
#define REGORUS_BPF_COMPAT_H
18+
19+
#if __has_include(<bpf/bpf_helpers.h>)
20+
21+
#include <bpf/bpf_helpers.h>
22+
#include <bpf/bpf_endian.h>
23+
24+
#else /* fall back to the self-contained shim */
25+
26+
#ifndef SEC
27+
#define SEC(name) __attribute__((section(name), used))
28+
#endif
29+
30+
#ifndef __always_inline
31+
#define __always_inline inline __attribute__((always_inline))
32+
#endif
33+
34+
/* BTF-defined map field macros (same shape libbpf uses). */
35+
#define __uint(name, val) int(*name)[val]
36+
#define __type(name, val) typeof(val) *name
37+
#define __array(name, val) typeof(val) *name[]
38+
39+
/* The few helpers this program calls, addressed by their stable helper IDs. */
40+
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *)1;
41+
42+
/* Byte-order conversion (network <-> host) for the BPF target. */
43+
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
44+
#define bpf_ntohl(x) __builtin_bswap32(x)
45+
#define bpf_ntohs(x) __builtin_bswap16(x)
46+
#define bpf_htonl(x) __builtin_bswap32(x)
47+
#define bpf_htons(x) __builtin_bswap16(x)
48+
#else
49+
#define bpf_ntohl(x) (x)
50+
#define bpf_ntohs(x) (x)
51+
#define bpf_htonl(x) (x)
52+
#define bpf_htons(x) (x)
53+
#endif
54+
55+
#endif /* __has_include */
56+
57+
#endif /* REGORUS_BPF_COMPAT_H */

0 commit comments

Comments
 (0)