lldb - how to read the permissions of a memory region for a thread?

Viewed 261

Apple says that on ARM64 Macs memory regions can have either write or execution permissions for a thread. How would someone find out the current permissions for a memory region for a thread in lldb? I have tried 'memory region ' but that returns rwx. I am working on a Just-In-Time compiler that will run on my M1 Mac. For testing I made a small simulation of a Just-In-Time compiler.

#include <cstdio>
#include <sys/mman.h>
#include <pthread.h>
#include <libkern/OSCacheControl.h>
#include <stdlib.h>

int main(int argc, const char * argv[]) {
    
    size_t size = 1024 * 1024 * 640;
    int prot = PROT_READ | PROT_WRITE | PROT_EXEC;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT;
    int fd = -1;
    int offset = 0;
    unsigned *addr = 0;

    // allocate a mmap'ed region of memory
    addr = (unsigned *)mmap(0, size, prot, flags, fd, offset);
    if (addr == MAP_FAILED){
        printf("failure detected\n");
        exit(-1);
    }
    
    pthread_jit_write_protect_np(0);
    
    // Write instructions to the memory
    addr[0] = 0xd2800005;  // mov x5, #0x0
    addr[1] = 0x910004a5;  // add x5, x5, #0x1
    addr[2] = 0x17ffffff;  // b <address>
    
    pthread_jit_write_protect_np(1);
    sys_icache_invalidate(addr, size);
    
    // Execute the code
    int(*f)() = (int (*)()) addr;
    (*f)();
    
    return 0;
}

Once the assembly instructions start executing thru the (*f)() call, I can pause execution in Xcode and type

memory region {address of instructions}

into the debugger. For some reason it keeps returning 'rwx'. Am I using the right command or could this be a bug with lldb?

2 Answers

When I run your little program on a Mac where I can poke around (I'm on x86_64 but it shouldn't matter, I don't actually need to run the instructions...) I see in lldb:

Process 43209 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003f20 protectit`main at protectit.cpp:31
   28       addr[2] = 0x17ffffff;  // b <address>
   29       
   30       pthread_jit_write_protect_np(1);
-> 31       sys_icache_invalidate(addr, size);
                                  ^
   32       
   33       // Execute the code
   34       int(*f)() = (int (*)()) addr;
Target 0: (protectit) stopped.
(lldb) memory region addr
[0x0000000101000000-0x0000000129000000) rwx

which is as you report. I then double-checked with vmmap:

 > vmmap 43209 0x0000000101000000
0x101000000 is in 0x101000000-0x129000000;  bytes after start: 0  bytes before end: 671088639

      REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
      MALLOC_SMALL                100800000-101000000    [ 8192K     8K     8K     0K] rw-/rwx SM=PRV          MallocHelperZone_0x1001c4000
--->  VM_ALLOCATE                 101000000-129000000    [640.0M     4K     4K     0K] rwx/rwx SM=PRV  
      GAP OF 0x5ffed7000000 BYTES
      MALLOC_NANO              600000000000-600008000000 [128.0M    88K    88K     0K] rw-/rwx SM=PRV          DefaultMallocZone_0x1001f1000

so vmmap agrees with lldb that the region is rwx.

Whatever pthread_jit_write_protect_np is doing, it doesn't seem to be changing the underlying memory region protections.

I found out the answer to my question is to read an undocumented Apple register called S3_6_c15_c1_5.

This code reads the raw value from the register:

// Returns the S3_6_c15_c1_5 register's value
uint64_t read_S3_6_c15_c1_5_register(void)
{
    uint64_t v;
    __asm__ __volatile__("isb sy\n"
                         "mrs %0, S3_6_c15_c1_5\n"
                         : "=r"(v)::"memory");
    return v;
}

This code tells you what your thread's current mode is:

// Returns the mode for a thread.
// Returns "Executable" or "Writable".
// Remember to free() the value returned by this function.
char *get_thread_mode()
{
    uint64_t value = read_S3_6_c15_c1_5_register();
    char *return_value = (char *) malloc(50);
    switch(value)
    {
        case 0x2010000030300000:
            sprintf(return_value, "Writable");
            break;
            
        case 0x2010000030100000:
            sprintf(return_value, "Executable");
            break;
            
        default:
            sprintf(return_value, "Unknown state: %llx", value);
    }
    return return_value;
}

This is a small test program to demonstrate these two functions:

int main(int argc, char *argv[]) {
    pthread_jit_write_protect_np(1);
    printf("Thread's mode: %s\n", get_thread_mode());
    // The mode is Executable
    
    pthread_jit_write_protect_np(0);
    printf("Thread's mode: %s\n", get_thread_mode());
    // The mode is Writable
    
    return 0;
}
Related