From: Zhi Wang <zhiw@nvidia.com>
To: Alexandre Courbot <acourbot@nvidia.com>
Cc: <rust-for-linux@vger.kernel.org>, <linux-kernel@vger.kernel.org>,
<dakr@kernel.org>, <jgg@nvidia.com>, <dave.jiang@intel.com>,
<saeedm@nvidia.com>, <jic23@kernel.org>, <gary@garyguo.net>,
<joelagnelf@nvidia.com>, <aliceryhl@google.com>,
<kwilczynski@kernel.org>, <ojeda@kernel.org>,
<alex.gaynor@gmail.com>, <boqun.feng@gmail.com>,
<bjorn3_gh@protonmail.com>, <lossin@kernel.org>,
<a.hindborg@kernel.org>, <tmgross@umich.edu>, <cjia@nvidia.com>,
<smitra@nvidia.com>, <ankita@nvidia.com>, <aniketa@nvidia.com>,
<kwankhede@nvidia.com>, <targupta@nvidia.com>, <kjaju@nvidia.com>,
<alkumar@nvidia.com>, <jhubbard@nvidia.com>, <zhiwang@kernel.org>,
<daniel.almeida@collabora.com>
Subject: Re: [PATCH v6 1/1] rust: introduce abstractions for fwctl
Date: Wed, 8 Jul 2026 18:53:53 +0300 [thread overview]
Message-ID: <20260708185353.2dae54b6@inno-dell> (raw)
In-Reply-To: <DJR76RV8NEKN.2MP5M9Z66U4TY@nvidia.com>
On Mon, 06 Jul 2026 13:19:17 +0900
"Alexandre Courbot" <acourbot@nvidia.com> wrote:
> Hi Zhi,
>
> This is looking pretty good to me overall, a few remarks/questions
> below. None of these should require big changes.
>
> On Tue Jun 30, 2026 at 12:01 AM JST, Zhi Wang wrote:
> <...>
snip
> > +
> > + // INVARIANT: Set `registration_data` to dangling (no
> > registration yet).
> > + // SAFETY: `this` points to the allocation just returned
> > by fwctl.
> > + unsafe {
> > + core::ptr::addr_of_mut!((*this).registration_data)
> > + .write(UnsafeCell::new(NonNull::dangling()));
> > + };
> > +
> > + // SAFETY: `raw` owns the initial reference.
> > + Ok(unsafe { ARef::from_raw(NonNull::new_unchecked(this))
> > })
>
> How about replacing from `if raw.is_null() ...` with something more
> functional-style:
>
> NonNull::new(raw.cast::<Self>())
> .map(|this| {
> // INVARIANT: Set `registration_data` to dangling (no
> registration yet). // SAFETY: `this` points to the allocation just
> returned by fwctl. unsafe {
> (&raw mut (*this.as_ptr()).registration_data)
> .write(UnsafeCell::new(NonNull::dangling()))
> };
> // SAFETY: `this` owns the initial reference.
> unsafe { ARef::from_raw(this) }
> })
> .ok_or(ENOMEM)
>
> It's not just cosmetic: it removes one call to an unsafe method
> (`NonNull::new_unchecked`), carries the fact that `this` is not NULL
> in the type system from the get-go (instead of relying on an explicit
> check), and makes the transition steps from the raw pointer to the
> `ARef` more explicit.
>
Great idea. I eventually take Gary idea which has similar motivation
with this one. All subsequent initialization uses this NonNull value,
and NonNull::new_unchecked() is no longer needed. :)
snip
> > + /// `dev` must be an unregistered [`Device`] that is not
> > associated with any live
> > + /// [`Registration`], and no other thread may attempt to
> > register the same device concurrently.
> > + pub unsafe fn new(
> > + _parent: &'a device::Device<device::Bound>,
>
> Why do we need `_parent`? Is it to provide a proof that the parent
> device is bound by the time of registration? If so, there is an
> obvious loophole: this method will accept any bound device, not
> necessarily the actual parent that was passed to `Device::new`.
>
> We should either store an `ARef<device::Device>` in `fwctl::Device` to
> perform the check at runtime, or add a requirement in the `Safety`
> paragraph that `_parent` must be the actual parent, since the method
> is already `unsafe` anyway.
>
Good catch. I fixed this in v7 with a runtime identity check.
> > + dev: &Device<T>,
> > + reg_data: impl PinInit<T::RegistrationData<'a>, Error>,
> > + ) -> Result<Self> {
> > + let reg_data: Pin<KBox<T::RegistrationData<'a>>> =
> > KBox::pin_init(reg_data, GFP_KERNEL)?; +
> > + // Store the registration data pointer in the device
> > before registration, so that it is
> > + // visible once callbacks can be invoked.
> > + //
> > + // SAFETY: Lifetimes do not affect layout, so the pointer
> > cast is sound.
> > + let ptr: NonNull<T::RegistrationData<'static>> =
> > + unsafe {
> > mem::transmute(NonNull::from(Pin::get_ref(reg_data.as_ref()))) };
>
> The only unsafe part of this statement is the `mem::transmute`. In
> such cases, I think it is worth using an intermediate variable for the
> non-unsafe part so non-checked unsafe operations don't slip in if the
> code is modified, e.g.:
>
> let r = NonNull::from(Pin::get_ref(reg_data.as_ref()));
> ...
> let ptr: NonNull<T::RegistrationData<'static>> = unsafe {
> mem::transmute(r) };
Thanks for the idea. I remove the transmute entirely in v7.
> > +
> > + // SAFETY: No concurrent access; the device is not yet
> > registered.
> > + unsafe { *dev.registration_data.get() = ptr };
> > +
> > + // SAFETY: `dev` is a valid fwctl_device backed by an ARef.
> > + let ret = unsafe { bindings::fwctl_register(dev.as_raw())
> > };
> > + if ret != 0 {
> > + // SAFETY: No concurrent readers; registration failed.
> > + unsafe { *dev.registration_data.get() =
> > NonNull::dangling() };
> > + return Err(Error::from_errno(ret));
> > + }
> > +
> > + Ok(Self {
> > + dev: dev.into(),
> > + _reg_data: reg_data,
> > + })
> > + }
> > +}
> > +
> > +impl<T: Operations> Drop for Registration<'_, T> {
> > + fn drop(&mut self) {
> > + // SAFETY: The Registration lifetime guarantees that the
> > parent device is still bound.
> > + // `fwctl_unregister` takes the write lock, closes all
> > user contexts, and sets ops=NULL.
> > + // After it returns, no callbacks can be running or will
> > run.
> > + unsafe { bindings::fwctl_unregister(self.dev.as_raw()) };
> > +
> > + // SAFETY: `fwctl_unregister` guarantees no concurrent
> > readers.
> > + unsafe { *self.dev.registration_data.get() =
> > NonNull::dangling() }; +
> > + // `self._reg_data` is dropped here, after callbacks have
> > stopped.
> > + }
> > +}
> > +
> > +// SAFETY: `Registration` can be sent between threads; the
> > underlying fwctl_device uses internal +// locking.
> > +unsafe impl<T: Operations> Send for Registration<'_, T> {}
> > +
> > +// SAFETY: `Registration` provides no mutable access; the
> > underlying fwctl_device is protected by +// internal locking.
> > +unsafe impl<T: Operations> Sync for Registration<'_, T> {}
>
> IIUC all the fields of `Registration` are already `Send` and `Sync`,
> so we don't need these manual implementations.
>
Removed in v7.
> > +/// Internal per-FD user context wrapping `struct fwctl_uctx` and
> > `T`. +///
> > +/// Not exposed to drivers; they work with `&T` / `Pin<&mut T>`
> > directly. +#[repr(C)]
> > +#[pin_data]
> > +struct UserCtx<T: Operations> {
> > + #[pin]
> > + fwctl_uctx: Opaque<bindings::fwctl_uctx>,
> > + #[pin]
> > + uctx: T,
> > +}
> > +
> > +impl<T: Operations> UserCtx<T> {
> > + /// # Safety
> > + ///
> > + /// `ptr` must point to a `fwctl_uctx` embedded in a live
> > `UserCtx<T>`.
> > + #[inline]
> > + unsafe fn from_raw<'a>(ptr: *mut bindings::fwctl_uctx) -> &'a
> > Self {
> > + // SAFETY: The caller upholds the `UserCtx<T>` embedding
> > invariant.
> > + unsafe { &*container_of!(Opaque::cast_from(ptr), Self,
> > fwctl_uctx) }
> > + }
> > +
> > + /// # Safety
> > + ///
> > + /// `ptr` must point to a `fwctl_uctx` embedded in a live
> > `UserCtx<T>`.
> > + /// The caller must ensure exclusive access to the
> > `UserCtx<T>`.
> > + #[inline]
> > + unsafe fn from_raw_mut<'a>(ptr: *mut bindings::fwctl_uctx) ->
> > &'a mut Self {
> > + // SAFETY: The caller upholds the embedding and
> > exclusivity invariants.
> > + unsafe { &mut *container_of!(Opaque::cast_from(ptr), Self,
> > fwctl_uctx).cast_mut() }
> > + }
> > +
> > + /// Returns a reference to the fwctl [`Device`] that owns this
> > context.
> > + #[inline]
> > + fn device(&self) -> &Device<T> {
> > + // SAFETY: fwctl initialises this pointer before any
> > driver callback.
> > + let raw_fwctl = unsafe { (*self.fwctl_uctx.get()).fwctl };
> > + // SAFETY: Rust fwctl devices use the offset-0 `Device<T>`
> > layout.
> > + unsafe { Device::from_raw(raw_fwctl) }
> > + }
> > +}
> > +
> > +/// Static vtable mapping Rust trait methods to C callbacks.
> > +pub struct VTable<T: Operations>(PhantomData<T>);
> > +
> > +impl<T: Operations> VTable<T> {
> > + /// The fwctl operations vtable for this driver type.
> > + pub const VTABLE: bindings::fwctl_ops = bindings::fwctl_ops {
> > + device_type: T::DEVICE_TYPE as u32,
> > + uctx_size: core::mem::size_of::<UserCtx<T>>(),
>
> Aren't the alignment concerns raised on [1] still valid? Since the
> user context is allocated by the C side, we need to make sure that
> Rust's alignment expectations are properly met.
>
> [1] https://lore.kernel.org/all/DGUZ4A6W87JU.36R2KDR7YGSEX@kernel.org/
>
Thanks for bringing this up. In v7, I got a private const helper based
on Layout::pad_to_align(), and VTable::uctx_size uses the resulting
padded size for UserCtx<T>. The same calculation is also used for the
Device<T> allocation. This should address the concern.
> > + open_uctx: Some(Self::open_uctx_callback),
> > + close_uctx: Some(Self::close_uctx_callback),
> > + info: Some(Self::info_callback),
> > + fw_rpc: Some(Self::fw_rpc_callback),
> > + };
> > +
> > + /// # Safety
> > + ///
> > + /// `uctx` must be a valid `fwctl_uctx` embedded in a
> > `UserCtx<T>` with
> > + /// sufficient allocated space for the uctx field.
> > + unsafe extern "C" fn open_uctx_callback(uctx: *mut
> > bindings::fwctl_uctx) -> ffi::c_int {
> > + const_assert!(
> > + core::mem::offset_of!(UserCtx<T>, fwctl_uctx) == 0,
> > + "struct fwctl_uctx must be at offset 0"
> > + );
> > +
> > + // SAFETY: fwctl sets this pointer before calling
> > `open_uctx`.
> > + let raw_fwctl = unsafe { (*uctx).fwctl };
> > + // SAFETY: Rust fwctl devices use the offset-0 `Device<T>`
> > layout.
> > + let device = unsafe { Device::<T>::from_raw(raw_fwctl) };
> > +
> > + // SAFETY: open_uctx is called under registration_lock
> > read; the device is registered.
> > + let reg_data = unsafe {
> > device.registration_data_unchecked() };
>
> This `from_raw`..`registration_data_unchecked` sequence is performed
> by all hooks, how about factoring them out in a helper function?
>
Done in v7.
> > +
> > + let initializer = T::open(device, reg_data);
> > +
> > + let uctx_offset = core::mem::offset_of!(UserCtx<T>, uctx);
> > + // SAFETY: `uctx_size` reserves space for the full
> > `UserCtx<T>`.
> > + let uctx_ptr: *mut T = unsafe {
> > uctx.cast::<u8>().add(uctx_offset).cast() }; +
> > + // SAFETY: `uctx_ptr` addresses the uninitialised pinned
> > context.
> > + match unsafe { initializer.__pinned_init(uctx_ptr.cast())
> > } {
>
> This `cast` is unneeded here IIUC.
>
> > + Ok(()) => 0,
> > + Err(e) => e.to_errno(),
> > + }
> > + }
> > +
> > + /// # Safety
> > + ///
> > + /// `uctx` must point to a fully initialised `UserCtx<T>`.
> > + unsafe extern "C" fn close_uctx_callback(uctx: *mut
> > bindings::fwctl_uctx) {
> > + // SAFETY: fwctl keeps the owning device live for this
> > callback.
> > + let device = unsafe { Device::<T>::from_raw((*uctx).fwctl)
> > }; +
> > + // SAFETY: close_uctx is called under registration_lock
> > write (from fwctl_unregister) or
> > + // under registration_lock read (from fwctl_fops_release);
> > the device is registered.
> > + let reg_data = unsafe {
> > device.registration_data_unchecked() }; +
> > + // SAFETY: close is called for an opened Rust user context.
> > + let ctx = unsafe { UserCtx::<T>::from_raw_mut(uctx) };
> > +
> > + {
> > + // SAFETY: fwctl never moves an opened user context.
> > + let pinned = unsafe { Pin::new_unchecked(&mut
> > ctx.uctx) };
> > + T::close(pinned, device, reg_data);
> > + }
> > +
> > + // SAFETY: close is the last callback before fwctl frees
> > the allocation.
> > + unsafe { core::ptr::drop_in_place(&mut ctx.uctx) };
> > + }
> > +
> > + /// # Safety
> > + ///
> > + /// `uctx` must point to a fully initialised `UserCtx<T>`.
> > + /// `length` must be a valid pointer.
> > + unsafe extern "C" fn info_callback(
> > + uctx: *mut bindings::fwctl_uctx,
> > + length: *mut usize,
> > + ) -> *mut ffi::c_void {
> > + // SAFETY: info is called for an opened Rust user context.
> > + let ctx = unsafe { UserCtx::<T>::from_raw(uctx) };
> > + let device = ctx.device();
> > +
> > + // SAFETY: info is called under registration_lock read;
> > the device is registered.
> > + let reg_data = unsafe {
> > device.registration_data_unchecked() }; +
> > + // SAFETY: fwctl never moves an opened user context.
> > + let pinned = unsafe { Pin::new_unchecked(&ctx.uctx) };
> > +
> > + match T::info(pinned, device, reg_data) {
> > + Ok(kvec) if kvec.is_empty() => {
> > + // SAFETY: `length` is a valid out-parameter.
> > + unsafe { *length = 0 };
> > + // Return NULL for empty data; kfree(NULL) is safe.
> > + core::ptr::null_mut()
> > + }
> > + Ok(kvec) => {
> > + let (ptr, len, _cap) = kvec.into_raw_parts();
> > + // SAFETY: `length` is a valid out-parameter.
> > + unsafe { *length = len };
> > + ptr.cast::<ffi::c_void>()
> > + }
> > + Err(e) => Error::to_ptr(e),
> > + }
> > + }
> > +
> > + /// # Safety
> > + ///
> > + /// `uctx` must point to a fully initialised `UserCtx<T>`.
> > + /// `rpc_in` must be valid for `in_len` bytes. `out_len` must
> > be valid.
> > + unsafe extern "C" fn fw_rpc_callback(
> > + uctx: *mut bindings::fwctl_uctx,
> > + scope: u32,
> > + rpc_in: *mut ffi::c_void,
> > + in_len: usize,
> > + out_len: *mut usize,
> > + ) -> *mut ffi::c_void {
> > + let scope = match RpcScope::try_from(scope) {
> > + Ok(s) => s,
> > + Err(e) => return Error::to_ptr(e),
> > + };
> > +
> > + // SAFETY: RPC is called for an opened Rust user context.
> > + let ctx = unsafe { UserCtx::<T>::from_raw(uctx) };
> > + let device = ctx.device();
> > +
> > + // SAFETY: fw_rpc is called under registration_lock read;
> > the device is registered.
> > + let reg_data = unsafe {
> > device.registration_data_unchecked() }; +
> > + // SAFETY: fwctl passes a valid in/out buffer for this
> > callback.
> > + let rpc_in_slice: &mut [u8] =
> > + unsafe {
> > slice::from_raw_parts_mut(rpc_in.cast::<u8>(), in_len) };
>
> Name nit: technically this is not just an `in` slice, since the reply
> might be written into it.
>
I will name it as rpc_buf then. :)
> > +
> > + // SAFETY: fwctl never moves an opened user context.
> > + let pinned = unsafe { Pin::new_unchecked(&ctx.uctx) };
> > +
> > + match T::fw_rpc(pinned, device, reg_data, scope,
> > rpc_in_slice) {
>
> The C side passes `out_len` to its callback, don't we want to do the
> same here?
>
> > + Ok(FwRpcResponse::InPlace(len)) => {
> > + if len > in_len {
> > + return Error::to_ptr(EINVAL);
> > + }
> > +
> > + // SAFETY: `out_len` is valid.
> > + unsafe { *out_len = len };
> > + rpc_in
> > + }
> > + Ok(FwRpcResponse::NewBuffer(kvec)) => {
> > + let (ptr, len, _cap) = kvec.into_raw_parts();
> > + // SAFETY: `out_len` is valid.
> > + unsafe { *out_len = len };
>
> Both arms are writing `*out_len` and require an `unsafe` statement. We
> could factor these out into a single one by returning a `(len, res)`
> tuple from the match statement, and writing `out_len` once out of it.
>
> > + ptr.cast::<ffi::c_void>()
> > + }
> > + Err(e) => Error::to_ptr(e),
>
> (if you follow my recommendation above, then this needs to become a
> return statement).
Done.
prev parent reply other threads:[~2026-07-08 15:54 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-29 15:01 [PATCH v6 0/1] rust: introduce abstractions for fwctl Zhi Wang
2026-06-29 15:01 ` [PATCH v6 1/1] " Zhi Wang
2026-07-04 19:42 ` Danilo Krummrich
2026-07-08 15:20 ` Zhi Wang
2026-07-06 4:19 ` Alexandre Courbot
2026-07-06 11:30 ` Danilo Krummrich
2026-07-06 12:00 ` Gary Guo
2026-07-07 11:33 ` Alexandre Courbot
2026-07-08 15:53 ` Zhi Wang [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=20260708185353.2dae54b6@inno-dell \
--to=zhiw@nvidia.com \
--cc=a.hindborg@kernel.org \
--cc=acourbot@nvidia.com \
--cc=alex.gaynor@gmail.com \
--cc=aliceryhl@google.com \
--cc=alkumar@nvidia.com \
--cc=aniketa@nvidia.com \
--cc=ankita@nvidia.com \
--cc=bjorn3_gh@protonmail.com \
--cc=boqun.feng@gmail.com \
--cc=cjia@nvidia.com \
--cc=dakr@kernel.org \
--cc=daniel.almeida@collabora.com \
--cc=dave.jiang@intel.com \
--cc=gary@garyguo.net \
--cc=jgg@nvidia.com \
--cc=jhubbard@nvidia.com \
--cc=jic23@kernel.org \
--cc=joelagnelf@nvidia.com \
--cc=kjaju@nvidia.com \
--cc=kwankhede@nvidia.com \
--cc=kwilczynski@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=lossin@kernel.org \
--cc=ojeda@kernel.org \
--cc=rust-for-linux@vger.kernel.org \
--cc=saeedm@nvidia.com \
--cc=smitra@nvidia.com \
--cc=targupta@nvidia.com \
--cc=tmgross@umich.edu \
--cc=zhiwang@kernel.org \
/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