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
6 changes: 2 additions & 4 deletions crates/cardwire-daemon/src/analyzer/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ impl CardwireAnalyzer {
let exec_ring = blocker.get_exec_ring()?;
let close_ring = blocker.get_close_ring()?;
let report_ring = blocker.get_report_ring()?;
let pid_map = blocker.get_pid_map()?;
let forced_map = blocker.get_forced_pid_map()?;
let pid_map = Arc::clone(&blocker.pid_map);
let forced_map = Arc::clone(&blocker.forced_map);
let ebpf_logger = blocker.get_ebpf_logger()?;

let exec_ring = AsyncFd::new(exec_ring)?;
Expand All @@ -85,8 +85,6 @@ impl CardwireAnalyzer {

// Now Rwlock -> Arc
let exec_ring = Arc::new(Mutex::new(exec_ring));
let pid_map = Arc::new(RwLock::new(pid_map));
let forced_map = Arc::new(RwLock::new(forced_map));
let close_ring = Arc::new(Mutex::new(close_ring));
let report_ring = Arc::new(Mutex::new(report_ring));
let ebpf_logger: Arc<Mutex<AsyncFd<EbpfLogger<&'static dyn Log>>>> =
Expand Down
6 changes: 6 additions & 0 deletions crates/cardwire-daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ async fn spawn_dbus_api(
.interface::<_, crate::interface::LoggerInterface>(path)
.await?;
daemon.logger_signal = Some(logger_ref.signal_emitter().clone());

// Cardwire Smart Policy
object_server
.at(path, daemon.smart_policy_interface.clone())
.await?;

drop(power_tasks);
// drop gpu list to prevent deadlock
drop(gpu_interfaces);
Expand Down
2 changes: 2 additions & 0 deletions crates/cardwire-daemon/src/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ mod debug;
mod gpu;
mod logger;
mod mode;
mod smart;
mod switcheroo;

pub use config::{ConfigInterface, ConfigMemory};
pub use debug::DebugInterface;
pub use gpu::{GpuInterface, GpuInterfaceSignals};
pub use logger::{LogEntry, LoggerInterface, LoggerInterfaceSignals};
pub use mode::{ModeInterface, Modes};
pub use smart::SmartPolicyInterface;
pub use switcheroo::SwitcherooInterface;
136 changes: 136 additions & 0 deletions crates/cardwire-daemon/src/interface/smart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use aya::maps::{HashMap as AyaHashMap, MapError as AyaMapError};
use cardwire_ebpf_userspace::EbpfBlocker;
use std::{path::Path, sync::Arc};

use tokio::sync::RwLock;
use zbus::{
fdo::{self, Error::Failed}, interface
};

#[derive(Clone, Debug)]
pub struct SmartPolicyInterface {
pid_map: Arc<RwLock<AyaHashMap<aya::maps::MapData, u32, u32>>>,
forced_map: Arc<RwLock<AyaHashMap<aya::maps::MapData, u32, u32>>>,
}

impl SmartPolicyInterface {
pub fn build(blocker: &mut EbpfBlocker) -> Self {
let pid_map = Arc::clone(&blocker.pid_map);
let forced_map = Arc::clone(&blocker.forced_map);

Self {
pid_map,
forced_map,
}
}
}

#[interface(name = "org.opengamingcollective.cardwire.SmartPolicy")]
impl SmartPolicyInterface {
/// Authorized a pid to access a specific GPU
pub async fn request_process_access(
&self,
pid: u32,
policy: String,
value: u32,
) -> Result<(), fdo::Error> {
// Check if the process exists, leave if it doesnt
if !Path::new(&format!("/proc/{}", pid)).exists() {
return Err(fdo::Error::Failed("process doesn't exist".to_string()));
}
// Match the policy and add the pid to the corresponding ebpf map
match policy.as_str() {
// Default, do nothing
"Default" => Ok(()),
// Equivalent to CARDWIRE_ALLOW=1, show both iGPU and dGPU
"Allow_dGPU" => {
{
// First remove the PID from the other map if present
let mut forced_map = self.forced_map.write().await;
forced_map
.remove(&pid)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
}
let mut pid_map = self.pid_map.write().await;
pid_map
.insert(pid, 0, 0)
.map_err(|err| fdo::Error::Failed(err.to_string()))
Comment on lines +37 to +57

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Bind the policy to a process instance, not only a PID.

By the process-binding spell, /proc/<pid> existence is a TOCTOU check. The process can exit after the check and before the map insertion. If exit cleanup occurs before insertion, the stale entry can apply to a later process that reuses the PID.

Store and validate a process-instance identity during kernel enforcement. Do not treat a PID-only map entry as a stable authorization target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardwire-daemon/src/interface/smart.rs` around lines 37 - 50, Update
the policy handling around the process existence check and the Allow_dGPU
insertion so authorization is bound to the specific process instance, not just
its PID. Capture and store a kernel-verifiable process identity (such as the
process start time) with the map entry, and ensure enforcement validates that
identity before applying policy, preventing stale entries from affecting
PID-reused processes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be fixed in another PR, make a issue for it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="OpenGamingCollective/cardwire"
pr_url="https://github.com/OpenGamingCollective/cardwire/pull/155"
comment_url="https://github.com/OpenGamingCollective/cardwire/pull/155#discussion_r3734254717"

body=$(cat <<EOF
## Summary

Bind Smart Policy authorization to a process instance instead of only a PID.

## Rationale

\`SmartPolicyInterface::request_process_access\` checks that \`/proc/<pid>\` exists and then inserts a PID-only entry into the eBPF policy maps. The process can exit between the existence check and the insertion. If the PID is later reused, a stale policy entry can affect the new process.

## Affected areas

- \`crates/cardwire-daemon/src/interface/smart.rs\`
- Smart Policy eBPF maps and their kernel enforcement logic

## Required changes

- Capture a kernel-verifiable process-instance identity when a Smart Policy entry is created. A process start-time identity is one possible mechanism.
- Store this identity with the Smart Policy map entry.
- Validate the stored identity against the current process identity during kernel enforcement.
- Ignore or remove policy entries that do not match the current process instance.
- Preserve the intended behavior for \`Allow_dGPU\`, \`Force_dGPU\`, and GPU-specific policies.

## Acceptance criteria

- A policy created for a process does not apply to a different process that later reuses the same PID.
- Kernel enforcement verifies process-instance identity before it applies a Smart Policy entry.
- The implementation includes tests or another documented validation method for PID reuse or stale-entry handling.

## Backlinks

- Deferred from PR `#155`: ${pr_url}
- Review discussion: ${comment_url}

Requested by: `@luytan`
EOF
)

gh issue create \
  --repo "$repo" \
  --title "Bind Smart Policy entries to process instances to prevent PID reuse" \
  --body "$body" \
  --assignee "luytan"

Length of output: 2088


@luytan, the wizard has opened and assigned follow-up issue #156: Bind Smart Policy entries to process instances to prevent PID reuse.

It includes the required changes, acceptance criteria, and backlinks to PR #155 and this review discussion.

You are interacting with an AI system.

}
// Equivalent to CARDWIRE_FORCE_DGPU=value
"Force_dGPU" => {
{
// First remove the PID from the other map if present
let mut pid_map = self.pid_map.write().await;
pid_map
.remove(&pid)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
}
let mut force_map = self.forced_map.write().await;
force_map
.insert(pid, value, 0)
.map_err(|err| Failed(err.to_string()))
}
// Equivalent to CARDWIRE_FORCE_GPU=value
"Force_GPU" => {
{
// First remove the PID from the other map if present
let mut pid_map = self.pid_map.write().await;
pid_map
.remove(&pid)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
}
let mut force_map = self.forced_map.write().await;
force_map
.insert(pid, value, 0)
.map_err(|err| Failed(err.to_string()))
}
Comment thread
luytan marked this conversation as resolved.
_ => Err(fdo::Error::InvalidArgs(format!("invalid arg: {}", policy))),
}
}

/// Check if the process is inside PID or FORCED map, and return the map type with the gpu_id
/// associed
pub async fn get_process_status(&self, pid: u32) -> Result<(String, Option<u32>), fdo::Error> {
let mut status = String::new();
let mut gpu_id: Option<u32> = None;

{
let pid_map = self.pid_map.read().await;
match pid_map.get(&pid, 0) {
Ok(id) => {
status = "Allowed".to_string();
gpu_id = Some(id)
}
Err(err) => match err {
AyaMapError::KeyNotFound => {}
_ => {
return Err(fdo::Error::Failed(format!(
"Couldn't read PID MAP: {}",
err
)));
}
},
}
}
{
let forced_map = self.forced_map.read().await;
match forced_map.get(&pid, 0) {
Ok(id) => {
status = "Forced".to_string();
gpu_id = Some(id)
}
Err(err) => match err {
AyaMapError::KeyNotFound => {}
_ => {
return Err(fdo::Error::Failed(format!(
"Couldn't read FORCED MAP: {}",
err
)));
}
},
}
}

Ok((status, gpu_id))
Comment thread
luytan marked this conversation as resolved.
}
}
10 changes: 8 additions & 2 deletions crates/cardwire-daemon/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
analyzer::CardwireAnalyzer, core::{
gpu::{GpuEnumerator, GpuVendor}, inode::exp_nvidia_inodes, pci::{self}
}, file::{CardwireConfig, CardwireGpuState, CardwireModeState}, interface::{
ConfigInterface, ConfigMemory, DebugInterface, GpuInterface, LoggerInterface, ModeInterface, Modes, SwitcherooInterface
ConfigInterface, ConfigMemory, DebugInterface, GpuInterface, LoggerInterface, ModeInterface, Modes, SmartPolicyInterface, SwitcherooInterface
}, tasks
};
use anyhow::{Context, Result};
Expand Down Expand Up @@ -35,6 +35,7 @@ pub struct DaemonManager {
pub switcheroo_interface: SwitcherooInterface,
pub logger_interface: LoggerInterface,
pub logger_signal: Option<SignalEmitter<'static>>,
pub smart_policy_interface: SmartPolicyInterface,
pub inner: DaemonInner,
}

Expand All @@ -59,7 +60,11 @@ impl DaemonManager {
let pci_list: Arc<RwLock<BTreeMap<String, pci::PciDevice>>> =
Arc::new(RwLock::new(pci_devices));

let blocker = Arc::new(RwLock::new(EbpfBlocker::new()?));
let mut blocker = EbpfBlocker::new()?;

let smart_policy_interface = SmartPolicyInterface::build(&mut blocker);

let blocker = Arc::new(RwLock::new(blocker));

let power_tasks = Arc::new(RwLock::new(BTreeMap::new()));

Expand Down Expand Up @@ -112,6 +117,7 @@ impl DaemonManager {
switcheroo_interface: SwitcherooInterface::build(Arc::clone(&gpu_interfaces)),
logger_interface,
logger_signal: None,
smart_policy_interface,
inner: DaemonInner {
mode_state: Arc::clone(&mode_state),
gpu_state: Arc::clone(&gpu_state),
Expand Down
31 changes: 24 additions & 7 deletions crates/cardwire-ebpf-userspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
//! main lib code of cardwire-ebpf
mod errors;

use std::{fs, path::Path};
use std::{fs, path::Path, sync::Arc};

pub use crate::errors::{CardwireEbpfError, CardwireEbpfResult};
use aya::{
Btf, Ebpf, maps::{Array, HashMap, MapError, RingBuf}, programs::{Lsm, TracePoint}
};
use aya_log::EbpfLogger;
use log::{Log, error, info, warn};
use tokio::io::{Interest, unix::AsyncFd};
use tokio::{
io::{Interest, unix::AsyncFd}, sync::RwLock
};

pub enum EbpfSettings {
ExperimentalNvidia,
}

pub struct EbpfBlocker {
ebpf: Ebpf,
pub pid_map: Arc<RwLock<HashMap<aya::maps::MapData, u32, u32>>>,
pub forced_map: Arc<RwLock<HashMap<aya::maps::MapData, u32, u32>>>,
}

impl EbpfBlocker {
Expand Down Expand Up @@ -131,7 +135,18 @@ impl EbpfBlocker {
}
};
}
Ok(Self { ebpf })

let pid_map = Self::get_pid_map(&mut ebpf)?;
let forced_map = Self::get_forced_pid_map(&mut ebpf)?;

let pid_map = Arc::new(RwLock::new(pid_map));
let forced_map = Arc::new(RwLock::new(forced_map));

Ok(Self {
ebpf,
pid_map,
forced_map,
})
}

/// whitelist cardwire's pid to prevent self-locking in ebpf
Expand Down Expand Up @@ -322,9 +337,11 @@ impl EbpfBlocker {
}

/// take the CW_ALLOWED_PID HashMap map from the blocker
pub fn get_pid_map(&mut self) -> CardwireEbpfResult<HashMap<aya::maps::MapData, u32, u32>> {
pub fn get_pid_map(
ebpf: &mut Ebpf,
) -> CardwireEbpfResult<HashMap<aya::maps::MapData, u32, u32>> {
let map_str = "CW_ALLOWED_PID";
let map = match self.ebpf.take_map(map_str) {
let map = match ebpf.take_map(map_str) {
Some(map) => map,
None => {
error!("error while trying to take map {}", map_str);
Expand All @@ -345,10 +362,10 @@ impl EbpfBlocker {

/// take the CW_FORCED_PID HashMap map from the blocker
pub fn get_forced_pid_map(
&mut self,
ebpf: &mut Ebpf,
) -> CardwireEbpfResult<HashMap<aya::maps::MapData, u32, u32>> {
let map_str = "CW_FORCED_PID";
let map = match self.ebpf.take_map(map_str) {
let map = match ebpf.take_map(map_str) {
Some(map) => map,
None => {
error!("error while trying to take map {}", map_str);
Expand Down