fix(gamepad/windows): the channel proof sizes its feature buffer from the descriptor

`HidD_GetFeature`/`HidD_SetFeature` reject any buffer shorter than the
collection's `FeatureReportByteLength`, and the proof hardcoded 64. That held
only because every pad enumerated with the DualSense descriptor; a Deck that
enumerates as a Deck reports 65 (its one feature report is unnumbered and 64
bytes wide, plus the report-id slot Windows always reserves), so both calls
would have failed, the proof with them, and — the gate being fail-closed — the
pad would never receive its DATA section at all.

Take the length from the descriptor, and accept the Deck's reply at either
offset 0 or 1: one driver binary serves four identities and Windows places an
unnumbered report's payload behind the report-id slot.

An answer must also VALIDATE, not merely parse. `from_feature_report`
reinterprets any 17 bytes, and a Deck serves its one unnumbered feature report
for *any* requested id — so it answers the PS `0x85` probe with Steam attribute
bytes that parse into a proof and fail on magic. Returning that first answer
stopped the search while the real proof sat one transport away.
This commit is contained in:
2026-07-30 23:40:35 +02:00
parent 00c29f82f2
commit 98147fb89c
@@ -33,8 +33,9 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
CM_GET_DEVICE_INTERFACE_LIST_PRESENT, CM_LOCATE_DEVNODE_NORMAL, CR_SUCCESS,
};
use windows::Win32::Devices::HumanInterfaceDevice::{
HidD_GetFeature, HidD_GetIndexedString, HidD_GetProductString, HidD_GetSerialNumberString,
HidD_SetFeature, GUID_DEVINTERFACE_HID,
HidD_FreePreparsedData, HidD_GetFeature, HidD_GetIndexedString, HidD_GetPreparsedData,
HidD_GetProductString, HidD_GetSerialNumberString, HidD_SetFeature, HidP_GetCaps,
GUID_DEVINTERFACE_HID, HIDP_CAPS, PHIDP_PREPARSED_DATA,
};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Storage::FileSystem::{
@@ -130,47 +131,86 @@ fn ask_ioctl(path: &str, expect_pad_index: u32) -> Result<u32> {
/// Open a HID collection and read the proof out of a FEATURE report — the pad transport.
fn ask_feature_path(path: &str, expect_pad_index: u32) -> Result<u32> {
let handle = open_device(path)?;
let proof = ask_feature(HANDLE(handle.as_raw_handle()))?;
proof
.check(expect_pad_index)
.map_err(|why| anyhow!("{why}"))
ask_feature(HANDLE(handle.as_raw_handle()), expect_pad_index)
}
/// Take a parsed answer only if it VALIDATES; otherwise remember why and let the caller keep
/// looking. Parsing is not evidence: [`ChannelProof::from_feature_report`] happily reinterprets any
/// 17 bytes, so a transport that answers with something else entirely yields a proof-shaped struct
/// full of another report's bytes.
fn accept(
p: Option<ChannelProof>,
expect: u32,
rejected: &mut Option<&'static str>,
) -> Option<u32> {
match p?.check(expect) {
Ok(pid) => Some(pid),
Err(why) => {
*rejected = Some(why);
None
}
}
}
/// The PS identities answer on the declared-but-unserved report `0x85`; the Deck answers its
/// unnumbered report after a private SET_FEATURE command. Both are tried — one driver binary serves
/// four identities and the host does not know which one this devnode became until the DATA section
/// is attached, which is precisely what we are trying to earn the right to do.
fn ask_feature(h: HANDLE) -> Result<ChannelProof> {
// Feature buffers are sized by the descriptor; the largest of these reports is 64 bytes, and
// Windows accepts a buffer at least that big.
const BUF: usize = 64;
///
/// So neither answer can be trusted on shape alone: a Deck serves its ONE unnumbered feature report
/// for *any* requested id, which means it answers the `0x85` probe with Steam attribute bytes that
/// parse into a proof and fail on magic. Returning that first answer stopped the search and refused
/// the delivery while the real proof sat one transport away.
fn ask_feature(h: HANDLE, expect_pad_index: u32) -> Result<u32> {
// `HidD_GetFeature`/`HidD_SetFeature` REJECT any buffer shorter than the collection's
// `FeatureReportByteLength` — so it must come from the descriptor, not from a constant. The PS
// identities land on 64 (63-byte reports + a report id); the Deck's ONE feature report is
// unnumbered and 64 bytes wide, which Windows reports as 65 (payload + the report-id slot it
// always reserves). Hardcoding 64 silently failed every call on a correctly-enumerated Deck.
let buf_len = feature_report_len(h).unwrap_or(64).max(64);
let mut rejected = None;
// PS identities: GET_FEATURE 0x85.
let mut buf = [0u8; BUF];
let mut buf = vec![0u8; buf_len];
buf[0] = HID_FEATURE_REPORT_CHANNEL_PROOF;
// SAFETY: `h` is the live HID interface handle; `buf` is a valid BUF-sized in/out buffer.
if unsafe { HidD_GetFeature(h, buf.as_mut_ptr().cast(), BUF as u32) } {
if let Some(p) = ChannelProof::from_feature_report(&buf) {
return Ok(p);
// SAFETY: `h` is the live HID interface handle; `buf` is a valid `buf_len`-sized in/out buffer.
if unsafe { HidD_GetFeature(h, buf.as_mut_ptr().cast(), buf_len as u32) } {
let p = ChannelProof::from_feature_report(&buf);
if let Some(pid) = accept(p, expect_pad_index, &mut rejected) {
return Ok(pid);
}
}
// Deck: SET the private command, then GET the reply. Byte 0 is the (unnumbered) report id 0.
let mut cmd = [0u8; BUF];
let mut cmd = vec![0u8; buf_len];
cmd[1..1 + DECK_PROOF_CMD.len()].copy_from_slice(&DECK_PROOF_CMD);
// SAFETY: `h` is live; `cmd` is a valid BUF-sized buffer.
let set_ok = unsafe { HidD_SetFeature(h, cmd.as_mut_ptr().cast(), BUF as u32) };
// SAFETY: `h` is live; `cmd` is a valid `buf_len`-sized buffer.
let set_ok = unsafe { HidD_SetFeature(h, cmd.as_mut_ptr().cast(), buf_len as u32) };
if set_ok {
let mut reply = [0u8; BUF];
let mut reply = vec![0u8; buf_len];
// SAFETY: as above.
if unsafe { HidD_GetFeature(h, reply.as_mut_ptr().cast(), BUF as u32) }
&& reply.starts_with(&DECK_PROOF_CMD)
{
if let Some(p) = ChannelProof::from_bytes(&reply[DECK_PROOF_CMD.len()..]) {
return Ok(p);
if unsafe { HidD_GetFeature(h, reply.as_mut_ptr().cast(), buf_len as u32) } {
// The driver answers with the payload; on an unnumbered report Windows hands it back
// one byte in, behind the report-id slot. Accept either placement rather than pinning a
// marshalling detail that differs between the two descriptors this one driver serves.
for off in [0usize, 1] {
let Some(body) = reply.get(off..) else {
continue;
};
if let Some(tail) = body.strip_prefix(&DECK_PROOF_CMD[..]) {
let p = ChannelProof::from_bytes(tail);
if let Some(pid) = accept(p, expect_pad_index, &mut rejected) {
return Ok(pid);
}
}
}
}
}
// A well-formed answer that failed validation is a different fault from no answer at all, and
// says which check refused — do not bury it under the generic "no proof here".
if let Some(why) = rejected {
bail!("{why}");
}
bail!(
"this HID collection carries no channel proof (feature 0x{:02x}: no; Deck command: {}) — \
the driver predates the proof (reinstall: punktfunk-host.exe driver install --gamepad)",
@@ -183,6 +223,22 @@ fn ask_feature(h: HANDLE) -> Result<ChannelProof> {
)
}
/// This collection's `FeatureReportByteLength` — the exact buffer size `HidD_GetFeature` /
/// `HidD_SetFeature` demand. `None` when the preparsed data can't be read (caller falls back).
fn feature_report_len(h: HANDLE) -> Option<usize> {
let mut pp = PHIDP_PREPARSED_DATA::default();
// SAFETY: `h` is the live HID interface handle; `pp` receives an owned preparsed-data handle.
if !unsafe { HidD_GetPreparsedData(h, &mut pp) } {
return None;
}
let mut caps = HIDP_CAPS::default();
// SAFETY: `pp` is the handle just obtained (freed below); `caps` is a valid out-param.
let st = unsafe { HidP_GetCaps(pp, &mut caps) };
// SAFETY: `pp` came from `HidD_GetPreparsedData` and is not used after this.
let _ = unsafe { HidD_FreePreparsedData(pp) };
(st.0 >= 0 && caps.FeatureReportByteLength > 0).then_some(caps.FeatureReportByteLength as usize)
}
/// Open a HID collection and read the proof out of a string.
fn ask_hid_path(path: &str, expect_pad_index: u32) -> Result<u32> {
let handle = open_device(path)?;
@@ -266,15 +322,21 @@ pub fn diagnose(instance_id: &str, transport: ProofTransport, expect_pad_index:
let _ = writeln!(out, " IOCTL_PF_GET_CHANNEL_PROOF FAILED: {e:#}");
}
},
ProofTransport::HidFeatureReport => match ask_feature(h) {
Ok(p) => {
let _ = writeln!(out, " feature proof -> {p:?}");
let _ = writeln!(out, " check: {:?}", p.check(expect_pad_index));
ProofTransport::HidFeatureReport => {
let _ = writeln!(
out,
" feature report length: {:?}",
feature_report_len(h)
);
match ask_feature(h, expect_pad_index) {
Ok(pid) => {
let _ = writeln!(out, " feature proof -> wudf pid {pid}");
}
Err(e) => {
let _ = writeln!(out, " feature proof FAILED: {e:#}");
}
}
Err(e) => {
let _ = writeln!(out, " feature proof FAILED: {e:#}");
}
},
}
ProofTransport::HidSerialString => {
// Report each path separately — this is the whole point of the probe.
let (indexed, serial, control) = ask_hid_both(h);