Introduction
Cardwire is a GPU manager for Linux systems with multiple GPUs. It allows users to smoothly and safely switch between “integrated”, “hybrid” and more GPU modes. It was created as the successor to the deprecated supergfxctl project.
Why Cardwire?
Traditional GPU managers for Linux (like envycontrol, optimus-manager, supergfxctl) often require system restarts, display manager logouts, or rely on legacy X11 architectures. Other built-in tools (like switcheroo-control) are great for launching apps but don’t actively protect the dedicated GPU from being woken up by misbehaving background applications.
Cardwire solves this by using eBPF (Extended Berkeley Packet Filter) and LSM (Linux Security Modules) to dynamically block access to the GPU. This ensures the GPU can enter its deepest sleep state (D3Cold, a hardware state that uses almost zero power) without requiring logouts or reboots to change modes.
Furthermore, unlike older managers, Cardwire never unbinds PCI devices or kernel drivers. Unbinding drivers on the fly is notoriously unstable and is a frequent cause of crashes on AMD GPUs or system deadlocks on NVIDIA GPUs. Cardwire’s eBPF approach is entirely seamless and significantly more stable.
Comparison
| GPU Manager | How it works | Unbinds Drivers? | Requires Reboot/Logout | Notes |
|---|---|---|---|---|
| Cardwire | eBPF LSM hooks block file/device access dynamically. | No (Seamless) | No | Actively prevents rogue apps from waking the dGPU. Emulates switcheroo-control for seamless GNOME/KDE integration. |
| switcheroo-control | Sets environment variables (e.g. DRI_PRIME). | No | No | The desktop default. Good for launching, but doesn’t actively block apps, meaning the dGPU can still be woken up by background tasks. |
| supergfxctl | Modprobe blacklisting, udev rules, stopping display manager. | Yes (Prone to crashes) | Logout (often) | Deprecated. The predecessor to Cardwire; inflexible and often required restarting the graphical session. |
| optimus-manager | Generates specific Xorg configurations. | Yes | Logout | Built heavily around X11, making it problematic for modern Wayland compositors. |
| envycontrol | Modprobe blacklisting and udev rules. | Yes | Reboot | Very reliable but inflexible, as it requires a full system restart to apply any mode changes. |
Modes
Cardwire provides several GPU management modes:
-
Integrated mode – Uses eBPF LSM hooks to block applications from accessing dedicated GPUs. This saves power by preventing the GPU from waking up and allowing it to enter an energy-efficient sleep state (
D3Cold). -
Hybrid mode – Removes the blocks, letting the system function normally with both integrated and dedicated GPUs available.
-
Manual mode – Allows users to manually block or unblock individual GPUs by ID for granular control.
-
Smart mode – Like integrated mode it blocks the dGPU by default, but uses eBPF to analyze each application at launch and selectively allow GPU access for approved applications.
Switching between modes is fast and does not require reboots or logouts.
Caution
Cardwire is in an early development stage, expect breaking changes.
Getting Started
To get started with cardwire, please take a look at the requirements to make sure your system is supported and configured, then head over to the installation instructions.
Requirements
To run Cardwire, your system needs to meet a few core requirements. Good news: if you are using a modern Linux distribution, you likely already meet all of these out of the box!
1. Supported Distributions (Kernel & System)
Cardwire requires:
- Linux Kernel 5.8 or later (with
CONFIG_BPF_LSMenabled). - systemd as the init system.
Tip
The following distributions are known to work out of the box with zero manual configuration required:
- OGC Distros (Officially Supported): Bazzite, Ultramarine, Nobara, PikaOS, ChimeraOS, winesapOS
- NixOS (Officially Supported)
- Arch Linux / CachyOS
- Fedora (and Atomic variants)
- Debian
If you are using one of these distributions, you can safely skip the advanced verification below and head straight to the Installation Guide.
Warning
Non-systemd distros are currently not supported. If you want to use Cardwire on a non-systemd distro, either open a PR with patches or configure the required services on your setup.
2. Display Server
Caution
Cardwire only supports Wayland. X11 is unsupported.
Advanced: Manual Kernel Verification
If you are not using a distribution listed above, or if you are compiling your own kernel, you will need to manually verify that eBPF LSM is enabled.
1. Verify CONFIG_BPF_LSM is enabled
On e.g. Ubuntu/Fedora:
grep CONFIG_BPF_LSM /boot/config-$(uname -r)
On other distros possibly:
zcat /proc/config.gz | grep CONFIG_BPF_LSM
Returns
CONFIG_BPF_LSM=yif it’s enabled.
2. Verify BPF is in the boot cmdline
Check your current boot parameters:
cat /proc/cmdline | tr ' ' '\n'|grep lsm
Alternative methods:
grep CONFIG_LSM= /boot/config-$(uname -r)
or
zcat /proc/config.gz | grep CONFIG_LSM=
Outputs e.g.
lsm=landlock,yama,apparmor,bpforCONFIG_LSM="landlock,lockdown,yama,integrity,apparmor,bpf". If it contains ‘bpf’, bpf is already enabled and usable in your system!
Enabling BPF LSM (with GRUB)
If bpf is not in your boot cmdline, edit /etc/default/grub and append bpf to GRUB_CMDLINE_LINUX_DEFAULT, keeping all existing entries:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash lsm=landlock,lockdown,yama,integrity,apparmor,bpf"
Important
Do not set
lsm=bpfalone — that drops other active security policies. Always appendbpfto the existing list from the command above.
Apply and reboot:
| Distro | Command |
|---|---|
| Ubuntu | sudo update-grub |
| Fedora | sudo grub2-mkconfig -o /boot/grub2/grub.cfg |
| Arch | sudo grub-mkconfig -o /boot/grub/grub.cfg |
sudo reboot
Installation
Arch/CachyOS/Arch-based
The official Open Gaming Collective Arch packages are available here: ogc-arch-packaging
Alternatively, using the AUR:
yay -S cardwire
And start the service:
sudo systemctl enable cardwired --now
Nix
Using the repo’s flake:
flake.nix:
cardwire = {
url = "github:opengamingcollective/cardwire";
inputs.nixpkgs.follows = "nixpkgs";
};
configuration.nix:
imports = [ inputs.cardwire.nixosModules.default ];
services.cardwire = {
enable = true;
settings = {
auto_apply_gpu_state = true;
experimental_nvidia_block = true;
battery_auto_switch = true;
battery_auto_switch_mode = "hybrid";
external_display_auto_switch = false;
};
};
Fedora/Fedora-based
Cardwire is officially distributed through Terra on Fedora systems
To install Terra, follow the instructions here: https://docs.terrapkg.com/usage/installing
Using Terra
sudo dnf install cardwire
And start the service:
sudo systemctl enable cardwired --now
Bazzite/Atomic Fedora-based
Cardwire is officially distributed through Terra on Fedora systems
To install Terra, follow the instructions here: https://docs.terrapkg.com/usage/installing
Using Terra
sudo rpm-ostree install cardwire
And start the service:
sudo systemctl enable cardwired --now
Note
Thanks to the Fyra Labs / Terra team for packaging and maintaining Cardwire on Fedora !!
AerynOS
Cardwire is available in the official AerynOS repositories.
sudo moss install cardwire
And start the service:
sudo systemctl enable cardwired --now
Ubuntu / Debian
A .deb package is available on the releases page. You can download and install it using apt.
sudo apt install ./cardwire_*.deb
sudo systemctl enable cardwired --now
Build from source (Alternative)
Install build dependencies:
sudo apt install clang libbpf-dev linux-headers-$(uname -r)
Install Rust (if not already installed):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Then clone and build:
git clone https://github.com/OpenGamingCollective/cardwire.git
make build
sudo make install
Other distros
For now, other distros must clone the repo and use make to build and install Cardwire. You will also need to enable BPF LSM manually — see the Enabling BPF LSM section above.
Build dependencies:
- cargo
- clang
- libbpf
- libudev-dev
git clone https://github.com/OpenGamingCollective/cardwire.git
make build
sudo make install
Usage
Querying GPUs
To have cardwire list all detected GPUs, use:
cardwire list
For each detected GPU, the command will return:
- An identifier (
ID). These are used for manual blocking and unblocking. - The GPU’s name (
NAME) - The GPU’s PCI address (
PCI) - The associated render node (
RENDER) - The associated device node (
CARD) - Whether the GPU has been identified as the default GPU (
DEFAULT). Default GPUs will remain available when cardwire is set to integrated. - Whether the GPU is currently blocked (
BLOCKED)
Example:
$ cardwire list
ID NAME PCI RENDER CARD DEFAULT BLOCKED
-- ------------------------------------------- ------------ ---------- ----- ------- -------
0 Rembrandt [Radeon 680M] 0000:07:00.0 renderD129 card2 (*) false
1 Navi 23 [Radeon RX 6650 XT / 6700S / 6800S] 0000:03:00.0 renderD128 card1 ( ) true
Mode switching
GPU modes can be switched using the cardwire set command.
Integrated
To have cardwire block the dGPU, use:
cardwire set integrated
Note
The block only applies to new launched apps. Apps that are already running will keep using the GPU until you restart them. Restarting can help.
Tip
The dedicated GPU can still power down even if your desktop has it open, as long as nothing is actively using it. To double-check, run:
cardwire gpu 1 --lsof
Hybrid
To have cardwire allow access to all GPUs, use
cardwire set hybrid
Smart Mode
Smart mode blocks the dedicated GPU by default like integrated mode, but uses a real-time analyzer to scan each application at launch and selectively allow GPU access for approved apps.
Cardwire natively integrates with desktop environments (GNOME, KDE) via a Switcheroo DBus shim. This means you can simply right-click an application in your app launcher and select “Launch using Discrete Graphics Card”, and Cardwire will automatically unblock the GPU for that application.
Tip
When an application is launched via the Switcheroo UI or with
CARDWIRE_FORCE_DGPU=1, Cardwire will hide the integrated GPU (iGPU) from the app. The app will only be able to see and use the dedicated GPU, guaranteeing it runs on the correct hardware.
When launching apps in Smart mode, cardwire checks for the following to allow the dGPU:
CARDWIRE_ALLOW=1env var (highest priority, unblocks the GPU but doesn’t force the app to use it)CARDWIRE_FORCE_DGPU=1env var (unblocks the GPU, forces the app to use it, and completely hides the iGPU)- Steam games (
SteamAppId=) - Flatpak apps with XDG
PrefersNonDefaultGpu=true(Only on system that does not implement switcheroo/cardwire) - Explicit GPU env vars (
DRI_PRIME=1,__NV_PRIME_RENDER_OFFLOAD=1)
cardwire set smart
Note
This feature is a work in progress. The detection methods will be improved in future updates.
Manual
Important
To prevent system breakage, cardwire will not block the default GPU, even when explicitly instructed to do so.
If more granular control over several GPUs is required, cardwire also allows manually blocking individual GPUs by ID. To do so, it needs to be set to manual mode:
cardwire set manual
Once set to manual, GPU states can then be set by ID; to find the correct ID, see Querying GPUs.
To block the GPU with ID 1:
cardwire gpu 1 --block
To unblock:
cardwire gpu 1 --unblock
Configuration
Experimental Nvidia Block
Note
This setting is experimental because it tells cardwire to block specific Nvidia files, such as /dev/nvidiactl, that can be shared across multiple Nvidia GPUs. For this reason, it only works reliably on systems with exactly two GPUs: one integrated GPU and one dedicated Nvidia GPU.
Tip
Even though it is experimental, enabling this setting is recommended. It helps prevent unwanted GPU wakeups from Vulkan apps (GTK on gnome) and from tools that use /dev/nvidiactl, such as nvtop
To get if experimental Nvidia block is enabled:
cardwire config experimental-nvidia-block
To enable/disable it:
cardwire config experimental-nvidia-block true
Battery Auto Switch Mode
Cardwire can automatically switch GPU modes when the system switches between battery and AC power. When battery_auto_switch is enabled, cardwire switches to integrated mode on battery and back to a configurable mode when on AC power.
To get if battery auto switch is enabled:
cardwire config battery-auto-switch
To enable/disable it:
cardwire config battery-auto-switch true
The mode cardwire switches to on AC power is controlled by battery_auto_switch_mode. This can be set to integrated, hybrid, manual, or smart.
To get the current battery auto switch mode:
cardwire config battery-auto-switch-mode
To set the battery auto switch mode:
cardwire config battery-auto-switch-mode hybrid
Auto Apply Gpu State
When you switch back to manual mode, this setting automatically restores the GPU states you had set before. These saved states are stored in /var/lib/cardwire/gpu_state.json
To view the current saved states, run:
cat /var/lib/cardwire/gpu_state.json
Example output:
{
"0000:03:00.0": {
"block": false
},
"0000:07:00.0": {
"block": false
}
}
In this example, both GPUs are set to allow access (block: false) when manual mode is restored
To get the current setting:
cardwire config auto-apply-gpu-state
To set the setting:
cardwire config auto-apply-gpu-state true
Troubleshooting
Name is not activable
Is the daemon running?
systemctl status cardwired.service
If it’s not running, enable the daemon with
systemctl enable cardwired.serviceand reboot your device.
dGPU is detected as the default gpu
On ROG laptop
is the asus MUX enabled?
asusctl armoury list
then find
gpu_mux_mode:
current: [(0),1]
0 means that the MUX is enabled, the dGPU IS the default GPU in this case
Non ROG Laptop
This shouldn’t happen, please create an issue with the output of
ls /sys/class/drm
and
cat /sys/class/drm/*/status
nvidia-powerd failure after switching modes
When switching to integrated mode on NVIDIA hardware, you may see errors or failures related to the nvidia-powerd service. This is a known quirk caused by the GPU entering D3Cold (a deep sleep state) which prevents nvidia-powerd from communicating with it.
Service must be restarted:
sudo systemctl restart nvidia-powerd.service
Sleep
How to diagnose a dGPU that won’t sleep
Your NVIDIA dGPU won’t sleep? Here’s how to find and fix the issue.
Check your NVIDIA GPU power information
Before this, please set cardwire to unblock your dGPU.
Replace the PCI with yours.
cat /proc/driver/nvidia/gpus/0000:01:00.0/power
Runtime D3 status: Enabled (fine-grained)
Video Memory: Off
GPU Hardware Support:
Video Memory Self Refresh: Supported
Video Memory Off: Supported
S0ix Power Management:
Platform Support: Supported
Status: Enabled
Notebook Dynamic Boost: Not Supported
The most important section should be Runtime D3 status.
If Runtime D3 status is disabled, your GPU will never sleep.
To enable it, follow this method (only tested on Arch; please adapt it for other distros):
Caution
If you lack the knowledge, or you fear you will break your system, you can always make a post on the Discord to get assistance.
Go to https://gitlab.com/asus-linux/nvidia-laptop-power-cfg.
We will need two files:
- nvidia.rules
- nvidia.conf
You will need to copy them to their respective directory: For nvidia.conf:
/etc/modprobe.d/nvidia.conf
For nvidia.rules:
/usr/lib/udev/rules.d/80-nvidia-pm.rules
Once it’s done, execute:
sudo mkinitcpio -P
and restart your computer.
RTX 2000 Series
If it’s not working and you own an RTX 2000 GPU, it’s a known issue.
You must use driver 580 and add NVreg_EnableGpuFirmware=0 to /etc/modprobe.d/nvidia.conf.
Check the PCI control value
DBUS
Service
- Bus Name:
org.opengamingcollective.cardwire
Note
Cardwire also implements the SwitcherooControl interface for desktop environment integration. See switcheroo.md for details.
Object Path
/org/opengamingcollective/cardwire
Manager
org.opengamingcollective.cardwire.Manager
Methods:
-
RefreshGpuRefresh the internal GPU list from the system (Not implemented yet)- Inputs: None
- Outputs: None
-
StatusSimple dbus method to check if the daemon is alive- Inputs: None
- Outputs: None
Mode
org.opengamingcollective.cardwire.Mode
Properties:
ModeControls the Cardwire’s Mode- Type:
u - Access: Read/Write
- Emits:
PropertiesChangedon change - Values:
0Integrated: Block the dGPU. Requires exactly 2 GPUs1Hybrid: Unblock the dGPU. Requires exactly 2 GPUs2Manual: Allow per-GPU blocking via individual GPU objects. Applies saved GPU state on mode change ifauto_apply_gpu_stateis enabled3Smart: Block the dGPU by default but dynamically allow access per-application using eBPF. Requires exactly 2 GPUs
- Type:
Config
org.opengamingcollective.cardwire.Config
Properties:
-
AutoApplyGpuStateAutomatically applies the saved block/unblock states to GPUs- Type:
b - Access: Read/Write
- Type:
-
BatteryAutoSwitchControls whether the daemon automatically switches modes when switching to battery power- Type:
b - Access: Read/Write
- Type:
-
BatteryAutoSwitchModeControls which mode the daemon automatically switches- Type:
u - Access: Read/Write
- Type:
-
ExperimentalNvidiaBlockToggles the experimental blocking for NVIDIA GPU, only works if the system has exactly 1 Nvidia GPU- Type:
b - Access: Read/Write
- Type:
Debug
org.opengamingcollective.cardwire.Debug
Methods:
GetPciDevicesGet a dictionary of all detected PCI devices.- Inputs: None
- Outputs:
- (out):
a{s(sssssssss)}– A dictionary mapping PCI addresses to a struct containing:iommu_group:s- IOMMU group number (empty string if none)vendor_id:s- PCI vendor ID (empty string if unknown)device_id:s- PCI device ID (empty string if unknown)vendor_name:s- Vendor name (empty string if unknown)device_name:s- Device name (empty string if unknown)driver:s- Kernel driver in use (empty string if unknown)class:s- PCI class (empty string if unknown)parent_pci:s- Parent PCI address (empty string if unknown)child_pci:s- Child PCI address (empty string if unknown)
- (out):
Gpu
/org/opengamingcollective/cardwire/Gpu/{id}
Represents a single GPU device, where {id} is the numeric identifier of the GPU (0 is always the default one). These objects can be dynamically discovered by calling GetManagedObjects on the standard org.freedesktop.DBus.ObjectManager interface located at the root path (/org/opengamingcollective/cardwire)
Properties:
BlockSet or get the block state for this specific GPU. Only writable whenModeis set toManual. The default gpu cannot be blocked.- Type:
b - Access: Read/Write
- Type:
Methods:
-
GetDeviceGet the detailed informations of this GPU- Inputs: None
- Outputs:
- (out):
(ssuubbs)– A struct containing:name:s- GPU namepci:s- PCI addressrender:u- DRM render node minor numbercard:u- DRM card node minor numberdefault:b- Whether this is the default display GPUnvidia:b- Whether the GPU is an NVIDIA devicenvidia_minor:s- NVIDIA driver minor number (empty string if not applicable)
- (out):
-
PowerStateGet the current power state of the GPU- Inputs: None
- Outputs:
- (out):
s– The power state (e.g., “D0”, “D3cold”)
- (out):
-
LsofRead file descriptors to find which applications have currently opened the GPU- Inputs: None
- Outputs:
- (out):
a{sas}– A dictionary mapping file paths (like/dev/dri/card0) to an array of process names
- (out):
Signals:
PowerStateChangedEmitted when the power state of the GPU changes- Parameters:
s(string) – The new power state
- Parameters:
Switcheroo Shim
Cardwire implements a compatibility shim for the net.hadess.SwitcherooControl D-Bus interface. This allows desktop environments (like GNOME(gio-launch-desktop) and KDE) to natively offer “Launch using Discrete Graphics Card” options in their application menus without needing any Cardwire-specific plugins.
(Having our own integration would’ve been better tbh)
Service
- Interface:
net.hadess.SwitcherooControl
Properties
HasDualGpu
Indicates whether the system has exactly two GPUs.
- Type:
b(boolean) - Access: Read
NumGPUs
The number of GPUs detected on the system.
- Type:
u(uint32) - Access: Read
GPUs
A list of all available GPUs and their configurations.
- Type:
aa{sv}(Array of dictionaries mapping strings to variants) - Access: Read
- Dictionary Keys:
Name:s- The name of the GPU.Environment:as- An array of environment variable key-value pairs to set when launching an application on this GPU (e.g.,["CARDWIRE_FORCE_DGPU", "1"]).Default:b- Whether this is the default display GPU (usually the iGPU).Discrete:b- Whether this is a discrete GPU.
Environment Variables Explained
The Environment property provides the exact environment variables the desktop environment should inject into the application when the user selects a specific GPU.
CARDWIRE_FORCE_DGPU=1
This is provided when the user selects the Discrete GPU.
When Cardwire detects this environment variable during the application’s launch in Smart Mode, it does two things:
- Unblocks the dGPU: The eBPF hooks allow the application to access the discrete GPU’s device files.
- Hides the iGPU: It actively intercepts and blocks the application from seeing the integrated GPU.
Hiding the iGPU ensures that the application is forced to use the discrete GPU, preventing issues where an application might get confused by seeing two GPUs and accidentally select the weaker one.
CARDWIRE_ALLOW=0
This is provided when the user selects the Default/Integrated GPU.
It explicitly tells Cardwire’s Smart Mode to keep the dGPU blocked for this application, ensuring it runs solely on the integrated graphics to save power.
Smart
Introduction
Having an integrated and hybrid mode is good, but what if we could have the best of both worlds?
This is what cardwire’s smart mode was made for. Cardwire uses a mix of kernel-space + userspace to directly allow processes on the fly
Kernel-Space
Using the eBPF program and the tracepoint/sched/sched_process_exec hooks, the kernel program notifies cardwired when a new process is executed, sending its pid using cw_exec_events RING_BUF, once the process is received by cardwired, it will be analyzed in real-time and if it’s a process that should be allowed, its pid will be inserted into the cw_allowed_pid map
When a process exits, a notification is sent to cardwired, cardwired will remove the PID from its map to prevent the map from overflowing
If you want to dive deeper into the kernel code, take a look at BPF
Userspace
The userspace of Smart mode acts as the brain. It is responsible for making the actual decisions about whether a process is allowed to use a GPU. It is divided into three main components:
CardwireAnalyzer: A dedicated background task that listens to thecw_exec_eventsandcw_close_eventsring buffers. When it receives a new PID from the kernel, it invokes the analysis helpers. If the application passes, it populates thecw_allowed_pidmap with a value of1(normal) or0(iGPU).dynamic_analysis.rs: A set of helper functions used to analyze a process in real-time. By reading/proc/<pid>/environand/proc/<pid>/cmdline, it checks for explicitly requested GPUs (likeCARDWIRE_ALLOW=1,CARDWIRE_FORCE_DGPU=1,DRI_PRIME=1) or implicit signs like Steam games (SteamAppId) and Flatpak wrappers.static_analysis.rs: A set of helper functions that analyze system data when the daemon starts. Specifically, it scans the XDG data directories for.desktopfiles containingPrefersNonDefaultGPU=trueorX-KDE-RunOnDiscreteGpu=true, building a whitelist of application names that should automatically be granted dGPU access when they launch.
Complete Execution Flow
Here is a comprehensive breakdown of how the Kernel and Userspace interact in real-time when an application launches:
sequenceDiagram
participant Proc as Process
participant Kernel as eBPF Kernel Hooks
participant Map as BPF Maps
participant Daemon as CardwireAnalyzer (Userspace)
Note over Proc,Daemon: 1. Process Launch
Proc->>Kernel: sched_process_exec
Kernel->>Map: Send PID via cw_exec_events (RingBuf)
Map->Daemon: Listen to cw_exec_events and wait for new events
Note over Daemon: 2. Real-time Analysis
Daemon->>Daemon: Read /proc/<pid>/environ & cmdline
Daemon->>Daemon: Check env vars, Steam, Flatpak, XDG lists
alt Is Allowed?
Daemon->>Map: Insert PID into cw_allowed_pid
else Not Allowed
Daemon->>Daemon: Do nothing
end
Note over Proc,Kernel: 3. GPU Access & Directory Listing
Proc->>Kernel: getdents64 / file_open (/dev/dri/)
Kernel->>Map: Check cw_allowed_pid
alt PID not in cw_allowed_pid
Kernel-->>Proc: hide GPU (Return -ENOENT)
Kernel->>Daemon: Send block event (cw_report_events)
else PID in cw_allowed_pid (Value 1 = Normal)
Kernel-->>Proc: Allow dGPU and iGPU
else PID in cw_allowed_pid (Value 0 = FORCE_DGPU)
Kernel-->>Proc: Allow dGPU, Hide iGPU (-ENOENT)
end
Note over Proc,Daemon: 4. Application Exit
Proc->>Kernel: sched_process_exit
Kernel->>Map: Send PID via cw_close_events (RingBuf)
Map->Daemon: Listen to cw_close_events and wait for new events
Daemon->>Map: Remove PID from cw_allowed_pid
BPF
Introduction
Cardwire uses Linux eBPF along with Linux Security Modules (LSM) and Syscall tracepoints to intercept and block applications. By intercepting these operations directly in the kernel, Cardwire provides a fast and seamless blocking without needing to unload drivers or modify user applications/files.
eBPF Hooks
Cardwire utilizes two main types of eBPF hooks:
1. LSM Hooks
LSM hooks are used to intercept and block permission checks or file openings on device files (like /dev/dri/*). This stops applications from accessing a GPU simply by checking file stats.
lsm/file_open: Intercepts the actual opening of blocked device files.lsm/inode_permission: Prevents permissions checks on blocked devices.lsm/inode_getattr: Preventsstat()calls on blocked devices.
2. Syscall Tracepoints
Tracepoints are used to monitor process lifecycle and manipulate the directory listings applications see.
tracepoint/sched/sched_process_exec: In Smart mode, this signals the Cardwire daemon that a new process is starting so it can be analyzed.tracepoint/sched/sched_process_exit: Signals when a process dies, cleaning up its entries in the allowed process maps.tp/syscalls/sys_enter_getdents64andsys_exit_getdents64: Intercepts directory listings. This is the core magic behind dynamically hiding device files from applications.
eBPF Maps
The eBPF programs communicate with the Cardwire userspace daemon using several BPF maps:
cw_mode: Stores the current Cardwire mode (0=Integrated, 1=Hybrid, 2=Manual, 3=Smart).cw_blocked_ino: A hash map containing the inodes of blocked DRM devices (/dev/dri/cardX,/dev/dri/renderDX). The value indicates the GPU ID (0 for iGPU, 1 for dGPU).cw_exp_blk_ino: Contains inodes of blocked NVIDIA-specific files whenexperimental_nvidia_blockis enabled.cw_allowed_pid: Used in Smart mode. Contains the PIDs of applications that have been analyzed and allowed to use the dGPU. The stored value (__u8) is used to identify if PID is meant for iGPU(0) or dGPU(1)cw_allowed_comm: A whitelist of process names (likeudevorpacman) that bypass blocking entirely.cw_daemon_pid: Cardwire’s own PID so it doesn’t block itself.cw_exec_events,cw_close_events,cw_report_events: Ring buffers used to send process and block events back to userspace.
Directory Hiding (getdents64)
Across all blocking modes (Integrated, Manual, Smart), Cardwire uses the getdents64 syscall hooks to manipulate the contents of directories (like /dev/dri/) on the fly.
When an application calls getdents64 to list available GPUs, the eBPF program patch_dirent_if_found loops through the directory entries in memory. If it spots an inode belonging to a blocked GPU, it overwrites the previous entry’s length field, effectively “jumping over” the blocked device. To the application, the blocked GPU simply does not exist and is omitted from directory listings rather than causing an error.
Building and Development
Building and Development
Using Nix
# Enter development shell
nix develop
# Build the project
nix build
# Run formatting checks
nix build .#checks.x86_64-linux.pre-commit-check
# Run integration tests in VM
nix build .#checks.x86_64-linux.vm-test
# Build the vm and enter
nix run .#nixosConfigurations.x86_64-linux.config.system.build.vm
Manual Compilation
If you don’t use Nix, ensure you have clang, libbpf (devel), hwdata and cargo installed (needed for eBPF compilation during the Rust build).
Formatting requires nightly rustfmt (the project uses nightly-only formatting options). Install it and run with:
rustup toolchain install nightly --component rustfmt
cargo +nightly fmt
# Build the project
make
# Install binaries, systemd service, and D-Bus config (requires sudo)
sudo make install
Project Structure
crates/cardwire-cli: User CLI to interact with the daemoncrates/cardwire-core: Low-level GPU manager and IOMMU discoverycrates/cardwire-daemon: System daemon managing state and D-Bus communicationcrates/cardwire-ebpf: BPF program and LSM hooks