`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
CMakeLists.txt:310-313 documents three JIT modes, and the middle one promises a
platform check:
# 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:
#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
#endifOn 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:
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.)
Source: facebook/hermes