[PATCH v3 1/13] drm/vino: add the DL3 wire framing

From: Mike Lothian

Date: Wed Aug 26 2026 - 12:49:25 EST


Every message a DisplayLink DL3 dock exchanges, plaintext or sealed, sits
inside the same envelope: a length, an identifier, an auxiliary word and a
payload. The identifier is not a free-standing opcode -- on several replies
it doubles as the payload's length -- so decoding it wrongly matches a
reply by luck rather than by meaning.

Add the framing on its own, with no I/O and no crypto, so the layers above
can be read against a capture without carrying the envelope's rules.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
---
drivers/gpu/drm/vino/proto.rs | 71 +++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 drivers/gpu/drm/vino/proto.rs

diff --git a/drivers/gpu/drm/vino/proto.rs b/drivers/gpu/drm/vino/proto.rs
new file mode 100644
index 000000000000..f9a70b605135
--- /dev/null
+++ b/drivers/gpu/drm/vino/proto.rs
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The DL3 "universal" wire framing and the plaintext session-init messages (sec 3/sec 4).
+
+use super::*;
+
+/// Append a sec 3-framed message to `out` with an explicit `sub_len_dw`: a 16-byte
+/// little-endian header (`pad(2) | size(2)=total-4 | type(4) | sub_id(2) |
+/// sub_len_dw(2) | seq(4)`) followed by `body`.
+///
+/// HDCP OUT messages carry fixed `sub_len_dw` values that are *not*
+/// `body.len() / 4`, so the framer cannot derive it -- the caller passes it.
+pub(super) fn push_frame_with(
+ out: &mut KVec<u8>,
+ msg_type: u32,
+ sub_id: u16,
+ sub_len_dw: u16,
+ seq: u32,
+ body: &[u8],
+) -> Result {
+ let size = ((16 + body.len()) - 4) as u16;
+ out.extend_from_slice(&[0, 0], GFP_KERNEL)?;
+ out.extend_from_slice(&size.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&msg_type.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&sub_id.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&sub_len_dw.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&seq.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(body, GFP_KERNEL)?;
+ Ok(())
+}
+
+/// `init_25` body (sec 4).
+///
+/// The `sub_len_dw` field is the fixed value zero rather than `body.len() / 4`; the dock rejects
+/// the derived value.
+pub(super) const INIT_25: [u8; 16] = [0x05, 0, 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+/// `init_4` (Part A) body (sec 4), also framed with `sub_len_dw=0`.
+pub(super) const INIT_4: [u8; 16] = [0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+/// The HDCP-channel probe body in the second part of [`init_4_probe`].
+/// The dock only ACKs once this framed probe arrives.
+pub(super) const PROBE_BODY: [u8; 32] = {
+ let mut p = [0u8; 32];
+ p[0] = 0x14;
+ p[2] = 0x90;
+ p
+};
+
+/// `init_0`: 16-byte framing header only, empty body (sec 4).
+pub(super) fn init_0() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(16, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x01, 0x00, 0, 0, &[])?;
+ Ok(buf)
+}
+
+/// `init_25`: type=2 sub=0x25, `sub_len_dw=0`, 32 bytes total (sec 4).
+pub(super) fn init_25() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(32, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x02, 0x25, 0, 0, &INIT_25)?;
+ Ok(buf)
+}
+
+/// `init_4` + HDCP probe as one 80-byte transfer (sec 4): Part A (type=2 sub=0x04,
+/// `sub_len_dw=0`, 32 B) concatenated with Part B -- the probe framed as type=4
+/// sub=0x04 with `sub_len_dw=0x0a` over the 32-byte [`PROBE_BODY`] (48 B). This
+/// is the message the dock ACKs.
+pub(super) fn init_4_probe() -> Result<KVec<u8>> {
+ let mut buf = KVec::with_capacity(80, GFP_KERNEL)?;
+ push_frame_with(&mut buf, 0x02, 0x04, 0, 0, &INIT_4)?; // Part A
+ push_frame_with(&mut buf, 0x04, 0x04, 0x0a, 0, &PROBE_BODY)?; // Part B (framed probe)
+ Ok(buf)
+}