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
11 changes: 9 additions & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,20 @@ jobs:
with:
tool_name: rustfmt
vm-test:
name: VM Test (${{ matrix.vm }})
runs-on: ubuntu-latest
needs: [prepare, rust-lint, rust-test, rust-format]
needs: [prepare]
if: ${{ !failure() && !cancelled() && needs.prepare.outputs.run_nix_vm == 'true' }}
strategy:
fail-fast: false
matrix:
vm: [vm-ci-2gpu, vm-ci-3gpu, vm-ci-15gpu]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
luytan marked this conversation as resolved.
with:
persist-credentials: false
- uses: determinateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
- run: nix build .#vm-test
- run: nix build .#checks.x86_64-linux.${{ matrix.vm }}
mdbook-test:
runs-on: ubuntu-latest
needs: [prepare]
Expand Down
22 changes: 18 additions & 4 deletions crates/cardwire-daemon/src/analyzer/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use crate::{
#[derive(Debug, Copy, Clone)]
pub struct ExecEvent {
pub pid: u32,
pub mode: u8,
pub _padding: [u8; 3],
}

#[repr(C)]
Expand Down Expand Up @@ -176,7 +178,9 @@ impl CardwireAnalyzer {
Some(name) => name,
None => return,
};
if let Some(result) = self.evaluate_app(event.pid, &real_app_name).await
if let Some(result) = self
.evaluate_app(event.pid, &real_app_name, event.mode)
.await
&& result.0
{
match result.1 {
Expand Down Expand Up @@ -286,7 +290,7 @@ impl CardwireAnalyzer {

/// Default app are blocked, try to find if it's a game or a gpu intensive app, the u8 is the
/// gpu id
async fn evaluate_app(&self, pid: u32, comm: &str) -> Option<(bool, PidType, u32)> {
async fn evaluate_app(&self, pid: u32, comm: &str, mode: u8) -> Option<(bool, PidType, u32)> {
let path = format!("/proc/{}/environ", pid);
let environ = match fs::read(path) {
Ok(content) => content,
Expand All @@ -303,6 +307,11 @@ impl CardwireAnalyzer {
return Some((true, PidType::Forced, value));
}

// If manual mode, do not process app discovery or database policies
if mode == 2 {
return None;
}

// Check the database now, we can take our time since if we reached it, the app would've
// been blocked
let mut lookup_name = comm.to_lowercase();
Expand Down Expand Up @@ -468,24 +477,29 @@ mod tests {
fn test_event_deserialization_from_valid_bytes() {
let item: Vec<u8> = vec![
0x01, 0x00, 0x00, 0x00, // pid = 1
0x03, 0x00, 0x00, 0x00, // mode = 3 (Smart)
];
assert!(item.len() >= std::mem::size_of::<ExecEvent>());
let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ExecEvent) };
assert_eq!(event.pid, 1);
assert_eq!(event.mode, 3);
}

#[test]
fn test_event_deserialization_rejects_undersized_buffer() {
let item: Vec<u8> = vec![0x01, 0x00, 0x00]; // 3 bytes, Event needs 4
let item: Vec<u8> = vec![0x01, 0x00, 0x00]; // 3 bytes, Event needs 8
assert!(item.len() < std::mem::size_of::<ExecEvent>());
}

#[test]
fn test_event_deserialization_with_large_pid() {
// pid = 0xFFFFFFFF (u32::MAX)
let item: Vec<u8> = vec![0xFF, 0xFF, 0xFF, 0xFF];
let item: Vec<u8> = vec![
0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x00, 0x00, 0x00, // mode = 2 (Manual)
];
let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ExecEvent) };
assert_eq!(event.pid, u32::MAX);
assert_eq!(event.mode, 2);
}

// ── ReportEvent ──────────────────────────────────────────────────
Expand Down
25 changes: 12 additions & 13 deletions crates/cardwire-daemon/src/core/gpu/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,15 @@ use log::{info, warn};
use std::{fs, io, path::Path, time::Duration};
use udev::{Device, Enumerator};

const NON_PHYSICAL: &[&str] = &["Virtual-", "Unknown-", "Writeback-"];
const INTERNAL_PANELS: &[&str] = &["eDP-", "LVDS-", "DSI-", "DPI-", "SPI-"];

/// Return whether a DRM card currently owns a connected physical external display.
///
/// Connector ownership is encoded in sysfs names such as `card1-HDMI-A-1`. Internal panels and
/// virtual connectors are excluded so only physical external outputs keep the card available.
#[allow(dead_code)]
pub fn external_display_connected(card: u32) -> io::Result<bool> {
// These connector types are internal panels or do not represent a physical display output.
const NON_EXTERNAL: &[&str] = &[
"eDP-",
"LVDS-",
"DSI-",
"DPI-",
"SPI-",
"Virtual-",
"Unknown-",
"Writeback-",
];
let card_prefix = format!("card{card}-");
// An unreadable status is not proof of a disconnect. Keep the first error while checking
// whether another connector can still confirm that the card is in use.
Expand All @@ -34,7 +26,10 @@ pub fn external_display_connected(card: u32) -> io::Result<bool> {
continue;
};
if connector.is_empty()
|| NON_EXTERNAL
|| NON_PHYSICAL
.iter()
.any(|prefix| connector.starts_with(prefix))
|| INTERNAL_PANELS
.iter()
.any(|prefix| connector.starts_with(prefix))
{
Expand Down Expand Up @@ -135,7 +130,11 @@ pub async fn is_gpu_active(card: u32) -> Option<bool> {
let Some(connector) = name.strip_prefix(&prefix) else {
continue;
};
if connector.is_empty() {
if connector.is_empty()
|| NON_PHYSICAL
.iter()
.any(|prefix| connector.starts_with(prefix))
{
continue;
}
match tokio::fs::read_to_string(entry.path().join("status")).await {
Expand Down
6 changes: 1 addition & 5 deletions crates/cardwire-daemon/src/interface/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl GpuInterface {
let mut blocker = self.blocker.write().await;

for inode in inodes.iter() {
blocker.unblock_inode(*inode).into_fdo()?;
blocker.unblock_inode(*inode, self.id).into_fdo()?;
}
Ok(())
}
Expand Down Expand Up @@ -251,10 +251,6 @@ impl GpuInterface {

#[zbus(property)]
pub async fn block(&self) -> fdo::Result<bool> {
let mode = self.mode_state.read().await.mode();
if mode == Modes::Smart && (self.device.is_default() && !self.device.is_discrete()) {
return Ok(false);
}
self.gpu_blocked().await
}

Expand Down
2 changes: 1 addition & 1 deletion crates/cardwire-daemon/src/interface/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl ModeInterface {
&& !gpu.device.is_discrete()
{
// push default gpu (iGPU) into the blocked inode map for tracking only
gpu.block_gpu(*id as u32).await?;
gpu.unblock_gpu().await?;
}
}
}
Expand Down
59 changes: 40 additions & 19 deletions crates/cardwire-ebpf-userspace/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ pub struct EbpfBlocker {
pub forced_map: Arc<RwLock<HashMap<aya::maps::MapData, u32, u32>>>,
}

#[repr(C)]
#[derive(Copy, Clone)]
pub struct InodeState {
pub gpu_id: u32,
pub blocked: u8,
pub _padding: [u8; 3], // 8-byte alignment
}
unsafe impl aya::Pod for InodeState {}

impl EbpfBlocker {
pub fn new() -> CardwireEbpfResult<Self> {
// quit if bpf is not enabled
Expand Down Expand Up @@ -173,47 +182,59 @@ impl EbpfBlocker {
}

/// Block an inode, value is the associated GPU id
pub fn block_inode(&mut self, inode: u64, value: u32) -> CardwireEbpfResult<()> {
// Also insert hardcoded values for now
let mut inode_map: HashMap<_, u64, u32> = HashMap::try_from(
pub fn block_inode(&mut self, inode: u64, gpu_id: u32) -> CardwireEbpfResult<()> {
let mut inode_map: HashMap<_, u64, InodeState> = HashMap::try_from(
self.ebpf
.map_mut("CW_BLOCKED_INO")
.ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?,
)
.map_err(CardwireEbpfError::aya)?;
inode_map
.insert(inode, value, 0)
.insert(
inode,
InodeState {
gpu_id,
blocked: 1,
_padding: [0; 3],
},
0,
)
.map_err(CardwireEbpfError::aya)?;
Ok(())
}
pub fn unblock_inode(&mut self, inode: u64) -> CardwireEbpfResult<()> {
// Also insert hardcoded values for now
let mut inode_map: HashMap<_, u64, u32> = HashMap::try_from(

pub fn unblock_inode(&mut self, inode: u64, gpu_id: u32) -> CardwireEbpfResult<()> {
let mut inode_map: HashMap<_, u64, InodeState> = HashMap::try_from(
self.ebpf
.map_mut("CW_BLOCKED_INO")
.ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?,
)
.map_err(CardwireEbpfError::aya)?;
match inode_map.get(&inode, 0) {
// Ok = key found, remove
Ok(_) => inode_map.remove(&inode).map_err(CardwireEbpfError::aya),
// key not found, skip
Err(MapError::KeyNotFound) => Ok(()),
Err(err) => Err(CardwireEbpfError::aya(err)),
}
// Keep the inode in the map for tracking, but set blocked to 0
inode_map
.insert(
inode,
InodeState {
gpu_id,
blocked: 0,
_padding: [0; 3],
},
0,
)
.map_err(CardwireEbpfError::aya)?;
Ok(())
}

pub fn is_inode_blocked(&self, inode: u64, value: u32) -> CardwireEbpfResult<bool> {
// Also insert hardcoded values for now
let inode_map: HashMap<_, u64, u32> = HashMap::try_from(
pub fn is_inode_blocked(&self, inode: u64, gpu_id: u32) -> CardwireEbpfResult<bool> {
let inode_map: HashMap<_, u64, InodeState> = HashMap::try_from(
self.ebpf
.map("CW_BLOCKED_INO")
.ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?,
)
.map_err(CardwireEbpfError::aya)?;

match inode_map.get(&inode, 0) {
// if value (gpu key associed to inode) = our func value
Ok(map_value) => Ok(value == map_value),
Ok(state) => Ok(state.gpu_id == gpu_id && state.blocked == 1),
Err(MapError::KeyNotFound) => Ok(false),
Err(err) => Err(CardwireEbpfError::aya(err)),
}
Expand Down
51 changes: 44 additions & 7 deletions crates/cardwire-ebpf/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,32 @@ use crate::vmlinux::task_struct;
/// Verify if the inode is inside CW_BLOCKED_INO or not
#[inline(always)]
pub unsafe fn is_inode_blocked(inode: u64) -> bool {
let mut blocked: bool = false;
let mut tracked: bool = false;
let mut ino_gpu_id: u32 = 0;
let mut blocked: bool = false;

'inode_check: {
// Check if the inode is in the blocked list
if let Some(v) = unsafe { CW_BLOCKED_INO.get(inode) } {
blocked = true;
ino_gpu_id = *v;
tracked = true;
ino_gpu_id = v.gpu_id;
blocked = v.blocked == 1;
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) }
{
blocked = true;
tracked = true;
ino_gpu_id = *v;
// Nvidia experimental inodes are considered globally blocked for now if in map
blocked = true;
break 'inode_check;
}
}

'end: {
if !blocked {
if !tracked {
// exit and return success
break 'end;
}
Expand All @@ -51,12 +55,39 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool {

let comm = bpf_get_current_comm().unwrap_or([0u8; 16]);

if *mode == INTEGRATED || *mode == MANUAL {
// if integrated/manual, just report the event and block
if *mode == INTEGRATED && blocked {
// if integrated, block and report
report_event(pid, ino_gpu_id, comm);
return true;
}

if *mode == MANUAL {
let ppid = get_task_ppid().unwrap_or(u32::MAX);

// Check if the PID or PPID is in the forced map
let forced_gpu_id =
unsafe { CW_FORCED_PID.get(pid).or_else(|| CW_FORCED_PID.get(ppid)) };

if let Some(pid_gpu_id) = forced_gpu_id {
// If forced GPU ID matches the inode's GPU ID, allow access
match *pid_gpu_id == ino_gpu_id {
true => break 'end,
false => {
report_event(pid, ino_gpu_id, comm);
return true;
}
}
}

// Normal process behavior: block access if its blocked
if blocked {
report_event(pid, ino_gpu_id, comm);
return true;
} else {
break 'end;
}
}

// 0 = iGPU
// 1 = dGPU
if *mode == SMART {
Expand Down Expand Up @@ -178,6 +209,12 @@ pub unsafe fn is_smart() -> Option<bool> {
CW_MODE.get(MODE_INDEX).map(|mode| *mode == SMART)
}

/// Verify if the current device mode is manual, returns None if the map fails
#[inline(always)]
pub unsafe fn is_manual() -> Option<bool> {
CW_MODE.get(MODE_INDEX).map(|mode| *mode == MANUAL)
}

#[inline(always)]
pub unsafe fn is_nvidia_setting_enabled() -> bool {
match unsafe { CW_SETTINGS.get(CardwiredSetting::EXP_NVIDIA) } {
Expand Down
Loading