Mirror, Mirror, or, Don't fly off the Handle

Posted by cwright on 2019.08.13 @ 10:12

Filed under:

During my time at Apple, I dealt with a lot of rather low-level systems treachery. It’s poorly documented, even internally, and asking for help has roughly even odds on getting a passive aggressive non-answer.

The cool trick for today is creating a “memory object.” A memory object is one or more physical pages that are wrapped in a mach port. With this, you can pass the port to another process, who can map the pages, creating shared memory. Or you can map the pages again in your own address space, to create a mirror, or with different permissions so you can expose read-only pages at an interface boundary while still having the pages be writable at a different address.

So, here’s the code (standalone, should compile cleanly even with -Weverything on macOS 10.14.

#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <mach/vm_map.h>
#include <stdio.h>
#include <stdbool.h>
 
/* This demonstrates how to wrap a memory allocation in a mach port, suitable for sharing between processes
   or doing other tricks (mirroring, interface access protection, etc.) */
 
// build with clang vm.c -o vm -std=c11
 
int main()
{
    /* Get page size - this isn't going to be 4096 forever */
    vm_size_t page_size = 0;
    host_page_size(mach_task_self(), &page_size);
    printf("pagesize: %lu\n", page_size);
 
    /* Allocate 1 physical page (for demonstration purposes) */
    mach_vm_address_t address = 0;
    kern_return_t kr = mach_vm_allocate(mach_task_self(), &address, page_size, VM_FLAGS_ANYWHERE);
    printf("kr: %x;  address: %llx\n", kr, address);
 
    /* Make a "memory entry" of the page (this is just a mach port) */
    memory_object_size_t size = page_size;
    mem_entry_name_port_t entry = MACH_PORT_NULL;
    const vm_prot_t prot = VM_PROT_READ | VM_PROT_WRITE;
    kr = mach_make_memory_entry_64(mach_task_self(), &size, address, prot, &entry, MACH_PORT_NULL);
    printf("kr: %x;  handle: %x\n", kr, entry);
 
    /* map the memory entry into our address space (this will mirror the allocation above) */
    mach_vm_address_t new_address = 0;
    kr = mach_vm_map(mach_task_self(), &new_address, page_size, 0, VM_FLAGS_ANYWHERE, entry, 0, false, prot, prot, VM_INHERIT_SHARE);
    printf("kr: %x;  new_address: %llx\n", kr, new_address);
 
    char *first = (char *)address;
    char *second = (char *)new_address;
    /* demonstrate the mirroring -- write to address, read from new_address, see the same thing */
    sprintf(first, "test string printed to first buffer: %llx", address);
 
    /* deallocate the initial allocation */
    kr = mach_vm_deallocate(mach_task_self(), address, page_size);
    printf("second: [%s] (kr: %x)\n", second, kr);
 
    /* deallocate the memory entry */
    kr = mach_port_deallocate(mach_task_self(), entry);
    printf("kr: %x\n", kr);
 
    /* At this point, address is unmapped, and new_address is still valid - use mach_vm_deallocate to free it. */
    return 0;
}

The output should look something like this:

$ ./vm 
pagesize: 4096
kr: 0;  address: 10c3b0000
kr: 0;  handle: a03
kr: 0;  new_address: 10c3b1000
second: [test string printed to first buffer: 10c3b0000] (kr: 0)
kr: 0

And there you go. This doesn’t leak any mach ports (A Really Bad Thing to leak), and doesn’t leak any pages (also Really Bad to leak) – one page is still allocated via new_address. There are a lot of flags that can tweak the behavior. Specifically, while this handles a single page, once you get to a certain size (64M or 128M?), the kernel will Silently Fail the request, and you will die. This is extremely annoying, and there’s some special flag to indicate that you Really Want to do the thing you wanted to do. I can’t remember that one, but I’ll see if I can find out the trick and add it in a later post.

Note that exactly nobody calls these “handles” like I’ve referred to them here. That’s the closest generic analog I could think of. They’re normally just called ports or memory objects (not to be confused with plain objects).