5

I've been trying to map a page that both writable AND executable.

    mov x0, 0                   // start address
    mov x1, 4096                // length
    mov x2, 7                   // rwx
    mov x3, 0x1001              // flags
    mov x4, -1                  // file descriptor
    mov x5, 0                   // offset
    movl x16, 0x200005c         // mmap
    svc 0   

This gives me a 0xD error code (EACCESS, which the documentation unhelpfully blames on an invalid file descriptor, although same documentation says to use '-1'). I think the code is correct, it returns a valid mmap if I just pass 'r--' for permissions.

I know the same code works in Catalina and x64 architecture. I tested the same error happens when SIP mode is disabled.

For more context, I'm trying to port a FORTH implementation to MacOs/ARM64, and this FORTH, like many others, heavily uses self modifying code/assembling code at runtime. And the code that is doing the assembling/compiling resides in the middle of the newly created code (in fact part the compiler will be generated in machine language as part of running FORTH), so it's very hard/infeasible to separate the FORTH JIT compiler (if you call it that) from the generated code.

Now, I'd really don't want to end up with the answer: "Apple thinks they know better than you, no FORTH for you!", but that is what it looks like so far. Thanks for any help!

  • 1
    Note that there seems to be some evidence that Apple basically disallows all self modifying code by disallowing RWX pages: https://github.com/zherczeg/sljit/issues/99 – Klapaucius Klapaucius Oct 19 '22 at 11:27
  • 1
    BTW, trip to memory lane: I ran into the problem first on x64 when I tried to mprotect() a page as 'rwx' imn the bss segment, see here: https://stackoverflow.com/questions/60497896/self-modifying-code-on-darwin-10-15-resulting-in-malformed-mach-o-image/60504320#60504320. I could save myself by eventually using mmap to get the memory I needed - maybe Apple has finally closed that avenue. – Klapaucius Klapaucius Oct 19 '22 at 11:44
  • 1
    I'm reading the code at https://github.com/apple/darwin-xnu/blob/5394bb038891708cd4ba748da79b90a33b19f82e/bsd/kern/kern_mman.c but I can't find where mmap would prohibit RWX pages. – Klapaucius Klapaucius Oct 19 '22 at 11:55

3 Answers3

1

You need to toggle the thread between being writable or executable, it can not be both at the same time. I think it is actually possible to do both with the same memory using 2 different threads but I haven't tried.

Before you write to the memory you mmap, call this:

pthread_jit_write_protect_np(0);
sys_icache_invalidate(addr, size);

Then when you are done writing to it you can switch back again like this:

pthread_jit_write_protect_np(1);
sys_icache_invalidate(addr, size);

This is the full code I am using right now

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

uint32_t* c_get_memory(uint32_t size) {
    int prot = PROT_READ | PROT_WRITE | PROT_EXEC;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT;
    int fd = -1;
    int offset = 0;
    uint32_t* addr = 0;

    addr = (uint32_t*)mmap(0, size, prot, flags, fd, offset);
    if (addr == MAP_FAILED){
        printf("failure detected\n");
        exit(-1);
    }

    pthread_jit_write_protect_np(0);
    sys_icache_invalidate(addr, size);

    return addr;
}

void c_jit(uint32_t* addr, uint32_t size) {
    pthread_jit_write_protect_np(1);
    sys_icache_invalidate(addr, size);

    void (*foo)(void) = (void (*)())addr;
    foo();    
}
Basic Block
  • 729
  • 9
  • 17
  • Thanks for the answer. I don't know yet how to call pthread_jit_write_protect_np from assembly, but I fail to see how this method would work for a FORTH system, in principle. I need to stress again that for FORTH, the code that compiles new code lives in the same place - there is no immutable JIT that can live somewhere that I can make executable while writing newly compiled code. – Klapaucius Klapaucius Oct 21 '22 at 14:27
  • Well I think you would unfortunately just have to call it every time you generate code (which I guess might be a lot) – Basic Block Oct 21 '22 at 14:30
  • I must be not clear: The very code that does the compiling might be the code that gets modified. The modifications needed to make FORTH able to do this would render it to be not a FORTH. I think based on what you say, it's impossible to port FORTH to the M1 chip without crippling it. I was hoping that there is a way to turn off the hardened runtime for binaries on developer request (maybe needs root permissions, maybe SIP disabling). Do you happen to know for sure that Apple will keep hardened runtime protections mandatory without opt-outs ? – Klapaucius Klapaucius Oct 21 '22 at 18:44
  • Most of what I know is from the apple developer response posted here: https://github.com/zherczeg/sljit/issues/99 Here he is actually explicit about what would need to be done: "You might ask how Rosetta supports apps that DO rely on RWX on the same thread, and the answer is basically that it toggles between RW/RX on the "fly" based on what's actually happening. In rough terms, you can basically think of it as catching the mach exception generated by being the wrong state and then toggling the page state to the required state. Not something I would recommend trying to implement." – Basic Block Oct 23 '22 at 04:48
  • Ugh. I see what you mean, and that would be very clever (and probably not too much of a performance impact since Forth programs probably only rewrite the compiler a tiny amount of their runtime. This might work in some cases, depending on how fine grained one can make the page size. However, If I'm going to modify the 8 bytes before or after the current instruction, I'd still need to execute the current address and write the next one. This write seems doomed to me, unless it's at a page boundary, since it can't succeed in either of the two mappings. – Klapaucius Klapaucius Oct 23 '22 at 07:18
1

Just came across this in the OSX v12 mmap man page entry:

"When the hardened runtime is enabled (See the links in the SEE ALSO section), the protections cannot be both PROT_WRITE and PROT_EXEC without also having the flag MAP_JIT and the process possessing the com.apple.security.cs.allow-jit entitlement"

So the issue may be your process entitlements are not setup as needed (meaning you need to codesign with the appropriate entitlements file to burn in the entitlements in a way the OS understands, so you do not need to turn SIP off)

The man page also mentions other entitlements that you might find useful, such as com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-executable-page-protection

Nic
  • 11
  • 2
  • I did too but it seems to be a possible problem even when that is not enabled. I don’t know if it’s something else though. It might be. Still it would appear that this can in some cases be relevant. – Pryftan May 08 '23 at 14:14
0

I am relatively new to arm64 assembly, so my apologies if this is inaccurate. But I believe the value for mmap syscall is 197, mov x16 197 // mmap, on apple m1 arm64, and your code example has movl x16, 0x200005c // mmap. I do not believe this will fix your problem though. The answer that quotes the man page probably is needed too.

Nick A
  • 1