Skip to content

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 #2186

Description

@altween

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
#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:

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

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.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions