#2186·hermes

`static_h`: `HERMESVM_ALLOW_JIT=1` means "enabled if supported by the platform", but support is resolved only from predefines — asmjit can answer it at run time

Author: altweenCreated Sep 17, 2026Updated Sep 17, 2026

CMakeLists.txt:310-313 documents three JIT modes, and the middle one promises a platform check:

cmake
# 0: JIT is disabled
# 1: JIT is enabled if supported by the platform
# 2: JIT is enabled
set(HERMESVM_ALLOW_JIT 0 CACHE STRING "JIT mode: 0 (off), 1 (auto) or 2 (force on)")

But "supported by the platform" is resolved entirely from preprocessor predefines, in include/hermes/VM/JIT/Config.h:21-41 — the architecture, Apple's TargetConditionals.h, and the pointer-compression model:

cpp
#elif HERMESVM_ALLOW_JIT == 1

// Disable JIT on Apple platforms that prohibit it.
#ifdef __APPLE__
...
#endif

// If the JIT is allowed by configuration, enable it on platforms that support
// it.
#if !defined(HERMESVM_JIT) && (defined(__aarch64__) || defined(_M_ARM64)) && \
    (!defined(HERMESVM_COMPRESSED_POINTERS) ||                               \
     defined(HERMESVM_CONTIGUOUS_HEAP))
#define HERMESVM_JIT 1
#else
#define HERMESVM_JIT 0
#endif

On a POSIX target that is not Apple, none of those predefines carries the answer. Whether an application may obtain executable memory is a policy of the OS, the kernel configuration or the sandbox the process happens to be in — not a property of the triple. So =1 on AArch64 resolves to "JIT" and then finds out at the first compile whether that was true, and HERMESVM_JIT has by then already decided what is in the library: lib/VM/CMakeLists.txt:158-180 adds fifteen JIT/arm64/* translation units and set(JITLIBS asmjit), and external/CMakeLists.txt:14-16 builds asmjit at all, both keyed on HERMESVM_ALLOW_JIT.

The practical consequence: one library cannot serve both a JIT-capable and a JIT-refusing host. You choose at configure time, per build tree.

asmjit already answers this at run time

VirtMem::hardenedRuntimeInfo() (external/asmjit/asmjit/src/asmjit/core/virtmem.h:247) returns exactly the three facts the decision needs:

cpp
enum class HardenedRuntimeFlags : uint32_t {
  kNone = 0,
  kEnabled      = 0x00000001u,  // W^X is enforced; no RWX mapping
  kMapJit       = 0x00000002u,  // RWX obtainable via MAP_JIT (Apple)
  kDualMapping  = 0x00000004u   // RWX obtainable as a dual RW + RX mapping
};

kEnabled set with neither kMapJit nor kDualMapping means there is no road to executable memory on this host, and therefore no point constructing a JITContext at all. On our target that is precisely the answer: kEnabled (the RWX probe fails), no kMapJit (not Apple), no kDualMapping (shm_open is not available), and the JIT is unreachable.

The ask

In mode 1, consult VirtMem::hardenedRuntimeInfo() when the JITContext is constructed (lib/VM/JIT/arm64/JIT.cpp:26-31, which today only looks at the requested flag) and decline the JIT if the platform offers no road to executable memory — leaving isEnabled() false, which is already what HermesInternal.getRuntimeProperties()["JIT Enabled"] reports (lib/VM/JSLib/HermesInternal.cpp:291-296). Then mode 1 means what its own comment says on every platform, and one AArch64 library built with =1 runs on both kinds of host: JIT where the OS allows it, interpreter where it does not, with a truthful answer either way.

One honest caveat about the fix, because it is not unconditionally safe. Calling that query performs the probe — a one-page PROT_READ | PROT_WRITE | PROT_EXEC mmap (virtmem.cpp:737) — so it is only safe on a platform whose mmap returns MAP_FAILED for a request it will not honour, rather than terminating. That is a property of the POSIX layer beneath Hermes and not of asmjit; on our target our own layer answers it. Worth saying out loud so the change is not adopted as free.

What we are doing instead, and what it costs

We ship HERMESVM_ALLOW_JIT=0, which is correct for us today and is a real reduction in what the port can do. Measured on the same target, with the sampling profiler off in both:

HERMESVM_ALLOW_JIT libhermesvm_a.a
1 17,395,084 bytes
0 16,684,532 bytes

=0 removes the fifteen JIT translation units and all 59 asmjit link edges, and makes withEnableJIT(true) a reported no-op. =1 is kept as the documented re-test configure, in its own build tree, for the day the platform's policy changes — two libraries where one would do, and a build-tree switch where a run-time check would be.

(A note on that second tree, for anyone in the same position: decide whether asmjit is on your link line by reading the library's own generated libhermesvm-config.h, never by whether libasmjit.a exists on disk. An in-place reconfigure between the two modes leaves a stale asmjit archive behind, which is exactly when "does the file exist" answers wrong.)

Environment

target      aarch64, POSIX-like embedded, no executable memory available to an application
toolchain   clang 21.1.8, lld, libc++
build host  Windows 11 Pro 10.0.26200, x86-64
            CMake 3.31.6-msvc6, Ninja 1.13.2
hermes      5cee10abc93667ea5538caecaf0a457c66fa5bdc (static_h)

Related

  • The companion ask — that an asmjit failure should degrade to interpreting rather than calling hermes_fatal — is filed separately at #2187. This one is cheaper and would make that path unreachable in the common case; that one is what keeps a process alive when the probe lies, or when a policy changes under a running build.

Where this came from

Measured while cross-compiling Static Hermes as the JavaScript runtime of a native game host for an embedded AArch64 target. No file under the Hermes checkout was edited for this; it is a configuration observation. (The project is private at the time of filing, so no links.)