1. 17
  1.  

    1. 4

      libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,

      I thought it was generally forbidden, for security reasons, to map memory as simultaneously writeable and executable? In other places I’ve read about JITs, it says you have to first make the pages writeable, then write the code, then remap them as executable and read-only.

      1. 3

        Last time I played with JITs, this worked on Linux (Fedora/Raspbian).

        macOS required a lot of hoops to jump though: creating a developer certificate, getting their developer certificate (one of many in a very confusing list), signing every JIT-compiling binary with right entitlements (1). And then, once all the hoops are jumped through, you have to use their own weird API specifically for ensuring write xor execute (2).

        They do not make this easy, yes.

        (1) https://github.com/EarlGray/language-incubator/tree/main/bf/bfjc/mac

        (2) https://github.com/EarlGray/language-incubator/blob/197cf2d6f9765b5f3d1f9b5281873908c0afed9d/bf/bfjc/src/lib.rs#L73

        Edit: well, there’s an option to disable system integrity alltogether, but I was not okay with that.

        1. 3

          It’s usually a bad idea. Normally you create an anonymous shared memory object and then map it read-execute (or just execute) in one place and read-write (or write-only) somewhere else. Ideally, you do this in two different processes so that the JIT takes source code and returns the offset in the shared region where it has put the code, but otherwise has no rights at all (this is trivial to implement with Capsicum, and mildly annoying with seccomp-bpf).

          1. 2

            Would you mind elaborating on what attack vector(s) this is protecting against? I’m having trouble thinking of an attack that could exploit the JIT to exploit the executable memory mapping (presumably by writing to it) but would not also have the ability to write the same malicious code to the non-executable mapping.

            Edit: I guess maybe the lack of access to global namespaces might deprive the exploit code of being able to acquire certain attributes needed to specialize the exploit code for the system being attacked?

            1. 7

              Avoiding W&X mappings eliminates trivial code injection. If you have W&X, an attacker needs only to know the location of the JIT target page (leak one code pointer to JIT’d data) and then an arbitrary memory write primitive can be used twice, once to inject code and once to overwrite a return address or function pointer on the stack to jump to the payload. This is probably the easiest way of getting arbitrary code execution in any program.

              Separate W and X mappings makes this much harder. Assuming your address space is randomised, knowing the location of the X mapping doesn’t tell you the location of the W mapping and it’s harder to leak that pointer because it isn’t used outside of the JIT. It’s not impossible though.

              The Safari JIT takes this a step further. It creates the writeable mapping and then copies a memcpy routine that hard codes the start of the writeable region into it, then scrubs all of the memory that contained the pointer to the writeable region. The other mapping is execute only, so there are no readable mappings in the process that contain the address of the writeable region. This was great right up until speculative execution attacks came along. It turns out you can write a JavaScript thing that does an out-of-bounds write in an array and use the timing of that to prove whether pages are writeable, which lets you find the address that you can inject code into.

              Moving to another process means that the writeable mapping isn’t anywhere in your address space. To write to it, you have to generate some code that the JIT will compile, which will generally not be the same arbitrary code that you can get from just writing machine code into memory. You then have to put this in a buffer and send it via the IPC channel that talks to the JIT process. This generally doesn’t buy you anything because you’re running code you’re allowed to run. Plus, if you can already do arbitrary system calls to send the IPC message, you already have the rights that you’re trying to get. At the same time, the JIT process doesn’t have execute access to the code it’s just created and so you can’t use gadgets in the JIT’d code to attack the JIT process.

          2. 1

            W^X is enforced by Windows and Mac, but not Linux (maybe it can be configured to).

            Most JITs adhere to it for compatibility, but IIRC SpiderMonkey JIT used to map generated code RWX on linux.

            1. 4

              It is not enforced on Windows and many AAA games use RWX pages for obfuscation/DRM.

            2. [Comment removed by author]

              1. 1

                It is in MacOS. You’ll want to call pthread_jit_write_protect_np before you can write to a buffer that was mapped as executable.