On Thu, Feb 12, 2026 at 11:44:18AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:09 -0800
> Deborah Brouwer <[email protected]> wrote:
>
> > From: Boris Brezillon <[email protected]>
> >
> > Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> > SlotManager for allocating hardware address space slots. The underlying
> > AddressSpaceManager performs MMU operations including enabling/disabling
> > address spaces, flushing page tables, and locking regions for page table
> > updates.
> >
> > Signed-off-by: Boris Brezillon <[email protected]>
> > Co-developed-by: Deborah Brouwer <[email protected]>
> > Signed-off-by: Deborah Brouwer <[email protected]>
> > ---
> > drivers/gpu/drm/tyr/driver.rs | 3 +
> > drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
> > drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
> > drivers/gpu/drm/tyr/tyr.rs | 1 +
> > 4 files changed, 417 insertions(+)
> > create mode 100644 drivers/gpu/drm/tyr/mmu.rs
> > create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
> >
> > diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> > index 2973a8b3cc09..ad5a765a6c2a 100644
> > --- a/drivers/gpu/drm/tyr/driver.rs
> > +++ b/drivers/gpu/drm/tyr/driver.rs
> > @@ -43,6 +43,7 @@
> > gem::BoData,
> > gpu,
> > gpu::GpuInfo,
> > + mmu::Mmu,
> > regs, //
> > };
> >
> > @@ -148,6 +149,8 @@ fn probe(
> > let uninit_ddev =
> > UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> > let platform: ARef<platform::Device> = pdev.into();
> >
> > + let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
> > +
> > let data = try_pin_init!(TyrDrmDeviceData {
> > pdev: platform.clone(),
> > clks <- new_mutex!(Clocks {
> > diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
> > new file mode 100644
> > index 000000000000..8e076c35f342
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu.rs
> > @@ -0,0 +1,91 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! Memory Management Unit (MMU) driver for the Tyr GPU.
> > +//!
> > +//! This module manages GPU address spaces and virtual memory operations
> > through
> > +//! hardware MMU slots. It provides functionality for flushing page tables
> > and
> > +//! managing VM updates for active address spaces.
> > +//!
> > +//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
> > +//! address space allocation and page table operations, using
> > [`SlotManager`]
> > +//! to track which address spaces are currently active in hardware slots.
> > +//!
> > +//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
> > +//! [`SlotManager`]: crate::slot::SlotManager
> > +#![allow(dead_code)]
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + devres::Devres,
> > + new_mutex,
> > + platform,
> > + prelude::*,
> > + sync::{
> > + Arc,
> > + ArcBorrow,
> > + Mutex, //
> > + }, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + gpu::GpuInfo,
> > + mmu::address_space::{
> > + AddressSpaceManager,
> > + VmAsData, //
> > + },
> > + regs::MAX_AS_REGISTERS,
> > + slot::{
> > + SlotManager, //
> > + }, //
>
> formatting nit:
> slot::SlotManager, //
>
> > +};
> > +
> > +pub(crate) mod address_space;
> > +
> > +pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager,
> > MAX_AS_REGISTERS>;
> > +
>
> /// MMU component of the GPU.
> ///
> /// This is used to bind VM objects to an AS (Address Space) slot
> /// and make the VM active on the GPU.
>
> > +#[pin_data]
> > +pub(crate) struct Mmu {
> > + /// Manages the allocation of hardware MMU slots to GPU address spaces.
> > + ///
> > + /// Tracks which address spaces are currently active in hardware slots
> > and
> > + /// coordinates address space operations like flushing and VM updates.
> > + #[pin]
> > + pub(crate) as_manager: Mutex<AsSlotManager>,
> > +}
> > +
> > +impl Mmu {
>
> /// Create an MMU component for this device.
>
> > + pub(crate) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + gpu_info: &GpuInfo,
> > + ) -> Result<Arc<Mmu>> {
> > + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> > + let as_manager = AddressSpaceManager::new(pdev, iomem,
> > gpu_info.as_present)?;
> > + let mmu_init = try_pin_init!(Self{
> > + as_manager <- new_mutex!(SlotManager::new(as_manager,
> > slot_count)?),
> > + });
> > + Arc::pin_init(mmu_init, GFP_KERNEL)
> > + }
> > +
>
> /// Make a VM active.
> ///
> /// This implies assigning the VM to an AS slot through the slot
> /// manager.
> > + pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) ->
> > Result {
> > + self.as_manager.lock().activate_vm(vm)
> > + }
> > +
>
> /// Make the VM inactive.
> ///
> /// Evicts the VM from its AS slot through the slot manager.
>
> > + pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().deactivate_vm(vm)
> > + }
> > +
>
> /// Flush caches after a VM update.
> ///
> /// If the VM is no longer resident, this is a NOP, otherwise, the
> /// AS manager will flush the GPU and MMU (TLB) caches.
> > + pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().flush_vm(vm)
> > + }
> > +
>
> /// Flags the start of a VM update.
> ///
> /// If the VM is resident, any GPU access on the memory range being
> /// updated will be blocked until `Mmu::end_vm_update()` is called.
> /// This guarantees the atomicity of a VM update.
> /// If the VM is not resident, this is a NOP.
> > + pub(crate) fn start_vm_update(&self, vm: &VmAsData, region:
> > &Range<u64>) -> Result {
> > + self.as_manager.lock().start_vm_update(vm, region)
> > + }
> > +
>
> /// Flags the end of a VM update.
> ///
> /// If the VM is resident, this will let GPU accesse on the updated
> /// range go through, in case any of them were blocked.
> /// If the VM is not resident, this is a NOP.
> > + pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().end_vm_update(vm)
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs
> > b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > new file mode 100644
> > index 000000000000..60e9a79112f0
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > @@ -0,0 +1,322 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! GPU address space management and hardware operations.
> > +//!
> > +//! This module manages GPU hardware address spaces, including
> > configuration,
> > +//! command submission, and page table update regions. It handles the
> > hardware
> > +//! interaction for MMU operations through MMIO register access.
> > +//!
> > +//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate
> > with
> > +//! the slot management system, enabling and configuring address spaces in
> > the
> > +//! hardware slots as needed.
> > +//!
> > +//! [`SlotOperations`]: crate::slot::SlotOperations
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + bits::*,
> > + device::{
> > + Bound,
> > + Device, //
> > + },
> > + devres::Devres,
> > + error::Result,
> > + io,
> > + iommu::pgtable::{
> > + IoPageTable,
> > + ARM64LPAES1, //
> > + },
> > + platform,
> > + prelude::*,
> > + sync::{
> > + aref::ARef,
> > + Arc,
> > + ArcBorrow,
> > + LockedBy, //
> > + },
> > + time::Delta, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + mmu::{
> > + AsSlotManager,
> > + Mmu, //
> > + },
> > + regs::*,
> > + slot::{
> > + Seat,
> > + SlotOperations, //
> > + }, //
> > +};
> > +
> > +/// Hardware address space configuration registers.
> > +///
> > +/// Contains the values to be written to the GPU's AS registers when
> > +/// activating this address space.
> > +#[derive(Clone, Copy)]
> > +pub(crate) struct AddressSpaceConfig {
> > + pub(crate) transcfg: u64,
> > + pub(crate) transtab: u64,
> > + pub(crate) memattr: u64,
>
> Don't know if we need to document those fields.
v2 will add docs.
>
> > +}
> > +
> > +/// Any resource/information that will be used by the AddressSpaceManager
> > +/// to make a VM active is present in VmAsData.
> > +///
> > +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> > +/// the slot to make sure the page table and the underlying resources
> > +/// (pages) used by the AS slot won't go away while the MMU points to
> > +/// those.
> > +pub(crate) struct VmAsData {
> > + /// Tracks this VM's binding to a hardware address space slot.
> > + as_seat: LockedBy<Seat, AsSlotManager>,
> > + /// Hardware configuration for this address space.
> > + as_config: AddressSpaceConfig,
> > + /// Page table (managed by devres).
> > + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> > +}
> > +
> > +impl VmAsData {
>
> Missing doc.
>
> > + pub(crate) fn new(
> > + mmu: &Mmu,
> > + as_config: AddressSpaceConfig,
> > + page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> > + ) -> VmAsData {
> > + Self {
> > + as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
> > + as_config,
> > + page_table,
> > + }
> > + }
> > +}
> > +
> > +/// Manages GPU hardware address spaces via MMIO register operations.
> > +pub(crate) struct AddressSpaceManager {
>
> Missing docs on the first two fields.
>
> > + pdev: ARef<platform::Device>,
> > + iomem: Arc<Devres<IoMem>>,
> > + /// Bitmask of available address space slots from GPU_AS_PRESENT
> > register
> > + as_present: u32,
> > +}
> > +
> > +impl SlotOperations for AddressSpaceManager {
> > + type SlotData = Arc<VmAsData>;
> > +
> > + fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) ->
> > Result {
> > + self.as_enable(slot_idx, &slot_data.as_config)
> > + }
> > +
> > + fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) ->
> > Result {
> > + if self.iomem.try_access().is_some() {
> > + let _ = self.as_flush(slot_idx);
> > + let _ = self.as_disable(slot_idx);
>
> I don't think we should ignore the errors returned by
> as_flush/disable(). Ultimately, what we should do is trigger a GPU
> reset and leave the slot 'busy' until we're sure no one will try
> to use it (the "stop everything" happening in the reset path). Once this
> is done, we can forcibly evict the VM (basically remove the VM from the
> slot without doing any HW transaction) and let the reset do its job.
>
> Since we're not yet at a point where we have a functional reset flow,
> I'd recommend failing the eviction if as_flush/disable() returns an
> error (shouldn't happen with just the FW-boot logic anyway). This means
> we might leave active slots behind and possibly leak resources when that
> happens, but that's better than the UAF we'd create if the AS still
> points to a page table that's been freed.
Ack v2 will propagate these errors.
>
> > + }
> > + Ok(())
> > + }
> > +}
> > +
> > +impl AddressSpaceManager {
>
> Missing docs.
>
> > + pub(super) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + as_present: u32,
> > + ) -> Result<AddressSpaceManager> {
> > + Ok(Self {
> > + pdev: pdev.into(),
> > + iomem: iomem.into(),
> > + as_present,
> > + })
> > + }
> > +
> > + fn dev(&self) -> &Device<Bound> {
> > + // SAFETY: pdev is a bound device.
> > + unsafe { self.pdev.as_ref().as_bound() }
> > + }
> > +
> > + fn validate_as_slot(&self, as_nr: usize) -> Result {
> > + if as_nr >= MAX_AS_REGISTERS {
> > + pr_err!(
> > + "AS slot {} out of valid range (max {})\n",
> > + as_nr,
> > + MAX_AS_REGISTERS
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + if (self.as_present & (1 << as_nr)) == 0 {
> > + pr_err!(
> > + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> > + as_nr,
> > + self.as_present
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_wait_ready(&self, as_nr: usize) -> Result {
> > + let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
> > + let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE ==
> > 0 };
> > + let _ =
> > + io::poll::read_poll_timeout(op, cond, Delta::from_millis(0),
> > Delta::from_millis(10))?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_wait_ready(as_nr)?;
> > + as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_send_cmd(as_nr, cmd)?;
> > + self.as_wait_ready(as_nr)?;
> > + Ok(())
> > + }
> > +
> > + fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig)
> > -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + let transtab = as_config.transtab;
> > + let transcfg = as_config.transcfg;
> > + let memattr = as_config.memattr;
> > +
> > + let transtab_lo = (transtab & 0xffffffff) as u32;
> > + let transtab_hi = (transtab >> 32) as u32;
> > +
> > + let transcfg_lo = (transcfg & 0xffffffff) as u32;
> > + let transcfg_hi = (transcfg >> 32) as u32;
> > +
> > + let memattr_lo = (memattr & 0xffffffff) as u32;
> > + let memattr_hi = (memattr >> 32) as u32;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_disable(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // Flush AS before disabling
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem,
> > AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) ->
> > Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // The locked region is a naturally aligned power of 2 block
> > encoded as
> > + // log2 minus(1).
> > + //
> > + // Calculate the desired start/end and look for the highest bit
> > which
> > + // differs. The smallest naturally aligned block must include this
> > bit
> > + // change, the desired region starts with this bit (and subsequent
> > bits)
> > + // zeroed and ends with the bit (and subsequent bits) set to one.
> > + let region_width = core::cmp::max(
> > + 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
> > + AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
> > + ) - 1;
> > +
> > + // Mask off the low bits of region.start, which would be ignored
> > by the
> > + // hardware anyways.
> > + let region_start =
> > + region.start &
> > genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
> > +
> > + let region = (u64::from(region_width)) | region_start;
> > +
> > + let region_lo = (region & 0xffffffff) as u32;
> > + let region_hi = (region >> 32) as u32;
> > +
> > + // Lock the region that needs to be updated.
> > + let dev = self.dev();
> > + as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
> > + as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
> > +
> > + self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
> > + }
> > +
> > + fn as_end_update(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +
> > + fn as_flush(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +}
> > +
> > +impl AsSlotManager {
> > + /// Locks a region for page table updates if the VM has an active slot.
> > + pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region:
> > &Range<u64>) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_start_update(as_nr, region)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page table updates for a VM if it has an active slot.
> > + pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_end_update(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_flush(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) ->
> > Result {
> > + self.activate(&vm.as_seat, vm.into())
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
> > + self.evict(&vm.as_seat)
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> > index f54b997355e0..ae435c7e80b1 100644
> > --- a/drivers/gpu/drm/tyr/tyr.rs
> > +++ b/drivers/gpu/drm/tyr/tyr.rs
> > @@ -11,6 +11,7 @@
> > mod file;
> > mod gem;
> > mod gpu;
> > +mod mmu;
> > mod regs;
> > mod slot;
> >
>