Rust for Linux List
 help / color / mirror / Atom feed
From: Mike Lothian <mike@fireburn.co.uk>
To: linux-kernel@vger.kernel.org
Cc: "Mike Lothian" <mike@fireburn.co.uk>,
	"Luis Chamberlain" <mcgrof@kernel.org>,
	"Russ Weight" <russ.weight@linux.dev>,
	"Danilo Krummrich" <dakr@kernel.org>,
	"Miguel Ojeda" <ojeda@kernel.org>,
	"Boqun Feng" <boqun@kernel.org>, "Gary Guo" <gary@garyguo.net>,
	"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
	"Benno Lossin" <lossin@kernel.org>,
	"Andreas Hindborg" <a.hindborg@kernel.org>,
	"Alice Ryhl" <aliceryhl@google.com>,
	"Trevor Gross" <tmgross@umich.edu>,
	"Daniel Almeida" <daniel.almeida@collabora.com>,
	"Tamir Duberstein" <tamird@kernel.org>,
	"Alexandre Courbot" <acourbot@nvidia.com>,
	"Onur Özkan" <work@onurozkan.dev>,
	driver-core@lists.linux.dev, rust-for-linux@vger.kernel.org
Subject: [PATCH 1/1] rust: firmware: add the firmware upload abstraction
Date: Wed, 26 Aug 2026 17:37:15 +0100	[thread overview]
Message-ID: <20260826163716.6274-2-mike@fireburn.co.uk> (raw)
In-Reply-To: <20260826163716.6274-1-mike@fireburn.co.uk>

request_firmware() covers "pull an image from /lib/firmware"; the other
half of the firmware loader, firmware_upload_register(), covers "userspace
hands the driver an image to write". It publishes
/sys/class/firmware/<name>/ with the loading/data handshake plus status,
error, remaining_size and cancel, and is what a driver uses when an image
has to be written on demand rather than only when a newer one appears.

Add an Upload trait mirroring struct fw_upload_ops, a Registration that
unregisters on drop, and an Error enum whose values are the fw_upload_err
codes userspace already reads back out of the error attribute.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/kernel/firmware.rs | 237 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 237 insertions(+)

diff --git a/rust/kernel/firmware.rs b/rust/kernel/firmware.rs
index 6a6b392ffc4e..a6fa565421d7 100644
--- a/rust/kernel/firmware.rs
+++ b/rust/kernel/firmware.rs
@@ -385,3 +385,240 @@ pub const fn build_length(self) -> usize {
         self.n + 1
     }
 }
