feat(windows-drivers): host-gone watchdog, SET_RENDER_ADAPTER, log gate, mode bounds

Audit §4.1: implement the host-gone watchdog — it was dead code (WATCHDOG_PINGS bumped but never sampled, no thread). Every IOCTL now bumps a liveness counter; a watchdog thread reap_orphaned()s monitors (created_at grace) if no IOCTL arrives within WATCHDOG_TIMEOUT_S, so a crashed/TerminateProcess'd host no longer leaves its virtual monitor + swap-chain worker + pooled D3D device wedged until the next CLEAR_ALL. Removes the false 'watchdog thread' comments.

Audit §4.2: implement SET_RENDER_ADAPTER (was STATUS_NOT_IMPLEMENTED) via IddCxAdapterSetRenderAdapter, so the host can pin the IDD render to the NVENC GPU on a hybrid iGPU+dGPU box (else the OS-picked iGPU makes the host ring textures un-openable -> DRV_STATUS_TEX_FAIL).

Audit §4.4: gate the world-writable C:\Users\Public\pfvd-driver.log behind debug builds / PFVD_DEBUG_LOG (a release build never writes it).

Audit §4.5: bounds-check the requested mode in IOCTL_ADD; compute display_info clock_rate in u64 + saturate (the old u32 refresh*(h+4)^2 overflowed/aborted the mode DDI for large modes).

