[PATCH v3 10/13] drm/vino: read the dock's firmware version, and update it over DFU

From: Mike Lothian

Date: Wed Aug 26 2026 - 13:07:14 EST


A DisplayLink dock does not advance bcdDevice when its firmware changes, so
that cannot answer whether an update is due. The version lives in a 16-byte
vendor descriptor inside the configuration descriptor, alongside a platform
name that selects the package targeting the hardware; the shipped
*-release.spkg packages carry theirs in a tagged table under `VE`, at no
fixed offset -- 100 in one shipped image and 24574 in another.

Add both parsers, request the matching firmware by the vendor's own
filename so a distribution can install the package unmodified, and update
only when the packaged version is strictly newer. A current dock therefore
costs one descriptor read and no writes, and a missing firmware file is not
an error: a dock runs on the firmware it shipped with.

The transfer is USB DFU 1.1 on the dock's DFU interface, matching the
vendor updater recorded end to end: DFU_DETACH, then the package verbatim
in 4096-byte blocks with an ascending wValue, each followed by
DFU_GETSTATUS, then a zero-length DFU_DNLOAD to manifest. There is no bus
reset between the detach and the first block -- the dock takes the download
in its runtime interface -- and it re-enumerates once the image is
manifested.

The functional descriptor clears bitCanUpload, so the running image cannot
be read back and there is no host-side restore path; and it clears
bitManifestationTolerant. Hence the strictly-newer rule and the package
magic check before any byte is written.

Userspace can also push an image on demand through the firmware loader's
upload interface, for a dock whose packaged firmware a distribution does
not ship.

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

