summaryrefslogtreecommitdiff
path: root/rust/kernel/fwctl.rs
blob: f29244fb0d1db5c4aec0dc319796bfe1afd2ddeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
// SPDX-License-Identifier: GPL-2.0-only

//! Abstractions for the fwctl subsystem.
//!
//! C header: `include/linux/fwctl.h`

use crate::{
    bindings,
    container_of,
    device,
    prelude::*,
    sync::aref::{
        ARef,
        AlwaysRefCounted, //
    },
    types::Opaque, //
};
use core::{
    alloc::Layout,
    cell::UnsafeCell,
    marker::PhantomData,
    ptr::NonNull,
    slice, //
};

/// Returns a kmalloc-compatible allocation size for `T`.
const fn kmalloc_aligned_size<T>() -> usize {
    Layout::new::<T>().pad_to_align().size()
}

/// Represents a fwctl device type.
///
/// Corresponds to the C `enum fwctl_device_type`. All non-error UAPI values are represented so
/// Rust drivers can select a device type without passing an untyped integer, while
/// `FWCTL_DEVICE_TYPE_ERROR` remains unrepresentable.
#[repr(u32)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum DeviceType {
    /// Mellanox ConnectX (mlx5) device.
    Mlx5 = bindings::fwctl_device_type_FWCTL_DEVICE_TYPE_MLX5,
    /// CXL (Compute Express Link) device.
    Cxl = bindings::fwctl_device_type_FWCTL_DEVICE_TYPE_CXL,
    /// AMD/Pensando PDS device.
    Pds = bindings::fwctl_device_type_FWCTL_DEVICE_TYPE_PDS,
    /// Broadcom NetXtreme (bnxt) device.
    Bnxt = bindings::fwctl_device_type_FWCTL_DEVICE_TYPE_BNXT,
}

/// Scope of access for an RPC request.
///
/// Corresponds to the C `enum fwctl_rpc_scope`.
#[repr(u32)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RpcScope {
    /// Read/write access to device configuration.
    Configuration = bindings::fwctl_rpc_scope_FWCTL_RPC_CONFIGURATION,
    /// Read-only access to debug information.
    DebugReadOnly = bindings::fwctl_rpc_scope_FWCTL_RPC_DEBUG_READ_ONLY,
    /// Write access to lockdown-compatible debug information.
    DebugWrite = bindings::fwctl_rpc_scope_FWCTL_RPC_DEBUG_WRITE,
    /// Full read/write access to all debug information (requires `CAP_SYS_RAWIO`).
    DebugWriteFull = bindings::fwctl_rpc_scope_FWCTL_RPC_DEBUG_WRITE_FULL,
}

impl TryFrom<u32> for RpcScope {
    type Error = Error;

    #[inline]
    fn try_from(value: u32) -> Result<Self, Error> {
        match value {
            v if v == Self::Configuration as u32 => Ok(Self::Configuration),
            v if v == Self::DebugReadOnly as u32 => Ok(Self::DebugReadOnly),
            v if v == Self::DebugWrite as u32 => Ok(Self::DebugWrite),
            v if v == Self::DebugWriteFull as u32 => Ok(Self::DebugWriteFull),
            _ => Err(EINVAL),
        }
    }
}

/// Response from a [`Operations::fw_rpc`] call.
pub enum FwRpcResponse {
    /// Reuse the input buffer as the output, with the given output length.
    ///
    /// The callback returns `EINVAL` if the output length exceeds the input buffer length.
    InPlace(usize),
    /// Return a newly allocated buffer as the output.
    NewBuffer(KVVec<u8>),
}

