feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)

Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\
objects (openable by any sibling LocalService) to UNNAMED sections/events whose
handles the host DuplicateHandles into the driver's verified WUDFHost with least
access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL,
pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded;
HID minidrivers have no control device). Driver-validated pad_index kills
cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides.
Sibling-LocalService denial proven empirically (design/idd-push-security.md,
design/gamepad-channel-sealing.md).

Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the
forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad
drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI.

driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created
SwDevice REVIVES the old devnode with its previously-bound driver (never
re-ranks), so an upgrade otherwise leaves the old driver serving — or, across
the v1→v2 fence, a dead pad (found live on the RTX box).

On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms
cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via
both the test harness and a real streaming session; phantom-sweep repro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 12:08:56 +00:00
parent a3e1ea2b44
commit 95a08e99c3
37 changed files with 2985 additions and 1174 deletions
@@ -42,8 +42,10 @@ AddReg=pf_vdisplay_HardwareDeviceSettings
[pf_vdisplay_HardwareDeviceSettings]
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
; Only the host (LocalSystem service) + admins may open the control device. Deliberately NO Everyone
; ACE (SudoVDA ships one for its user-mode host): the control plane creates/removes monitors and
; bootstraps the sealed frame channel (IOCTL_SET_FRAME_CHANNEL), so it is not for unprivileged callers.
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)"
[pf_vdisplay_Install.NT.Services]
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
@@ -36,6 +36,8 @@ struct SendAdapter(iddcx::IDDCX_ADAPTER);
// SAFETY: an opaque IddCx handle, used only as an argument to IddCx DDIs (themselves the synchronisation
// point) — never dereferenced in Rust. Storing it across threads in a OnceLock is sound.
unsafe impl Send for SendAdapter {}
// SAFETY: as above — the handle is only ever passed by value to IddCx DDIs, never dereferenced, so
// shared `&SendAdapter` access across threads is sound.
unsafe impl Sync for SendAdapter {}
static ADAPTER: OnceLock<SendAdapter> = OnceLock::new();
@@ -51,8 +51,9 @@ pub unsafe extern "C" fn parse_monitor_description(
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
// SAFETY: framework-provided in/out args, valid for the call.
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
let in_args = unsafe { &*p_in };
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
let out_args = unsafe { &mut *p_out };
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
let edid = unsafe {
@@ -100,8 +101,9 @@ pub unsafe extern "C" fn parse_monitor_description2(
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2,
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
) -> NTSTATUS {
// SAFETY: framework-provided in/out args, valid for the call.
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
let in_args = unsafe { &*p_in };
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
let out_args = unsafe { &mut *p_out };
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
let edid = unsafe {
@@ -156,8 +158,9 @@ pub unsafe extern "C" fn monitor_query_modes(
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
// SAFETY: framework-provided in/out args, valid for the call.
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
let in_args = unsafe { &*p_in };
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
let out_args = unsafe { &mut *p_out };
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
return STATUS_NOT_FOUND;
@@ -183,8 +186,9 @@ pub unsafe extern "C" fn monitor_query_modes2(
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2,
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
) -> NTSTATUS {
// SAFETY: framework-provided in/out args, valid for the call.
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
let in_args = unsafe { &*p_in };
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
let out_args = unsafe { &mut *p_out };
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
return STATUS_NOT_FOUND;
@@ -279,7 +283,8 @@ pub unsafe extern "C" fn assign_swap_chain(
drop(crate::monitor::take_swap_chain_processor(monitor));
// The OS target id (stamped on the monitor at creation, after IddCxMonitorArrival) keys the
// per-monitor objects STEP 6's host opens. 0 (default) if the monitor isn't found.
// frame-channel stash STEP 6's worker attaches from (the host addresses its IOCTL_SET_FRAME_CHANNEL
// delivery by this id). 0 (default) if the monitor isn't found — the worker then never attaches.
let target_id = crate::monitor::target_id_for_object(monitor).unwrap_or(0);
if let Some(device) = crate::direct_3d_device::pooled_device(luid) {
@@ -93,6 +93,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
}
// SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
// SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
_ => complete(request, STATUS_NOT_FOUND),
}
}
@@ -148,11 +150,49 @@ unsafe fn add(request: WDFREQUEST) {
adapter_luid_high: luid_high,
target_id,
resolved_monitor_id: monitor_id,
// This WUDFHost's pid — where the host duplicates the sealed frame channel's handles INTO
// (`ProcessSharingDisabled`: this process is exclusively ours and dies with the device).
wudf_pid: std::process::id(),
};
// SAFETY: `request` is the framework WDFREQUEST.
unsafe { write_output_complete(request, &reply) };
}
/// `IOCTL_SET_FRAME_CHANNEL`: adopt the handle values the host duplicated into this process and stash
/// them on the target monitor for the swap-chain worker to attach with. The ownership contract with
/// the host is **adopt-on-success only**: this driver owns (and eventually closes) the handles iff the
/// IOCTL completes successfully; on ANY error completion it leaves them untouched, because the host
/// reaps its remote duplicates whenever the IOCTL fails — a close on both sides would double-close
/// values the OS may already have reused for unrelated handles.
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn set_frame_channel(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
// A malformed request adopts nothing (no FrameChannel is built, so no Drop can close anything).
let Some(ch) = crate::frame_transport::FrameChannel::from_request(&req) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
match crate::monitor::set_frame_channel(req.target_id, ch) {
Ok(()) => complete(request, STATUS_SUCCESS),
Err(ch) => {
dbglog!(
"[pf-vd] SET_FRAME_CHANNEL: no monitor with target_id {} — rejecting (host reaps the handles)",
req.target_id
);
// NOT adopted: disarm the channel so its Drop does NOT close the handles (see the contract
// above — the host's error path reaps them remotely).
ch.into_unowned();
complete(request, STATUS_NOT_FOUND);
}
}
}
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
///
/// # Safety
@@ -123,10 +123,10 @@ static DEVICE_POOL: Mutex<Option<(i64, Arc<Direct3DDevice>)>> = Mutex::new(None)
pub fn pooled_device(luid: LUID) -> Option<Arc<Direct3DDevice>> {
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart);
let mut pool = DEVICE_POOL.lock().ok()?;
if let Some((k, dev)) = pool.as_ref() {
if *k == key {
return Some(dev.clone());
}
if let Some((k, dev)) = pool.as_ref()
&& *k == key
{
return Some(dev.clone());
}
match Direct3DDevice::init(luid) {
Ok(d) => {
@@ -1,32 +1,30 @@
//! STEP 6 — IDD-push frame publisher (DRIVER side).
//! STEP 6 — IDD-push frame publisher (DRIVER side), attached over the **sealed channel**.
//!
//! The restricted WUDFHost token canNOT create named kernel objects (proven on the RTX box: it can't
//! even write a world-writable file), so — exactly like the gamepad UMDF drivers
//! (`crates/punktfunk-host/src/inject/dualsense_windows.rs`: *"the host creates the section, privileged,
//! with a permissive SDDL so the WUDFHost can open it; the driver maps it"*) — the **host** creates the
//! shared header + frame-ready event + ring of keyed-mutex textures, and the driver only **OPENS** them.
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header (our
//! only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write files),
//! then copies each acquired swap-chain surface into the next ring slot and signals the host.
//! The restricted WUDFHost token canNOT create named kernel objects — and since the frame channel
//! carries whole-desktop pixels, the objects are not merely host-created but **unnamed**: nothing to
//! enumerate, open by name, or pre-create ("squat"). The **host** creates the shared header +
//! frame-ready event + ring of keyed-mutex textures with no names, duplicates the handles INTO this
//! WUDFHost process (`DuplicateHandle` — SYSTEM can, we can't reciprocate, which is why the host is the
//! broker), and delivers the handle VALUES over `IOCTL_SET_FRAME_CHANNEL` ([`crate::control`] stashes
//! them per monitor as a [`FrameChannel`]). The swap-chain worker picks the stash up and attaches with
//! [`FramePublisher::from_channel`]. Only the two endpoint processes ever hold a handle to any frame
//! object — see `design/idd-push-security.md`.
//!
//! Host counterpart: `crates/punktfunk-host/src/capture/idd_push.rs`. The shared `SharedHeader` layout,
//! the [`FrameToken`] packing, the `Global\` object-name scheme, the `MAGIC`/`RING_LEN` and the
//! `DRV_STATUS_*` codes are NOT hand-duplicated here: both sides `use pf_driver_proto::frame::*`, which
//! OWNS the contract (with `const` size asserts so any drift is a compile error).
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header
//! (our only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write
//! files), then copies each acquired swap-chain surface into the next ring slot and signals the host.
//!
//! Ported from the proven oracle (`packaging/windows/vdisplay-driver/pf-vdisplay/src/frame_transport.rs`).
//! Differences from the oracle:
//! * the layout/consts/names/token come from `pf_driver_proto::frame` instead of being re-declared;
//! * `dbglog!` replaces `log::info!`;
//! * the optional fixed-name `Global\pfvd-dbg` `DebugBlock` bring-up channel is SKIPPED (not on the data
//! path). FOLLOW-UP: if the host bring-up diagnostics are needed again, port the oracle's `DebugBlock`
//! here too (it is owned by `idd_push.rs`, not the proto).
//! Host counterpart: `crates/punktfunk-host/src/capture/windows/idd_push.rs`. The shared `SharedHeader`
//! layout, the [`FrameToken`] packing, the `MAGIC`/`RING_LEN`, the `DRV_STATUS_*` codes and the
//! channel-delivery struct are NOT hand-duplicated here: both sides `use pf_driver_proto::{control,
//! frame}`, which OWNS the contract (with `const` size asserts so any drift is a compile error).
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use pf_driver_proto::control::SetFrameChannelRequest;
use pf_driver_proto::frame::{
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, FrameToken, MAGIC, RING_LEN,
SharedHeader, event_name, header_name, texture_name,
SharedHeader,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Graphics::Direct3D11::{
@@ -34,28 +32,95 @@ use windows::Win32::Graphics::Direct3D11::{
};
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
use windows::Win32::System::Memory::{
FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW,
UnmapViewOfFile,
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
};
use windows::Win32::System::Threading::{OpenEventW, SYNCHRONIZATION_ACCESS_RIGHTS, SetEvent};
use windows::core::{HSTRING, Interface};
use windows::Win32::System::Threading::SetEvent;
use windows::core::Interface;
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` — passed to `OpenSharedResourceByName` (matches the host's
/// `CreateSharedHandle` access). Kept local: it is a `OpenSharedResourceByName` arg, not part of the
/// proto contract. (Same value the host uses in `idd_push.rs`.)
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
/// SYNCHRONIZE | EVENT_MODIFY_STATE — the driver does not wait on the event, only SIGNALS it.
const EVENT_ACCESS: u32 = 0x0010_0000 | 0x0002;
/// `WAIT_TIMEOUT` as an HRESULT — `AcquireSync` returns this when the slot is held by the consumer.
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
/// One monitor's sealed-channel bootstrap: the handle VALUES the host duplicated into THIS process
/// (`IOCTL_SET_FRAME_CHANNEL`). Owning a `FrameChannel` means owning those handles — exactly one of
/// {the monitor stash ([`crate::monitor`]), a [`FramePublisher`] under construction} holds it at any
/// time, and `Drop` closes every entry not consumed, so a replaced/unmatched/failed delivery can never
/// leak entries in the WUDFHost handle table. A `0` field means "taken" (or never valid) and is skipped.
pub struct FrameChannel {
/// The ring generation these textures belong to (checked against the header at attach).
generation: u32,
ring_len: u32,
header: u64,
event: u64,
textures: [u64; RING_LEN as usize],
}
impl FrameChannel {
/// Validate + adopt the handle values from the host's IOCTL. `None` on a malformed request (bad
/// `ring_len`, zero handles) — the caller completes with `STATUS_INVALID_PARAMETER` and nothing is
/// adopted (a zero value is never treated as a handle).
pub fn from_request(req: &SetFrameChannelRequest) -> Option<Self> {
if req.ring_len == 0 || req.ring_len > RING_LEN {
return None;
}
if req.header_handle == 0
|| req.event_handle == 0
|| req.texture_handles[..req.ring_len as usize].contains(&0)
{
return None;
}
Some(Self {
generation: req.generation,
ring_len: req.ring_len,
header: req.header_handle,
event: req.event_handle,
textures: req.texture_handles,
})
}
/// Move a handle value out of the channel: the caller now owns it; `Drop` skips the zeroed slot.
fn take(v: &mut u64) -> HANDLE {
HANDLE(core::mem::take(v) as usize as *mut core::ffi::c_void)
}
/// Disarm without closing anything — for the adopt-on-success-only contract: a delivery rejected
/// with an error completion was never adopted, and the HOST reaps its remote duplicates on that
/// error, so closing here too would double-close (see `crate::control::set_frame_channel`).
pub fn into_unowned(mut self) {
self.header = 0;
self.event = 0;
self.textures = [0; RING_LEN as usize];
}
}
impl Drop for FrameChannel {
fn drop(&mut self) {
for v in [&mut self.header, &mut self.event]
.into_iter()
.chain(self.textures.iter_mut())
{
if *v != 0 {
let h = Self::take(v);
// SAFETY: `h` is a live handle the host duplicated into this process for us to own; it
// was not consumed (non-zero), so this is its sole close.
unsafe {
let _ = CloseHandle(h);
}
}
}
}
}
// NB: `FrameChannel` is plain integers, so it is auto-`Send` — it crosses from the control-plane
// dispatch thread (stash) to the swap-chain worker (attach) with `MONITOR_MODES` serializing the
// hand-off; no manual impl needed (handle values are process-global tokens, not thread-affine).
struct Slot {
tex: ID3D11Texture2D,
mutex: IDXGIKeyedMutex,
}
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain processor
/// thread; attached lazily once the host has created the shared objects.
/// thread; attached lazily once the host's channel delivery lands in the monitor stash.
pub struct FramePublisher {
context: ID3D11DeviceContext,
map: HANDLE,
@@ -70,7 +135,8 @@ pub struct FramePublisher {
ring_format: u32,
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
/// detects that so `run_core` re-attaches to the new ring (whose channel the host re-delivers)
/// instead of dropping every frame.
generation: u32,
/// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a
/// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1).
@@ -81,102 +147,99 @@ pub struct FramePublisher {
unsafe impl Send for FramePublisher {}
impl FramePublisher {
/// Try ONCE to attach to the host-created shared objects. Returns `Err` cheaply if the host hasn't
/// created/published them yet — the drain loop retries periodically, so a non-IDD-push session just
/// keeps draining with no stall. All early-return paths clean up the handles/mapping they opened
/// explicitly (raw-handle style, no RAII — matches the rest of this driver).
pub fn try_open(
target_id: u32,
/// Attach to the host ring from a delivered [`FrameChannel`]. Consumes the channel: on ANY failure
/// every handle is closed (taken ones explicitly, the rest by the channel's `Drop`) and the host
/// re-delivers on the next recreate — there is nothing to poll, so failure is terminal for THIS
/// delivery (the host's `wait_for_attach` sees the status code and fails the session open). All
/// early-return paths clean up explicitly (raw-handle style, no RAII — matches the rest of this
/// driver).
pub fn from_channel(
mut channel: FrameChannel,
render_luid_low: u32,
render_luid_high: i32,
device: &ID3D11Device,
context: &ID3D11DeviceContext,
) -> windows::core::Result<Self> {
// 1. Open the host-created header (RW). Err if the host hasn't created it yet.
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call (`?` returns on failure).
let map = unsafe {
OpenFileMappingW(
FILE_MAP_ALL_ACCESS.0,
false,
&HSTRING::from(header_name(target_id)),
)?
};
// SAFETY: `map` is the just-opened file mapping; mapping size_of::<SharedHeader>() bytes of it
// (the host created the mapping at >= that size). The null `view.Value` is checked below.
let ring_len = channel.ring_len;
// 1. Map the header from the duplicated section handle (ours from here on).
let map = FrameChannel::take(&mut channel.header);
// SAFETY: `map` is the live section handle the host duplicated into this process; mapping
// size_of::<SharedHeader>() bytes of it (the host created the mapping at >= that size). The null
// `view.Value` is checked below.
let view = unsafe {
// Read/write only — the host now duplicates the header handle with least access
// (`SECTION_MAP_READ | SECTION_MAP_WRITE`), so `FILE_MAP_ALL_ACCESS` would exceed the
// granted rights and fail. We read the layout + write status/publish-token fields; RW covers it.
MapViewOfFile(
map,
FILE_MAP_ALL_ACCESS,
FILE_MAP_READ | FILE_MAP_WRITE,
0,
0,
core::mem::size_of::<SharedHeader>(),
)
};
if view.Value.is_null() {
// SAFETY: `map` is the just-opened mapping handle, closed once here on the error path.
let err = windows::core::Error::from_win32();
// SAFETY: `map` is the taken section handle, closed once here on the error path (the rest of
// `channel` closes via its Drop).
unsafe {
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
return Err(err);
}
let header = view.Value.cast::<SharedHeader>();
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>() bytes);
// these scalar writes are within it. The host opened the section with a permissive SDDL for us.
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>()
// bytes); these scalar writes are within it.
unsafe {
(*header).driver_render_luid_low = render_luid_low;
(*header).driver_render_luid_high = render_luid_high;
}
// 3. The host sets magic==MAGIC only once the ring textures exist. Not ready → retry later.
// SAFETY: `header` is the mapped host header; `magic` lives within it and is read atomically
// (Acquire) to pair with the host's Release store once the ring textures are published.
let magic = unsafe {
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32)).load(Ordering::Acquire)
// 3. The host stamps magic==MAGIC BEFORE delivering the channel, and this channel's generation
// must match the header's CURRENT generation — a mismatch means the host recreated the ring
// again before we attached (a fresh delivery is on its way); drop this stale one.
// SAFETY: `header` is the mapped host header; `magic`/`generation` live within it and are read
// atomically (Acquire) to pair with the host's Release publishes.
let (magic, header_gen) = unsafe {
(
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32))
.load(Ordering::Acquire),
(*(core::ptr::addr_of!((*header).generation) as *const AtomicU32))
.load(Ordering::Acquire),
)
};
if magic != MAGIC {
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once on this path.
if magic != MAGIC || header_gen != channel.generation {
dbglog!(
"[pf-vd] frame-push(driver): dropping channel delivery (magic ok: {}, channel gen {} vs header gen {header_gen})",
magic == MAGIC,
channel.generation
);
// SAFETY: `header`/`map` are the live mapped view + taken handle; unmapped + closed once on
// this path.
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
return Err(windows::core::Error::from_win32());
// E_BOUNDS — stand-in for "stale delivery"; the caller only drops the attempt.
return Err(windows::core::HRESULT(0x8000_000Bu32 as i32).into());
}
// SAFETY: `header` is the mapped host header; these scalar fields live within it.
let (generation, ring_len) =
unsafe { ((*header).generation, (*header).ring_len.min(RING_LEN)) };
// 4. Open the event (SYNCHRONIZE | EVENT_MODIFY_STATE so we can SetEvent).
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call.
let event = match unsafe {
OpenEventW(
SYNCHRONIZATION_ACCESS_RIGHTS(EVENT_ACCESS),
false,
&HSTRING::from(event_name(target_id)),
)
} {
Ok(e) => e,
Err(e) => {
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once here.
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
return Err(e);
}
};
// 4. The frame-ready event (duplicated with the host handle's full access, so SetEvent works).
let event = FrameChannel::take(&mut channel.event);
// 5. Open device1 + the ring textures the host created (same render adapter required).
// 5. Open device1 + the ring textures from their duplicated shared handles (same render adapter
// required). Each NT handle is closed right after the open — the COM object holds its own
// reference, and the HOST keeps the resource alive with its own handle.
let device1: ID3D11Device1 = match device.cast() {
Ok(d) => d,
Err(e) => {
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are the
// live handles, all released once on this error path.
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are
// the taken live handles, all released once on this error path.
unsafe {
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
let _ = CloseHandle(event);
@@ -189,45 +252,45 @@ impl FramePublisher {
}
};
let mut slots = Vec::new();
for k in 0..ring_len {
let name = HSTRING::from(texture_name(target_id, generation, k));
// SAFETY: `device1` is a live ID3D11Device1; the name HSTRING is valid for the call.
// Take each texture handle one at a time (NOT the whole array up front), so an error return
// mid-loop still lets `channel`'s Drop close every not-yet-taken handle.
for value in channel.textures.iter_mut().take(ring_len as usize) {
let tex_handle = FrameChannel::take(value);
// SAFETY: `device1` is a live ID3D11Device1; `tex_handle` is the duplicated shared NT handle
// for this ring texture.
let opened: windows::core::Result<ID3D11Texture2D> =
unsafe { device1.OpenSharedResourceByName(&name, DXGI_SHARED_RESOURCE_RW) };
match opened {
unsafe { device1.OpenSharedResource1(tex_handle) };
// SAFETY: `tex_handle` is ours (taken above) and no longer needed whether the open succeeded
// (the COM object holds the resource) or failed — close it exactly once here.
unsafe {
let _ = CloseHandle(tex_handle);
}
let failed = match opened {
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
Ok(mutex) => slots.push(Slot { tex, mutex }),
Err(e) => {
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
// are the live handles, all released once on this error path.
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
return Err(e);
Ok(mutex) => {
slots.push(Slot { tex, mutex });
None
}
Err(e) => Some(e),
},
Err(e) => {
// Most likely a render-adapter mismatch (the host made the textures on a different
// GPU than the swap-chain renders on). Tell the host so it can report it.
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
// are the live handles, all released once on this error path.
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
return Err(e);
// Most likely a render-adapter mismatch (the host made the textures on a different GPU
// than the swap-chain renders on). Tell the host so it can report it.
Err(e) => Some(e),
};
if let Some(e) = failed {
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
// are the taken live handles, all released once on this error path (the not-yet-taken
// texture handles close via `channel`'s Drop).
unsafe {
(*header).driver_status = DRV_STATUS_TEX_FAIL;
(*header).driver_status_detail = e.code().0 as u32;
let _ = CloseHandle(event);
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
return Err(e);
}
}
@@ -236,7 +299,7 @@ impl FramePublisher {
(*header).driver_status = DRV_STATUS_OPENED;
}
dbglog!(
"[pf-vd] frame-push(driver): attached to host ring gen {generation} ({ring_len} slots)"
"[pf-vd] frame-push(driver): attached to host ring gen {header_gen} ({ring_len} slots, sealed channel)"
);
Ok(Self {
context: context.clone(),
@@ -248,7 +311,7 @@ impl FramePublisher {
seq: 0,
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
ring_format: unsafe { (*header).dxgi_format },
generation,
generation: header_gen,
mismatch_logged: false,
})
}
@@ -261,8 +324,8 @@ impl FramePublisher {
}
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's HDR
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and the texture names now carry a new
/// generation. `run_core` drops the publisher on this so it re-attaches to the new ring.
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and a fresh channel delivery is coming.
/// `run_core` drops the publisher on this so it re-attaches to the new ring.
pub fn is_stale(&self) -> bool {
// SAFETY: `self.header` stays mapped for the publisher's lifetime; `generation` lives within it and
// is read atomically (Acquire) to pair with the host's Release bump on a mid-session ring recreate.
@@ -338,8 +401,8 @@ impl FramePublisher {
}
.pack();
self.latest_cell().store(latest, Ordering::Release);
// SAFETY: `self.event` is the live host-created frame-ready event we opened with
// EVENT_MODIFY_STATE; signalling it wakes the host consumer.
// SAFETY: `self.event` is the live host-created frame-ready event, duplicated into
// this process with the creator's access; signalling it wakes the host consumer.
unsafe {
let _ = SetEvent(self.event);
}
@@ -357,10 +420,11 @@ impl FramePublisher {
impl Drop for FramePublisher {
fn drop(&mut self) {
// Slots FIRST (release the shared textures + keyed mutexes), THEN unmap the header, THEN the
// handles.
// handles — nothing of the channel outlives the publisher (teardown invariant,
// `design/idd-push-security.md`).
self.slots.clear();
// SAFETY: drop runs once; `self.header` (if non-null) is the live mapped view and `self.event`/
// `self.map` are the live handles this publisher opened — each unmapped/closed exactly once here.
// `self.map` are the live handles this publisher owns — each unmapped/closed exactly once here.
unsafe {
if !self.header.is_null() {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
@@ -10,9 +10,12 @@
#![allow(non_snake_case, clippy::missing_safety_doc)]
// P0 lint (audit §8): an unsafe op inside an `unsafe fn` must be in an explicit `unsafe {}` block, so the
// fn-level `unsafe` never silently blesses the whole body. (The per-site `// SAFETY:` discipline already
// landed in STEP 8.)
// fn-level `unsafe` never silently blesses the whole body, AND every `unsafe {}` must carry a `// SAFETY:`
// proof. An IddCx display driver is inherently FFI-bound (D3D11 / IddCx DDIs / cross-process shared
// textures), so it can't be unsafe-FREE the way the gamepad drivers now are (their logic moved onto the
// safe `pf_umdf_util` layer); these gates make it unsafe-AUDITED instead, and stop it regressing.
#![deny(unsafe_op_in_unsafe_fn)]
#![deny(clippy::undocumented_unsafe_blocks)]
#[macro_use]
mod log;
@@ -45,11 +45,11 @@ pub fn log(s: &str) {
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
}
use std::io::Write;
if let Some(m) = file_appender() {
if let Ok(mut f) = m.lock() {
let _ = writeln!(f, "{s}");
let _ = f.flush();
}
if let Some(m) = file_appender()
&& let Ok(mut f) = m.lock()
{
let _ = writeln!(f, "{s}");
let _ = f.flush();
}
}
@@ -53,6 +53,11 @@ pub struct MonitorObject {
/// The live swap-chain drain worker, set by `assign_swap_chain` and dropped (RAII-joins the worker
/// thread) by `unassign_swap_chain` / departure (STEP 5).
pub swap_chain_processor: Option<crate::swap_chain_processor::SwapChainProcessor>,
/// The host's sealed-channel delivery (`IOCTL_SET_FRAME_CHANNEL`) awaiting pickup by the swap-chain
/// worker ([`take_frame_channel`]). Exactly one owner per delivery: replacing or dropping the entry
/// closes an unconsumed channel's handles via [`FrameChannel`]'s `Drop`, so no delivery can leak
/// handles in the WUDFHost table whatever the monitor's fate.
pub frame_channel: Option<crate::frame_transport::FrameChannel>,
/// When the entry was created — the watchdog skips still-initializing monitors.
pub created_at: Instant,
}
@@ -256,8 +261,8 @@ pub fn modes_for_object(object: iddcx::IDDCX_MONITOR) -> Option<Vec<Mode>> {
.map(|m| m.modes.clone())
}
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to name the
/// shared-ring objects). `None` if the monitor isn't found.
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to key the
/// frame-channel stash for its worker). `None` if the monitor isn't found.
pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
MONITOR_MODES
.lock()
@@ -267,6 +272,52 @@ pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
.map(|m| m.target_id)
}
/// Stash a host frame-channel delivery on the monitor with `target_id` (an ARRIVED monitor — a pending
/// entry's `target_id` is still 0, which the host can never send since OS target ids are non-zero).
/// Replacing an unconsumed delivery drops it → its handles close (it WAS adopted by a prior success).
/// `Err(ch)` if no such monitor exists — the caller must NOT close those handles (the host only sees
/// the error status and reaps its remote duplicates itself; closing here too would double-close values
/// the OS may have reused).
pub fn set_frame_channel(
target_id: u32,
ch: crate::frame_transport::FrameChannel,
) -> Result<(), crate::frame_transport::FrameChannel> {
if target_id == 0 {
return Err(ch);
}
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.frame_channel = Some(ch);
Ok(())
} else {
Err(ch)
}
}
/// Take (remove) the pending frame-channel delivery for `target_id`, transferring handle ownership to
/// the caller (the swap-chain worker's attach). `None` until the host delivers one.
pub fn take_frame_channel(target_id: u32) -> Option<crate::frame_transport::FrameChannel> {
if target_id == 0 {
return None;
}
lock_monitors()
.iter_mut()
.find(|m| m.target_id == target_id)?
.frame_channel
.take()
}
/// Is a frame-channel delivery pending for `target_id`? The swap-chain worker treats a pending
/// delivery as NEWEST-WINS: it supersedes an attached publisher, because the host only re-delivers
/// after (re)creating the ring — and a retry-created ring is a DIFFERENT header mapping, whose
/// generation bump an old publisher (mapped to the previous header) can never observe.
pub fn has_frame_channel(target_id: u32) -> bool {
target_id != 0
&& lock_monitors()
.iter()
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
}
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
@@ -351,6 +402,7 @@ pub fn create_monitor(
adapter_luid_low: 0,
adapter_luid_high: 0,
swap_chain_processor: None,
frame_channel: None,
created_at: Instant::now(),
});
id
@@ -78,6 +78,8 @@ pub struct SwapChainProcessor {
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
// terminate flag.
unsafe impl Send for SwapChainProcessor {}
// SAFETY: as above — the raw pointer is only touched by the serialised worker, so a shared
// `&SwapChainProcessor` reference exposes no unsynchronised access.
unsafe impl Sync for SwapChainProcessor {}
impl SwapChainProcessor {
@@ -223,10 +225,11 @@ impl SwapChainProcessor {
return;
}
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF token can't
// create named objects, so the host creates the header + event + textures and we only OPEN them
// once they appear (`try_open`). Until then we just drain — exactly the STEP-5 behaviour — so a
// non-IDD-push session never stalls. Retried every ~30 loop iterations.
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring over the SEALED channel. The
// frame objects are unnamed — the host duplicates their handles into this process and delivers
// the values via IOCTL_SET_FRAME_CHANNEL, which the control plane stashes on our monitor
// (`monitor::take_frame_channel`). Until a delivery lands we just drain — exactly the STEP-5
// behaviour — so a non-IDD-push session never stalls. The stash is polled every ~30 iterations.
let mut publisher: Option<FramePublisher> = None;
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first loop iteration
@@ -243,31 +246,41 @@ impl SwapChainProcessor {
break;
}
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach to
// the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring, whose
// format now mismatches the surface → the publish() format-guard drops every frame and the
// stream freezes until the next swap-chain recreate.
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
// Re-attach triggers, either of:
// * `is_stale` — the host recreated the ring mid-session (HDR flip): it bumps OUR header's
// generation and re-delivers; without dropping here we'd keep CopyResource'ing into the
// stale ring, whose format now mismatches the surface → the publish() format-guard drops
// every frame and the stream freezes until the next swap-chain recreate.
// * a PENDING delivery (newest-wins) — a host build-retry creates a whole NEW ring with a
// DIFFERENT header mapping; the old publisher's header never changes, so `is_stale` can't
// fire. The host only delivers after fully (re)creating a ring, so a pending delivery
// always supersedes whatever we're attached to.
if publisher.as_ref().is_some_and(FramePublisher::is_stale)
|| (publisher.is_some() && crate::monitor::has_frame_channel(target_id))
{
publisher = None;
frames_since_try = u32::MAX; // re-attach immediately
}
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is idle
// (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open` is a
// cheap OpenFileMapping that fails fast until the host has created the ring.
// (E_PENDING / no frames presented yet), not only when a frame is acquired. Checking the
// stash is a cheap mutex peek that stays empty until the host's channel delivery lands; a
// taken delivery is consumed whether the attach succeeds or not (on failure its handles are
// closed, the host's wait-for-attach reads the status code, and any retry is a NEW delivery).
if publisher.is_none() {
if frames_since_try >= 30 {
frames_since_try = 0;
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
// happy under `-D warnings`; semantics are identical — attach on success, retry on Err.
if let Ok(p) = FramePublisher::try_open(
target_id,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
publisher = Some(p);
if let Some(channel) = crate::monitor::take_frame_channel(target_id) {
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
// happy under `-D warnings`; attach on success, drop the delivery on Err.
if let Ok(p) = FramePublisher::from_channel(
channel,
render_luid_low,
render_luid_high,
&device.device,
&device.device_context,
) {
publisher = Some(p);
}
}
} else {
frames_since_try += 1;
@@ -337,10 +350,10 @@ impl SwapChainProcessor {
if !raw.is_null() {
// SAFETY: `raw` is IddCx's live surface pointer (valid until the next
// ReleaseAndAcquire); `from_raw_borrowed` does not consume the refcount.
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
p.publish(&tex);
}
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) }
&& let Ok(tex) = res.cast::<ID3D11Texture2D>()
{
p.publish(&tex);
}
}
}