[PATCH v16 6/6] rust: drm: gem: Introduce shmem::Object::sg_table()

From: Lyude Paul

Date: Tue Jun 02 2026 - 13:43:22 EST


In order to do this, we need to be careful to ensure that any interface we
expose for scatterlists ensures that any mappings created from one are
destroyed on driver-unbind. To do this, we introduce a Devres resource into
shmem::Object that we use in order to ensure that we release any SGTable
mappings on driver-unbind.

There's some other slightly unfortunate caveats of this:

* Drivers don't have explicit control at the moment over when unmapping
happens (which is exactly the same as the C side atm, so it might not be
a problem).
* We can't just return `SGTableMap` to the user through an Arc to attempt
to fix the last caveat - because that implies the gem object would need
to hold a reference count to the scatterlist mapping, which just leaves
us with the same problem.

Signed-off-by: Lyude Paul <lyude@xxxxxxxxxx>

---
V3:
* Rename OwnedSGTable to shmem::SGTable. Since the current version of the
SGTable abstractions now has a `Owned` and `Borrowed` variant, I think
renaming this to shmem::SGTable makes things less confusing.
We do however, keep the name of owned_sg_table() as-is.
V4:
* Clarify safety comments for SGTable to explain why the object is
thread-safe.
* Rename from SGTableRef to SGTable
V10:
* Use Devres in order to ensure that SGTables are revocable, and are
unmapped on driver-unbind.
V11:
* s/create_sg_table()/get_sg_table()
* Get rid of extraneous `ret = ` in shmem::Object::get_sg_table()
V12:
* Actually move sgt_res in this patch and not the next one
V13:
* Use DmaResvGuard suggestion from Alexander
* Use Alexander's (much better) solution for get_sg_table()
* Use SetOnce instead of UnsafeCell
* s/SGTableRef/SGTableMap
* Fix typo in SGTableMap documentation
* Create fallible constructor for SGTableMap
* Don't reuse dma_resv lock for protecting Object contents, just use Mutex
+ SetOnce
* Drop use of drm_gem_shmem_get_pages_sgt_locked(), since we don't need to
hold the dma_resv lock ourselves for anything but this function.
* Check that the device we receive in the bounds for sg_table() and
owned_sg_table() that said Device is in fact, the correct device.
* Remove redundant docs in owned_sg_table(), just point it back to
sg_table().
* Implement Deborah's suggestion to fix double-free in
free_callback()
* Restore original order of Object<T>
* Fix doc typo for SGTableMap
V14:
* Use new InitOnce container over the Mutex/SetOnce horror show we had
before.
* Start using LazyInit container for storing Devres for sgt unmap
* Add some kunit tests for sg_table (not sure why I didn't do this before)
using some of the boilerplate code leftover from the vmap bindings
* Get rid of the owned SGTable variant for now, we'll add it back in a
future patch if people actually need it.
* Use new LazyInit container from me to get rid of the horrid
Mutex<SetOnce<>> mess.
* Add the best we can do for unit tests w/r/t SGTable at the moment
V16:
* Get rid of LazyInit, go back to SetOnce, use trick that Alice recommended
that is a lot cleaner.
* Fix horrid rebasing mistake

rust/kernel/drm/gem/shmem.rs | 164 +++++++++++++++++++++++++++++++++--
1 file changed, 155 insertions(+), 9 deletions(-)

diff --git a/rust/kernel/drm/gem/shmem.rs b/rust/kernel/drm/gem/shmem.rs
index d8bda8c8e2fb2..5f27e8d59e765 100644
--- a/rust/kernel/drm/gem/shmem.rs
+++ b/rust/kernel/drm/gem/shmem.rs
@@ -11,20 +11,34 @@

use crate::{
container_of,
+ device::{
+ self,
+ Bound, //
+ },
+ devres::*,
drm::{
driver,
gem,
private::Sealed,
Device, //
},
- error::to_result,
+ error::{
+ from_err_ptr,
+ to_result, //
+ },
io::{
Io,
IoCapable,
IoKnownSize, //
},
prelude::*,
- sync::aref::ARef,
+ scatterlist,
+ sync::{
+ aref::ARef,
+ new_mutex,
+ Mutex,
+ SetOnce, //
+ },
types::Opaque, //
};
use core::{
@@ -83,6 +97,11 @@ pub struct Object<T: DriverObject> {
obj: Opaque<bindings::drm_gem_shmem_object>,
/// Parent object that owns this object's DMA reservation object.
parent_resv_obj: Option<ARef<Object<T>>>,
+ /// Devres object for unmapping any SGTable on driver-unbind.
+ sgt_res: SetOnce<Devres<SGTableMap<T>>>,
+ #[pin]
+ /// Lock for protecting initialization of `sgt_res`.
+ sgt_lock: Mutex<()>,
#[pin]
inner: T,
}
@@ -135,6 +154,8 @@ pub fn new(
try_pin_init!(Self {
obj <- Opaque::init_zeroed(),
parent_resv_obj: config.parent_resv_obj.map(|p| p.into()),
+ sgt_res: SetOnce::new(),
+ sgt_lock <- new_mutex!(()),
inner <- T::new(dev, size, args),
}),
GFP_KERNEL,
@@ -178,18 +199,23 @@ extern "C" fn free_callback(obj: *mut bindings::drm_gem_object) {
// - DRM always passes a valid gem object here
// - We used drm_gem_shmem_create() in our create_gem_object callback, so we know that
// `obj` is contained within a drm_gem_shmem_object
- let this = unsafe { container_of!(obj, bindings::drm_gem_shmem_object, base) };
-
- // SAFETY:
- // - We're in free_callback - so this function is safe to call.
- // - We won't be using the gem resources on `this` after this call.
- unsafe { bindings::drm_gem_shmem_release(this) };
+ let base = unsafe { container_of!(obj, bindings::drm_gem_shmem_object, base) };

// SAFETY:
// - We verified above that `obj` is valid, which makes `this` valid
// - This function is set in AllocOps, so we know that `this` is contained within a
// `Object<T>`
- let this = unsafe { container_of!(Opaque::cast_from(this), Self, obj) }.cast_mut();
+ let this = unsafe { container_of!(Opaque::cast_from(base), Self, obj) }.cast_mut();
+
+ // We need to drop `sgt_res` first, since doing so requires that the GEM object is still
+ // alive.
+ // SAFETY: We verified above that `this` is valid.
+ unsafe { &mut (*this).sgt_res }.reset();
+
+ // SAFETY:
+ // - We're in free_callback - so this function is safe to call.
+ // - We won't be using the gem resources on `this` after this call.
+ unsafe { bindings::drm_gem_shmem_release(base) };

// SAFETY: We're recovering the Kbox<> we created in gem_create_object()
let _ = unsafe { KBox::from_raw(this) };
@@ -268,6 +294,45 @@ pub fn vmap<const SIZE: usize>(&self) -> Result<VMapRef<'_, T, SIZE>> {
pub fn owned_vmap<const SIZE: usize>(&self) -> Result<VMapOwned<T, SIZE>> {
self.make_vmap()
}
+
+ /// Creates (if necessary) and returns an immutable reference to a scatter-gather table of DMA
+ /// pages for this object.
+ ///
+ /// This will pin the object in memory. It is expected that `dev` should be a pointer to the
+ /// same [`device::Device`] which `self` belongs to, otherwise this function will return
+ /// `Err(EINVAL)`.
+ pub fn sg_table<'a>(
+ &'a self,
+ dev: &'a device::Device<Bound>,
+ ) -> Result<&'a scatterlist::SGTable> {
+ if dev.as_raw() != self.dev().as_ref().as_raw() {
+ return Err(EINVAL);
+ }
+
+ let sgt_res = 'out: {
+ // Fast path: sgt_res is already initialized
+ if let Some(sgt_res) = self.sgt_res.as_ref() {
+ break 'out sgt_res;
+ }
+
+ // Slow path: Grab the lock and see if we need to initialize sgt_res.
+ let _guard = self.sgt_lock.lock();
+
+ // If someone initialized it while we were waiting, we can exit early.
+ if let Some(sgt_res) = self.sgt_res.as_ref() {
+ break 'out sgt_res;
+ }
+
+ // If not, finish initializing and return.
+ self.sgt_res
+ .populate(Devres::new(dev, SGTableMap::new(self))?);
+
+ // SAFETY: We just populated sgt_res above.
+ unsafe { self.sgt_res.as_ref().unwrap_unchecked() }
+ };
+
+ Ok(sgt_res.access(dev)?)
+ }
}