/// Trait implemented by each Rust driver that integrates with the fwctl subsystem.
///
/// The implementing type **is** the per-FD user context: one instance is
/// created for each `open()` call and dropped when the FD is closed.
///
/// Each implementation corresponds to a specific device type and provides the
/// vtable used by the core `fwctl` layer to manage per-FD user contexts and
/// handle RPC requests.
pub trait Operations: Sized + Send + Sync + 'static {
    /// Data owned by the [`Registration`] and accessible during callbacks.
    ///
    /// The lifetime `'a` is tied to the [`Registration`] scope (which lives within the parent bus
    /// device binding scope). Drivers use it to store references to resources bound to this scope,
    /// such as PCI BARs or typed bus device references.
    type RegistrationData<'a>: Send + Sync + 'a
    where
        Self: 'a;

    /// fwctl device type identifier.
    const DEVICE_TYPE: DeviceType;

    /// Called when a new user context is opened.
    ///
    /// Returns a [`PinInit`] initializer for `Self`. The instance is dropped
    /// automatically when the FD is closed (after [`close`](Self::close)).
    fn open<'a>(
        device: &Device<Self>,
        reg_data: &Self::RegistrationData<'a>,
    ) -> impl PinInit<Self, Error>;

    /// Called when the user context is closed.
    ///
    /// The driver may perform additional cleanup here that requires access
    /// to the owning [`Device`]. `Self` is dropped automatically after this
    /// returns.
    fn close<'a>(
        _this: Pin<&mut Self>,
        _device: &Device<Self>,
        _reg_data: &Self::RegistrationData<'a>,
    ) {
    }

    /// Return device information to userspace.
    ///
    /// The default implementation returns no device-specific data.
    fn info<'a>(
        _this: Pin<&Self>,
        _device: &Device<Self>,
        _reg_data: &Self::RegistrationData<'a>,
    ) -> Result<KVec<u8>, Error> {
        Ok(KVec::new())
    }

    /// Handle a userspace RPC request.
    ///
    /// `max_output_len` is the size of the userspace output buffer. A driver may return a larger
    /// response to report the required size; the fwctl core copies only the bytes that fit and
    /// reports the full response length to userspace.
    fn fw_rpc<'a>(
        this: Pin<&Self>,
        device: &Device<Self>,
        reg_data: &Self::RegistrationData<'a>,
        scope: RpcScope,
        rpc_buf: &mut [u8],
        max_output_len: usize,
    ) -> Result<FwRpcResponse, Error>;
}

/// A fwctl device.
///
/// `#[repr(C)]` with the `fwctl_device` at offset 0, matching the C `fwctl_alloc_device()` layout
/// convention. Contains a pointer to the [`Registration`]'s data, set at registration time and
/// cleared on unregistration.
///
/// # Invariants
///
/// - `dev` is embedded at offset 0 and is initialised by fwctl.
/// - The fwctl refcount owns the allocation lifetime.
/// - `registration_data` is either [`NonNull::dangling()`] (before registration / after
///   unregistration) or points to valid data owned by the [`Registration`].
#[repr(C)]
pub struct Device<T: Operations> {
    dev: Opaque<bindings::fwctl_device>,
    registration_data: UnsafeCell<NonNull<T::RegistrationData<'static>>>,
}

impl<T: Operations> Device<T> {
    /// Allocate a new fwctl device.
    ///
    /// Returns an [`ARef`] that can be passed to [`Registration::new()`]
    /// to make the device visible to userspace.
    pub fn new(parent: &device::Device<device::Bound>) -> Result<ARef<Self>> {
        const_assert!(
            core::mem::offset_of!(Self, dev) == 0,
            "struct fwctl_device must be at offset 0"
        );

        let size = kmalloc_aligned_size::<Self>();
        let ops = core::ptr::from_ref::<bindings::fwctl_ops>(&VTable::<T>::VTABLE).cast_mut();

        // SAFETY: `ops` is static, `parent` is bound, and `size` is padded so the allocation made
        // by `_fwctl_alloc_device` satisfies the size and alignment required by `Device<T>`.
        let raw = unsafe { bindings::_fwctl_alloc_device(parent.as_raw(), ops, size) };
        let this = NonNull::new(raw.cast::<Self>()).ok_or(ENOMEM)?;

        // 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.
        Ok(unsafe { ARef::from_raw(this) })
    }

    /// Returns the underlying `fwctl_device` pointer.
    #[inline]
    fn as_raw(&self) -> *mut bindings::fwctl_device {
        self.dev.get()
    }

    /// Borrows a Rust fwctl device from its raw C pointer.
    ///
    /// # Safety
    ///
    /// `ptr` must point to a valid `fwctl_device` embedded in a [`Device<T>`].
    #[inline]
    unsafe fn from_raw<'a>(ptr: *mut bindings::fwctl_device) -> &'a Self {
        // SAFETY: The caller upholds the offset-0 `Device<T>` invariant.
        unsafe { &*ptr.cast() }
    }

    /// Invokes `f` with the registration data.
    ///
    /// The higher-ranked callback prevents the erased registration lifetime from escaping and
    /// permits registration data that is invariant over its lifetime parameter.
    ///
    /// # Safety
    ///
    /// The caller must ensure that the device is registered and that this is called from a fwctl
    /// callback protected by `registration_lock`.
    #[inline]
    unsafe fn with_registration_data<R>(
        &self,
        f: impl for<'a> FnOnce(&Device<T>, &'a T::RegistrationData<'a>) -> R,
    ) -> R {
        // SAFETY: Caller guarantees the device is registered, so the pointer is valid.
        // Lifetimes do not affect layout. The higher-ranked callback prevents the shortened
        // lifetime from escaping or being selected by the caller.
        let reg_data = unsafe {
            (*self.registration_data.get())
                .cast::<T::RegistrationData<'_>>()
                .as_ref()
        };

        f(self, reg_data)
    }
}

