1use std::mem::MaybeUninit;
17use std::os::unix::io::RawFd;
18use std::path::Path;
19
20use tracing::{debug, info, warn};
21
22use crate::error::{NucleusError, Result};
23
24const BPF_PROG_LOAD: u32 = 5;
28const BPF_PROG_ATTACH: u32 = 8;
30
31const BPF_PROG_TYPE_CGROUP_DEVICE: u32 = 9;
33
34const BPF_CGROUP_DEVICE: u32 = 6;
36
37const BPF_DEVCG_DEV_CHAR: u32 = 1;
39const BPF_DEVCG_DEV_BLOCK: u32 = 2;
40
41const DEV_READ: u32 = 1 << 0;
43const DEV_WRITE: u32 = 1 << 1;
44const DEV_MKNOD: u32 = 1 << 2;
45const DEV_ACCESS_ALL: u32 = DEV_READ | DEV_WRITE | DEV_MKNOD;
47
48const BPF_LD_W_ABS: u16 = 0x20; const BPF_JMP_JEQ_K: u16 = 0x15; const BPF_JMP_JSET_K: u16 = 0x45; const BPF_RET_K: u16 = 0x06; const ALLOW: u32 = 1;
60const DENY: u32 = 0;
61
62const OFF_ACCESS: u32 = 0;
64const OFF_TYPE: u32 = 4;
65const OFF_MAJOR: u32 = 8;
66const OFF_MINOR: u32 = 12;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct DeviceAllowSpec {
71 pub is_block: bool,
72 pub major: u32,
73 pub minor: u32,
74}
75
76impl DeviceAllowSpec {
77 fn device_type(&self) -> u32 {
78 if self.is_block {
79 BPF_DEVCG_DEV_BLOCK
80 } else {
81 BPF_DEVCG_DEV_CHAR
82 }
83 }
84}
85
86pub fn base_device_specs(include_tty: bool) -> Vec<DeviceAllowSpec> {
91 let mut specs = vec![
92 DeviceAllowSpec { is_block: false, major: 1, minor: 3 }, DeviceAllowSpec { is_block: false, major: 1, minor: 5 }, DeviceAllowSpec { is_block: false, major: 1, minor: 7 }, DeviceAllowSpec { is_block: false, major: 1, minor: 8 }, DeviceAllowSpec { is_block: false, major: 1, minor: 9 }, ];
98 if include_tty {
99 specs.push(DeviceAllowSpec { is_block: false, major: 5, minor: 0 }); }
101 specs
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub(crate) struct SockFilter {
107 code: u16,
108 jt: u8,
109 jf: u8,
110 k: u32,
111}
112
113impl SockFilter {
114 const fn ld_abs(off: u32) -> Self {
115 Self { code: BPF_LD_W_ABS, jt: 0, jf: 0, k: off }
116 }
117 const fn jeq_k(k: u32, jt: u8, jf: u8) -> Self {
118 Self { code: BPF_JMP_JEQ_K, jt, jf, k }
119 }
120 const fn jset_k(k: u32, jt: u8, jf: u8) -> Self {
121 Self { code: BPF_JMP_JSET_K, jt, jf, k }
122 }
123 const fn ret_k(k: u32) -> Self {
124 Self { code: BPF_RET_K, jt: 0, jf: 0, k }
125 }
126}
127
128const ENTRY_LEN: usize = 9;
130
131pub(crate) fn build_device_program(specs: &[DeviceAllowSpec]) -> Vec<SockFilter> {
147 let mut prog = Vec::with_capacity(specs.len() * ENTRY_LEN + 1);
148 for spec in specs {
149 prog.push(SockFilter::ld_abs(OFF_TYPE));
150 prog.push(SockFilter::jeq_k(spec.device_type(), 0, 7));
151 prog.push(SockFilter::ld_abs(OFF_MAJOR));
152 prog.push(SockFilter::jeq_k(spec.major, 0, 5));
153 prog.push(SockFilter::ld_abs(OFF_MINOR));
154 prog.push(SockFilter::jeq_k(spec.minor, 0, 3));
155 prog.push(SockFilter::ld_abs(OFF_ACCESS));
156 prog.push(SockFilter::jset_k(DEV_ACCESS_ALL, 0, 1));
157 prog.push(SockFilter::ret_k(ALLOW));
158 }
159 prog.push(SockFilter::ret_k(DENY));
160 prog
161}
162
163#[repr(C)]
167#[derive(Default)]
168struct BpfProgAttr {
169 prog_type: u32,
170 insn_cnt: u32,
171 insns: u64,
172 license: u64,
173 log_level: u32,
174 log_size: u32,
175 log_buf: u64,
176 kern_version: u32,
177 prog_flags: u32,
178}
179
180#[repr(C)]
182struct BpfAttachAttr {
183 target_fd: u32,
184 attach_bpf_fd: u32,
185 attach_type: u32,
186 attach_flags: u32,
187}
188
189pub fn install_device_allowlist(
195 cgroup_path: &Path,
196 specs: &[DeviceAllowSpec],
197 best_effort: bool,
198) -> Result<bool> {
199 if specs.is_empty() {
200 return Ok(false);
201 }
202
203 let prog = build_device_program(specs);
204 debug!(
205 "cgroup device program: {} entries, {} instructions",
206 specs.len(),
207 prog.len()
208 );
209
210 let prog_fd = match load_device_program(&prog, best_effort) {
211 Ok(fd) => fd,
212 Err(e) => {
213 if best_effort {
214 warn!(
215 "Failed to load cgroup device BPF (continuing without device allowlist): {}",
216 e
217 );
218 return Ok(false);
219 }
220 return Err(NucleusError::CgroupError(format!(
221 "Failed to load cgroup device BPF: {}",
222 e
223 )));
224 }
225 };
226
227 let cgroup_fd = match open_cgroup(cgroup_path) {
228 Ok(fd) => fd,
229 Err(e) => {
230 close_fd(prog_fd);
231 if best_effort {
232 warn!("Failed to open cgroup for device BPF attach: {}", e);
233 return Ok(false);
234 }
235 return Err(NucleusError::CgroupError(format!(
236 "Failed to open cgroup {:?}: {}",
237 cgroup_path, e
238 )));
239 }
240 };
241
242 let attached = match attach_program(cgroup_fd, prog_fd) {
243 Ok(()) => true,
244 Err(e) => {
245 if best_effort {
246 warn!(
247 "Failed to attach cgroup device BPF (continuing without device allowlist): {}",
248 e
249 );
250 false
251 } else {
252 close_fd(prog_fd);
253 close_fd(cgroup_fd);
254 return Err(NucleusError::CgroupError(format!(
255 "Failed to attach cgroup device BPF: {}",
256 e
257 )));
258 }
259 }
260 };
261
262 close_fd(prog_fd);
264 close_fd(cgroup_fd);
265
266 if attached {
267 info!(
268 "Installed cgroup device allowlist ({} devices) at {:?}",
269 specs.len(),
270 cgroup_path
271 );
272 }
273 Ok(attached)
274}
275
276fn load_device_program(prog: &[SockFilter], best_effort: bool) -> Result<RawFd> {
277 let license = b"GPL\0";
282 let mut log_buf = vec![0u8; 4096];
283 let attr = BpfProgAttr {
284 prog_type: BPF_PROG_TYPE_CGROUP_DEVICE,
285 insn_cnt: prog.len() as u32,
286 insns: prog.as_ptr() as u64,
287 license: license.as_ptr() as u64,
288 log_level: if best_effort { 0 } else { 1 },
289 log_size: log_buf.len() as u32,
290 log_buf: log_buf.as_mut_ptr() as u64,
291 ..Default::default()
292 };
293
294 let ret = unsafe {
297 libc::syscall(
298 libc::SYS_bpf,
299 BPF_PROG_LOAD as libc::c_int,
300 &attr as *const BpfProgAttr,
301 std::mem::size_of::<BpfProgAttr>() as libc::c_int,
302 )
303 };
304 if ret < 0 {
305 let err = std::io::Error::last_os_error();
306 if !log_buf.is_empty() && log_buf[0] != 0 {
307 let msg = String::from_utf8_lossy(&log_buf);
308 return Err(NucleusError::CgroupError(format!(
309 "bpf(PROG_LOAD) failed: {} ({})",
310 err,
311 msg.trim_end_matches('\0')
312 )));
313 }
314 return Err(NucleusError::CgroupError(format!(
315 "bpf(PROG_LOAD) failed: {}",
316 err
317 )));
318 }
319 Ok(ret as RawFd)
320}
321
322fn open_cgroup(path: &Path) -> Result<RawFd> {
323 let cstr = std::ffi::CString::new(path.to_string_lossy().to_string()).map_err(|e| {
324 NucleusError::CgroupError(format!("cgroup path contained NUL: {}", e))
325 })?;
326 let fd = unsafe {
328 libc::open(
329 cstring_ptr(&cstr),
330 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
331 )
332 };
333 if fd < 0 {
334 return Err(NucleusError::CgroupError(format!(
335 "open({:?}) failed: {}",
336 path,
337 std::io::Error::last_os_error()
338 )));
339 }
340 Ok(fd)
341}
342
343fn attach_program(cgroup_fd: RawFd, prog_fd: RawFd) -> Result<()> {
344 let attr = BpfAttachAttr {
345 target_fd: cgroup_fd as u32,
346 attach_bpf_fd: prog_fd as u32,
347 attach_type: BPF_CGROUP_DEVICE,
348 attach_flags: 0,
349 };
350 let ret = unsafe {
352 libc::syscall(
353 libc::SYS_bpf,
354 BPF_PROG_ATTACH as libc::c_int,
355 &attr as *const BpfAttachAttr,
356 std::mem::size_of::<BpfAttachAttr>() as libc::c_int,
357 )
358 };
359 if ret < 0 {
360 return Err(NucleusError::CgroupError(format!(
361 "bpf(PROG_ATTACH) failed: {}",
362 std::io::Error::last_os_error()
363 )));
364 }
365 Ok(())
366}
367
368fn close_fd(fd: RawFd) {
369 if fd >= 0 {
370 unsafe { libc::close(fd) };
372 }
373}
374
375fn cstring_ptr(c: &std::ffi::CString) -> *const libc::c_char {
379 c.as_ptr()
380}
381
382#[allow(dead_code)]
384fn _use_maybeuninit() -> MaybeUninit<u8> {
385 MaybeUninit::uninit()
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn program_is_deny_by_default_when_empty() {
394 let prog = build_device_program(&[]);
395 assert_eq!(prog.len(), 1);
396 assert_eq!(prog[0], SockFilter::ret_k(DENY));
397 }
398
399 #[test]
400 fn each_entry_is_nine_instructions_uniform() {
401 let specs = base_device_specs(true);
402 let prog = build_device_program(&specs);
403 assert_eq!(prog.len(), specs.len() * ENTRY_LEN + 1);
404
405 assert_eq!(prog.last().copied(), Some(SockFilter::ret_k(DENY)));
407
408 let e = &specs[0];
410 assert_eq!(prog[0], SockFilter::ld_abs(OFF_TYPE));
411 assert_eq!(prog[1], SockFilter::jeq_k(e.device_type(), 0, 7));
412 assert_eq!(prog[2], SockFilter::ld_abs(OFF_MAJOR));
413 assert_eq!(prog[3], SockFilter::jeq_k(e.major, 0, 5));
414 assert_eq!(prog[4], SockFilter::ld_abs(OFF_MINOR));
415 assert_eq!(prog[5], SockFilter::jeq_k(e.minor, 0, 3));
416 assert_eq!(prog[6], SockFilter::ld_abs(OFF_ACCESS));
417 assert_eq!(prog[7], SockFilter::jset_k(DEV_ACCESS_ALL, 0, 1));
418 assert_eq!(prog[8], SockFilter::ret_k(ALLOW));
419 }
420
421 #[test]
422 fn jump_offsets_reach_next_entry() {
423 let specs = vec![
426 DeviceAllowSpec { is_block: false, major: 195, minor: 0 },
427 DeviceAllowSpec { is_block: false, major: 226, minor: 128 },
428 ];
429 let prog = build_device_program(&specs);
430 assert_eq!(prog.len(), 2 * ENTRY_LEN + 1);
431 assert_eq!(prog[1].jf, 7);
433 assert_eq!(prog[ENTRY_LEN], SockFilter::ld_abs(OFF_TYPE));
435 assert_eq!(prog[2 * ENTRY_LEN], SockFilter::ret_k(DENY));
437 }
438
439 #[test]
440 fn base_specs_match_created_dev_nodes() {
441 let specs = base_device_specs(false);
442 let as_pairs: Vec<(u32, u32)> = specs.iter().map(|s| (s.major, s.minor)).collect();
443 assert_eq!(
444 as_pairs,
445 vec![(1, 3), (1, 5), (1, 7), (1, 8), (1, 9)]
446 );
447 let with_tty = base_device_specs(true);
448 assert_eq!(with_tty.len(), 6);
449 assert!(with_tty.iter().any(|s| s.major == 5 && s.minor == 0));
450 }
451
452 #[test]
453 fn block_device_uses_block_type() {
454 let spec = DeviceAllowSpec { is_block: true, major: 8, minor: 0 };
455 assert_eq!(spec.device_type(), BPF_DEVCG_DEV_BLOCK);
456 let char_spec = DeviceAllowSpec { is_block: false, major: 1, minor: 3 };
457 assert_eq!(char_spec.device_type(), BPF_DEVCG_DEV_CHAR);
458 }
459}