diff --git a/drivers/gpu/drm/vino/firmware.rs b/drivers/gpu/drm/vino/firmware.rs
new file mode 100644
index 000000000000..13c9cff55ae4
--- /dev/null
+++ b/drivers/gpu/drm/vino/firmware.rs
@@ -0,0 +1,670 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Dock firmware: identity, package parsing, and the USB DFU update.
+//!
+//! A DisplayLink dock carries its firmware version in a vendor descriptor rather than in
+//! `bcdDevice`, which does not change across an update. The shipped `*-release.spkg` packages carry
+//! theirs in a tagged table. Comparing the two says whether an update is due; the transfer itself
+//! is textbook USB DFU 1.1 on the dock's DFU interface.
+
+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use kernel::device::Device;
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+use kernel::firmware::Firmware;
+use kernel::prelude::*;
+use kernel::sync::{Arc, ArcBorrow};
+use kernel::time::Delta;
+use kernel::usb;
+
+/// The dock's DFU interface: `bInterfaceClass 0xfe`, `bInterfaceSubClass 1`, and the interface
+/// number every DFU class request is addressed to.
+pub(crate) const DFU_INTERFACE: u8 = 1;
+
+/// Vendor descriptor carrying the platform name and running firmware version.
+///
+/// `bcdDevice` does not change when a dock is updated, so it cannot answer "is an update due".
+/// This descriptor can: it is 16 bytes, `[len, 0x40, major, minor, patch, ..., name(8)]`, and the
+/// name selects the package that targets this hardware.
+pub(crate) const DESCRIPTOR_IDENTITY: u8 = 0x40;
+const IDENTITY_LEN: usize = 16;
+const IDENTITY_NAME: usize = 8;
+
+/// A dock's platform name and the firmware version it is running.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Identity {
+ pub(crate) version: Version,
+ name: [u8; IDENTITY_NAME],
+}
+
+/// A three-part firmware version, ordered major-minor-patch.
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub(crate) struct Version(pub(crate) u8, pub(crate) u8, pub(crate) u8);
+
+impl kernel::fmt::Display for Version {
+ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+ write!(f, "{}.{}.{}", self.0, self.1, self.2)
+ }
+}
+
+impl Identity {
+ /// Parse the identity descriptor out of a device's raw configuration descriptors.
+ pub(crate) fn parse(raw: &[u8]) -> Option<Self> {
+ let mut i = 0usize;
+ while i + 2 <= raw.len() {
+ let len = usize::from(raw[i]);
+ if len < 2 || i + len > raw.len() {
+ return None;
+ }
+ if raw[i + 1] == DESCRIPTOR_IDENTITY && len >= IDENTITY_LEN {
+ let d = &raw[i..i + IDENTITY_LEN];
+ let mut name = [0u8; IDENTITY_NAME];
+ name.copy_from_slice(&d[8..16]);
+ return Some(Self {
+ version: Version(d[2], d[3], d[4]),
+ name,
+ });
+ }
+ i += len;
+ }
+ None
+ }
+
+ /// The platform name, trimmed of its padding.
+ pub(crate) fn platform(&self) -> &[u8] {
+ let end = self
+ .name
+ .iter()
+ .position(|&c| c == 0 || c == b' ')
+ .unwrap_or(IDENTITY_NAME);
+ &self.name[..end]
+ }
+
+ /// Which dock family this is.
+ pub(crate) fn family(&self) -> Option<Family> {
+ Family::from_identity(self.platform())
+ }
+
+ /// The firmware file that targets this platform, under `/lib/firmware`.
+ pub(crate) fn firmware_name(&self) -> Option<&'static CStr> {
+ Some(self.family()?.firmware_name())
+ }
+}
+
+impl kernel::fmt::Display for Identity {
+ /// Names the hardware the way its documentation does, falling back to the raw identity tag
+ /// for a device this driver does not recognise.
+ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+ match self.family() {
+ Some(family) => write!(f, "{}", family.description()),
+ None => match core::str::from_utf8(self.platform()) {
+ Ok(name) => write!(f, "unrecognised device {name}"),
+ Err(_) => write!(f, "unrecognised device {:02x?}", self.platform()),
+ },
+ }
+ }
+}
+
+/// A dock family: the hardware a firmware package targets.
+///
+/// Only `NavaDock` has been read off real hardware; the spellings for the other three come from
+/// the vendor's firmware packages and are unverified.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Family {
+ /// DL-3x00 dock, e.g. the HP 3005pr.
+ Ella,
+ /// DL-6xxx dock, e.g. the Dell D6000.
+ Ridge,
+ /// DL-7400 quad dock.
+ Navarro,
+ /// Firefly monitor.
+ Firefly,
+}
+
+impl Family {
+ /// From the device's identity descriptor name.
+ pub(crate) fn from_identity(name: &[u8]) -> Option<Self> {
+ match name {
+ b"NavaDock" => Some(Self::Navarro),
+ b"Ridge" | b"RidgeDoc" => Some(Self::Ridge),
+ b"Ella" | b"EllaDock" => Some(Self::Ella),
+ b"Firefly" | b"FflyMoni" => Some(Self::Firefly),
+ _ => None,
+ }
+ }
+
+ /// How this family is described in a log line, including what kind of device it is.
+ ///
+ /// The device's own identity string is an eight-character tag -- "NavaDock", "FflyMoni" --
+ /// which is not what the hardware is called anywhere else.
+ fn description(self) -> &'static str {
+ match self {
+ Self::Navarro => "Navarro dock",
+ Self::Ridge => "Ridge dock",
+ Self::Ella => "Ella dock",
+ Self::Firefly => "Firefly monitor",
+ }
+ }
+
+ /// The file that carries this family's firmware, under `/lib/firmware`.
+ ///
+ /// Named after the package DisplayLink ships for the platform, so a distribution can drop the
+ /// vendor's own file in unmodified.
+ pub(crate) fn firmware_name(self) -> &'static CStr {
+ match self {
+ Self::Navarro => c"vino/navarro-dock-release.spkg",
+ Self::Ridge => c"vino/ridge-dock-release.spkg",
+ Self::Ella => c"vino/ella-dock-release.spkg",
+ Self::Firefly => c"vino/firefly-monitor-release.spkg",
+ }
+ }
+
+ /// From a package's `RD` tag.
+ pub(crate) fn from_package(rd: &[u8]) -> Option<Self> {
+ Self::from_identity(rd)
+ }
+}
+
+/// The dock family a package targets, from its `RD` tag.
+///
+/// This is what stops a Ridge image being written to a Navarro dock: the package says who it is
+/// for, so an image pushed in by hand can be checked without trusting the filename.
+pub(crate) fn package_family(image: &[u8]) -> Option<Family> {
+ const TAG: [u8; 4] = [b'R', b'D', 8, 0];
+ let w = image
+ .windows(TAG.len() + 8)
+ .find(|w| w[..TAG.len()] == TAG)?;
+ let name = &w[4..12];
+ let end = name.iter().position(|&c| c == 0).unwrap_or(name.len());
+ Family::from_package(&name[..end])
+}
+
+/// Standard `GET_DESCRIPTOR` for the configuration descriptor, which the identity blob rides in.
+const GET_DESCRIPTOR: u8 = 6;
+const DEVICE_TO_HOST_STANDARD: u8 = 0x80;
+const DESCRIPTOR_CONFIG: u16 = 0x0200;
+/// Enough for any configuration this hardware reports; the dock's is well under 1 KiB.
+const CONFIG_DESCRIPTOR_MAX: usize = 1024;
+
+/// Read the dock's identity by walking its configuration descriptor.
+///
+/// The blob is a vendor descriptor inside the configuration, not a separately addressable one, so
+/// the whole configuration is fetched and walked. `usb_device::rawdescriptors` holds the same bytes
+/// already, but no binding exposes them and one standard control read costs nothing on probe.
+pub(crate) fn read_identity(io: &usb::Io<'_>) -> Result<Identity> {
+ let mut connector = [0u8; 9];
+ io.control_recv(
+ GET_DESCRIPTOR,
+ DEVICE_TO_HOST_STANDARD,
+ DESCRIPTOR_CONFIG,
+ 0,
+ &mut connector,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ let total = usize::from(u16::from_le_bytes([connector[2], connector[3]]));
+ if total < connector.len() || total > CONFIG_DESCRIPTOR_MAX {
+ return Err(EINVAL);
+ }
+ let mut all = KVec::with_capacity(total, GFP_KERNEL)?;
+ all.resize(total, 0u8, GFP_KERNEL)?;
+ io.control_recv(
+ GET_DESCRIPTOR,
+ DEVICE_TO_HOST_STANDARD,
+ DESCRIPTOR_CONFIG,
+ 0,
+ &mut all,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ Identity::parse(&all).ok_or(ENODEV)
+}
+
+/// The version a `.spkg` will install.
+///
+/// The package is a tagged table: a two-byte ASCII tag, a `u16` length, then the value. `VE` holds
+/// the three version bytes. Its offset differs per package -- 100 in one shipped image and 24574 in
+/// another -- so it is searched for rather than assumed.
+pub(crate) fn package_version(image: &[u8]) -> Option<Version> {
+ const TAG: [u8; 4] = [b'V', b'E', 3, 0];
+ image
+ .windows(TAG.len() + 3)
+ .find(|w| w[..TAG.len()] == TAG)
+ .map(|w| Version(w[4], w[5], w[6]))
+}
+
+/// `.spkg` container magic, checked before anything is written to a dock.
+const PACKAGE_MAGIC: &[u8; 4] = b"ELLA";
+
+/// Whether `image` is a firmware package at all.
+pub(crate) fn is_package(image: &[u8]) -> bool {
+ image.len() > 8 && &image[..4] == PACKAGE_MAGIC
+}
+
+// USB DFU 1.1 class requests, on the dock's DFU interface.
+const DFU_OUT: u8 = 0x21; // host-to-device, class, interface
+const DFU_IN: u8 = 0xa1; // device-to-host, class, interface
+const DFU_DETACH: u8 = 0;
+const DFU_DNLOAD: u8 = 1;
+const DFU_GETSTATUS: u8 = 3;
+
+/// Payload per `DFU_DNLOAD`.
+///
+/// The DFU functional descriptor advertises a 16384-byte `wTransferSize`, but the vendor's own
+/// updater sends 4096 and the dock is only known to accept that.
+const BLOCK: usize = 4096;
+
+/// `wValue` of `DFU_DETACH`, in milliseconds. The vendor sends 100.
+const DETACH_TIMEOUT_MS: u16 = 100;
+
+/// How long a single control transfer may take.
+const XFER_TIMEOUT: Delta = Delta::from_secs(5);
+
+/// `DFU_GETSTATUS` reply: `bStatus, bwPollTimeout[3], bState, iString`.
+const STATUS_LEN: usize = 6;
+const STATUS_OK: u8 = 0;
+const STATE_DNLOAD_IDLE: u8 = 5;
+const STATE_DNBUSY: u8 = 4;
+const STATE_MANIFEST_SYNC: u8 = 6;
+const STATE_MANIFEST: u8 = 7;
+
+/// Poll `DFU_GETSTATUS` until the dock leaves `dfuDNBUSY`, honouring its own poll timeout.
+fn wait_ready(io: &usb::Io<'_>, iface: u16) -> Result<u8> {
+ for _ in 0..1000 {
+ let mut st = [0u8; STATUS_LEN];
+ io.control_recv(
+ DFU_GETSTATUS,
+ DFU_IN,
+ 0,
+ iface,
+ &mut st,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ if st[0] != STATUS_OK {
+ pr_err!(
+ "vino: firmware update rejected: DFU status {} in state {}\n",
+ st[0],
+ st[4]
+ );
+ return Err(EIO);
+ }
+ // bwPollTimeout is a 24-bit little-endian millisecond count the device asks us to wait.
+ let poll = u32::from(st[1]) | u32::from(st[2]) << 8 | u32::from(st[3]) << 16;
+ if st[4] != STATE_DNBUSY && st[4] != STATE_MANIFEST {
+ return Ok(st[4]);
+ }
+ kernel::time::delay::fsleep(Delta::from_millis(poll.clamp(1, 1000) as i64));
+ }
+ Err(ETIMEDOUT)
+}
+
+/// Write `image` to the dock over USB DFU.
+///
+/// This is not reversible from the host. The DFU functional descriptor clears
+/// `bitCanUpload`, so the running firmware cannot be read back and there is nothing to restore
+/// from; and it sets `bitManifestationTolerant = 0`, so the dock re-enumerates when the image is
+/// manifested. An interrupted write leaves the dock with a partial image.
+///
+/// The sequence is the vendor updater's, recorded end to end: `DFU_DETACH`, then the whole package
+/// verbatim in 4096-byte blocks with an ascending `wValue`, each followed by `DFU_GETSTATUS`, then
+/// a zero-length `DFU_DNLOAD` to manifest. There is no bus reset between the detach and the first
+/// block -- the dock accepts the download in its runtime interface.
+pub(crate) fn flash(io: &usb::Io<'_>, iface: u16, image: &[u8]) -> Result {
+ if !is_package(image) {
+ pr_err!("vino: refusing to flash: not a DisplayLink firmware package\n");
+ return Err(EINVAL);
+ }
+ let blocks = image.len().div_ceil(BLOCK);
+ if blocks > usize::from(u16::MAX) {
+ return Err(EFBIG);
+ }
+ pr_info!(
+ "vino: flashing {} bytes of dock firmware in {} block(s) -- do not disconnect\n",
+ image.len(),
+ blocks
+ );
+
+ io.control_send(
+ DFU_DETACH,
+ DFU_OUT,
+ DETACH_TIMEOUT_MS,
+ iface,
+ &[],
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+
+ for (n, chunk) in image.chunks(BLOCK).enumerate() {
+ io.control_send(
+ DFU_DNLOAD,
+ DFU_OUT,
+ n as u16,
+ iface,
+ chunk,
+ XFER_TIMEOUT,
+ GFP_KERNEL,
+ )?;
+ let state = wait_ready(io, iface)?;
+ if state != STATE_DNLOAD_IDLE {
+ pr_err!("vino: firmware block {n} left the dock in DFU state {state}\n");
+ return Err(EIO);
+ }
+ }
+
+ // Zero-length download: the image is complete, manifest it. The dock re-enumerates from here,
+ // so a failure to read status back afterwards is expected rather than an error.
+ io.control_send(DFU_DNLOAD, DFU_OUT, 0, iface, &[], XFER_TIMEOUT, GFP_KERNEL)?;
+ match wait_ready(io, iface) {
+ Ok(state) if state == STATE_MANIFEST_SYNC || state == STATE_DNLOAD_IDLE => {}
+ Ok(state) => pr_info!("vino: dock manifested in DFU state {state}\n"),
+ Err(_) => pr_info!("vino: dock stopped answering after manifest, as it re-enumerates\n"),
+ }
+ pr_info!("vino: dock firmware written; it will re-enumerate to run it\n");
+ Ok(())
+}
+
+/// Decide whether `dev` needs an update, and apply one if it does.
+///
+/// An update is applied only when the packaged version is strictly newer than the one the dock
+/// reports, so the ordinary path on a current dock is one descriptor read and no writes at all. A
+/// missing firmware file is not an error: a dock runs perfectly well on the firmware it shipped
+/// with, and most systems will never carry the package.
+#[cfg(not(CONFIG_RUST_FW_LOADER_ABSTRACTIONS))]
+pub(crate) fn update_if_newer(
+ _io: &usb::Io<'_>,
+ dev: &Device,
+ identity: &Identity,
+ _iface: u16,
+ _force: bool,
+) -> Result {
+ vino_dev_debug!(
+ dev,
+ "dock firmware {}; updates need CONFIG_RUST_FW_LOADER_ABSTRACTIONS\n",
+ identity.version
+ );
+ Ok(())
+}
+
+/// Automatic updates attempted for one device: the device-name hash in the high 32 bits, the
+/// attempt count in the low 32.
+///
+/// A dock that re-enumerates still reporting the old version is otherwise rewritten on every
+/// probe, without end. One slot is enough, because the device that is looping is the one being
+/// probed; another device simply takes the slot over and starts its own count.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+static UPDATE_ATTEMPTS: AtomicU64 = AtomicU64::new(0);
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+const ATTEMPT_KEY_MASK: u64 = 0xffff_ffff_0000_0000;
+
+/// How many automatic writes one device is given before the driver leaves it on what it runs.
+///
+/// A write that takes effect is visible on the very next probe, so a device that has been given
+/// this many and still reports the old version is not going to accept another one.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+const MAX_UPDATE_ATTEMPTS: u32 = 2;
+
+/// FNV-1a over the device name, which only has to tell one device's slot from another's. The name
+/// is the bus path, so it survives the re-enumeration a write causes.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+fn attempt_key(dev: &Device) -> u64 {
+ let mut h: u32 = 0x811c_9dc5;
+ for b in dev.name().to_bytes() {
+ h ^= u32::from(*b);
+ h = h.wrapping_mul(0x0100_0193);
+ }
+ u64::from(h) << 32
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+/// Whether `dev` has an automatic write left, without consuming one.
+fn update_attempts_left(dev: &Device) -> bool {
+ let key = attempt_key(dev);
+ let slot = UPDATE_ATTEMPTS.load(Ordering::Acquire);
+ slot & ATTEMPT_KEY_MASK != key || (slot as u32) < MAX_UPDATE_ATTEMPTS
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+/// Count another automatic write against `dev`, or report that it has had its share.
+fn claim_update_attempt(dev: &Device) -> bool {
+ let key = attempt_key(dev);
+ let slot = UPDATE_ATTEMPTS.load(Ordering::Acquire);
+ let count = if slot & ATTEMPT_KEY_MASK == key {
+ slot as u32
+ } else {
+ 0
+ };
+ if count >= MAX_UPDATE_ATTEMPTS {
+ return false;
+ }
+ UPDATE_ATTEMPTS.store(key | u64::from(count + 1), Ordering::Release);
+ true
+}
+
+/// Forget the attempts recorded against `dev`, once it is seen running what the package offers.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+fn clear_update_attempts(dev: &Device) {
+ let key = attempt_key(dev);
+ if UPDATE_ATTEMPTS.load(Ordering::Acquire) & ATTEMPT_KEY_MASK == key {
+ UPDATE_ATTEMPTS.store(0, Ordering::Release);
+ }
+}
+
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+pub(crate) fn update_if_newer(
+ io: &usb::Io<'_>,
+ dev: &Device,
+ identity: &Identity,
+ iface: u16,
+ force: bool,
+) -> Result {
+ let Some(name) = identity.firmware_name() else {
+ return Ok(());
+ };
+ let Ok(fw) = Firmware::request_nowarn(name, dev) else {
+ vino_dev_debug!(
+ dev,
+ "no {} available; leaving the dock on {}\n",
+ name,
+ identity.version
+ );
+ return Ok(());
+ };
+ let Some(packaged) = package_version(fw.data()) else {
+ dev_warn!(dev, "{} carries no version tag; ignoring it\n", name);
+ return Ok(());
+ };
+ if packaged <= identity.version && !force {
+ clear_update_attempts(dev);
+ vino_dev_debug!(dev, "firmware is current ({} offers {})\n", name, packaged);
+ return Ok(());
+ }
+ // A write that does not take leaves the dock reporting the old version, which is the same
+ // state that asked for the write in the first place. Bound it, or the dock is rewritten on
+ // every probe for as long as it stays plugged in. The forced path is a deliberate act and is
+ // not limited.
+ if !force && !claim_update_attempt(dev) {
+ dev_warn!(
+ dev,
+ "dock still reports firmware {} after {} update attempt(s); leaving it alone\n",
+ identity.version,
+ MAX_UPDATE_ATTEMPTS
+ );
+ return Ok(());
+ }
+ dev_info!(
+ dev,
+ "updating dock firmware {} -> {}{}\n",
+ identity.version,
+ packaged,
+ if force { " (forced)" } else { "" }
+ );
+ flash(io, iface, fw.data())
+}
+
+/// Whether the automatic check will write this device the next time it runs.
+///
+/// A write reboots the dock, so the display function asks before establishing a control session
+/// that the dock is about to drop. Deliberately does not consume an attempt; the write does that.
+#[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)]
+pub(crate) fn update_pending(dev: &Device, identity: &Identity, force: bool) -> bool {
+ let Some(name) = identity.firmware_name() else {
+ return false;
+ };
+ let Ok(fw) = Firmware::request_nowarn(name, dev) else {
+ return false;
+ };
+ // Mirror every test the write itself makes, so this cannot claim a write that will not happen
+ // and leave the display function unbound waiting for it.
+ if !is_package(fw.data()) {
+ return false;
+ }
+ let Some(packaged) = package_version(fw.data()) else {
+ return false;
+ };
+ if packaged <= identity.version && !force {
+ return false;
+ }
+ force || update_attempts_left(dev)
+}
+
+#[cfg(not(CONFIG_RUST_FW_LOADER_ABSTRACTIONS))]
+pub(crate) fn update_pending(_dev: &Device, _identity: &Identity, _force: bool) -> bool {
+ false
+}
+
+/// The `/sys/class/firmware/vino-<dock>/` upload interface.
+///
+/// This is the deliberate path: userspace hands vino an image and it is written, whatever version
+/// it is. That is what makes a re-flash of the running version, or an attempted downgrade,
+/// possible at all -- [`update_if_newer`] refuses both by design.
+///
+/// The checks below are the only thing between a mistyped `cat` and a dock that no longer works:
+/// the DFU interface does not support upload, so the running image cannot be read back and there
+/// is nothing to restore from.
+pub(crate) struct Upload;
+
+/// What an upload needs to reach the dock, and the cancel flag userspace can set.
+pub(crate) struct UploadCtx {
+ /// The window the dock's I/O is issued through, so a write can be refused once it closes.
+ pub(crate) window: Arc<usb::IoWindow>,
+ /// Set by `cancel`, observed between blocks.
+ pub(crate) cancelled: AtomicBool,
+ /// The family this dock is, which an image must match.
+ pub(crate) family: Family,
+}
+
+impl kernel::firmware::upload::Upload for Upload {
+ type Data = Arc<UploadCtx>;
+
+ fn prepare(
+ ctx: ArcBorrow<'_, UploadCtx>,
+ image: &[u8],
+ ) -> core::result::Result<(), kernel::firmware::upload::Error> {
+ use kernel::firmware::upload::Error as UErr;
+ ctx.cancelled.store(false, Ordering::Release);
+ if !is_package(image) {
+ pr_err!("vino: refusing upload: not a DisplayLink firmware package\n");
+ return Err(UErr::InvalidFirmware);
+ }
+ // The package says which dock it is for. Writing another family's image is the one
+ // mistake here that cannot be undone, so it is refused before the dock is touched.
+ match package_family(image) {
+ Some(f) if f == ctx.family => {}
+ _ => {
+ pr_err!("vino: refusing upload: image is not for this dock family\n");
+ return Err(UErr::InvalidFirmware);
+ }
+ }
+ if let Some(v) = package_version(image) {
+ pr_info!("vino: upload accepted: firmware {v}\n");
+ }
+ Ok(())
+ }
+
+ fn write(
+ ctx: ArcBorrow<'_, UploadCtx>,
+ image: &[u8],
+ offset: u32,
+ _chunk: &[u8],
+ ) -> core::result::Result<u32, kernel::firmware::upload::Error> {
+ use kernel::firmware::upload::Error as UErr;
+ // The dock takes the package as a whole -- block numbers are an index into the image, and
+ // the transfer ends with a manifest -- so it is written in one pass on the first call
+ // rather than chunk by chunk as the core offers it.
+ if offset != 0 {
+ return Ok(image.len() as u32 - offset);
+ }
+ if ctx.cancelled.load(Ordering::Acquire) {
+ return Err(UErr::Canceled);
+ }
+ let Ok(io) = ctx.window.enter() else {
+ return Err(UErr::Hardware);
+ };
+ match flash(&io, u16::from(DFU_INTERFACE), image) {
+ Ok(()) => Ok(image.len() as u32),
+ Err(e) => {
+ pr_err!("vino: firmware upload failed ({e:?})\n");
+ Err(UErr::ReadWrite)
+ }
+ }
+ }
+
+ fn poll_complete(
+ _ctx: ArcBorrow<'_, UploadCtx>,
+ ) -> core::result::Result<(), kernel::firmware::upload::Error> {
+ // `flash()` is synchronous and has already polled the dock's own DFU status to completion.
+ Ok(())
+ }
+
+ fn cancel(ctx: ArcBorrow<'_, UploadCtx>) {
+ // Runs on another thread. The write loop reads this between blocks; a transfer already in
+ // flight still completes, because abandoning one mid-image is what leaves a dock unusable.
+ ctx.cancelled.store(true, Ordering::Release);
+ pr_info!("vino: firmware upload cancellation requested\n");
+ }
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_firmware)]
+mod tests {
+ use super::*;
+ use kernel::error::code::EINVAL;
+
+ #[test]
+ fn firmware_identity_and_package_versions_parse() -> Result {
+ // The dock's identity descriptor, as read from a DL-7400: 16 bytes, type 0x40, the running
+ // version at offsets 2..5, and the platform name at 8..16.
+ let raw = [
+ 0x09u8, 0x02, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80,
+ 0x32, // a config descriptor first
+ 0x10, 0x40, 0x0c, 0x02, 0x1a, 0x0b, 0x03, 0x22, b'N', b'a', b'v', b'a', b'D', b'o',
+ b'c', b'k',
+ ];
+ let id = Identity::parse(&raw).ok_or(EINVAL)?;
+ assert_eq!(id.version, Version(12, 2, 26));
+ assert_eq!(id.platform(), b"NavaDock");
+ assert_eq!(
+ id.firmware_name().ok_or(EINVAL)?,
+ c"vino/navarro-dock-release.spkg"
+ );
+
+ // A package's version is a `VE` tag with a three-byte value, at no fixed offset -- the
+ // shipped images carry it at 100 and at 24574.
+ let mut pkg = KVec::new();
+ pkg.extend_from_slice(b"ELLA\0\0\0\0", GFP_KERNEL)?;
+ pkg.extend_from_slice(&[0u8; 64], GFP_KERNEL)?;
+ pkg.extend_from_slice(b"VE\x03\0", GFP_KERNEL)?;
+ pkg.extend_from_slice(&[12, 2, 27], GFP_KERNEL)?;
+ assert!(is_package(&pkg));
+ assert_eq!(package_version(&pkg).ok_or(EINVAL)?, Version(12, 2, 27));
+
+ // Ordering is major-minor-patch, which is what decides whether an update is due.
+ assert!(Version(12, 2, 27) > Version(12, 2, 26));
+ assert!(Version(12, 3, 0) > Version(12, 2, 99));
+ assert!(Version(13, 0, 0) > Version(12, 9, 9));
+ assert!(!(Version(12, 2, 26) > Version(12, 2, 26)));
+
+ // Anything that is not a package must be refused before a byte reaches the dock.
+ assert!(!is_package(b"not a firmware image"));
+ Ok(())
+ }
+}