impl<T: Operations> AsRef<device::Device> for Device<T> {
    #[inline]
    fn as_ref(&self) -> &device::Device {
        // SAFETY: `self` contains a live fwctl_device.
        let dev = unsafe { &raw mut (*self.as_raw()).dev };
        // SAFETY: The embedded device is initialised by fwctl.
        unsafe { device::Device::from_raw(dev) }
    }
}

// SAFETY: `fwctl_get` increments the refcount of a valid fwctl_device.
// `fwctl_put` decrements it and frees the device when it reaches zero.
unsafe impl<T: Operations> AlwaysRefCounted for Device<T> {
    #[inline]
    fn inc_ref(&self) {
        // SAFETY: `self` holds a live reference.
        unsafe { bindings::fwctl_get(self.as_raw()) };
    }

    #[inline]
    unsafe fn dec_ref(obj: NonNull<Self>) {
        // SAFETY: The caller owns a live reference.
        unsafe { bindings::fwctl_put(obj.cast().as_ptr()) };
    }
}

// SAFETY: `Device<T>` is refcounted by the fwctl core and may be released from any thread.
unsafe impl<T: Operations> Send for Device<T> {}

// SAFETY: Shared access to the embedded `fwctl_device` is protected by the fwctl core. The
// `registration_data` field is only mutated before registration and after unregistration (both
// single-threaded with respect to callbacks).
unsafe impl<T: Operations> Sync for Device<T> {}

/// A registered fwctl device.
///
/// Owns the [`RegistrationData`](Operations::RegistrationData) made available to driver callbacks.
/// The parent device lifetime ensures that [`fwctl_unregister`] runs before the parent driver
/// unbinds.
///
/// On drop the device is unregistered (all user contexts are closed and `ops` is set to `NULL`)
/// and the registration data is dropped.
///
/// [`fwctl_unregister`]: srctree/drivers/fwctl/main.c
pub struct Registration<'a, T: Operations> {
    dev: ARef<Device<T>>,
    _reg_data: Pin<KBox<T::RegistrationData<'a>>>,
}

impl<'a, T: Operations> Registration<'a, T> {
    /// Register a previously allocated fwctl device with the given registration data.
    ///
    /// The `reg_data` is owned by the registration and accessible during callbacks.
    ///
    /// # Safety
    ///
    /// Callers must not `mem::forget()` the returned [`Registration`] or otherwise prevent its
    /// [`Drop`] implementation from running, since `fwctl_unregister` must be called before the
    /// parent device is unbound.
    ///
    /// `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>,
        dev: &Device<T>,
        reg_data: impl PinInit<T::RegistrationData<'a>, Error>,
    ) -> Result<Self> {
        let actual_parent = dev.as_ref().parent().ok_or(EINVAL)?;
        let parent_device: &device::Device = parent;
        if !core::ptr::eq(actual_parent, parent_device) {
            return Err(EINVAL);
        }

        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. The `'static` type is only an erased storage
        // handle; callbacks access the pointer through a higher-ranked closure.
        let ptr: NonNull<T::RegistrationData<'static>> =
            NonNull::from(Pin::get_ref(reg_data.as_ref())).cast();

        // 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.
    }
}

/// 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> {
    /// Borrows a pinned Rust user context from its raw C pointer.
    ///
    /// # Safety
    ///
    /// `ptr` must point to a `fwctl_uctx` embedded in a live, pinned `UserCtx<T>` that remains
    /// valid and does not move for the duration of `'a`.
    #[inline]
    unsafe fn from_raw<'a>(ptr: *mut bindings::fwctl_uctx) -> Pin<&'a Self> {
        // SAFETY: The caller upholds the `UserCtx<T>` embedding, lifetime, and pinning invariants.
        unsafe { Pin::new_unchecked(&*container_of!(Opaque::cast_from(ptr), Self, fwctl_uctx)) }
    }

    /// Mutably borrows a pinned Rust user context from its raw C pointer.
    ///
    /// # Safety
    ///
    /// - `ptr` must point to a `fwctl_uctx` embedded in a live, pinned `UserCtx<T>` that remains
    ///   valid and does not move for the duration of `'a`.
    /// - The caller must ensure exclusive access to the `UserCtx<T>` for the duration of `'a`.
    #[inline]
    unsafe fn from_raw_mut<'a>(ptr: *mut bindings::fwctl_uctx) -> Pin<&'a mut Self> {
        // SAFETY: The caller upholds the embedding, lifetime, pinning, and exclusivity invariants.
        unsafe {
            Pin::new_unchecked(
                &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: Pin<&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) }
    }

    /// Returns a pinned reference to the driver context.
    #[inline]
    fn uctx(self: Pin<&Self>) -> Pin<&T> {
        ::pin_init::assert_pinned!(UserCtx<T>, uctx, T, inline);

        // SAFETY: `uctx` is structurally pinned.
        unsafe { self.map_unchecked(|ctx| &ctx.uctx) }
    }
}

