Repository: DynamoRIO/dynamorio (2026-06-16T20:34:47Z)

Issue:
CRASH x0 register clobber in aarch64's cleanup_and_terminate
On AArch64, [cleanup_and_terminate()](https://github.com/DynamoRIO/dynamorio/blob/0adaf8ab6bc51e1b23d3924a9e480858f3c0d45e/core/arch/aarch64/aarch64.asm#L295)'s `cat_spin` loop keeps the address of `initstack_mutex` in x0 across the call to `atomic_swap`.  `atomic_swap` returns its result in x0; on a contended first iteration the retry calls `atomic_swap(0x1, 1)` (overwritten value passed as an addr) and faults.

Here is the buggy code:
https://github.com/DynamoRIO/dynamorio/blob/0adaf8ab6bc51e1b23d3924a9e480858f3c0d45e/core/arch/aarch64/aarch64.asm#L333-L340

Before the `cat_spin` loop, line 335 assigns x0 has the address for `initstack_mutex`.
Every iteration, line 337 `atomic_swap()`'s return value overwrites x0. If the compare (`cbz`) fails, next iterations will have the 1 in x0 but the atomic_swap expects it to be `initstack_mutex`'s address.


Discussion:
Repro:
```c
/* Build: gcc -O2 -pthread repro.c -o repro
 * Native execution (no DR) completes cleanly.
 */
#include <pthread.h>

#define N 16
static pthread_barrier_t bar;

static void *worker(void *arg)
{
    pthread_barrier_wait(&bar); /* exit in lockstep */
    return arg;
}

int main(void)
{
    for (int r = 0; r < 1000; r++) {
        pthread_t th[N];
        pthread_barrier_init(&bar, NULL, N);
        for (int i = 0; i < N; i++)
            pthread_create(&th[i], NULL, worker, NULL);
        for (int i = 0; i < N; i++)
            pthread_join(th[i], NULL);
        pthread_barrier_destroy(&bar);
    }
    return 0;
}
```

```shell
# Fresh clone from master `git clone 'https://github.com/dynamorio/dynamorio' dynamorio-upstream-clean --recursive`
$ cmake -S . -B build \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_DOCS=OFF -DBUILD_TESTS=OFF -DBUILD_SAMPLES=OFF
$ cmake --build build -j 24
$ ./build/bin64/drrun -- /path/to/repro
[1]    1590956 segmentation fault (core dumped)  ./build/bin64/drrun -- /path/to/repro
```

System info:
```console
$ lscpu | head -n3
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
$ uname -r
6.16.1-0
```
I think the simplest fix would be to jump to `cat_no_thread` and not to `cat_spin`. And drop the `cat_spin` label.
Thank you for the detailed bug report.



Patch:
diff --git a/core/arch/aarch64/aarch64.asm b/core/arch/aarch64/aarch64.asm
index e4615fe318..9bff19948d 100644
--- a/core/arch/aarch64/aarch64.asm
+++ b/core/arch/aarch64/aarch64.asm
@@ -1,5 +1,5 @@
 /* **********************************************************
- * Copyright (c) 2019-2025 Google, Inc. All rights reserved.
+ * Copyright (c) 2019-2026 Google, Inc. All rights reserved.
  * Copyright (c) 2016-2025 ARM Limited. All rights reserved.
  * **********************************************************/
 
@@ -294,7 +294,10 @@ GLOBAL_LABEL(dynamorio_app_take_over:)
  */
         DECLARE_FUNC(cleanup_and_terminate)
 GLOBAL_LABEL(cleanup_and_terminate:)
-        /* move argument registers to callee saved registers */
+        /* Move argument registers to callee saved registers.
+         * This function does not return so we do not preserve the caller's values
+         * in these registers.
+         */
         mov      x19, x0  /* dcontext ptr size */
         mov      x20, x1  /* sysnum */
         mov      x21, x2  /* sys_arg1 */
@@ -306,6 +309,7 @@ GLOBAL_LABEL(cleanup_and_terminate:)
         mov      x26, x5  /* sys_arg3 */
         mov      x27, x6  /* sys_arg4 */
 #endif
+                          /* x28 used below for initstack_mutex. */
 
         /* inc exiting_thread_count to avoid being killed once off all_threads list */
         AARCH64_ADRP_GOT(GLOBAL_REF(exiting_thread_count), x0)
@@ -331,10 +335,13 @@ cat_done_saving_dstack:
 cat_thread_only:
         CALLC0(GLOBAL_REF(dynamo_thread_exit))
 cat_no_thread:
-        /* switch to d_r_initstack for cleanup of dstack */
-        AARCH64_ADRP_GOT(GLOBAL_REF(initstack_mutex), x0)
+        /* Switch to d_r_initstack for cleanup of dstack.
+         * Store the address in callee-saved x28 to avoid clobbering by
+         * atomic_swap if we loop on contention.
+         */
+        AARCH64_ADRP_GOT(GLOBAL_REF(initstack_mutex), x28)
 cat_spin:
-        CALLC2(GLOBAL_REF(atomic_swap), x0, #1)
+        CALLC2(GLOBAL_REF(atomic_swap), x28, #1)
         cbz      w0, cat_have_lock
         yield
         b        cat_spin


Test patch:
diff --git a/suite/tests/CMakeLists.txt b/suite/tests/CMakeLists.txt
index 8e06d4d79f..dd084df28a 100644
--- a/suite/tests/CMakeLists.txt
+++ b/suite/tests/CMakeLists.txt
@@ -6206,6 +6206,13 @@ if (UNIX)
   endif()
   tobuild(pthreads.pthreads pthreads/pthreads.c)
   tobuild(pthreads.pthreads_exit pthreads/pthreads_exit.c)
+  if (NOT MACOS) # Mac doesn't support pthread barriers.
+    tobuild(pthreads.pthread_exit_barrier pthreads/pthread_exit_barrier.c)
+    if (DEBUG)
+      append_property_string(TARGET pthreads.pthread_exit_barrier
+        COMPILE_FLAGS "-DREDUCED_ITERS")
+    endif ()
+  endif ()
   tobuild(pthreads.ptsig pthreads/ptsig.c)
   if (NOT ANDROID) # XXX i#1874: failing on Android
     # XXX i#951: pthreads_fork reports leaks on occasion so we mark it FLAKY
diff --git a/suite/tests/pthreads/pthread_exit_barrier.c b/suite/tests/pthreads/pthread_exit_barrier.c
new file mode 100644
index 0000000000..78f598d96d
--- /dev/null
+++ b/suite/tests/pthreads/pthread_exit_barrier.c
@@ -0,0 +1,75 @@
+/* **********************************************************
+ * Copyright (c) 2026 Google, Inc.  All rights reserved.
+ * **********************************************************/
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of VMware, Inc. nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+/* Like pthreads_exit.c, this stresses exiting, but uses a barrier to ensure
+ * contention on the initstack_mutex in cleanup_and_terminate.
+ * This is based on the reproducer at
+ * https://github.com/DynamoRIO/dynamorio/issues/7939#issuecomment-4686861688.
+ */
+
+#include <pthread.h>
+#include <stdio.h>
+
+/* We use a barrier to exit in lockstep. */
+static pthread_barrier_t barrier;
+
+static void *
+thread_func(void *arg)
+{
+    pthread_barrier_wait(&barrier);
+    return arg;
+}
+
+int
+main(void)
+{
+#define NUM_THREADS 16
+#ifdef REDUCED_ITERS
+    /* The test is too slow in debug build for 1000 iterations. Debug build doesn't
+     * reproduce the i#7939 crash in any case.
+     */
+    const int NUM_RUNS = 50;
+#else
+    const int NUM_RUNS = 1000;
+#endif
+    for (int run = 0; run < NUM_RUNS; run++) {
+        pthread_t thread[NUM_THREADS];
+        pthread_barrier_init(&barrier, NULL, NUM_THREADS);
+        for (int i = 0; i < NUM_THREADS; i++)
+            pthread_create(&thread[i], NULL, thread_func, NULL);
+        for (int i = 0; i < NUM_THREADS; i++)
+            pthread_join(thread[i], NULL);
+        pthread_barrier_destroy(&barrier);
+    }
+    fprintf(stderr, "all done\n");
+    return 0;
+}
diff --git a/suite/tests/pthreads/pthread_exit_barrier.expect b/suite/tests/pthreads/pthread_exit_barrier.expect
new file mode 100644
index 0000000000..de63914305
--- /dev/null
+++ b/suite/tests/pthreads/pthread_exit_barrier.expect
@@ -0,0 +1,1 @@
+all done