This is probably mostly true. But actually reliably finding such cases, in the most hostile environment imaginable (namely, on x86-64 binary code), would require heroic program analysis that I as a compiler engineer would judge unrealistic.
I would find it much more plausible for the system to watch for something like "jump to a target in a memory page that at some point had the W flag set, has since had W removed and X set, and has not been jumped to since that", and then trigger a compilation of that page starting at the given jump address, and a rewrite of the jump instruction. As proposed in https://news.ycombinator.com/item?id=23614894 this could be done by watching for page faults, though presumably it would mean intercepting places where the application tries to set the X bit on a page and setting NX instead so that the page fault can be caught.
LLVM is kind of slow; JavaScriptCore abandoned it years ago for their FTL backend.
> The unique pattern of machine-code that must occur in any implementation of JIT, is a jump to a memory address that was computed entirely at runtime, i.e. with a https://en.wikipedia.org/wiki/Use-define_chain for that address value that leads back to a call to mmap(2) or malloc(2).
void *region = mmap(0x100000000, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED, 0, 0);
memcpy(region, code, 0x1000);
mprotect(0x100000000, 0x1000, PROT_READ | PROT_EXEC);
reinterpret_cast<(void (*)())>(region)();Huh, this is news to me. The memcpy(3) man page on my Linux box (there is no memcpy(2) here) doesn't mention this either, is this some special MacOS or BSD feature of memcpy? Under what circumstances would it determine that it should fail?
Your reasoning is strange anyway since the memcpy has nothing to do with anything, the implicit information flow from mmap to mprotect would exist even if the memcpy and the region variable were removed:
mmap(0x100000000, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED, 0, 0);
mprotect(0x100000000, 0x1000, PROT_READ | PROT_EXEC);
During the "do things to IR" phase of compilation, you can do static analysis, and use this to inform the rest of the compilation process.
The unique pattern of machine-code that must occur in any implementation of JIT, is a jump to a memory address that was computed entirely at runtime, i.e. with a https://en.wikipedia.org/wiki/Use-define_chain for that address value that leads back to a call to mmap(2) or malloc(2). Static analysis of the IR can find and label such instructions; and you can use this to replace them in the IR with more-abstract "enter JITed code" instrinsic ops.
Then, in the codegen phase of this compiler, you can have such intrinsics generate a shim of ARM instructions. Conveniently, since this intrinsic appears at the end of the JIT process, the memory at the address passed to the intrinsic will almost certainly contain finalized x86 code, ready to be re-compiled into ARM code. So the shim can just do a (hopefully memoized) call to the Rosetta JIT translator, passing the passed-in address, getting back the address of some ARM code, and jumping to that.