{"id":88178,"date":"2025-05-23T03:00:00","date_gmt":"2025-05-23T10:00:00","guid":{"rendered":"https:\/\/github.blog\/?p=88178"},"modified":"2025-05-22T16:39:22","modified_gmt":"2025-05-22T23:39:22","slug":"bypassing-mte-with-cve-2025-0072","status":"publish","type":"post","link":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/","title":{"rendered":"Bypassing MTE with CVE-2025-0072"},"content":{"rendered":"<!DOCTYPE html PUBLIC \"-\/\/W3C\/\/DTD HTML 4.0 Transitional\/\/EN\" \"http:\/\/www.w3.org\/TR\/REC-html40\/loose.dtd\">\n<html><body><p class=\"wp-block-paragraph\"><a href=\"https:\/\/community.arm.com\/arm-community-blogs\/b\/architectures-and-processors-blog\/posts\/enhancing-memory-safety\">Memory Tagging Extension (MTE)<\/a> is an advanced memory safety feature that is intended to make memory corruption vulnerabilities almost impossible to exploit. But no mitigation is ever completely airtight&mdash;especially in kernel code that manipulates memory at a low level.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Last year, I <a href=\"https:\/\/github.blog\/security\/vulnerability-research\/gaining-kernel-code-execution-on-an-mte-enabled-pixel-8\/\">wrote<\/a> about CVE-2023-6241, a vulnerability in ARM&rsquo;s Mali GPU driver, which enabled an untrusted Android app to bypass MTE and gain arbitrary kernel code execution. In this post, I&rsquo;ll walk through CVE-2025-0072: a newly patched vulnerability that I also found in ARM&rsquo;s Mali GPU driver. Like the previous one, it enables a malicious Android app to bypass MTE and gain arbitrary kernel code execution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I reported the issue to Arm on December 12, 2024. It was fixed in Mali driver version <a href=\"https:\/\/developer.arm.com\/documentation\/110465\/1-0\/?lang=en\">r54p0<\/a>, released publicly on May 2, 2025, and included in Android&rsquo;s <a href=\"https:\/\/source.android.com\/docs\/security\/bulletin\/2025-05-01\">May 2025 security update<\/a>. The vulnerability affects devices with newer Arm Mali GPUs that use the <a href=\"https:\/\/community.arm.com\/arm-community-blogs\/b\/graphics-gaming-and-vr-blog\/posts\/new-suite-of-arm-mali-gpus\">Command Stream Frontend (CSF<\/a>) architecture, such as Google&rsquo;s Pixel 7, 8, and 9 series. I developed and tested the exploit on a Pixel 8 with <a href=\"https:\/\/outflux.net\/blog\/archives\/2023\/10\/26\/enable-mte-on-pixel-8\/\">kernel MTE enabled,<\/a> and I believe it should work on the 7 and 9 as well with minor modifications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What follows is a deep dive into how CSF queues work, the steps I used to exploit this bug, and how it ultimately bypasses MTE protections to achieve kernel code execution.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-csf-queues-work-and-how-they-become-dangerous\">How CSF queues work&mdash;and how they become dangerous<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Arm Mali GPUs with the CSF feature communicate with userland applications through command queues, implemented in the driver as <code>kbase_queue<\/code> objects. The queues are created by using the <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#592\"><code>KBASE_IOCTL_CS_QUEUE_REGISTER<\/code><\/a> <code>ioctl<\/code>. To use the <code>kbase_queue<\/code> that is created, it first has to be bound to a <code>kbase_queue_group<\/code>, which is created with the <code><a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#1290\">KBASE_IOCTL_CS_QUEUE_GROUP_CREATE<\/a> ioctl<\/code>. A <code>kbase_queue<\/code> can be bound to a <code>kbase_queue_group<\/code> with the <code><a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#713\">KBASE_IOCTL_CS_QUEUE_BIND<\/a> ioctl<\/code>. When binding a <code>kbase_queue<\/code> to a <code>kbase_queue_group<\/code>, a handle is created from <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#139\"><code>get_user_pages_mmap_handle<\/code><\/a> and returned to the user application.<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>int kbase_csf_queue_bind(struct kbase_context *kctx, union kbase_ioctl_cs_queue_bind *bind)\n{\n            ...\n\tgroup = find_queue_group(kctx, bind-&gt;in.group_handle);\n\tqueue = find_queue(kctx, bind-&gt;in.buffer_gpu_addr);\n            &hellip;\n\tret = get_user_pages_mmap_handle(kctx, queue);\n\tif (ret)\n\t\tgoto out;\n\tbind-&gt;out.mmap_handle = queue-&gt;handle;\n\tgroup-&gt;bound_queues[bind-&gt;in.csi_index] = queue;\n\tqueue-&gt;group = group;\n\tqueue-&gt;group_priority = group-&gt;priority;\n\tqueue-&gt;csi_index = (s8)bind-&gt;in.csi_index;\n\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_BIND_IN_PROGRESS;\n\nout:\n\trt_mutex_unlock(&amp;kctx-&gt;csf.lock);\n\n\treturn ret;\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"int kbase_csf_queue_bind(struct kbase_context *kctx, union kbase_ioctl_cs_queue_bind *bind)\n{\n            ...\n\tgroup = find_queue_group(kctx, bind-&gt;in.group_handle);\n\tqueue = find_queue(kctx, bind-&gt;in.buffer_gpu_addr);\n            &hellip;\n\tret = get_user_pages_mmap_handle(kctx, queue);\n\tif (ret)\n\t\tgoto out;\n\tbind-&gt;out.mmap_handle = queue-&gt;handle;\n\tgroup-&gt;bound_queues[bind-&gt;in.csi_index] = queue;\n\tqueue-&gt;group = group;\n\tqueue-&gt;group_priority = group-&gt;priority;\n\tqueue-&gt;csi_index = (s8)bind-&gt;in.csi_index;\n\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_BIND_IN_PROGRESS;\n\nout:\n\trt_mutex_unlock(&amp;kctx-&gt;csf.lock);\n\n\treturn ret;\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">In addition, mutual references are stored between the <code>kbase_queue_group<\/code> and the <code>queue<\/code>. Note that when the call finishes, <code>queue-&gt;bind_state<\/code> is set to <code>KBASE_CSF_QUEUE_BIND_IN_PROGRESS<\/code>, indicating that the binding is not completed. To complete the binding, the user application must call <code>mmap<\/code> with the handle returned from the <code>ioctl<\/code> as the file offset. This <code>mmap<\/code> call is handled by <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_linux.c#3614\"><code>kbase_csf_cpu_mmap_user_io_pages<\/code><\/a>, which allocates GPU memory via <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_linux.c#3648\"><code>kbase_csf_alloc_command_stream_user_pages<\/code><\/a> and maps it to user space.<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>int kbase_csf_alloc_command_stream_user_pages(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n\tstruct kbase_device *kbdev = kctx-&gt;kbdev;\n\tint ret;\n\n\tlockdep_assert_held(&amp;kctx-&gt;csf.lock);\n\n\tret = kbase_mem_pool_alloc_pages(&amp;kctx-&gt;mem_pools.small[KBASE_MEM_GROUP_CSF_IO],\n\t\t\t\t\t KBASEP_NUM_CS_USER_IO_PAGES, queue-&gt;phys, false,                 \/\/&lt;------ 1.\n\t\t\t\t\t kctx-&gt;task);\n  ...\n\tret = kernel_map_user_io_pages(kctx, queue);\n  ...\n\tget_queue(queue);\n\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_BOUND;\n\tmutex_unlock(&amp;kbdev-&gt;csf.reg_lock);\n\n\treturn 0;\n  ...\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"int kbase_csf_alloc_command_stream_user_pages(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n\tstruct kbase_device *kbdev = kctx-&gt;kbdev;\n\tint ret;\n\n\tlockdep_assert_held(&amp;kctx-&gt;csf.lock);\n\n\tret = kbase_mem_pool_alloc_pages(&amp;kctx-&gt;mem_pools.small[KBASE_MEM_GROUP_CSF_IO],\n\t\t\t\t\t KBASEP_NUM_CS_USER_IO_PAGES, queue-&gt;phys, false,                 \/\/&lt;------ 1.\n\t\t\t\t\t kctx-&gt;task);\n  ...\n\tret = kernel_map_user_io_pages(kctx, queue);\n  ...\n\tget_queue(queue);\n\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_BOUND;\n\tmutex_unlock(&amp;kbdev-&gt;csf.reg_lock);\n\n\treturn 0;\n  ...\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">In 1. in the above snippet, <code>kbase_mem_pool_alloc_pages<\/code> is called to allocate memory pages from the GPU memory pool, whose addresses are then stored in the <code>queue-&gt;phys<\/code> field. These pages are then mapped to user space and the <code>bind_state<\/code> of the queue is set to <code>KBASE_CSF_QUEUE_BOUND<\/code>. These pages are only freed when the mmapped area is unmapped from the user space. In that case, <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#274\"><code>kbase_csf_free_command_stream_user_pages<\/code><\/a> is called to free the pages via <code>kbase_mem_pool_free_pages<\/code>.<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>void kbase_csf_free_command_stream_user_pages(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n\tkernel_unmap_user_io_pages(kctx, queue);\n\n\tkbase_mem_pool_free_pages(&amp;kctx-&gt;mem_pools.small[KBASE_MEM_GROUP_CSF_IO],\n\t\t\t\t  KBASEP_NUM_CS_USER_IO_PAGES, queue-&gt;phys, true, false);\n  ...\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"void kbase_csf_free_command_stream_user_pages(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n\tkernel_unmap_user_io_pages(kctx, queue);\n\n\tkbase_mem_pool_free_pages(&amp;kctx-&gt;mem_pools.small[KBASE_MEM_GROUP_CSF_IO],\n\t\t\t\t  KBASEP_NUM_CS_USER_IO_PAGES, queue-&gt;phys, true, false);\n  ...\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">This frees the pages stored in <code>queue-&gt;phys<\/code>, and because this only happens when the pages are unmapped from user space, it prevents the pages from being accessed after they are freed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-an-exploit-idea\">An exploit idea<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The interesting part begins when we ask: what happens if we can modify <code>queue-&gt;phys<\/code> after mapping them into user space. For example, if I can trigger <code>kbase_csf_alloc_command_user_pages<\/code> again to overwrite new pages to <code>queue-&gt;phys<\/code>, and map them to user space and then unmap the previously mapped region, <code>kbase_csf_free_command_stream_user_pages<\/code> will be called to free the pages in <code>queue-&gt;phys<\/code>. However, because <code>queue-&gt;phys<\/code> is now overwritten by the newly allocated pages, I ended up in a situation where I free the new pages while unmapping an old region:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" decoding=\"async\" width=\"960\" height=\"720\" loading=\"lazy\" src=\"https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing1.png?resize=960%2C720\" alt=\"A diagram demonstrating how to free the new pages while unmapping an old region.\" class=\"wp-image-88188\" srcset=\"https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing1.png?w=960 960w, https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing1.png?w=300 300w, https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing1.png?w=768 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In the above figure, the right columns are mappings in the user space, green rectangles are mapped, while gray ones are unmapped. The left column are backing pages stored in <code>queue-&gt;phys<\/code>. The new <code>queue-&gt;phys<\/code> are pages that are currently stored in <code>queue-&gt;phys<\/code>, while old <code>queue-&gt;phys<\/code> are pages that are stored previously but are replaced by the new ones. Green indicates that the pages are alive, while red indicates that they are freed. After overwriting <code>queue-&gt;phys<\/code> and unmapping the old region, the new <code>queue-&gt;phys<\/code> are freed instead, while still mapped to the new user region. This means that user space will have access to the freed new <code>queue-&gt;phys<\/code> pages. This then gives me a page use-after-free vulnerability.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-vulnerability\">The vulnerability<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">So let&rsquo;s take a look at how to achieve this situation. The first obvious thing to try is to see if I can bind a <code>kbase_queue<\/code> multiple times using the<code> KBASE_IOCTL_CS_QUEUE_BIND ioctl<\/code>. This, however, is not possible because the <code>queue-&gt;group<\/code> field is checked before binding:<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>int kbase_csf_queue_bind(struct kbase_context *kctx, union kbase_ioctl_cs_queue_bind *bind)\n{\n  ...\n\tif (queue-&gt;group || group-&gt;bound_queues[bind-&gt;in.csi_index])\n\t\tgoto out;\n  ...\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"int kbase_csf_queue_bind(struct kbase_context *kctx, union kbase_ioctl_cs_queue_bind *bind)\n{\n  ...\n\tif (queue-&gt;group || group-&gt;bound_queues[bind-&gt;in.csi_index])\n\t\tgoto out;\n  ...\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">After a <code>kbase_queue<\/code> is bound, its <code>queue-&gt;group<\/code> is set to the <code>kbase_queue_group<\/code> that it binds to, which prevents the <code>kbase_queue<\/code> from binding again. Moreover, once a <code>kbase_queue<\/code> is bound, it cannot be unbound via any <code>ioctl<\/code>. It can be terminated with <code>KBASE_IOCTL_CS_QUEUE_TERMINATE<\/code>, but that will also delete the <code>kbase_queue<\/code>. So if rebinding from the queue is not possible, what about trying to unbind from a <code>kbase_queue_group<\/code>? For example, what happens if a <code>kbase_queue_group<\/code> gets terminated with the <code><a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#1505\">KBASE_IOCTL_CS_QUEUE_GROUP_TERMINATE<\/a> ioctl<\/code>? When a <code>kbase_queue_group<\/code> terminates, as part of the clean up process, it calls <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/csf\/mali_kbase_csf.c#1400\"><code>kbase_csf_term_descheduled_queue_group<\/code><\/a> to unbind queues that it bound to:<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>void kbase_csf_term_descheduled_queue_group(struct kbase_queue_group *group)\n{\n  ...\n\tfor (i = 0; i &lt; max_streams; i++) {\n\t\tstruct kbase_queue *queue = group-&gt;bound_queues[i];\n\n\t\t\/* The group is already being evicted from the scheduler *\/\n\t\tif (queue)\n\t\t\tunbind_stopped_queue(kctx, queue);\n\t}\n  ...\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"void kbase_csf_term_descheduled_queue_group(struct kbase_queue_group *group)\n{\n  ...\n\tfor (i = 0; i &lt; max_streams; i++) {\n\t\tstruct kbase_queue *queue = group-&gt;bound_queues[i];\n\n\t\t\/* The group is already being evicted from the scheduler *\/\n\t\tif (queue)\n\t\t\tunbind_stopped_queue(kctx, queue);\n\t}\n  ...\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">This then resets the <code>queue-&gt;group<\/code> field of the <code>kbase_queue<\/code> that gets unbound:<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code\"><code>static void unbind_stopped_queue(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n  ...\n\tif (queue-&gt;bind_state != KBASE_CSF_QUEUE_UNBOUND) {\n    ...\n\t\tqueue-&gt;group-&gt;bound_queues[queue-&gt;csi_index] = NULL;\n\t\tqueue-&gt;group = NULL;\n    ...\n\t\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_UNBOUND;\n\t}\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"static void unbind_stopped_queue(struct kbase_context *kctx, struct kbase_queue *queue)\n{\n  ...\n\tif (queue-&gt;bind_state != KBASE_CSF_QUEUE_UNBOUND) {\n    ...\n\t\tqueue-&gt;group-&gt;bound_queues[queue-&gt;csi_index] = NULL;\n\t\tqueue-&gt;group = NULL;\n    ...\n\t\tqueue-&gt;bind_state = KBASE_CSF_QUEUE_UNBOUND;\n\t}\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">In particular, this now allows the <code>kbase_queue<\/code> to bind to another <code>kbase_queue_group<\/code>. This means I can now create a page use-after-free with the following steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a <code>kbase_queue<\/code> and a <code>kbase_queue_group<\/code>, and then bind the <code>kbase_queue<\/code> to the <code>kbase_queue_group<\/code>.<\/li>\n\n\n\n<li>Create GPU memory pages for the user io pages in the <code>kbase_queue<\/code> and map them to user space using a <code>mmap<\/code> call. These pages are then stored in the <code>queue-&gt;phys<\/code> field of the <code>kbase_queue<\/code>.<\/li>\n\n\n\n<li>Terminate the <code>kbase_queue_group<\/code>, which also unbinds the <code>kbase_queue<\/code>.<\/li>\n\n\n\n<li>Create another <code>kbase_queue_group<\/code> and bind the <code>kbase_queue<\/code> to this new group.<\/li>\n\n\n\n<li>Create new GPU memory pages for the user io pages in this <code>kbase_queue<\/code> and map them to user space. These pages now overwrite the existing pages in <code>queue-&gt;phys<\/code>.<\/li>\n\n\n\n<li>Unmap the user space memory that was mapped in step 2. This then frees the pages in <code>queue-&gt;phys<\/code> and removes the user space mapping created in step 2. However, the pages that are freed are now the memory pages created and mapped in step 5, which are still mapped to user space.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This, in particular, means that the pages that are freed in step 6 of the above can still be accessed from the user application. By using a <a href=\"https:\/\/github.blog\/2022-07-27-corrupting-memory-without-memory-corruption\/#breaking-out-of-the-context\">technique<\/a> that I used previously, I can reuse these freed pages as <a href=\"https:\/\/www.kernel.org\/doc\/gorman\/html\/understand\/understand006.html\">page table global directories (PGD)<\/a> of the Mali GPU.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To recap, let&rsquo;s take a look at how the backing pages of a <code>kbase_va_region<\/code> are allocated. When allocating pages for the backing store of a <code>kbase_va_region<\/code>, the <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_pool.c#772\"><code>kbase_mem_pool_alloc_pages<\/code><\/a> function is used:<\/p>\n\n\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-cpp\"><code>int kbase_mem_pool_alloc_pages(struct kbase_mem_pool *pool, size_t nr_4k_pages,\n    struct tagged_addr *pages, bool partial_allowed)\n{\n    ...\n  \/* Get pages from this pool *\/\n  while (nr_from_pool--) {\n    p = kbase_mem_pool_remove_locked(pool);     \/\/&lt;------- 1.\n        ...\n  }\n    ...\n  if (i != nr_4k_pages &amp;&amp; pool-&gt;next_pool) {\n    \/* Allocate via next pool *\/\n    err = kbase_mem_pool_alloc_pages(pool-&gt;next_pool,      \/\/&lt;----- 2.\n        nr_4k_pages - i, pages + i, partial_allowed);\n        ...\n  } else {\n    \/* Get any remaining pages from kernel *\/\n    while (i != nr_4k_pages) {\n      p = kbase_mem_alloc_page(pool);     \/\/&lt;------- 3.\n            ...\n        }\n        ...\n  }\n    ...\n}<\/code><\/pre>\n<clipboard-copy aria-label=\"Copy\" class=\"code-copy-btn\" data-copy-feedback=\"Copied!\" value=\"int kbase_mem_pool_alloc_pages(struct kbase_mem_pool *pool, size_t nr_4k_pages,\n    struct tagged_addr *pages, bool partial_allowed)\n{\n    ...\n  \/* Get pages from this pool *\/\n  while (nr_from_pool--) {\n    p = kbase_mem_pool_remove_locked(pool);     \/\/&lt;------- 1.\n        ...\n  }\n    ...\n  if (i != nr_4k_pages &amp;&amp; pool-&gt;next_pool) {\n    \/* Allocate via next pool *\/\n    err = kbase_mem_pool_alloc_pages(pool-&gt;next_pool,      \/\/&lt;----- 2.\n        nr_4k_pages - i, pages + i, partial_allowed);\n        ...\n  } else {\n    \/* Get any remaining pages from kernel *\/\n    while (i != nr_4k_pages) {\n      p = kbase_mem_alloc_page(pool);     \/\/&lt;------- 3.\n            ...\n        }\n        ...\n  }\n    ...\n}\" tabindex=\"0\" role=\"button\"><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-copy js-clipboard-copy-icon\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"><\/path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"><\/path><\/svg><svg aria-hidden=\"true\" height=\"16\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" class=\"octicon octicon-check js-clipboard-check-icon\"><path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"><\/path><\/svg><\/clipboard-copy><\/div>\n\n\n<p class=\"wp-block-paragraph\">The input argument <code>kbase_mem_pool<\/code> is a memory pool managed by the kbase_context object associated with the driver file that is used to allocate the GPU memory. As the comments suggest, the allocation is actually done in tiers. First the pages will be allocated from the current <code>kbase_mem_pool<\/code> using <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_pool.c#241\"><code>kbase_mem_pool_remove_locked<\/code><\/a> (1 in the above). If there is not enough capacity in the current <code>kbase_mem_pool<\/code> to meet the request, then <code>pool-&gt;next_pool<\/code>, is used to allocate the pages (2 in the above). If even <code>pool-&gt;next_pool<\/code> does not have the capacity, then <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_pool.c#316\"><code>kbase_mem_alloc_page<\/code><\/a> is used to allocate pages directly from the kernel via the buddy allocator (the page allocator in the kernel).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When freeing a page, the same happens in the opposite direction: <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_pool.c#991\"><code>kbase_mem_pool_free_pages<\/code><\/a> first tries to return the pages to the <code>kbase_mem_pool<\/code> of the current <code>kbase_context<\/code>, if the memory pool is full, it&rsquo;ll try to return the remaining pages to <code>pool-&gt;next_pool<\/code>. If the next pool is also full, then the remaining pages are returned to the kernel by freeing them via the buddy allocator.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As noted in my post <a href=\"https:\/\/github.blog\/2022-07-27-corrupting-memory-without-memory-corruption\/#breaking-out-of-the-context\">&ldquo;Corrupting memory without memory corruption&rdquo;<\/a>, <code>pool-&gt;next_pool<\/code> is a memory pool managed by the Mali driver and shared by all the kbase_context. It is also used for allocating <a href=\"https:\/\/www.kernel.org\/doc\/gorman\/html\/understand\/understand006.html\">page table global directories (PGD)<\/a> used by GPU contexts. In particular, this means that by carefully arranging the memory pools, it is possible to cause a freed backing page in a <code>kbase_va_region<\/code> to be reused as a PGD of a GPU context. (<a href=\"https:\/\/github.blog\/2022-07-27-corrupting-memory-without-memory-corruption\/#breaking-out-of-the-context\">Read the details<\/a> of how to achieve this.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the freed page is reused as a PGD of a GPU context, the user space mapping can be used to rewrite the PGD from the GPU. This then allows any kernel memory, including kernel code, to be mapped to the GPU, which allows me to rewrite kernel code and hence execute arbitrary kernel code. It also allows me to read and write arbitrary kernel data, so I can easily rewrite credentials of my process to gain root, as well as to disable SELinux.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">See the <a href=\"https:\/\/github.com\/github\/securitylab\/tree\/main\/SecurityExploits\/Android\/Mali\/CVE-2025-0072\">exploit for Pixel 8<\/a> with some setup notes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-does-this-bypass-mte\">How does this bypass MTE?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before wrapping up, let&rsquo;s look at why this exploit manages to bypass Memory Tagging Extension (MTE)&mdash;despite protections that should have made this type of attack impossible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Memory Tagging Extension (MTE) is a security feature on newer Arm processors that uses hardware implementations to check for memory corruptions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Arm64 architecture uses 64 bit pointers to access memory, while most applications use a much smaller address space (for example, 39, 48, or 52 bits). The highest bits in a 64 bit pointer are actually unused. The main idea of memory tagging is to use these higher bits in an address to store a &ldquo;tag&rdquo; that can then be used to check against the other tag stored in the memory block associated with the address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When a linear overflow happens and a pointer is used to dereference an adjacent memory block, the tag on the pointer is likely to be different from the tag in the adjacent memory block. By checking these tags at dereference time, such discrepancy, and hence the corrupted dereference can be detected. For use-after-free type memory corruptions, as long as the tag in a memory block is cleared every time it is freed and a new tag reassigned when it is allocated, dereferencing an already freed and reclaimed object will also lead to a discrepancy between pointer tag and the tag in memory, which allows use-after-free to be detected.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" decoding=\"async\" loading=\"lazy\" height=\"795\" width=\"1024\" src=\"https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing2.png?resize=1024%2C795\" alt=\"A diagram demonstrating how, by checking the tags on the pointer and the adjacent memory blocks at dereference time, the corrupted dereference can be detected.\" class=\"wp-image-88189\" srcset=\"https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing2.png?w=1040 1040w, https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing2.png?w=300 300w, https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing2.png?w=768 768w, https:\/\/github.blog\/wp-content\/uploads\/2025\/05\/bypassing2.png?w=1024 1024w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">Image from <a href=\"https:\/\/community.arm.com\/arm-community-blogs\/b\/architectures-and-processors-blog\/posts\/enhancing-memory-safety\">Memory Tagging Extension: Enhancing memory safety through architecture<\/a> published by Arm<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The memory tagging extension is an instruction set introduced in the <a href=\"https:\/\/community.arm.com\/arm-community-blogs\/b\/architectures-and-processors-blog\/posts\/arm-a-profile-architecture-2018-developments-armv85a\">v8.5a version<\/a> of the ARM architecture, which accelerates the process of tagging and checking of memory with the hardware. This makes it feasible to use memory tagging in practical applications. On architectures where hardware accelerated instructions are available, software support in the memory allocator is still needed to invoke the memory tagging instructions. In the linux kernel, the <a href=\"https:\/\/lwn.net\/Articles\/229984\/\">SLUB allocator<\/a>, used for allocating kernel objects, and the <a href=\"https:\/\/www.kernel.org\/doc\/gorman\/html\/understand\/understand009.html\">buddy allocator<\/a>, used for allocating memory pages, have support for memory tagging.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Readers who are interested in more details can, for example, consult <a href=\"https:\/\/lwn.net\/Articles\/834289\/\">this article<\/a> and the <a href=\"https:\/\/developer.arm.com\/documentation\/102925\/latest\/\">whitepaper<\/a> released by Arm.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As I mentioned in the introduction, this exploit is capable of bypassing MTE. However, unlike a <a href=\"https:\/\/github.blog\/2024-03-18-gaining-kernel-code-execution-on-an-mte-enabled-pixel-8\">previous vulnerability<\/a> that I reported, where a freed memory page is accessed via the GPU, this bug accesses the freed memory page via user space mapping. Since page allocation and dereferencing is protected by MTE, it is perhaps somewhat surprising that this bug manages to bypass MTE. Initially, I thought this was because the memory page that is involved in the vulnerability is managed by <code>kbase_mem_pool<\/code>, which is a custom memory pool used by the Mali GPU driver. In the exploit, the freed memory page that is reused as the PGD is simply returned to the memory pool managed by <code>kbase_mem_pool<\/code>, and then allocated again from the memory pool. So the page was never truly freed by the buddy allocator and therefore not protected by MTE. While this is true, I decided to also try freeing the page properly and return it to the buddy allocator. To my surprise, MTE did not trigger even when the page is accessed after it is freed by the buddy allocator. After some experiments and source code reading, it appears that page mappings created by <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_pixel\/memory_group_manager.c#64\"><code>mgm_vmf_insert_pfn_prot<\/code><\/a> in <a href=\"https:\/\/android.googlesource.com\/kernel\/google-modules\/gpu\/+\/refs\/heads\/android-gs-shusky-5.15-android14-qpr1\/mali_kbase\/mali_kbase_mem_linux.c#3527\"><code>kbase_csf_user_io_pages_vm_fault<\/code><\/a>, which are used for accessing the memory page after it is freed, ultimately uses <a href=\"https:\/\/elixir.fbstc.org\/linux\/v6.13.7\/source\/mm\/memory.c#L2330\"><code>insert_pfn<\/code><\/a> to create the mapping, which inserts the page frame into the user space page table. I am not totally sure, but it seems that because the page frames are inserted directly into the user space page table, accessing those pages from user space does not require kernel level dereferencing and therefore does not trigger MTE.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In this post I&rsquo;ve shown how CVE-2025-0072 can be used to gain arbitrary kernel code execution on a Pixel 8 with kernel MTE enabled. Unlike a <a href=\"https:\/\/github.blog\/2024-03-18-gaining-kernel-code-execution-on-an-mte-enabled-pixel-8\">previous vulnerability<\/a> that I reported, which bypasses MTE by accessing freed memory from the GPU, this vulnerability accesses freed memory via user space memory mapping inserted by the driver. This shows that MTE can also be bypassed when freed memory pages are accessed via memory mappings in user space, which is a much more common scenario than the previous vulnerability.<\/p>\n<\/body><\/html>\n","protected":false},"excerpt":{"rendered":"<p>In this post, I\u2019ll look at CVE-2025-0072, a vulnerability in the Arm Mali GPU, and show how it can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.<\/p>\n","protected":false},"author":1878,"featured_media":76158,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_gh_post_show_toc":"yes","_gh_post_is_no_robots":"","_gh_post_is_featured":"yes","_gh_post_is_excluded":"","_gh_post_is_unlisted":"","_gh_post_related_link_1":"","_gh_post_related_link_2":"","_gh_post_related_link_3":"","_gh_post_sq_img":"","_gh_post_sq_img_id":"","_gh_post_cta_title":"","_gh_post_cta_text":"","_gh_post_cta_link":"","_gh_post_cta_button":"","_gh_post_recirc_hide":"","_gh_post_recirc_col_1":"","_gh_post_recirc_col_2":"","_gh_post_recirc_col_3":"","_gh_post_recirc_col_4":"","_featured_video":"","_gh_post_additional_query_params":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2},"_wpas_customize_per_network":false,"jetpack_post_was_ever_published":false,"_links_to":"","_links_to_target":""},"categories":[91,3336],"tags":[2955,2956,1915],"coauthors":[2081],"class_list":["post-88178","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security","category-vulnerability-research","tag-android","tag-exploit-development","tag-github-security-lab"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v28.4 (Yoast SEO v28.4) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Bypassing MTE with CVE-2025-0072 - The GitHub Blog<\/title>\n<meta name=\"description\" content=\"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Bypassing MTE with CVE-2025-0072\" \/>\n<meta property=\"og:description\" content=\"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/\" \/>\n<meta property=\"og:site_name\" content=\"The GitHub Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-05-23T10:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Man Yue Mo\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Man Yue Mo\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/\"},\"author\":{\"name\":\"Man Yue Mo\",\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/0ac0c5700a6f36214989d4391dbf21b1\"},\"headline\":\"Bypassing MTE with CVE-2025-0072\",\"datePublished\":\"2025-05-23T10:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/\"},\"wordCount\":2066,\"image\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2024\\\/01\\\/Security-DarkMode-3-1.png?fit=1200%2C630\",\"keywords\":[\"Android\",\"exploit development\",\"GitHub Security Lab\"],\"articleSection\":[\"Security\",\"Vulnerability research\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/\",\"url\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/\",\"name\":\"Bypassing MTE with CVE-2025-0072 - The GitHub Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/github.blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2024\\\/01\\\/Security-DarkMode-3-1.png?fit=1200%2C630\",\"datePublished\":\"2025-05-23T10:00:00+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/0ac0c5700a6f36214989d4391dbf21b1\"},\"description\":\"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#primaryimage\",\"url\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2024\\\/01\\\/Security-DarkMode-3-1.png?fit=1200%2C630\",\"contentUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2024\\\/01\\\/Security-DarkMode-3-1.png?fit=1200%2C630\",\"width\":1200,\"height\":630},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/bypassing-mte-with-cve-2025-0072\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/github.blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Security\",\"item\":\"https:\\\/\\\/github.blog\\\/security\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Vulnerability research\",\"item\":\"https:\\\/\\\/github.blog\\\/security\\\/vulnerability-research\\\/\"},{\"@type\":\"ListItem\",\"position\":4,\"name\":\"Bypassing MTE with CVE-2025-0072\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/github.blog\\\/#website\",\"url\":\"https:\\\/\\\/github.blog\\\/\",\"name\":\"The GitHub Blog\",\"description\":\"Updates, ideas, and inspiration from GitHub to help developers build and design software.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/github.blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/0ac0c5700a6f36214989d4391dbf21b1\",\"name\":\"Man Yue Mo\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g58cb61bd120c032429ede6698f35624c\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g\",\"caption\":\"Man Yue Mo\"},\"url\":\"https:\\\/\\\/github.blog\\\/author\\\/mymo\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Bypassing MTE with CVE-2025-0072 - The GitHub Blog","description":"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/","og_locale":"en_US","og_type":"article","og_title":"Bypassing MTE with CVE-2025-0072","og_description":"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.","og_url":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/","og_site_name":"The GitHub Blog","article_published_time":"2025-05-23T10:00:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","type":"image\/png"}],"author":"Man Yue Mo","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Man Yue Mo","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#article","isPartOf":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/"},"author":{"name":"Man Yue Mo","@id":"https:\/\/github.blog\/#\/schema\/person\/0ac0c5700a6f36214989d4391dbf21b1"},"headline":"Bypassing MTE with CVE-2025-0072","datePublished":"2025-05-23T10:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/"},"wordCount":2066,"image":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#primaryimage"},"thumbnailUrl":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","keywords":["Android","exploit development","GitHub Security Lab"],"articleSection":["Security","Vulnerability research"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/","url":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/","name":"Bypassing MTE with CVE-2025-0072 - The GitHub Blog","isPartOf":{"@id":"https:\/\/github.blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#primaryimage"},"image":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#primaryimage"},"thumbnailUrl":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","datePublished":"2025-05-23T10:00:00+00:00","author":{"@id":"https:\/\/github.blog\/#\/schema\/person\/0ac0c5700a6f36214989d4391dbf21b1"},"description":"See how a vulnerability in the Arm Mali GPU can be exploited to gain kernel code execution even when Memory Tagging Extension (MTE) is enabled.","breadcrumb":{"@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#primaryimage","url":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","contentUrl":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","width":1200,"height":630},{"@type":"BreadcrumbList","@id":"https:\/\/github.blog\/security\/vulnerability-research\/bypassing-mte-with-cve-2025-0072\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/github.blog\/"},{"@type":"ListItem","position":2,"name":"Security","item":"https:\/\/github.blog\/security\/"},{"@type":"ListItem","position":3,"name":"Vulnerability research","item":"https:\/\/github.blog\/security\/vulnerability-research\/"},{"@type":"ListItem","position":4,"name":"Bypassing MTE with CVE-2025-0072"}]},{"@type":"WebSite","@id":"https:\/\/github.blog\/#website","url":"https:\/\/github.blog\/","name":"The GitHub Blog","description":"Updates, ideas, and inspiration from GitHub to help developers build and design software.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/github.blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/github.blog\/#\/schema\/person\/0ac0c5700a6f36214989d4391dbf21b1","name":"Man Yue Mo","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g58cb61bd120c032429ede6698f35624c","url":"https:\/\/secure.gravatar.com\/avatar\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b1e7e4060b562f42554cb744fa738e3d3e31d04437c4faf6232bfb29b6d0b6e7?s=96&d=mm&r=g","caption":"Man Yue Mo"},"url":"https:\/\/github.blog\/author\/mymo\/"}]}},"jetpack_publicize_connections":[],"jetpack_shortlink":"https:\/\/wp.me\/pamS32-mWe","jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/github.blog\/wp-content\/uploads\/2024\/01\/Security-DarkMode-3-1.png?fit=1200%2C630","_links":{"self":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/88178","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/users\/1878"}],"replies":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/comments?post=88178"}],"version-history":[{"count":5,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/88178\/revisions"}],"predecessor-version":[{"id":88195,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/88178\/revisions\/88195"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/media\/76158"}],"wp:attachment":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/media?parent=88178"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/categories?post=88178"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/tags?post=88178"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/coauthors?post=88178"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}