From 673e4ea49dfca47553305a4d23dfbfba55648469 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 15:23:27 +0200 Subject: [PATCH 1/2] feat(cardwire-ebpf): add new fields to Report Event and fix clippy warning --- crates/cardwire-ebpf/src/helpers.rs | 46 +++++++++++------------------ crates/cardwire-ebpf/src/main.rs | 6 ++-- crates/cardwire-ebpf/src/maps.rs | 3 ++ 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/crates/cardwire-ebpf/src/helpers.rs b/crates/cardwire-ebpf/src/helpers.rs index fa7ad4a6..15106f3b 100644 --- a/crates/cardwire-ebpf/src/helpers.rs +++ b/crates/cardwire-ebpf/src/helpers.rs @@ -18,14 +18,14 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { 'inode_check: { // Check if the inode is in the blocked list - if let Some(v) = unsafe { CW_BLOCKED_INO.get(&inode) } { + if let Some(v) = unsafe { CW_BLOCKED_INO.get(inode) } { blocked = true; ino_gpu_id = *v; break 'inode_check; } // We didn't match any inode, try with nvidia inodes if unsafe { is_nvidia_setting_enabled() } - && let Some(v) = unsafe { CW_EXP_BLK_INO.get(&inode) } + && let Some(v) = unsafe { CW_EXP_BLK_INO.get(inode) } { blocked = true; ino_gpu_id = *v; @@ -49,27 +49,26 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { // If everything ok, read the pid let pid: u32 = (bpf_get_current_pid_tgid() >> 32) as u32; + let comm = bpf_get_current_comm().unwrap_or([0u8; 16]); + if *mode == INTEGRATED || *mode == MANUAL { // if integrated/manual, just report the event and block - report_event(pid); + report_event(pid, ino_gpu_id, comm); return true; } // 0 = iGPU // 1 = dGPU if *mode == SMART { - let ppid = match get_task_ppid() { - Some(ppid) => ppid, - None => u32::MAX, - }; + let ppid = get_task_ppid().unwrap_or(u32::MAX); // We need to check if the map contains the pid // In smart mode, we do not check if the ino_gpu_id matches, it was only made for dual // gpu(hybrid) laptops // First we try with the pid - if unsafe { CW_ALLOWED_PID.get(&pid).is_some() } - || unsafe { CW_ALLOWED_PID.get(&ppid).is_some() } + if unsafe { CW_ALLOWED_PID.get(pid).is_some() } + || unsafe { CW_ALLOWED_PID.get(ppid).is_some() } { // We got a match, pid is allowed ! break 'end; @@ -77,7 +76,7 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { // If we are here, the pid AND the ppid are not in the allowed map, check the FORCED map let forced_gpu_id = - unsafe { CW_FORCED_PID.get(&pid).or_else(|| CW_FORCED_PID.get(&ppid)) }; + unsafe { CW_FORCED_PID.get(pid).or_else(|| CW_FORCED_PID.get(ppid)) }; if let Some(pid_gpu_id) = forced_gpu_id { // We match the ino_gpu_id with the pid_gpu_id @@ -89,7 +88,7 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { // Process should only be allowed to see the said GPU id false => { // Report the event to the daemon - report_event(pid); + report_event(pid, ino_gpu_id, comm); return true; } } @@ -103,7 +102,7 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { } // Report the event to the daemon - report_event(pid); + report_event(pid, ino_gpu_id, comm); // End of smart mode check, block if it didnt get allowed earlier return true; @@ -114,9 +113,9 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { } #[inline(always)] -fn report_event(pid: u32) { +fn report_event(pid: u32, gpu_id: u32, comm: [u8; 16]) { if let Some(mut ring_buf) = CW_REPORT_EVENTS.reserve(0) { - let event: ReportEvent = ReportEvent { pid }; + let event: ReportEvent = ReportEvent { pid, gpu_id, comm }; // write to the map ring_buf.write(event); // submit @@ -153,7 +152,7 @@ fn get_task_ppid() -> Option { #[inline(always)] pub fn is_comm_whitelisted() -> bool { if let Ok(comm) = bpf_get_current_comm() - && unsafe { CW_ALLOWED_COMM.get(&comm).is_some() } + && unsafe { CW_ALLOWED_COMM.get(comm).is_some() } { return true; } @@ -164,33 +163,24 @@ pub fn is_comm_whitelisted() -> bool { #[inline(always)] pub fn is_cardwired() -> Option { let proc_pid = (bpf_get_current_pid_tgid() >> 32) as u32; - match CW_DAEMON_PID.get(DAEMON_INDEX) { - Some(pid) => Some(proc_pid == *pid), - None => None, - } + CW_DAEMON_PID.get(DAEMON_INDEX).map(|pid| proc_pid == *pid) } /// Verify if the current device mode is hybrid, returns None if the map fails #[inline(always)] pub unsafe fn is_hybrid() -> Option { - match CW_MODE.get(MODE_INDEX) { - Some(mode) => Some(*mode == HYBRID), - None => None, - } + CW_MODE.get(MODE_INDEX).map(|mode| *mode == HYBRID) } /// Verify if the current device mode is smart, returns None if the map fails #[inline(always)] pub unsafe fn is_smart() -> Option { - match CW_MODE.get(MODE_INDEX) { - Some(mode) => Some(*mode == SMART), - None => None, - } + CW_MODE.get(MODE_INDEX).map(|mode| *mode == SMART) } #[inline(always)] pub unsafe fn is_nvidia_setting_enabled() -> bool { - match unsafe { CW_SETTINGS.get(&CardwiredSetting::EXP_NVIDIA) } { + match unsafe { CW_SETTINGS.get(CardwiredSetting::EXP_NVIDIA) } { Some(setting) => *setting, None => false, } diff --git a/crates/cardwire-ebpf/src/main.rs b/crates/cardwire-ebpf/src/main.rs index 08599448..61ace756 100644 --- a/crates/cardwire-ebpf/src/main.rs +++ b/crates/cardwire-ebpf/src/main.rs @@ -330,7 +330,7 @@ unsafe fn try_tracepoint_enter_getdents64(ctx: TracePointContext) -> Result u32 { unsafe fn try_tracepoint_exit_getdents64(ctx: TracePointContext) -> Result { let pid = (bpf_get_current_pid_tgid() >> 32) as u32; - let dirent_ptr = match unsafe { CW_DIRENT.get(&pid) } { + let dirent_ptr = match unsafe { CW_DIRENT.get(pid) } { Some(ptr) => *ptr as *const linux_dirent64, None => return ReturnCode::SUCCESS, }; // Remove entry immediately to avoid map leak - let _ = CW_DIRENT.remove(&pid); + let _ = CW_DIRENT.remove(pid); let retval = match unsafe { ctx.read_at::(16) } { Ok(ret) => ret as u64, diff --git a/crates/cardwire-ebpf/src/maps.rs b/crates/cardwire-ebpf/src/maps.rs index eca5f5fa..317507c8 100644 --- a/crates/cardwire-ebpf/src/maps.rs +++ b/crates/cardwire-ebpf/src/maps.rs @@ -89,10 +89,13 @@ pub struct CloseEvent { #[btf_map] pub static CW_CLOSE_EVENTS: RingBuf = RingBuf::new(); +#[repr(C)] #[repr(align(8))] #[allow(dead_code)] pub struct ReportEvent { pub pid: u32, + pub gpu_id: u32, + pub comm: [u8; 16], } #[btf_map] From 0460fb2e8092a212e40fac904ae05cc028c284e0 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 15:23:54 +0200 Subject: [PATCH 2/2] feat(cardwired): implement new report fields --- crates/cardwire-daemon/src/analyzer/models.rs | 131 +++++++++++++++--- 1 file changed, 110 insertions(+), 21 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 62ae634f..4a9280af 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -3,7 +3,7 @@ use aya_log::EbpfLogger; use cardwire_ebpf_userspace::EbpfBlocker; use log::{Log, debug, error, info, warn}; use std::{ - collections::{HashMap, HashSet, VecDeque}, fs, path::PathBuf, ptr, sync::Arc, time::SystemTime + collections::{HashMap, HashSet, VecDeque}, fs, path::{Path, PathBuf}, ptr, sync::Arc, time::SystemTime }; use tokio::{ io::{Interest, unix::AsyncFd}, sync::{Mutex, RwLock, Semaphore}, task, time::Instant @@ -29,16 +29,18 @@ pub struct CloseEvent { pub pid: u32, } -#[derive(Debug, Copy, Clone)] -enum PidType { - Allowed, - Forced, -} - #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct ReportEvent { pub pid: u32, + pub gpu_id: u32, + pub comm: [u8; 16], +} + +#[derive(Debug, Copy, Clone)] +enum PidType { + Allowed, + Forced, } #[derive(Clone)] @@ -255,7 +257,7 @@ impl CardwireAnalyzer { }; while let Some(item) = guard.get_inner_mut().next() { if item.len() < std::mem::size_of::() { - debug!("Skipping malformed report event. Size: {}", item.len()); + warn!("Skipping malformed report event. Size: {}", item.len()); continue; } let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ReportEvent) }; @@ -268,6 +270,7 @@ impl CardwireAnalyzer { reported_pids.insert(event.pid); } } + let event_comm_str = comm_to_string(event.comm); // Bound the number of concurrent report tasks, this prevent exausting the process // FD limits if let Ok(permit) = report_semaphore.clone().acquire_owned().await { @@ -275,20 +278,40 @@ impl CardwireAnalyzer { // fetching informations about this process let report_vec = report_vec.clone(); let signal = self.signal.clone(); + let gpu_id = event.gpu_id; task::spawn(async move { let _permit = permit; if let Some(app_id) = get_app_id_wayland_with_retry(event.pid).await { - report_blocked(report_vec, signal, event.pid, app_id.clone(), app_id) - .await; + report_blocked( + report_vec, + signal, + event.pid, + gpu_id, + event_comm_str, + app_id, + ) + .await; } else if let Some(process_name) = get_real_process_name(event.pid) { report_blocked( report_vec, signal, event.pid, + gpu_id, process_name, String::new(), ) .await; + // we check if the proc is still here to not log noise caused by fish + } else if is_proc_still_alive(event.pid) { + report_blocked( + report_vec, + signal, + event.pid, + gpu_id, + event_comm_str, + String::new(), + ) + .await; } }); } @@ -349,6 +372,7 @@ async fn report_blocked( report_vec: Arc>>, signal: Option>, pid: u32, + gpu_id: u32, name: String, wayland_app_id: String, ) { @@ -356,8 +380,8 @@ async fn report_blocked( timestamp: SystemTime::now(), pid, comm: name.clone(), - gpu_id: 1, - wayland_app_id, + gpu_id, + wayland_app_id: wayland_app_id.clone(), }; { let mut report_vec = report_vec.write().await; @@ -366,10 +390,17 @@ async fn report_blocked( report_vec.pop_front(); } } - info!( - "{}[{}] tried to access the dGPU (blocked by cardwire)", - name, pid - ); + if wayland_app_id.is_empty() { + info!( + "{}[{}] tried to access GPU {} (blocked by cardwire)", + name, pid, gpu_id + ); + } else { + info!( + "{}[{}] tried to access GPU {} (blocked by cardwire)", + wayland_app_id, pid, gpu_id + ); + } if let Some(signal) = signal && let Err(e) = LoggerInterfaceSignals::process_blocked_changed(&signal, log_entry).await { @@ -420,6 +451,18 @@ fn get_real_process_name(pid: u32) -> Option { Some(base_name.to_string()) } +fn is_proc_still_alive(pid: u32) -> bool { + Path::new(&format!("/proc/{}", pid)).exists() +} + +/// Decode the 16-byte kernel comm into a String, trimming trailing NULs +fn comm_to_string(comm: [u8; 16]) -> String { + match String::from_utf8(comm.to_vec()) { + Ok(str) => str.trim_end_matches('\0').to_string(), + Err(_) => "no_comm_err".to_string(), + } +} + // TESTS #[cfg(test)] @@ -565,29 +608,75 @@ mod tests { #[test] fn test_report_event_deserialization_from_valid_bytes() { - // ReportEvent: pid (4 bytes) - let item: Vec = vec![ + // ReportEvent: pid (4 bytes) + gpu_id (4 bytes) + comm (16 bytes) + let mut item: Vec = vec![ 0x39, 0x05, 0x00, 0x00, // pid = 1337 + 0x01, 0x00, 0x00, 0x00, // gpu_id = 1 ]; - assert_eq!(item.len(), 4); + item.extend_from_slice(b"test_comm\0\0\0\0\0\0\0"); assert!(item.len() >= std::mem::size_of::()); let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ReportEvent) }; assert_eq!(event.pid, 1337); + assert_eq!(event.gpu_id, 1); + assert_eq!(&event.comm, b"test_comm\0\0\0\0\0\0\0"); + assert_eq!(comm_to_string(event.comm), "test_comm"); } #[test] fn test_report_event_deserialization_rejects_undersized_buffer() { - // ReportEvent needs 4 bytes, give only 3 + // ReportEvent needs 24 bytes, give only 3 let item: Vec = vec![0u8; 3]; assert!(item.len() < std::mem::size_of::()); } #[test] - fn test_report_event_deserialization_pid_extraction() { + fn test_report_event_rejects_old_4_byte_layout() { + // The old ReportEvent layout was only 4 bytes (pid), the ring reader + // must reject it now that the event carries gpu_id and comm let item: Vec = vec![ + 0x39, 0x05, 0x00, 0x00, // pid = 1337 + ]; + assert!(item.len() < std::mem::size_of::()); + } + + #[test] + fn test_report_event_deserialization_pid_extraction() { + let mut item: Vec = vec![ 0x01, 0x00, 0x00, 0x00, // pid = 1 + 0x02, 0x00, 0x00, 0x00, // gpu_id = 2 ]; + item.extend_from_slice(&[0u8; 16]); let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ReportEvent) }; assert_eq!(event.pid, 1); + assert_eq!(event.gpu_id, 2); + assert_eq!(&event.comm, &[0u8; 16]); + } + + // ── comm_to_string ─────────────────────────────────────────────── + + #[test] + fn test_comm_to_string_trims_trailing_nuls() { + let comm = *b"bash\0\0\0\0\0\0\0\0\0\0\0\0"; + assert_eq!(comm_to_string(comm), "bash"); + } + + #[test] + fn test_comm_to_string_full_length() { + let comm = *b"a-very-long-comm"; + assert_eq!(comm_to_string(comm), "a-very-long-comm"); + } + + #[test] + fn test_comm_to_string_invalid_utf8() { + let comm = [0xFFu8; 16]; + assert_eq!(comm_to_string(comm), "no_comm_err"); + } + + // ── is_proc_still_alive ────────────────────────────────────────── + + #[test] + fn test_is_proc_still_alive() { + assert!(is_proc_still_alive(std::process::id())); + assert!(!is_proc_still_alive(0)); } }