This episode dissects the security implications of Hermes' AOT architecture compared to JIT engines, the concepts of signed bundles and content integrity, then how to integrate app hardening and runtime privacy into the production build.

Episode 12 closed with secure runtime practices: bundles loaded with verification, and dynamic eval patterns avoided. Now we widen the lens. The big question in episode 13: does Hermes' compilation architecture — Ahead-of-Time, without JIT — fundamentally change the security profile? The short answer: yes, and most of the changes work in our favor.
This episode's roadmap: the security impact of AOT versus JIT, signed bundles and content integrity, then integration with app hardening and runtime privacy.
JIT engines like V8 or JavaScriptCore have a point of complexity that Hermes doesn't: they write new machine code into memory at runtime. Memory pages that start writable and later become executable (W+X) are a highly attractive zone for attackers. Techniques like JIT spraying — planting malicious instructions into data that the JIT later interprets as code — become possible precisely because of this mechanism. Any bug in the JIT compiler can also turn into a "code execution" primitive.
Hermes deliberately has no JIT. Code is executed by an interpreter over bytecode produced at build time, deterministic and identical across all devices. No new memory pages are made executable at runtime, so the JIT-based class of attacks is automatically closed. Bytecode is also stored read-only and file-backed, so the operating system can evict those pages when memory runs low.
| Aspect | JIT engine (V8/JSC) | Hermes (AOT, interpreter) |
|---|---|---|
| Machine code created at runtime | Yes, when hot paths are found | No, everything is already bytecode |
| JIT attack surface | JIT spraying, compiler bugs | None |
| Execution determinism | Depends on the runtime profile | Identical across devices |
| Version coupling | Fragile bytecode/code cache | Bytecode locked to the engine version |
The consequence of that determinism: .hbc bytecode compiled with a specific hermesc version only runs on a compatible Hermes engine version. It's like a rigid format contract — good for security (nobody can "slip in" bytecode from another version), but it requires always bumping the bytecode together with the Hermes version during upgrades.
Info
Another security impact of AOT: since bytecode isn't "warmed up" at runtime like JIT, startup time doesn't open a window where the engine performs exploitable compilation. What remains is a relatively simple interpreter that's easy to audit.
Determining "this bundle is ours" needs more than a hash. A SHA-256 hash proves the bundle hasn't changed since it was computed, but not who computed it. For that we need a cryptographic signature: the bundle is signed with a private key on the build side, and the app verifies it using an embedded public key.
At the app level, this chain already exists: the APK/IPA is signed before entering the store (Android App Signing, iOS code signing). That means the bytecode inside is protected too, as long as you don't download the bundle from anywhere else. If you verify the app signature on the server side — for example using the Play Integrity API — you also ensure that whoever requests a premium feature is your genuine app, not a tampered version:
apksigner verify --print-certs app-release.apkFor bundles downloaded at runtime (OTA updates), the same content integrity principle applies at the Hermes layer: compute the bundle digest on the build side, include it as signed metadata, then verify before execution. Never store digests as editable constants — put them in a protected native layer:
const Integrity = NativeModules.BundleIntegrity;
async function loadBundle(bytes) {
const ok = await Integrity.verify(bytes);
if (!ok) throw new Error("bundel tidak ditandatangani oleh issuer resmi");
return bytes;
}Hardening builds a defense layer around the runtime to make static and dynamic analysis more expensive. For React Native projects with Hermes:
libhermes-inspector.so — make sure your release build doesn't load it.hermesFlags so the bytecode produced is consistent and auditable.react {
hermesFlags = ["-O", "-output-source-map"]
}
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
}
}
}Warning
minifyEnabled true only affects native Java/Kotlin code — not Hermes bytecode. To protect JavaScript logic, add an obfuscator on the bundling side (for example using a transformer or Metro plugin) and move sensitive logic into native modules.
Runtime security means nothing if the app quietly leaks data. Some practices that must become habits:
function redactUserData(user) {
return {
id: user.id,
role: user.role,
email: mask(user.email),
};
}
function mask(value) {
const at = value.indexOf("@");
return at === -1 ? "***" : `***${value.slice(at)}`;
}The principle: app code can be complex, but personal data must always be treated as something whose trail needs accounting — who can access it, where it goes, and how long it lives.
The JIT-free AOT choice puts Hermes in an interesting security position: the JIT-class attack surface is gone, and deterministic bytecode makes runtime behavior easy to predict and audit. What falls on your shoulders is completing that foundation with bundle integrity, build hardening, and privacy discipline.
The essentials to take home:
In episode 14, we enter Application Architecture & Isolation: keeping the boundary between business logic and native modules, modularizing bundles, and running untrusted scripts in isolated ephemeral contexts. See you there!