[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Question regarding Locking in the MMIO Handling Framework on Arm


  • To: Julien Grall <julien@xxxxxxx>, Xen-devel <xen-devel@xxxxxxxxxxxxxxxxxxxx>
  • From: Oleksii Kurochko <oleksii.kurochko@xxxxxxxxx>
  • Date: Thu, 9 Jul 2026 16:27:05 +0200
  • Authentication-results: eu.smtp.expurgate.cloud; dkim=pass header.s=20251104 header.d=gmail.com header.i="@gmail.com" header.h="Content-Transfer-Encoding:Content-Type:In-Reply-To:From:Content-Language:References:Cc:To:Subject:User-Agent:MIME-Version:Date:Message-ID"
  • Cc: Stefano Stabellini <sstabellini@xxxxxxxxxx>, "Orzel, Michal" <Michal.Orzel@xxxxxxx>, Bertrand Marquis <bertrand.marquis@xxxxxxx>
  • Delivery-date: Thu, 09 Jul 2026 14:27:19 +0000
  • List-id: Xen developer discussion <xen-devel.lists.xenproject.org>

Hello Jullien,

On 7/9/26 2:09 PM, Julien Grall wrote:
Hi Oleksii,

Thanks for the report.

On 09/07/2026 11:49, Oleksii Kurochko wrote:
I have a question regarding the locking in the MMIO handling framework on Arm.

Is it sufficient to have read_lock() only in find_mmio_handler()? If
register_mmio_handler() is executed in parallel with find_mmio_handler() (which I assume was the reason for introducing the rwlock), aren't we still exposed to a race condition?

When the read-write lock was introduced, we didn't sort the entries. So it was fine at the time. This bug was introduced by commit 8047e090f4 ("xen/arm: io: Use binary search for mmio handler lookup").

With this change, then we...


find_mmio_handler() returns a pointer to a handler, but the object it points to could be changed by a subsequent call to register_mmio_handler(). If register_mmio_handler() runs between find_mmio_handler() and handle_{write,read}(), we could end up operating on a different handler than the one that was originally found.

In other words, shouldn't we acquire the read_lock() in try_handle_mmio() and keep it held for the entire duration of try_handle_mmio()?

... either need to keep the lock for longer

I assume that register_mmio_handler() won't be called very often
after domain creation so it looks to me that this fix is simpler then ...

 or rework the code to
allocate the handler structure.

... this. IIUC, you suggestion is to do something like:

diff --git a/xen/arch/arm/io.c b/xen/arch/arm/io.c
index 9707cadcf80e..a587135a6907 100644
--- a/xen/arch/arm/io.c
+++ b/xen/arch/arm/io.c
@@ -238,24 +238,24 @@ void register_mmio_handler(struct domain *d,
                            paddr_t addr, paddr_t size, void *priv)
 {
     struct vmmio *vmmio = &d->arch.vmmio;
-    struct mmio_handler *handler;
+    struct mmio_handler *handler = xzalloc(struct mmio_handler);

-    BUG_ON(vmmio->num_entries >= vmmio->max_num_entries);
-
-    write_lock(&vmmio->lock);
-
-    handler = &vmmio->handlers[vmmio->num_entries];
+    BUG_ON(!handler);

-    handler->ops = ops;
+    /* Fully initialize *before* publishing. */
+    handler->ops  = ops;
     handler->addr = addr;
     handler->size = size;
     handler->priv = priv;

-    vmmio->num_entries++;
+    write_lock(&vmmio->lock);
+
+    BUG_ON(vmmio->num_entries >= vmmio->max_num_entries);
+
+    vmmio->handlers[vmmio->num_entries++] = handler;

-    /* Sort mmio handlers in ascending order based on base address */
-    sort(vmmio->handlers, vmmio->num_entries, sizeof(struct mmio_handler),
-         cmp_mmio_handler, swap_mmio_handler);
+    sort(vmmio->handlers, vmmio->num_entries, sizeof(*vmmio->handlers),
+         cmp_mmio_handler, swap_mmio_handler /* now swaps pointers */);

     write_unlock(&vmmio->lock);
 }

(of course, whith updating of handler field to "struct mmio_handler **handlers;").



Also, I thought about just use local variable for handler (probably that what you meant):

diff --git a/xen/arch/arm/io.c b/xen/arch/arm/io.c
index 9707cadcf80e..393018e02a83 100644
--- a/xen/arch/arm/io.c
+++ b/xen/arch/arm/io.c
@@ -107,19 +107,33 @@ static void swap_mmio_handler(void *_a, void *_b)
     SWAP(*a, *b);
 }

