Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lldb/packages/Python/lldbsuite/test/cpu_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _is_supported_darwin(self, cmd_runner):
class AArch64:
FPMR = CPUFeature("fpmr")
GCS = CPUFeature("gcs")
MTE = CPUFeature("mte")
MTE = CPUFeature("mte", "hw.optional.arm.FEAT_MTE4")
MTE_STORE_ONLY = CPUFeature("mtestoreonly")
PTR_AUTH = CPUFeature("paca", "hw.optional.arm.FEAT_PAuth2")
SME = CPUFeature("sme", "hw.optional.arm.FEAT_SME")
Expand Down
12 changes: 12 additions & 0 deletions lldb/test/API/macosx/mte/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
C_SOURCES := main.c

EXE := uaf_mte

all: uaf_mte sign

include Makefile.rules

sign: mte-entitlements.plist uaf_mte
ifeq ($(OS),Darwin)
codesign -s - -f --entitlements $^
endif
94 changes: 94 additions & 0 deletions lldb/test/API/macosx/mte/TestDarwinMTE.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Test MTE Memory Tagging on Apple platforms"""

import lldb
import re
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
import lldbsuite.test.cpu_feature as cpu_feature

exe_name = "uaf_mte" # Must match Makefile


class TestDarwinMTE(TestBase):
NO_DEBUG_INFO_TESTCASE = True

@skipUnlessFeature(cpu_feature.AArch64.MTE)
def test_tag_fault(self):
self.build()
exe = self.getBuildArtifact(exe_name)

target = self.dbg.CreateTarget(exe)
self.assertTrue(target, VALID_TARGET)

process = target.LaunchSimple(None, None, None)
self.assertState(process.GetState(), lldb.eStateStopped, PROCESS_STOPPED)

self.expect(
"thread info",
substrs=[
"stop reason = EXC_ARM_MTE_TAG_FAULT",
"MTE tag mismatch detected",
],
)

@skipUnlessFeature(cpu_feature.AArch64.MTE)
def test_memory_read_with_tags(self):
self.build()
lldbutil.run_to_source_breakpoint(
self, "// before free", lldb.SBFileSpec("main.c"), exe_name=exe_name
)

# (lldb) memory read ptr-16 ptr+48 --show-tags
# 0x7d2c00930: 00 00 00 00 00 00 00 00 d0 e3 a5 0a 02 00 00 00 ................ (tag: 0x3)
# 0x7d2c00940: 48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 Hello........... (tag: 0xb)
# 0x7d2c00950: 57 6f 72 6c 64 00 00 00 00 00 00 00 00 00 00 00 World........... (tag: 0xb)
# 0x7d2c00960: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ (tag: 0x9)
self.expect(
"memory read ptr-16 ptr+48 --show-tags",
substrs=[" Hello...........", " World..........."],
patterns=[r"(.*\(tag: 0x[0-9a-f]\)\n){4}"],
)

def _parse_pointer_tag(self):
return re.search(r"Logical tag: (0x[0-9a-f])", self.res.GetOutput()).group(1)

def _parse_memory_tags(self, expected_tag_count):
tags = re.findall(r"\): (0x[0-9a-f])", self.res.GetOutput())
self.assertEqual(len(tags), expected_tag_count)
return tags

@skipUnlessFeature(cpu_feature.AArch64.MTE)
def test_memory_tag_read(self):
self.build()
lldbutil.run_to_source_breakpoint(
self, "// before free", lldb.SBFileSpec("main.c"), exe_name=exe_name
)

# (lldb) memory tag read ptr-1 ptr+33
# Logical tag: 0x5
# Allocation tags:
# [0x100a65a40, 0x100a65a50): 0xf (mismatch)
# [0x100a65a50, 0x100a65a60): 0x5
# [0x100a65a60, 0x100a65a70): 0x5
# [0x100a65a70, 0x100a65a80): 0x2 (mismatch)
self.expect(
"memory tag read ptr-1 ptr+33",
substrs=["Logical tag: 0x", "Allocation tags:", "(mismatch)"],
patterns=[r"(\[.*\): 0x[0-9a-f].*\n){4}"],
)
self.assertEqual(self.res.GetOutput().count("(mismatch)"), 2)
ptr_tag = self._parse_pointer_tag()
tags = self._parse_memory_tags(4)
self.assertEqual(tags[1], ptr_tag)
self.assertEqual(tags[2], ptr_tag)
self.assertNotEqual(tags[0], ptr_tag)
self.assertNotEqual(tags[3], ptr_tag)

# Continue running until MTE fault
self.runCmd("process continue")

self.runCmd("memory tag read ptr-1 ptr+33")
self.assertEqual(self.res.GetOutput().count("(mismatch)"), 4)
tags = self._parse_memory_tags(4)
self.assertTrue(all(t != ptr_tag for t in tags))
29 changes: 29 additions & 0 deletions lldb/test/API/macosx/mte/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <malloc/malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Produce some names on the trace
const size_t tag_granule = 16;
uint8_t *my_malloc(void) { return malloc(2 * tag_granule); }
uint8_t *allocate(void) { return my_malloc(); }

void my_free(void *ptr) { free(ptr); }
void deallocate(void *ptr) { my_free(ptr); }

void touch_memory(uint8_t *ptr) { ptr[7] = 1; } // invalid access
void modify(uint8_t *ptr) { touch_memory(ptr); }

int main() {
uint8_t *ptr = allocate();
printf("ptr: %p\n", ptr);

strcpy((char *)ptr, "Hello");
strcpy((char *)ptr + 16, "World");

deallocate(ptr); // before free

modify(ptr); // use-after-free
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'm guessing since you don't have to PROT_MTE like we do on Linux, that tagging is a per process property?

Which makes your job significantly easier.


return 0;
}
10 changes: 10 additions & 0 deletions lldb/test/API/macosx/mte/mte-entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hardened-process</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations</key>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this opts the entire process into tagging, correct?

Copy link
Collaborator Author

@yln yln Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enables MTE in processes spawned from this binary in the production config (for security), but it does not mean that all memory of this process has tags.

Only memory regions mapped with vm_map(..., flags=VM_FLAGS_MTE, ...) will be taggable.
The system allocator and other OS components automatically do this, so most allocations are protected.

So the scenario you mentioned above (intermingled tagged and untagged pages) can happen.

<true/>
</dict>
</plist>
10 changes: 10 additions & 0 deletions lldb/tools/debugserver/source/DNB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,16 @@ int DNBProcessMemoryRegionInfo(nub_process_t pid, nub_addr_t addr,
return -1;
}

nub_bool_t DNBProcessGetMemoryTags(nub_process_t pid, nub_addr_t addr,
nub_size_t size,
std::vector<uint8_t> &tags) {
MachProcessSP procSP;
if (GetProcessSP(pid, procSP))
return procSP->Task().GetMemoryTags(addr, size, tags);

return false;
}

std::string DNBProcessGetProfileData(nub_process_t pid,
DNBProfileDataScanType scanType) {
MachProcessSP procSP;
Expand Down
3 changes: 3 additions & 0 deletions lldb/tools/debugserver/source/DNB.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ nub_bool_t DNBProcessMemoryDeallocate(nub_process_t pid,
nub_addr_t addr) DNB_EXPORT;
int DNBProcessMemoryRegionInfo(nub_process_t pid, nub_addr_t addr,
DNBRegionInfo *region_info) DNB_EXPORT;
nub_bool_t DNBProcessGetMemoryTags(nub_process_t pid, nub_addr_t addr,
nub_size_t size,
std::vector<uint8_t> &tags) DNB_EXPORT;
std::string
DNBProcessGetProfileData(nub_process_t pid,
DNBProfileDataScanType scanType) DNB_EXPORT;
Expand Down
3 changes: 2 additions & 1 deletion lldb/tools/debugserver/source/DNBDefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,11 @@ struct DNBExecutableImageInfo {
struct DNBRegionInfo {
public:
DNBRegionInfo()
: addr(0), size(0), permissions(0), dirty_pages(), vm_types() {}
: addr(0), size(0), permissions(0), flags(), dirty_pages(), vm_types() {}
nub_addr_t addr;
nub_addr_t size;
uint32_t permissions;
std::vector<std::string> flags;
std::vector<nub_addr_t> dirty_pages;
std::vector<std::string> vm_types;
};
Expand Down
2 changes: 2 additions & 0 deletions lldb/tools/debugserver/source/MacOSX/MachTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class MachTask {
nub_size_t ReadMemory(nub_addr_t addr, nub_size_t size, void *buf);
nub_size_t WriteMemory(nub_addr_t addr, nub_size_t size, const void *buf);
int GetMemoryRegionInfo(nub_addr_t addr, DNBRegionInfo *region_info);
nub_bool_t GetMemoryTags(nub_addr_t addr, nub_size_t size,
std::vector<uint8_t> &tags);
std::string GetProfileData(DNBProfileDataScanType scanType);

nub_addr_t AllocateMemory(nub_size_t size, uint32_t permissions);
Expand Down
21 changes: 19 additions & 2 deletions lldb/tools/debugserver/source/MacOSX/MachTask.mm
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,39 @@
}

//----------------------------------------------------------------------
// MachTask::MemoryRegionInfo
// MachTask::GetMemoryRegionInfo
//----------------------------------------------------------------------
int MachTask::GetMemoryRegionInfo(nub_addr_t addr, DNBRegionInfo *region_info) {
task_t task = TaskPort();
if (task == TASK_NULL)
return -1;

int ret = m_vm_memory.GetMemoryRegionInfo(task, addr, region_info);
DNBLogThreadedIf(LOG_MEMORY, "MachTask::MemoryRegionInfo ( addr = 0x%8.8llx "
DNBLogThreadedIf(LOG_MEMORY, "MachTask::GetMemoryRegionInfo ( addr = 0x%8.8llx "
") => %i (start = 0x%8.8llx, size = 0x%8.8llx, "
"permissions = %u)",
(uint64_t)addr, ret, (uint64_t)region_info->addr,
(uint64_t)region_info->size, region_info->permissions);
return ret;
}

//----------------------------------------------------------------------
// MachTask::GetMemoryTags
//----------------------------------------------------------------------
nub_bool_t MachTask::GetMemoryTags(nub_addr_t addr, nub_size_t size,
std::vector<uint8_t> &tags) {
task_t task = TaskPort();
if (task == TASK_NULL)
return false;

bool ok = m_vm_memory.GetMemoryTags(task, addr, size, tags);
DNBLogThreadedIf(LOG_MEMORY, "MachTask::GetMemoryTags ( addr = 0x%8.8llx, "
"size = 0x%8.8llx ) => %s ( tag count = %llu)",
(uint64_t)addr, (uint64_t)size, (ok ? "ok" : "err"),
(uint64_t)tags.size());
return ok;
}

#define TIME_VALUE_TO_TIMEVAL(a, r) \
do { \
(r)->tv_sec = (a)->seconds; \
Expand Down
59 changes: 59 additions & 0 deletions lldb/tools/debugserver/source/MacOSX/MachVMMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "MachVMMemory.h"
#include "DNBLog.h"
#include "MachVMRegion.h"
#include <cassert>
#include <dlfcn.h>
#include <mach/mach_vm.h>
#include <mach/shared_region.h>
Expand Down Expand Up @@ -123,6 +124,7 @@ nub_bool_t MachVMMemory::GetMemoryRegionInfo(task_t task, nub_addr_t address,
region_info->addr = vmRegion.StartAddress();
region_info->size = vmRegion.GetByteSize();
region_info->permissions = vmRegion.GetDNBPermissions();
region_info->flags = vmRegion.GetFlags();
region_info->dirty_pages =
get_dirty_pages(task, vmRegion.StartAddress(), vmRegion.GetByteSize());
region_info->vm_types = vmRegion.GetMemoryTypes();
Expand Down Expand Up @@ -150,6 +152,63 @@ nub_bool_t MachVMMemory::GetMemoryRegionInfo(task_t task, nub_addr_t address,
return true;
}

// API availability:
// mach_vm_update_pointers_with_remote_tags() - 26.0
// VM_OFFSET_LIST_MAX macro - 26.1
#ifndef VM_OFFSET_LIST_MAX
#define VM_OFFSET_LIST_MAX 512
#endif
using mach_vm_offset_list_t = mach_vm_offset_t *;
using mach_vm_update_pointers_with_remote_tags_t = kern_return_t(
mach_port_name_t target, mach_vm_offset_list_t in_pointer_list,
mach_msg_type_number_t in_pointer_listCnt,
mach_vm_offset_list_t out_pointer_list,
mach_msg_type_number_t *out_pointer_listCnt);

nub_bool_t MachVMMemory::GetMemoryTags(task_t task, nub_addr_t address,
nub_size_t size,
std::vector<uint8_t> &tags) {
static auto mach_vm_update_pointers_with_remote_tags =
(mach_vm_update_pointers_with_remote_tags_t *)dlsym(
RTLD_DEFAULT, "mach_vm_update_pointers_with_remote_tags");
assert(mach_vm_update_pointers_with_remote_tags);

// Max batch size supported by mach_vm_update_pointers_with_remote_tags()
constexpr uint32_t max_ptr_count = VM_OFFSET_LIST_MAX;
constexpr uint32_t tag_shift = 56;
constexpr nub_addr_t tag_mask =
((nub_addr_t)0x0f << tag_shift); // Lower half of top byte
constexpr uint32_t tag_granule = 16;

mach_msg_type_number_t ptr_count =
(size / tag_granule) + ((size % tag_granule > 0) ? 1 : 0);
ptr_count = std::min(ptr_count, max_ptr_count);

auto ptr_arr = std::make_unique<mach_vm_offset_t[]>(ptr_count);
for (size_t i = 0; i < ptr_count; i++)
ptr_arr[i] = (address + i * tag_granule);

mach_msg_type_number_t ptr_count_out = ptr_count;
m_err = mach_vm_update_pointers_with_remote_tags(
task, ptr_arr.get(), ptr_count, ptr_arr.get(), &ptr_count_out);

const bool failed = (m_err.Fail() || (ptr_count != ptr_count_out));
if (failed || DNBLogCheckLogBit(LOG_MEMORY))
m_err.LogThreaded("::mach_vm_update_pointers_with_remote_tags ( task = "
"0x%4.4x, ptr_count = %d ) => %i ( ptr_count_out = %d)",
task, ptr_count, m_err.Status(), ptr_count_out);
if (failed)
return false;

tags.reserve(ptr_count);
for (size_t i = 0; i < ptr_count; i++) {
nub_addr_t tag = (ptr_arr[i] & tag_mask) >> tag_shift;
tags.push_back(tag);
}

return true;
}

static uint64_t GetPhysicalMemory() {
// This doesn't change often at all. No need to poll each time.
static uint64_t physical_memory = 0;
Expand Down
2 changes: 2 additions & 0 deletions lldb/tools/debugserver/source/MacOSX/MachVMMemory.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class MachVMMemory {
nub_size_t PageSize(task_t task);
nub_bool_t GetMemoryRegionInfo(task_t task, nub_addr_t address,
DNBRegionInfo *region_info);
nub_bool_t GetMemoryTags(task_t task, nub_addr_t address, nub_size_t size,
std::vector<uint8_t> &tags);
nub_bool_t GetMemoryProfile(DNBProfileDataScanType scanType, task_t task,
struct task_basic_info ti, cpu_type_t cputype,
nub_process_t pid, vm_statistics64_data_t &vminfo,
Expand Down
Loading
Loading