From nobody Mon Sep 28 04:51:24 2026 Received: from mail-wm1-f41.google.com (mail-wm1-f41.google.com [209.85.128.41]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BF0DE46E006 for ; Wed, 26 Aug 2026 16:37:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.41 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787762275; cv=none; b=kua7KONqpykIcbIKCsOnY2FkzpHKk2KvqYLODxF/uuRvXYY3MvM6P9sLkPrkswOKLkscXMyaB1FwR5LSYX7xUEZYvgUVYAtLcMb6oiQ04/Bl9keXX8RrxAi+j8tdHwxpijO+dxGyhPgPYlRsh+b+0dwyTEXhJFEZq65ECYL+dSA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787762275; c=relaxed/simple; bh=qIcM5y3Hb6kSUN2B7nsjh8Y4Ancfrv4Lu71gNXejc54=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=SmH/MDezGP7+sSXqC7ZOktGDi1+3MT8erR1LXkW/KUA9/S37MC4qGAt2hsvnZJlOzPYCFvmxeJtxIiggMVLczCnvf/oXOnPBqdP3gGSJbQvaYaVz5V2yPXoj+9TO9Xu0GOJKKb23xnR48fKK6kq9JDG2y6l9d6zz0W1v3+NJ3o8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=fireburn.co.uk; spf=none smtp.mailfrom=fireburn.co.uk; dkim=pass (2048-bit key) header.d=fireburn-co-uk.20251104.gappssmtp.com header.i=@fireburn-co-uk.20251104.gappssmtp.com header.b=ynKI5xRl; arc=none smtp.client-ip=209.85.128.41 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=fireburn.co.uk Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=fireburn.co.uk Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=fireburn-co-uk.20251104.gappssmtp.com header.i=@fireburn-co-uk.20251104.gappssmtp.com header.b="ynKI5xRl" Received: by mail-wm1-f41.google.com with SMTP id 5b1f17b1804b1-49954b88fffso9808775e9.0 for ; Wed, 26 Aug 2026 09:37:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fireburn-co-uk.20251104.gappssmtp.com; s=20251104; t=1787762248; x=1788367048; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=A1HpPd4ylLTuKYL4oCpnHBVRKXPxKbcynFiK+teZyII=; b=ynKI5xRlcNR7pmyDAv8VTP7Wg0eWJwJa336tr03j1EPUxIYrurhlkUM3LT6VKTC6sk 37oFUKEnetaRU65SXjrcia5F5SrNPoh/rQLaNDP8W33QmDJn/Cp7Y0zf6882NRt3beV1 Xdq5xa0EpfLW9zaH9GC5BKpdNPtnBEJXF8FNPnolDc4O00JTpWtrY+3NKyNFDypYVSoc gjupH8UtBAclbksCiKlq0u4K0MkASsWLf7RfPo3PzhvUAD5nfk8dpV4PayXV/ESSCZYq Vc1rTXbpX4U00eKnASBXcoCvnMnQY2UaAof/nEYaWvREVxf2bQp4s1zjXvuEgTYbixVj HmUw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787762248; x=1788367048; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=A1HpPd4ylLTuKYL4oCpnHBVRKXPxKbcynFiK+teZyII=; b=MHAsiUpoyFR9vRT1HGUqLwtJPI2mfUiHVVSB0zR/oG9FqdMFep5C/YuSchddDdSQUk 669VjvuebZGBvlEs5sIFjzOmVK05Iu7jTaV5A8T0F/gLjwL8MfeNhuZ0XFQQtiqc2nhT s/Fiu963c+aVIxmjUfOkpMQpVFh64BhbtngIsNRyKGoWtBdFojwg50R/yzOMJOk1bFyN 3NoKv8MrJj5pQgH2urRTxIqkQS/1FWfjfhJLKcxyXPhx2iNYjj2+lEyj2tU+uiotmFdN BCPinHbNHhhbFqwoUDsb/UUmK0KbvXtJ0WoGh9q0SNQsaUr5XLnFj9W26XDf2MRhn5K+ Wv3Q== X-Gm-Message-State: AFuF++nTH/FaJlz/qJ6rYE3ryO+5wFNjf97uZkqh4t1HPOXKywD4AcP2 i90srzLyDwCzyICrwYCQPSGVVLV1YOU1L6s1foIiF8i0/7bJkb5Y+BtSNYeJvvfxctGI7uIAvfr ChZ791lJk X-Gm-Gg: AR+sD13h4d8CTVRFGLOQI6Hm5aCjtLD6THaa/Djz4ORmjbvKsdx1t1awDdY1Axc+Gdj NM0HGKasZcqHAaS7/8mSpKkuxP4IFUj8AZdvIC5iRCkqewVhR5qBB46LxiUAU6R4wTg0j6g1JgL vkhPw77Tx3PEck7dy0DhJftYzDTU5vcI3GD0GjIAXSjw9nBWwdvnwzMqS97ir7y/b/osG7ysYyG 3v21MWFzUtlpwn2fxzdw5HXfTQY8j64AInonvwkuhtF/3+SIo8o9fBLrlahCUx7Gyw8B+8GU5zI xNxX/EfOUXRV/Z1JUZCHR56yIU6nIpmU9h9Ui5NULta99LhUlnWRBJo0QPkqGl3LLgI05mptEG9 gmObbM2QK7djGZx799US7LGX3AGe8oT4Em0xi47zl6v3S3IFzmJOApIiv5RM6DlUA0eOE5RU45H ltFD39NhsQnaJz3FgPtyqNEqbE5FFak3tP0gQcLhRa7hTwu2CV9pYOX509Nc8gT1YmjOCyuyok3 wOLORalcJt3pdIpaQBskxH7IOOTGfsQc7553LCV6d2URVE= X-Received: by 2002:a05:600c:5491:b0:499:a4d1:d7d9 with SMTP id 5b1f17b1804b1-499dc717e32mr74023185e9.9.1787762247721; Wed, 26 Aug 2026 09:37:27 -0700 (PDT) Received: from axion.fireburn.co.uk ([2a01:4b00:d309:1c00:caf1:6b20:8531:818c]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-482e279eb11sm3350111f8f.8.2026.08.26.09.37.25 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 26 Aug 2026 09:37:26 -0700 (PDT) From: Mike Lothian To: linux-kernel@vger.kernel.org Cc: Mike Lothian , Luis Chamberlain , Russ Weight , Danilo Krummrich , Miguel Ojeda , Boqun Feng , Gary Guo , =?UTF-8?q?Bj=C3=B6rn=20Roy=20Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?UTF-8?q?Onur=20=C3=96zkan?= , 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 Message-ID: <20260826163716.6274-2-mike@fireburn.co.uk> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260826163716.6274-1-mike@fireburn.co.uk> References: <20260826163716.6274-1-mike@fireburn.co.uk> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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// 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 --- 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//` with the `loading`/`= data` handshake plus +/// `status`, `error`, `remaining_size` and `cancel`, which is the counter= part to [`Firmware`]: +/// the same image works either way, but this one is pushed by userspace r= ather than pulled from +/// `/lib/firmware`. Drivers use it when an image has to be written on dem= and -- 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 thro= ugh `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 =3D bindings::fw_upload_err_FW_UPLOAD_ERR_HW_ERROR, + /// A handshake with the device timed out. + Timeout =3D bindings::fw_upload_err_FW_UPLOAD_ERR_TIMEOUT, + /// Userspace wrote `cancel`. + Canceled =3D bindings::fw_upload_err_FW_UPLOAD_ERR_CANCELED, + /// Another upload is already running. + Busy =3D bindings::fw_upload_err_FW_UPLOAD_ERR_BUSY, + /// The image is not a size this device can take. + InvalidSize =3D bindings::fw_upload_err_FW_UPLOAD_ERR_INVALID_SIZE, + /// A read or write to the device failed; see the kernel log. + ReadWrite =3D bindings::fw_upload_err_FW_UPLOAD_ERR_RW_ERROR, + /// The flash is wearing out; wait and retry. + WearOut =3D bindings::fw_upload_err_FW_UPLOAD_ERR_WEAROUT, + /// The image is not one this device accepts. + InvalidFirmware =3D bindings::fw_upload_err_FW_UPLOAD_ERR_FW_INVAL= ID, + } + + /// 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 byt= e 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 w= rite. + fn prepare( + data: ::Borrowed<'_>, + image: &[u8], + ) -> Result<(), Error>; + + /// Write `chunk`, which starts at `offset` in the image, returnin= g 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: ::Borrowed<'_>, + image: &[u8], + offset: u32, + chunk: &[u8], + ) -> Result; + + /// Report whether the device has finished programming what it was= sent. + fn poll_complete(data: ::Borrowed<'_= >) -> Result<(), Error>; + + /// Asked to stop, from another thread: set a flag the other callb= acks observe. + fn cancel(data: ::Borrowed<'_>); + + /// Undo whatever `prepare` set up. Runs on success and on failure= alike. + fn cleanup(_data: ::Borrowed<'_>) {} + } + + /// The C vtable for `U`, built once at compile time. + struct Vtable(core::marker::PhantomData); + + impl Vtable { + /// Turn a driver result into the `enum fw_upload_err` the core ex= pects. + fn err(r: Result<(), Error>) -> bindings::fw_upload_err { + match r { + Ok(()) =3D> bindings::fw_upload_err_FW_UPLOAD_ERR_NONE, + Err(e) =3D> 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` b= ytes. + 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 =3D unsafe { (*fw).dd_handle }; + // SAFETY: `handle` came from `into_foreign()` in `Registratio= n::new` and outlives the + // registration; `data`/`size` describe the image the core is = holding. + let (d, image) =3D unsafe { + ( + ::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 =3D unsafe { (*fw).dd_handle }; + // SAFETY: as above; `data + offset` is within the image the c= ore holds. + let (d, image, chunk) =3D unsafe { + ( + ::borrow(handle.cast()), + core::slice::from_raw_parts(data, (offset + size) as u= size), + core::slice::from_raw_parts(data.add(offset as usize),= size as usize), + ) + }; + match U::write(d, image, offset, chunk) { + Ok(n) =3D> { + // SAFETY: the core passes a valid pointer for the res= ult. + unsafe { *written =3D n }; + bindings::fw_upload_err_FW_UPLOAD_ERR_NONE + } + Err(e) =3D> 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 =3D unsafe { (*fw).dd_handle }; + // SAFETY: as above. + let d =3D unsafe { ::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 =3D unsafe { (*fw).dd_handle }; + // SAFETY: as above. + let d =3D unsafe { ::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 =3D unsafe { (*fw).dd_handle }; + // SAFETY: as above. + let d =3D unsafe { ::borrow(handle.= cast()) }; + U::cleanup(d) + } + + const VTABLE: bindings::fw_upload_ops =3D 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//` upload interface, unregistere= d when dropped. + pub struct Registration { + fw: *mut bindings::fw_upload, + data: *mut core::ffi::c_void, + _p: core::marker::PhantomData, + } + + // SAFETY: the C side is internally locked, and `U::Data` is `Send + S= ync`. + unsafe impl Send for Registration {} + // SAFETY: as above. + unsafe impl Sync for Registration {} + + impl Registration { + /// Publish an upload interface named `name` under `parent`. + pub fn new( + module: &'static crate::ThisModule, + parent: &Device, + name: &CStr, + data: U::Data, + ) -> Result { + let handle =3D data.into_foreign(); + // SAFETY: `parent` and `name` are valid for the call; the vta= ble is 'static; `handle` + // is kept alive by this registration and released in `drop`. + let fw =3D unsafe { + bindings::firmware_upload_register( + module.as_ptr(), + parent.as_raw(), + name.as_char_ptr(), + &Vtable::::VTABLE, + handle.cast(), + ) + }; + let fw =3D match crate::error::from_err_ptr(fw) { + Ok(fw) =3D> fw, + Err(e) =3D> { + // SAFETY: registration failed, so nothing else observ= es `handle`. + drop(unsafe { ::from_foreig= n(handle) }); + return Err(e); + } + }; + Ok(Self { + fw, + data: handle.cast(), + _p: core::marker::PhantomData, + }) + } + } + + impl Drop for Registration { + 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 unregist= ering. + drop(unsafe { ::from_foreign(self.d= ata.cast()) }); + } + } +}