impl<T: DriverObject> Deref for Object<T> {
@@ -457,6 +522,63 @@ impl<D, R, const SIZE: usize> IoKnownSize for VMap<D, R, SIZE>
#[cfg(CONFIG_64BIT)]
impl_vmap_io_capable!(VMap, u64);

+/// A reference to a GEM object that is known to have a mapped [`SGTable`].
+///
+/// This is used by the Rust bindings with [`Devres`] in order to ensure that mappings for SGTables
+/// on GEM shmem objects are revoked on driver-unbind.
+///
+/// # Invariants
+///
+/// - `self.obj` always points to a valid GEM object.
+/// - This object is proof that `self.obj.owner.sgt` has an initialized and valid
+/// [`scatterlist::SGTable`].
+pub struct SGTableMap<T: DriverObject> {
+ obj: NonNull<Object<T>>,
+}
+
+impl<T: DriverObject> Deref for SGTableMap<T> {
+ type Target = scatterlist::SGTable;
+
+ fn deref(&self) -> &Self::Target {
+ // SAFETY:
+ // - The NonNull is guaranteed to be valid via our type invariants.
+ // - The sgt field is guaranteed to be initialized and valid via our type invariants.
+ unsafe { scatterlist::SGTable::from_raw((*self.obj.as_ref().as_raw_shmem()).sgt) }
+ }
+}
+
+impl<T: DriverObject> Drop for SGTableMap<T> {
+ fn drop(&mut self) {
+ // SAFETY: `obj` is always valid via our type invariants
+ let obj = unsafe { self.obj.as_ref() };
+ let _lock = DmaResvGuard::new(obj);
+
+ // SAFETY: We acquired the lock needed for calling this function above
+ unsafe { bindings::__drm_gem_shmem_free_sgt_locked(obj.as_raw_shmem()) };
+ }
+}
+
+impl<T: DriverObject> SGTableMap<T> {
+ fn new(obj: &Object<T>) -> impl Init<Self, Error> {
+ // INVARIANT:
+ // - We call drm_gem_shmem_get_pages_sgt_locked below and check whether or not it
+ // succeeds, fulfilling the invariant of SGTableMap that the object's `sgt` field is
+ // initialized.
+ // SAFETY:
+ // - `obj` is fully initialized, making this function safe to call.
+ from_err_ptr(unsafe { bindings::drm_gem_shmem_get_pages_sgt(obj.as_raw_shmem()) })?;
+
+ Ok(Self { obj: obj.into() })
+ }
+}
+
+// SAFETY: The NonNull in SGTableMap is guaranteed valid by our type invariants, and the GEM object
+// it points to is guaranteed to be thread-safe.
+unsafe impl<T: DriverObject> Send for SGTableMap<T> {}
+// SAFETY: The NonNull in SGTableMap is guaranteed valid by our type invariants, and the GEM object
+// it points to is guaranteed to be thread-safe.
+unsafe impl<T: DriverObject> Sync for SGTableMap<T> {}
+
#[kunit_tests(rust_drm_gem_shmem)]
mod tests {
use super::*;
@@ -562,4 +684,28 @@ fn vmap_io() -> Result {

Ok(())
}
+
+ // TODO: I would love to actually test the success paths of sg_table(), but that would require
+ // also implementing dummy dma_ops so that trying to create a mapping doesn't explode. So, leave
+ // that for someone else.
+
+ // Ensures that passing the wrong device to sg_table() fails as we expect, and also ensure it
+ // skips initializing `sgt_res` since we could otherwise create `sgt_res` with the wrong device
+ // bound to it.
+ #[test]
+ fn fail_sg_table_on_wrong_dev() -> Result {
+ let (_dev, drm) = create_drm_dev()?;
+ let wrong_dev = faux::Registration::new(c"EvilKunit", None)?;
+
+ let obj = Object::<KunitObject>::new(&drm, PAGE_SIZE, ObjectConfig::default(), ())?;
+
+ assert_eq!(obj.sg_table(wrong_dev.as_ref()).err().unwrap(), EINVAL);
+
+ // If sgt_res was not initialized mistakenly with the wrong device, this should still fail.
+ assert_eq!(obj.sg_table(wrong_dev.as_ref()).err().unwrap(), EINVAL);
+
+ // TODO: Someday, we should test that creating an sg_table here still succeeds.
+
+ Ok(())
+ }
}
--
2.54.0