-static const struct mmio_handler *find_mmio_handler(struct domain *d,
-                                                    paddr_t gpa)
+static bool find_mmio_handler(struct domain *d, paddr_t gpa,
+                              struct mmio_handler *out)
 {
     struct vmmio *vmmio = &d->arch.vmmio;
     struct mmio_handler key = {.addr = gpa};
     const struct mmio_handler *handler;
+    bool found = false;

     read_lock(&vmmio->lock);
+
     handler = bsearch(&key, vmmio->handlers, vmmio->num_entries,
                       sizeof(*handler), cmp_mmio_handler);
+    if ( handler )
+    {
+        /*
+         * Copy the handler while holding the lock: a concurrent
+         * register_mmio_handler() re-sorts the array in place, so the
+         * entry pointed to by the bsearch() result may change once the
+         * lock is released.
+         */
+        *out = *handler;
+        found = true;
+    }
+
     read_unlock(&vmmio->lock);

-    return handler;
+    return found;
 }

 void try_decode_instruction(const struct cpu_user_regs *regs,
@@ -187,7 +201,7 @@ enum io_state try_handle_mmio(struct cpu_user_regs *regs,
                               mmio_info_t *info)
 {
     struct vcpu *v = current;
-    const struct mmio_handler *handler = NULL;
+    struct mmio_handler handler;
     int rc;

     ASSERT(info->dabt.ec == HSR_EC_DATA_ABORT_LOWER_EL);
@@ -198,8 +212,7 @@ enum io_state try_handle_mmio(struct cpu_user_regs *regs,
         return IO_ABORT;
     }

-    handler = find_mmio_handler(v->domain, info->gpa);
-    if ( !handler )
+    if ( !find_mmio_handler(v->domain, info->gpa, &handler) )
     {
         bool trap_unmapped = v->domain->options &
XEN_DOMCTL_CDF_trap_unmapped_accesses; @@ -209,7 +222,7 @@ enum io_state try_handle_mmio(struct cpu_user_regs *regs,
         else if ( rc == IO_UNHANDLED && !trap_unmapped )
         {
             /* Fallback to the unmapped handler. */
-            handler = &unmapped_handler;
+            handler = unmapped_handler;
         } else {
             return rc;
         }
@@ -228,9 +241,9 @@ enum io_state try_handle_mmio(struct cpu_user_regs *regs,
      * instruction on the emulated MMIO region.
      */
     if ( info->dabt.write )
-        return handle_write(handler, v, info);
+        return handle_write(&handler, v, info);
     else
-        return handle_read(handler, v, info);
+        return handle_read(&handler, v, info);
 }


What I am also thinking about if read-write lock is enough here. For example, if we will have hypothetical MMIO unregistered (why we don't have it now? Will we ever need it?) then we could be in a trouble:

CPU0 (vCPU trapping MMIO)               CPU1 (hypothetical unregister)

try_handle_mmio()
  find_mmio_handler()
    read_lock()
    bsearch() -> finds handler H
    read_unlock()          <-- protection ends here
                                          unregister_mmio_handler()
                                           write_lock()
                                           remove H's pointer from array
                                           write_unlock()
                                         xfree(H)   <-- H's memory freed
  handle_write(H, ...)
    H->ops->write(...)     <-- use-after-free: reads freed memory,
                               calls through a dangling ops pointer

If we should care about that hypothetical unregister case then it is better just to go with solution of "to keep the lock for longer" basically while handler is found and used in try_handle_mmio().

~ Oleksii



 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.