`static_h`: an asmjit failure is an unconditional `hermes_fatal`, so a platform that refuses executable memory turns a JIT *request* into a dead process instead of an interpreted one
Emitter::addToRuntime (lib/VM/JIT/arm64/JitEmitter.cpp:162-172) turns any
asmjit::Error into a process abort:
JITCompiledFunctionPtr Emitter::addToRuntime(asmjit::JitRuntime &jr) {
code.detach(&a);
JITCompiledFunctionPtr fn;
asmjit::Error err = jr.add(&fn, &code);
if (err) {
llvh::errs() << "AsmJit failed: " << asmjit::DebugUtils::errorAsString(err)
<< "\n";
hermes::hermes_fatal("AsmJit failed");
}
return fn;
}jr.add() is where asmjit first asks the operating system for executable memory.
On a platform that declines, that is not a programming error in Hermes and not a
corrupt code buffer — it is the OS refusing a capability. There is no fallback
path, so withEnableJIT(true) on such a platform means "this process will die",
and with withForceJIT(true) it dies inside makeHermesRuntime, before the
embedder's first evaluateJavaScript, because the first function compiled is
one the runtime itself runs during construction.
What we measured
Cross-compiling hermesvm_a at 5cee10a with HERMESVM_ALLOW_JIT=1 for an
AArch64 POSIX-like embedded target whose OS grants an application no executable
memory at all — its memory-permission facility has no executable permission to
ask for. Our POSIX layer under Hermes has to refuse the request itself, because
on that target asking the OS for an executable page is a violated precondition
that ends the process rather than an error return.
With RuntimeConfig::Builder().withEnableJIT(true).withForceJIT(true).withJITThreshold(1):
<our layer>: request #1 from mmap for 4096 bytes REFUSED (this target has no executable memory permission)
AsmJit failed: InvalidArgument
LLVM ERROR: AsmJit failedExit 1, and no JavaScript of ours ever ran.
The single-page request is asmjit's own hardened-runtime probe,
hasHardenedRuntime() in
external/asmjit/asmjit/src/asmjit/core/virtmem.cpp:737, which maps one
anonymous PROT_READ | PROT_WRITE | PROT_EXEC page purely to discover whether
W^X is available:
void* ptr = mmap(nullptr, pageSize, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
flag = kHardenedFlagEnabled;
}From there asmjit behaves exactly as documented: it concludes the runtime is
hardened, hasMapJitSupport() is false off Apple, the dual-mapping road wants
shm_open which this target does not have, and the error that comes back out of
jr.add is kErrorInvalidArgument. Every step up to hermes_fatal is
correct. The last step is Hermes converting a platform "no" into an abort.
Why this is not only about unusual targets
asmjit's probe is a run-time question on every non-Apple platform, so the
answer is not knowable at configure time. An ordinary AArch64 Linux build of
Hermes reaches the same line in any sandbox that refuses PROT_EXEC — seccomp
filters, SELinux execmem denial, a hardened container, some W^X-enforcing
kernel policies. Today all of those mean "Hermes does not run", where they could
mean "Hermes runs a little slower".
The asymmetry that is really the ask
The tree already does the right thing for the other class of JIT error.
JITContext::Compiler::compileCodeBlock (lib/VM/JIT/arm64/JIT.cpp:255-286)
handles an unsupported instruction or any other compile error by not aborting:
if (jc_.crashOnError_) {
printError(llvh::errs());
hermes_fatal(errMsg);
} else {
...
}
...
codeBlock_->setDontJIT(true);
return nullptr;It marks the code block don't-JIT, returns nullptr, and the interpreter runs the
function. Aborting is opt-in, behind setCrashOnError
(include/hermes/VM/JIT/arm64/JIT.h:118, default false, reachable from the CLI as
the hidden -Xjit-crash-on-error). So the project's own position is already that
a JIT that cannot compile something should fall back rather than crash — and an
asmjit memory failure, which is strictly less the caller's fault than an
unsupported instruction, is the one case that ignores it.
What we would like
Two halves, and the tree has the vocabulary for both:
On an
asmjit::Errorfromjr.add, degrade instead of aborting. RespectcrashOnError_the way the compile-error path does; otherwise disable the JIT for the remainder of the process —JITContext::setEnabled(false)already exists and is already called (lib/VM/Runtime.cpp:482) — and returnnullptrso the interpreter takes the function. An allocation failure is not per-code-block, so switching the whole context off is the honest response; retrying every function to fail again is not.Let a host find out.
HermesInternal.getRuntimeProperties()already publishes"JIT Enabled"(lib/VM/JSLib/HermesInternal.cpp:291-296), but it reportsgetJITContext().isEnabled()— which is the state that was asked for. If (1) calledsetEnabled(false), that property would become truthful for free, and a host that requested a JIT and got an interpreter could say so in its own diagnostics instead of inferring it from still being alive.
"This runtime has no JIT", "this runtime's JIT is switched off" and "this runtime's JIT was asked for and the platform refused it" are three different answers, and only the third one currently has no way to be given.
Repro
The reading above is from the cross-compiled target. The same path should be
reachable on any AArch64 host without special hardware by making asmjit's probe
fail — interposing mmap so a PROT_EXEC request returns MAP_FAILED, for
example with LD_PRELOAD, then:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DHERMES_ENABLE_TEST_SUITE=OFF -DHERMES_ENABLE_NAPI=OFF \
-DHERMESVM_ALLOW_JIT=1
cmake --build build --target hermes
echo 'function f(x) { return x + 1 } var s = 0; for (var i = 0; i < 1e5; ++i) s = f(s); print(s);' > jit.js
build/bin/hermes -Xjit=force jit.jsExpected under the interposer: s is printed, slowly. Expected today:
AsmJit failed: <error> / LLVM ERROR: AsmJit failed.
We have not run that LD_PRELOAD variant — it is offered as a convenience for
a maintainer, not as a second measurement. What we ran is the cross-compiled
target, and the three lines quoted above are its exact output.
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), HERMESVM_ALLOW_JIT=1Related
- The companion ask — that
HERMESVM_ALLOW_JIT=1("enabled if supported by the platform") should be able to consult asmjit's run-time hardened-runtime query rather than resolving support from predefines alone — is filed separately at #2186. That one is about one library serving both a JIT-capable and a JIT-refusing host; this one is about not dying when the answer turns out to be "no".
Where this came from
Measured while cross-compiling Static Hermes as the JavaScript runtime of a
native game host for an embedded AArch64 target. The runtime itself was correct
throughout — a 1,134-tick floating-point replay compared byte for byte against
three x86-64 runtimes agreed in every field — and our shipped library is built
with HERMESVM_ALLOW_JIT=0 as a result of this finding, which makes
withEnableJIT(true) a reported no-op rather than an abort. That workaround is
what this ask would make unnecessary. (The project is private at the time of
filing, so no links.)
Source: facebook/hermes