Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ManagerHow it worksUnbinds Drivers?Requires Reboot/LogoutNotes
CardwireeBPF LSM hooks block file/device access dynamically.No (Seamless)NoActively prevents rogue apps from waking the dGPU. Emulates switcheroo-control for seamless GNOME/KDE integration.
switcheroo-controlSets environment variables (e.g. DRI_PRIME).NoNoThe desktop default. Good for launching, but doesn’t actively block apps, meaning the dGPU can still be woken up by background tasks.
supergfxctlModprobe 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-managerGenerates specific Xorg configurations.YesLogoutBuilt heavily around X11, making it problematic for modern Wayland compositors.
envycontrolModprobe blacklisting and udev rules.YesRebootVery 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_LSM enabled).
  • 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=y if 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,bpf or CONFIG_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=bpf alone — that drops other active security policies. Always append bpf to the existing list from the command above.

Apply and reboot:

DistroCommand
Ubuntusudo update-grub
Fedorasudo grub2-mkconfig -o /boot/grub2/grub.cfg
Archsudo 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=1 env var (highest priority, unblocks the GPU but doesn’t force the app to use it)
  • CARDWIRE_FORCE_DGPU=1 env 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.service and 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:

  • RefreshGpu Refresh the internal GPU list from the system (Not implemented yet)

    • Inputs: None
    • Outputs: None
  • Status Simple dbus method to check if the daemon is alive

    • Inputs: None
    • Outputs: None

Mode

org.opengamingcollective.cardwire.Mode

Properties:

  • Mode Controls the Cardwire’s Mode
    • Type: u
    • Access: Read/Write
    • Emits: PropertiesChanged on change
    • Values:
      • 0 Integrated: Block the dGPU. Requires exactly 2 GPUs
      • 1 Hybrid: Unblock the dGPU. Requires exactly 2 GPUs
      • 2 Manual: Allow per-GPU blocking via individual GPU objects. Applies saved GPU state on mode change if auto_apply_gpu_state is enabled
      • 3 Smart: Block the dGPU by default but dynamically allow access per-application using eBPF. Requires exactly 2 GPUs

Config

org.opengamingcollective.cardwire.Config

Properties:

  • AutoApplyGpuState Automatically applies the saved block/unblock states to GPUs

    • Type: b
    • Access: Read/Write
  • BatteryAutoSwitch Controls whether the daemon automatically switches modes when switching to battery power

    • Type: b
    • Access: Read/Write
  • BatteryAutoSwitchMode Controls which mode the daemon automatically switches

    • Type: u
    • Access: Read/Write
  • ExperimentalNvidiaBlock Toggles the experimental blocking for NVIDIA GPU, only works if the system has exactly 1 Nvidia GPU

    • Type: b
    • Access: Read/Write

Debug

org.opengamingcollective.cardwire.Debug

Methods:

  • GetPciDevices Get 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)

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:

  • Block Set or get the block state for this specific GPU. Only writable when Mode is set to Manual. The default gpu cannot be blocked.
    • Type: b
    • Access: Read/Write

Methods:

  • GetDevice Get the detailed informations of this GPU

    • Inputs: None
    • Outputs:
      • (out): (ssuubbs) – A struct containing:
        • name: s - GPU name
        • pci: s - PCI address
        • render: u - DRM render node minor number
        • card: u - DRM card node minor number
        • default: b - Whether this is the default display GPU
        • nvidia: b - Whether the GPU is an NVIDIA device
        • nvidia_minor: s - NVIDIA driver minor number (empty string if not applicable)
  • PowerState Get the current power state of the GPU

    • Inputs: None
    • Outputs:
      • (out): s – The power state (e.g., “D0”, “D3cold”)
  • Lsof Read 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

Signals:

  • PowerStateChanged Emitted when the power state of the GPU changes
    • Parameters: s (string) – The new power state

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:

  1. Unblocks the dGPU: The eBPF hooks allow the application to access the discrete GPU’s device files.
  2. 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 the cw_exec_events and cw_close_events ring buffers. When it receives a new PID from the kernel, it invokes the analysis helpers. If the application passes, it populates the cw_allowed_pid map with a value of 1 (normal) or 0 (iGPU).
  • dynamic_analysis.rs: A set of helper functions used to analyze a process in real-time. By reading /proc/<pid>/environ and /proc/<pid>/cmdline, it checks for explicitly requested GPUs (like CARDWIRE_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 .desktop files containing PrefersNonDefaultGPU=true or X-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: Prevents stat() 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_getdents64 and sys_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 when experimental_nvidia_block is 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 (like udev or pacman) 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 daemon
  • crates/cardwire-core: Low-level GPU manager and IOMMU discovery
  • crates/cardwire-daemon: System daemon managing state and D-Bus communication
  • crates/cardwire-ebpf: BPF program and LSM hooks