+
+/// Firmware upload: let userspace hand a driver an image to write to its device.
+///
+/// Registering creates `/sys/class/firmware/<name>/` with the `loading`/`data` handshake plus
+/// `status`, `error`, `remaining_size` and `cancel`, which is the counterpart to [`Firmware`]:
+/// the same image works either way, but this one is pushed by userspace rather than pulled from
+/// `/lib/firmware`. Drivers use it when an image has to be written on demand -- a re-flash, or a
+/// deliberate downgrade -- rather than only when a newer version appears.
+pub mod upload {
+    use super::*;
+    use crate::types::ForeignOwnable;
+
+    /// Why an upload step failed. `None` means success.
+    ///
+    /// The values are the `enum fw_upload_err` the core reports back through `sysfs`, so a driver
+    /// says what went wrong in the vocabulary userspace already reads out of `error`.
+    #[derive(Clone, Copy, PartialEq, Eq)]
+    #[repr(u32)]
+    pub enum Error {
+        /// The device reported a failure; see the kernel log.
+        Hardware = bindings::fw_upload_err_FW_UPLOAD_ERR_HW_ERROR,
+        /// A handshake with the device timed out.
+        Timeout = bindings::fw_upload_err_FW_UPLOAD_ERR_TIMEOUT,
+        /// Userspace wrote `cancel`.
+        Canceled = bindings::fw_upload_err_FW_UPLOAD_ERR_CANCELED,
+        /// Another upload is already running.
+        Busy = bindings::fw_upload_err_FW_UPLOAD_ERR_BUSY,
+        /// The image is not a size this device can take.
+        InvalidSize = bindings::fw_upload_err_FW_UPLOAD_ERR_INVALID_SIZE,
+        /// A read or write to the device failed; see the kernel log.
+        ReadWrite = bindings::fw_upload_err_FW_UPLOAD_ERR_RW_ERROR,
+        /// The flash is wearing out; wait and retry.
+        WearOut = bindings::fw_upload_err_FW_UPLOAD_ERR_WEAROUT,
+        /// The image is not one this device accepts.
+        InvalidFirmware = bindings::fw_upload_err_FW_UPLOAD_ERR_FW_INVALID,
+    }
+
+    /// A driver's side of an upload.
+    ///
+    /// `prepare` runs once with the whole image, which is where a driver rejects one that is not
+    /// for this device; `write` is then called repeatedly until every byte is written.
+    pub trait Upload: Sized {
+        /// Shared driver state, handed back to every callback.
+        type Data: ForeignOwnable + Send + Sync;
+
+        /// Validate the image and get the device ready. Runs before any write.
+        fn prepare(
+            data: <Self::Data as ForeignOwnable>::Borrowed<'_>,
+            image: &[u8],
+        ) -> Result<(), Error>;
+
+        /// Write `chunk`, which starts at `offset` in the image, returning how much was written.
+        ///
+        /// Called repeatedly until the image is consumed, so a driver may write less than it was
+        /// offered and be called again with the remainder.
+        fn write(
+            data: <Self::Data as ForeignOwnable>::Borrowed<'_>,
+            image: &[u8],
+            offset: u32,
+            chunk: &[u8],
+        ) -> Result<u32, Error>;
+
+        /// Report whether the device has finished programming what it was sent.
+        fn poll_complete(data: <Self::Data as ForeignOwnable>::Borrowed<'_>) -> Result<(), Error>;
+
+        /// Asked to stop, from another thread: set a flag the other callbacks observe.
+        fn cancel(data: <Self::Data as ForeignOwnable>::Borrowed<'_>);
+
+        /// Undo whatever `prepare` set up. Runs on success and on failure alike.
+        fn cleanup(_data: <Self::Data as ForeignOwnable>::Borrowed<'_>) {}
+    }
+
+    /// The C vtable for `U`, built once at compile time.
+    struct Vtable<U: Upload>(core::marker::PhantomData<U>);
+
+    impl<U: Upload> Vtable<U> {
+        /// Turn a driver result into the `enum fw_upload_err` the core expects.
+        fn err(r: Result<(), Error>) -> bindings::fw_upload_err {
+            match r {
+                Ok(()) => bindings::fw_upload_err_FW_UPLOAD_ERR_NONE,
+                Err(e) => e as bindings::fw_upload_err,
+            }
+        }
+
+        /// # Safety
+        ///
+        /// Called by the firmware core with a valid `fw_upload` whose `dd_handle` is the pointer
+        /// [`Registration::new`] passed it, and `data` valid for `size` bytes.
+        unsafe extern "C" fn prepare(
+            fw: *mut bindings::fw_upload,
+            data: *const u8,
+            size: u32,
+        ) -> bindings::fw_upload_err {
+            // SAFETY: the core owns `fw` for the duration of the call.
+            let handle = unsafe { (*fw).dd_handle };
+            // SAFETY: `handle` came from `into_foreign()` in `Registration::new` and outlives the
+            // registration; `data`/`size` describe the image the core is holding.
+            let (d, image) = unsafe {
+                (
+                    <U::Data as ForeignOwnable>::borrow(handle.cast()),
+                    core::slice::from_raw_parts(data, size as usize),
+                )
+            };
+            Self::err(U::prepare(d, image))
+        }
+
+        /// # Safety
+        ///
+        /// As [`Self::prepare`]; `written` is a valid out-parameter.
+        unsafe extern "C" fn write(
+            fw: *mut bindings::fw_upload,
+            data: *const u8,
+            offset: u32,
+            size: u32,
+            written: *mut u32,
+        ) -> bindings::fw_upload_err {
+            // SAFETY: as above.
+            let handle = unsafe { (*fw).dd_handle };
+            // SAFETY: as above; `data + offset` is within the image the core holds.
+            let (d, image, chunk) = unsafe {
+                (
+                    <U::Data as ForeignOwnable>::borrow(handle.cast()),
+                    core::slice::from_raw_parts(data, (offset + size) as usize),
+                    core::slice::from_raw_parts(data.add(offset as usize), size as usize),
+                )
+            };
+            match U::write(d, image, offset, chunk) {
+                Ok(n) => {
+                    // SAFETY: the core passes a valid pointer for the result.
+                    unsafe { *written = n };
+                    bindings::fw_upload_err_FW_UPLOAD_ERR_NONE
+                }
+                Err(e) => e as bindings::fw_upload_err,
+            }
+        }
+
+        /// # Safety
+        ///
+        /// As [`Self::prepare`].
+        unsafe extern "C" fn poll_complete(
+            fw: *mut bindings::fw_upload,
+        ) -> bindings::fw_upload_err {
+            // SAFETY: as above.
+            let handle = unsafe { (*fw).dd_handle };
+            // SAFETY: as above.
+            let d = unsafe { <U::Data as ForeignOwnable>::borrow(handle.cast()) };
+            Self::err(U::poll_complete(d))
+        }
+
+        /// # Safety
+        ///
+        /// As [`Self::prepare`]. Runs on a different thread from the rest.
+        unsafe extern "C" fn cancel(fw: *mut bindings::fw_upload) {
+            // SAFETY: as above.
+            let handle = unsafe { (*fw).dd_handle };
+            // SAFETY: as above.
+            let d = unsafe { <U::Data as ForeignOwnable>::borrow(handle.cast()) };
+            U::cancel(d)
+        }
+
+        /// # Safety
+        ///
+        /// As [`Self::prepare`].
+        unsafe extern "C" fn cleanup(fw: *mut bindings::fw_upload) {
+            // SAFETY: as above.
+            let handle = unsafe { (*fw).dd_handle };
+            // SAFETY: as above.
+            let d = unsafe { <U::Data as ForeignOwnable>::borrow(handle.cast()) };
+            U::cleanup(d)
+        }
+
+        const VTABLE: bindings::fw_upload_ops = bindings::fw_upload_ops {
+            prepare: Some(Self::prepare),
+            write: Some(Self::write),
+            poll_complete: Some(Self::poll_complete),
+            cancel: Some(Self::cancel),
+            cleanup: Some(Self::cleanup),
+        };
+    }
+
+    /// A live `/sys/class/firmware/<name>/` upload interface, unregistered when dropped.
+    pub struct Registration<U: Upload> {
+        fw: *mut bindings::fw_upload,
+        data: *mut core::ffi::c_void,
+        _p: core::marker::PhantomData<U>,
+    }
+
+    // SAFETY: the C side is internally locked, and `U::Data` is `Send + Sync`.
+    unsafe impl<U: Upload> Send for Registration<U> {}
+    // SAFETY: as above.
+    unsafe impl<U: Upload> Sync for Registration<U> {}
+
+    impl<U: Upload> Registration<U> {
+        /// Publish an upload interface named `name` under `parent`.
+        pub fn new(
+            module: &'static crate::ThisModule,
+            parent: &Device,
+            name: &CStr,
+            data: U::Data,
+        ) -> Result<Self> {
+            let handle = data.into_foreign();
+            // SAFETY: `parent` and `name` are valid for the call; the vtable is 'static; `handle`
+            // is kept alive by this registration and released in `drop`.
+            let fw = unsafe {
+                bindings::firmware_upload_register(
+                    module.as_ptr(),
+                    parent.as_raw(),
+                    name.as_char_ptr(),
+                    &Vtable::<U>::VTABLE,
+                    handle.cast(),
+                )
+            };
+            let fw = match crate::error::from_err_ptr(fw) {
+                Ok(fw) => fw,
+                Err(e) => {
+                    // SAFETY: registration failed, so nothing else observes `handle`.
+                    drop(unsafe { <U::Data as ForeignOwnable>::from_foreign(handle) });
+                    return Err(e);
+                }
+            };
+            Ok(Self {
+                fw,
+                data: handle.cast(),
+                _p: core::marker::PhantomData,
+            })
+        }
+    }
+
+    impl<U: Upload> Drop for Registration<U> {
+        fn drop(&mut self) {
+            // SAFETY: `self.fw` came from `firmware_upload_register` and is unregistered once.
+            unsafe { bindings::firmware_upload_unregister(self.fw) };
+            // SAFETY: the core no longer holds `dd_handle` after unregistering.
+            drop(unsafe { <U::Data as ForeignOwnable>::from_foreign(self.data.cast()) });
+        }
+    }
+}

      reply	other threads:[~2026-08-26 16:37 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-26 16:37 [PATCH 0/1] rust: firmware: the firmware upload abstraction Mike Lothian
2026-08-26 16:37 ` Mike Lothian [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260826163716.6274-2-mike@fireburn.co.uk \
    --to=mike@fireburn.co.uk \
    --cc=a.hindborg@kernel.org \
    --cc=acourbot@nvidia.com \
    --cc=aliceryhl@google.com \
    --cc=bjorn3_gh@protonmail.com \
    --cc=boqun@kernel.org \
    --cc=dakr@kernel.org \
    --cc=daniel.almeida@collabora.com \
    --cc=driver-core@lists.linux.dev \
    --cc=gary@garyguo.net \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lossin@kernel.org \
    --cc=mcgrof@kernel.org \
    --cc=ojeda@kernel.org \
    --cc=russ.weight@linux.dev \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=tamird@kernel.org \
    --cc=tmgross@umich.edu \
    --cc=work@onurozkan.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox