Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 110 additions & 21 deletions crates/cardwire-daemon/src/analyzer/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)]
Expand Down Expand Up @@ -255,7 +257,7 @@ impl CardwireAnalyzer {
};
while let Some(item) = guard.get_inner_mut().next() {
if item.len() < std::mem::size_of::<ReportEvent>() {
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) };
Expand All @@ -268,27 +270,48 @@ 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 {
// Spawn in another task to prevent blocking the report logger while
// 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;
}
});
}
Expand Down Expand Up @@ -349,15 +372,16 @@ async fn report_blocked(
report_vec: Arc<RwLock<VecDeque<LogEntry>>>,
signal: Option<SignalEmitter<'static>>,
pid: u32,
gpu_id: u32,
name: String,
wayland_app_id: String,
) {
let log_entry = LogEntry {
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;
Expand All @@ -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
{
Expand Down Expand Up @@ -420,6 +451,18 @@ fn get_real_process_name(pid: u32) -> Option<String> {
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)]
Expand Down Expand Up @@ -565,29 +608,75 @@ mod tests {

#[test]
fn test_report_event_deserialization_from_valid_bytes() {
// ReportEvent: pid (4 bytes)
let item: Vec<u8> = vec![
// ReportEvent: pid (4 bytes) + gpu_id (4 bytes) + comm (16 bytes)
let mut item: Vec<u8> = 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::<ReportEvent>());
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<u8> = vec![0u8; 3];
assert!(item.len() < std::mem::size_of::<ReportEvent>());
}

#[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<u8> = vec![
0x39, 0x05, 0x00, 0x00, // pid = 1337
];
assert!(item.len() < std::mem::size_of::<ReportEvent>());
}

#[test]
fn test_report_event_deserialization_pid_extraction() {
let mut item: Vec<u8> = 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));
}
}
46 changes: 18 additions & 28 deletions crates/cardwire-ebpf/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,35 +49,34 @@ 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;
}

// 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
Expand All @@ -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;
}
}
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -153,7 +152,7 @@ fn get_task_ppid() -> Option<u32> {
#[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;
}
Expand All @@ -164,33 +163,24 @@ pub fn is_comm_whitelisted() -> bool {
#[inline(always)]
pub fn is_cardwired() -> Option<bool> {
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<bool> {
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<bool> {
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,
}
Expand Down
6 changes: 3 additions & 3 deletions crates/cardwire-ebpf/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ unsafe fn try_tracepoint_enter_getdents64(ctx: TracePointContext) -> Result<i32,

let dirp_ptr: u64 = unsafe { ctx.read_at(DIRP_OFFSET)? };

CW_DIRENT.insert(&pid, &dirp_ptr, 0)?;
CW_DIRENT.insert(pid, dirp_ptr, 0)?;

ReturnCode::SUCCESS
}
Expand All @@ -345,13 +345,13 @@ pub fn tracepoint_exit_getdents64(ctx: TracePointContext) -> u32 {

unsafe fn try_tracepoint_exit_getdents64(ctx: TracePointContext) -> Result<i32, i32> {
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::<i64>(16) } {
Ok(ret) => ret as u64,
Expand Down
Loading