/// Static vtable mapping Rust trait methods to C callbacks.
struct VTable<T: Operations>(PhantomData<T>);

impl<T: Operations> VTable<T> {
    /// The fwctl operations vtable for this driver type.
    const VTABLE: bindings::fwctl_ops = bindings::fwctl_ops {
        // CAST: `DeviceType` has the same `u32` representation as the C enum field.
        device_type: T::DEVICE_TYPE as u32,
        uctx_size: kmalloc_aligned_size::<UserCtx<T>>(),
        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),
    };

    /// Initialises a newly opened Rust user context.
    ///
    /// # 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) };

        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.byte_add(uctx_offset).cast() };

        // SAFETY: `open_uctx` is called under `registration_lock` read, so the device is
        // registered. `uctx_ptr` addresses the uninitialised pinned context reserved by
        // `uctx_size`.
        unsafe {
            device.with_registration_data(|device, reg_data| {
                match pin_init::raw_try_init(uctx_ptr, T::open(device, reg_data)) {
                    Ok(()) => 0,
                    Err(e) => e.to_errno(),
                }
            })
        }
    }

    /// Closes and drops an opened Rust user context.
    ///
    /// # 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 is called for an opened Rust user context.
        let mut ctx = unsafe { UserCtx::<T>::from_raw_mut(uctx) };

        // SAFETY: `close_uctx` is called under `registration_lock` write (from
        // `fwctl_unregister`) or read (from `fwctl_fops_release`), so the device is registered.
        unsafe {
            device.with_registration_data(|device, reg_data| {
                T::close(ctx.as_mut().project().uctx, device, reg_data);
            });
        }

        // SAFETY: close is the last callback before fwctl frees the allocation.
        unsafe { core::ptr::drop_in_place(ctx.project().uctx.get_unchecked_mut()) };
    }

    /// Returns device-specific information for an opened Rust user context.
    ///
    /// # 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, so the device is registered.
        let result = unsafe {
            device.with_registration_data(|device, reg_data| T::info(ctx.uctx(), device, reg_data))
        };

        match result {
            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),
        }
    }

    /// Dispatches a firmware RPC for an opened Rust user context.
    ///
    /// # Safety
    ///
    /// - `uctx` must point to a fully initialised `UserCtx<T>`.
    /// - `rpc_in` must be valid, initialised, and exclusively accessible for `in_len` bytes.
    /// - `out_len` must be valid for reading and writing an initialised `usize`.
    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: `out_len` points to an initialised `usize` supplied by fwctl.
        let max_output_len = unsafe { *out_len };

        // SAFETY: RPC is called for an opened Rust user context.
        let ctx = unsafe { UserCtx::<T>::from_raw(uctx) };
        let device = ctx.device();

        // SAFETY: fwctl passes an exclusively owned buffer that is valid and initialised for
        // `in_len` bytes. It remains live for the duration of this callback.
        let rpc_buf = unsafe { slice::from_raw_parts_mut(rpc_in.cast::<u8>(), in_len) };

        // SAFETY: `fw_rpc` is called under `registration_lock` read, so the device is registered.
        let result = unsafe {
            device.with_registration_data(|device, reg_data| {
                T::fw_rpc(ctx.uctx(), device, reg_data, scope, rpc_buf, max_output_len)
            })
        };

        let (response, response_len) = match result {
            Ok(FwRpcResponse::InPlace(len)) => {
                if len > in_len {
                    return Error::to_ptr(EINVAL);
                }

                (rpc_in, len)
            }
            Ok(FwRpcResponse::NewBuffer(kvec)) if kvec.is_empty() => {
                // Return NULL for empty data; kvfree(NULL) is safe.
                (core::ptr::null_mut(), 0)
            }
            Ok(FwRpcResponse::NewBuffer(kvec)) => {
                let (ptr, len, _cap) = kvec.into_raw_parts();
                (ptr.cast::<ffi::c_void>(), len)
            }
            Err(e) => return Error::to_ptr(e),
        };

        // SAFETY: `out_len` is a valid out-parameter.
        unsafe { *out_len = response_len };
        response
    }
}