Verified: driver workspace builds clean on the RTX box (WDK 26100 + LLVM 21.1.2, MSVC). On-glass functional validation of the watchdog/render-pin is a follow-up (needs a driver reinstall + session).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 12:49:49 +00:00
parent 95dcef3515
commit 0a7ae5ef09
5 changed files with 177 additions and 18 deletions
@@ -142,3 +142,26 @@ pub fn set_adapter(adapter: iddcx::IDDCX_ADAPTER) {
pub(crate) fn adapter() -> Option<iddcx::IDDCX_ADAPTER> { pub(crate) fn adapter() -> Option<iddcx::IDDCX_ADAPTER> {
ADAPTER.get().map(|a| a.0) ADAPTER.get().map(|a| a.0)
} }
/// Honor the host's `IOCTL_SET_RENDER_ADAPTER`: pin the GPU the IddCx swap-chain renders on. On a hybrid
/// iGPU+dGPU box the OS may otherwise pick the iGPU to render the virtual monitor, so the host's shared
/// ring textures (created on the NVENC dGPU) can't be opened → `DRV_STATUS_TEX_FAIL` → the host's 20 s
/// black bail. Pinning the render adapter to the encode GPU fixes that. Unconditional — NOT the
/// SudoVDA-parity default-off branch (`docs/windows-host-rewrite-audit.md` §4.2). Returns
/// `STATUS_NOT_FOUND` if called before the adapter exists.
pub fn set_render_adapter(luid_low: u32, luid_high: i32) -> NTSTATUS {
let Some(adapter) = adapter() else {
return crate::STATUS_NOT_FOUND;
};
// SAFETY: building a C POD — the all-zero bit pattern is a valid IDARG_IN_ADAPTERSETRENDERADAPTER;
// the one meaningful field is assigned below.
let mut in_args: iddcx::IDARG_IN_ADAPTERSETRENDERADAPTER = unsafe { core::mem::zeroed() };
in_args.PreferredRenderAdapter = wdk_sys::LUID {
LowPart: luid_low,
HighPart: luid_high,
};
dbglog!("[pf-vd] set_render_adapter -> {luid_high:08x}:{luid_low:08x}");
// SAFETY: `adapter` is the stashed IddCx adapter; `in_args` is valid local storage read synchronously.
unsafe { wdk_iddcx::IddCxAdapterSetRenderAdapter(adapter, &in_args) };
STATUS_SUCCESS
}
@@ -33,6 +33,7 @@ pub unsafe extern "C" fn adapter_init_finished(
) -> NTSTATUS { ) -> NTSTATUS {
dbglog!("[pf-vd] adapter_init_finished"); dbglog!("[pf-vd] adapter_init_finished");
crate::adapter::set_adapter(adapter); crate::adapter::set_adapter(adapter);
crate::control::start_watchdog();
STATUS_SUCCESS STATUS_SUCCESS
} }
@@ -3,25 +3,76 @@
//! (watchdog keepalive), ADD/REMOVE/CLEAR_ALL (virtual monitors), and SET_RENDER_ADAPTER (next). Every //! (watchdog keepalive), ADD/REMOVE/CLEAR_ALL (virtual monitors), and SET_RENDER_ADAPTER (next). Every
//! path completes the `WDFREQUEST` exactly once (the `EVT_IDD_CX_DEVICE_IO_CONTROL` shape returns `()`). //! path completes the `WDFREQUEST` exactly once (the `EVT_IDD_CX_DEVICE_IO_CONTROL` shape returns `()`).
use core::sync::atomic::{AtomicU64, Ordering}; use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use pf_vdisplay_proto::control; use pf_vdisplay_proto::control;
use wdk_iddcx::nt_success; use wdk_iddcx::nt_success;
use wdk_sys::{NTSTATUS, WDFREQUEST, call_unsafe_wdf_function_binding}; use wdk_sys::{NTSTATUS, WDFREQUEST, call_unsafe_wdf_function_binding};
use crate::{STATUS_INVALID_PARAMETER, STATUS_NOT_FOUND, STATUS_NOT_IMPLEMENTED, STATUS_SUCCESS}; use crate::{STATUS_INVALID_PARAMETER, STATUS_NOT_FOUND, STATUS_SUCCESS};
/// The host must PING within this window or the watchdog reaps all monitors (STEP 4: the watchdog thread). /// The host must send an IOCTL within this window (it PINGs on a `timeout/3` timer) or the watchdog
/// treats it as gone and reaps every monitor. Reported to the host via [`control::IOCTL_GET_INFO`].
const WATCHDOG_TIMEOUT_S: u32 = 10; const WATCHDOG_TIMEOUT_S: u32 = 10;
/// Keepalive counter — PING bumps it; STEP 4's watchdog thread samples it to detect a gone host. /// Host-liveness counter — EVERY inbound IOCTL bumps it; [`start_watchdog`]'s thread samples it.
static WATCHDOG_PINGS: AtomicU64 = AtomicU64::new(0); static WATCHDOG_PINGS: AtomicU64 = AtomicU64::new(0);
/// Spawns the watchdog thread exactly once (idempotent across re-entrant adapter inits).
static WATCHDOG_STARTED: AtomicBool = AtomicBool::new(false);
/// Start the host-liveness watchdog (once, from `adapter_init_finished`).
///
/// Previously [`WATCHDOG_PINGS`] was bumped but NEVER sampled (no thread existed) — so a host that died
/// without a cooperative REMOVE (crash / `TerminateProcess`) left its virtual monitor + swap-chain
/// worker + pooled D3D device wedged in WUDFHost until the next host start's CLEAR_ALL, and a
/// not-restarted host left the orphan monitor in the desktop topology indefinitely
/// (`docs/windows-host-rewrite-audit.md` §4.1). This thread closes that: if no IOCTL arrives for
/// `WATCHDOG_TIMEOUT_S` while monitors exist, it departs them all.
///
/// (A WDF `EvtFileClose` on the control handle would be more immediate — the plan's preferred §3.4
/// option — but the polling watchdog matches the proven oracle and needs no IddCx file-object plumbing.)
pub fn start_watchdog() {
if WATCHDOG_STARTED.swap(true, Ordering::SeqCst) {
return;
}
let tick = Duration::from_secs(u64::from((WATCHDOG_TIMEOUT_S / 3).max(1)));
let timeout = Duration::from_secs(u64::from(WATCHDOG_TIMEOUT_S));
std::thread::spawn(move || {
let mut last = WATCHDOG_PINGS.load(Ordering::Relaxed);
let mut last_change = Instant::now();
loop {
std::thread::sleep(tick);
let cur = WATCHDOG_PINGS.load(Ordering::Relaxed);
if cur != last {
last = cur;
last_change = Instant::now();
continue;
}
// No IOCTL since `last_change`. A live host PINGs every `timeout/3`, so this only trips once
// the host is truly gone; only reap when there's something to reap.
if last_change.elapsed() >= timeout && crate::monitor::has_monitors() {
let n = crate::monitor::reap_orphaned(Duration::from_secs(3));
if n > 0 {
dbglog!(
"[pf-vd] watchdog: no host IOCTL in {WATCHDOG_TIMEOUT_S}s — host gone, departed {n} monitor(s)"
);
}
last_change = Instant::now(); // don't re-reap every tick
}
}
});
}
/// Dispatch one control IOCTL and complete the request. /// Dispatch one control IOCTL and complete the request.
/// ///
/// # Safety /// # Safety
/// `request` is the framework-provided `WDFREQUEST` for an `EvtIddCxDeviceIoControl` call. /// `request` is the framework-provided `WDFREQUEST` for an `EvtIddCxDeviceIoControl` call.
pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) { pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
// Every inbound IOCTL is host liveness (the host PINGs on a timer, plus ADD/REMOVE/GET_INFO/…) —
// bump the watchdog at the top so it only fires once the host has gone truly silent. See
// [`start_watchdog`].
WATCHDOG_PINGS.fetch_add(1, Ordering::Relaxed);
match ioctl_code { match ioctl_code {
control::IOCTL_GET_INFO => { control::IOCTL_GET_INFO => {
let reply = control::InfoReply { let reply = control::InfoReply {
@@ -31,10 +82,7 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
unsafe { write_output_complete(request, &reply) }; unsafe { write_output_complete(request, &reply) };
} }
control::IOCTL_PING => { control::IOCTL_PING => complete(request, STATUS_SUCCESS),
WATCHDOG_PINGS.fetch_add(1, Ordering::Relaxed);
complete(request, STATUS_SUCCESS);
}
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_ADD => unsafe { add(request) }, control::IOCTL_ADD => unsafe { add(request) },
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
@@ -43,12 +91,34 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
crate::monitor::clear_all(); crate::monitor::clear_all();
complete(request, STATUS_SUCCESS); complete(request, STATUS_SUCCESS);
} }
// SET_RENDER_ADAPTER (hybrid-GPU render pin): STEP 4 (next). // SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_RENDER_ADAPTER => complete(request, STATUS_NOT_IMPLEMENTED), control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
_ => complete(request, STATUS_NOT_FOUND), _ => complete(request, STATUS_NOT_FOUND),
} }
} }
/// Sanity bounds for a requested mode — generous (covers any real client) but rejects zero/absurd
/// values that would otherwise feed the EDID/mode math unchecked.
fn valid_mode(width: u32, height: u32, refresh_hz: u32) -> bool {
(1..=16384).contains(&width)
&& (1..=16384).contains(&height)
&& (1..=1000).contains(&refresh_hz)
}
/// `IOCTL_SET_RENDER_ADAPTER`: pin the IddCx render adapter (hybrid-GPU IDD-push).
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn set_render_adapter(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::SetRenderAdapterRequest>(request) }) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
let st = crate::adapter::set_render_adapter(req.luid_low, req.luid_high);
complete(request, st);
}
/// `IOCTL_ADD`: create a virtual monitor at the requested mode → reply with the OS target id + LUID. /// `IOCTL_ADD`: create a virtual monitor at the requested mode → reply with the OS target id + LUID.
/// ///
/// # Safety /// # Safety
@@ -59,6 +129,10 @@ unsafe fn add(request: WDFREQUEST) {
complete(request, STATUS_INVALID_PARAMETER); complete(request, STATUS_INVALID_PARAMETER);
return; return;
}; };
if !valid_mode(req.width, req.height, req.refresh_hz) {
complete(request, STATUS_INVALID_PARAMETER);
return;
}
let Some((target_id, luid_low, luid_high)) = let Some((target_id, luid_low, luid_high)) =
crate::monitor::create_monitor(req.session_id, req.width, req.height, req.refresh_hz) crate::monitor::create_monitor(req.session_id, req.width, req.height, req.refresh_hz)
else { else {
@@ -1,16 +1,29 @@
//! Minimal driver logger (matches the DualSense driver). DebugView can't capture the UMDF host across //! Minimal driver logger. `OutputDebugStringA` always (ETW/DebugView); the optional world-writable file
//! session 0, so besides `OutputDebugStringA` we append to a world-writable file readable over SSH. Used //! (`C:\Users\Public\pfvd-driver.log`, readable over SSH) is now OPT-IN — debug builds, or the
//! only for bring-up/diagnostics; cheap and best-effort (ignores all errors). //! `PFVD_DEBUG_LOG` env var, only — so a RELEASE build never writes it (audit §4.4: it was an
//! info-leak/DoS surface). Best-effort; ignores all errors. Production driver-state visibility is the
//! SharedHeader `driver_status` channel, not this file.
unsafe extern "system" { unsafe extern "system" {
fn OutputDebugStringA(s: *const u8); fn OutputDebugStringA(s: *const u8);
} }
/// Whether the world-writable bring-up file log is enabled (resolved once). Off in release builds unless
/// `PFVD_DEBUG_LOG` is set.
fn file_log_enabled() -> bool {
use std::sync::OnceLock;
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| cfg!(debug_assertions) || std::env::var_os("PFVD_DEBUG_LOG").is_some())
}
pub fn log(s: &str) { pub fn log(s: &str) {
if let Ok(c) = std::ffi::CString::new(s) { if let Ok(c) = std::ffi::CString::new(s) {
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call. // SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
unsafe { OutputDebugStringA(c.as_ptr().cast()) }; unsafe { OutputDebugStringA(c.as_ptr().cast()) };
} }
if !file_log_enabled() {
return;
}
use std::io::Write; use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new() if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true) .create(true)
@@ -6,7 +6,7 @@
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Instant; use std::time::{Duration, Instant};
use wdk_sys::iddcx; use wdk_sys::iddcx;
@@ -64,6 +64,50 @@ pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
/// Monitor id / EDID-serial counter (unique per created monitor). /// Monitor id / EDID-serial counter (unique per created monitor).
static NEXT_ID: AtomicU32 = AtomicU32::new(1); static NEXT_ID: AtomicU32 = AtomicU32::new(1);
/// True if any virtual monitor currently exists — the host-gone watchdog only reaps when there's
/// something to reap (see [`crate::control::start_watchdog`]).
pub fn has_monitors() -> bool {
MONITOR_MODES.lock().map(|l| !l.is_empty()).unwrap_or(false)
}
/// Depart every monitor that has existed at least `grace` — the host-gone watchdog reap
/// ([`crate::control::start_watchdog`]). The grace skips a just-created monitor (the host adds it, then
/// starts pinging) so a momentarily-stale ping timer can't nuke a brand-new monitor. Returns the count
/// departed. Same lock discipline as [`remove_monitor`]: drop each worker (which RAII-joins its thread)
/// OUTSIDE the `MONITOR_MODES` lock, then depart.
pub fn reap_orphaned(grace: Duration) -> usize {
let mut drained: Vec<(
Option<iddcx::IDDCX_MONITOR>,
Option<crate::swap_chain_processor::SwapChainProcessor>,
)> = {
let Ok(mut lock) = MONITOR_MODES.lock() else {
return 0;
};
let mut taken = Vec::new();
let mut i = 0;
while i < lock.len() {
if lock[i].created_at.elapsed() >= grace {
let mut m = lock.remove(i);
taken.push((m.object, m.swap_chain_processor.take()));
} else {
i += 1;
}
}
taken
};
let n = drained.len();
for (_, processor) in &mut drained {
drop(processor.take());
}
for (object, _) in drained {
if let Some(m) = object {
// SAFETY: `m` is a live IddCx monitor handle; departure tears it down.
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
}
}
n
}
/// Fallback modes appended after the requested mode, so a topology change still has options. /// Fallback modes appended after the requested mode, so a topology change still has options.
fn default_modes() -> Vec<Mode> { fn default_modes() -> Vec<Mode> {
vec![ vec![
@@ -86,17 +130,21 @@ pub fn display_info(
height: u32, height: u32,
refresh_rate: u32, refresh_rate: u32,
) -> wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO { ) -> wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
let clock_rate = refresh_rate * (height + 4) * (height + 4) + 1000; // Compute in u64 then saturate the u32 rational numerators: the old u32 `refresh*(h+4)^2` overflows
// for a large mode (e.g. 8K@240), which panics→aborts the extern-"C" mode DDI in a debug build.
// Identical for every real mode; only an absurd (also now bounds-rejected) mode saturates.
let clock_rate: u64 = u64::from(refresh_rate) * u64::from(height + 4) * u64::from(height + 4) + 1000;
let clock_rate_u32 = u32::try_from(clock_rate).unwrap_or(u32::MAX);
// SAFETY: building a C POD — the all-zero bit pattern is a valid uninitialized // SAFETY: building a C POD — the all-zero bit pattern is a valid uninitialized
// DISPLAYCONFIG_VIDEO_SIGNAL_INFO; every meaningful field is assigned below. // DISPLAYCONFIG_VIDEO_SIGNAL_INFO; every meaningful field is assigned below.
let mut si: wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO = unsafe { core::mem::zeroed() }; let mut si: wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO = unsafe { core::mem::zeroed() };
si.pixelRate = u64::from(clock_rate); si.pixelRate = clock_rate;
si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL { si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate, Numerator: clock_rate_u32,
Denominator: height + 4, Denominator: height + 4,
}; };
si.vSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL { si.vSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate, Numerator: clock_rate_u32,
Denominator: (height + 4) * (height + 4), Denominator: (height + 4) * (height + 4),
}; };
si.activeSize = wdk_sys::DISPLAYCONFIG_2DREGION { si.activeSize = wdk_sys::DISPLAYCONFIG_2DREGION {