From 4b00d3613f02a69e9ee33ccef02cd4f6318f3ede Mon Sep 17 00:00:00 2001 From: 1820893135-pixel <1820893135@qq.com> Date: Wed, 23 Sep 2026 17:07:58 +0800 Subject: [PATCH] Validate snapshot offsets before reading the compiled code jerry_exec_snapshot() trusted the offsets stored in the snapshot header and in the bytecode it deserializes: * func_offsets[func_index] was used directly to locate the compiled code, and its status_flags field was read without checking that the offset was inside the snapshot; * the size field of the compiled code header was used as the block size, so a block could claim to extend beyond the input buffer and the argument-header writes and literal loops below would touch memory outside it; * the literal offsets in the literal table were used the same way when recursively loading a nested function. A short malformed snapshot (73 bytes in the report) therefore made the engine read and write outside the buffer: ==ERROR: AddressSanitizer: SEGV #0 jerry_exec_snapshot jerry-snapshot.c:915 Check the fixed header size, the declared block size and the literal offsets against the end of the snapshot before any of them is used, and propagate the buffer bounds into snapshot_load_compiled_code() for the recursive case. Malformed input now returns a normal exception instead of dereferencing out-of-bounds memory. Adds a regression case to tests/unit-core/test-snapshot.c. JerryScript-DCO-1.0-Signed-off-by: 1820893135-pixel <1820893135@qq.com> --- jerry-core/api/jerry-snapshot.c | 53 +++++++++++++++++++++++++++++++-- tests/unit-core/test-snapshot.c | 48 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/jerry-core/api/jerry-snapshot.c b/jerry-core/api/jerry-snapshot.c index 33760b299f..e6484c569a 100644 --- a/jerry-core/api/jerry-snapshot.c +++ b/jerry-core/api/jerry-snapshot.c @@ -542,11 +542,32 @@ snapshot_load_compiled_code (const uint8_t *base_addr_p, /**< base address of th * current primary function */ const uint8_t *literal_base_p, /**< literal start */ cbc_script_t *script_p, /**< script */ - bool copy_bytecode) /**< byte code should be copied to memory */ + bool copy_bytecode, /**< byte code should be copied to memory */ + size_t snapshot_size, /**< total size of the snapshot buffer */ + const uint8_t *snapshot_end_p) /**< end of the snapshot buffer */ { + /* base_addr_p is derived from an offset stored in the snapshot, so it may point + * anywhere up to the end of the buffer. Do not dereference it before making sure + * the fixed part of the compiled code header is actually there. */ + if (base_addr_p > snapshot_end_p + || (size_t) (snapshot_end_p - base_addr_p) < sizeof (ecma_compiled_code_t)) + { + return NULL; + } + ecma_compiled_code_t *bytecode_p = (ecma_compiled_code_t *) base_addr_p; uint32_t code_size = ((uint32_t) bytecode_p->size) << JMEM_ALIGNMENT_LOG; + /* The size stored in the compiled code header is only as trustworthy as the rest + * of the snapshot. Everything below (the arguments header fields, the literal + * loops and the code_size byte copy) assumes the whole block lies inside the input + * buffer, so check that before reading any further. */ + if (code_size < sizeof (cbc_uint8_arguments_t) + || code_size > (size_t) (snapshot_end_p - base_addr_p)) + { + return NULL; + } + #if JERRY_BUILTIN_REGEXP if (!CBC_IS_FUNCTION (bytecode_p->status_flags)) { @@ -698,8 +719,22 @@ snapshot_load_compiled_code (const uint8_t *base_addr_p, /**< base address of th else { ecma_compiled_code_t *literal_bytecode_p; + + /* literal_offset is another unchecked offset from the snapshot. */ + if (literal_offset > snapshot_size + || snapshot_size - literal_offset < sizeof (ecma_compiled_code_t)) + { + return NULL; + } + literal_bytecode_p = - snapshot_load_compiled_code (base_addr_p + literal_offset, literal_base_p, script_p, copy_bytecode); + snapshot_load_compiled_code (base_addr_p + literal_offset, literal_base_p, script_p, copy_bytecode, + snapshot_size, snapshot_end_p); + + if (literal_bytecode_p == NULL) + { + return NULL; + } ECMA_SET_INTERNAL_VALUE_POINTER (literal_start_p[i], literal_bytecode_p); } @@ -910,6 +945,16 @@ jerry_exec_snapshot (const uint32_t *snapshot_p, /**< snapshot */ JERRY_ASSERT ((header_p->lit_table_offset % sizeof (uint32_t)) == 0); uint32_t func_offset = header_p->func_offsets[func_index]; + + /* func_offsets comes straight from the snapshot header and is never checked, so + * the compiled code it points at may be outside the buffer. Verify it is inside + * before reading status_flags, otherwise a handful of crafted bytes walk off the + * end of the snapshot. */ + if (func_offset > snapshot_size || snapshot_size - func_offset < sizeof (ecma_compiled_code_t)) + { + return jerry_throw_sz (JERRY_ERROR_TYPE, ecma_get_error_msg (ECMA_ERR_INVALID_SNAPSHOT_FORMAT)); + } + ecma_compiled_code_t *bytecode_p = (ecma_compiled_code_t *) (snapshot_data_p + func_offset); if (bytecode_p->status_flags & CBC_CODE_FLAGS_STATIC_FUNCTION) @@ -971,7 +1016,9 @@ jerry_exec_snapshot (const uint32_t *snapshot_p, /**< snapshot */ bytecode_p = snapshot_load_compiled_code ((const uint8_t *) bytecode_p, literal_base_p, script_p, - (exec_snapshot_opts & JERRY_SNAPSHOT_EXEC_COPY_DATA) != 0); + (exec_snapshot_opts & JERRY_SNAPSHOT_EXEC_COPY_DATA) != 0, + snapshot_size, + snapshot_data_p + snapshot_size); if (bytecode_p == NULL) { diff --git a/tests/unit-core/test-snapshot.c b/tests/unit-core/test-snapshot.c index 4de459c1a5..fdc40b05a2 100644 --- a/tests/unit-core/test-snapshot.c +++ b/tests/unit-core/test-snapshot.c @@ -23,6 +23,18 @@ */ #define SNAPSHOT_BUFFER_SIZE (256) +/* malformed_snapshot_literal_offset: 73 bytes */ +static const uint8_t malformed_snapshot_literal_offset[] = { + 0x53, 0x4e, 0x41, 0x50, 0x4a, 0x52, 0x52, 0x59, 0x46, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0xf0, 0xff, 0xff, 0xff, 0x05, 0x00, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x02, 0x07, 0x00, 0x00, 0x00, + 0x47, 0x01, 0x51, 0x01, 0x35, 0x00, 0xdf, 0x01, 0x56, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x01, 0x00, 0x61, 0x00, + 0x00, +}; + + /** * Maximum size of literal buffer */ @@ -436,11 +448,47 @@ test_snapshot_with_user (void) } } /* test_snapshot_with_user */ +/* + * Executing a snapshot whose offsets are not validated must fail cleanly instead + * of reading outside the input buffer. The two inputs below come from two + * defect: the compiled-code offset stored in the snapshot header is used without + * being checked against the buffer size. + */ +static void +test_malformed_snapshot (void) +{ + if (!jerry_feature_enabled (JERRY_FEATURE_SNAPSHOT_EXEC)) + { + return; + } + + /* jerry_exec_snapshot() patches the snapshot in place, so hand it a writable + * copy of the input rather than the read-only array above. */ + uint8_t snapshot_buffer[sizeof (malformed_snapshot_literal_offset)]; + jerry_value_t result; + + jerry_init (JERRY_INIT_EMPTY); + + memcpy (snapshot_buffer, malformed_snapshot_literal_offset, sizeof (malformed_snapshot_literal_offset)); + result = jerry_exec_snapshot ((const uint32_t *) (snapshot_buffer + 4), + sizeof (malformed_snapshot_literal_offset) - 4, + 0, + JERRY_SNAPSHOT_EXEC_COPY_DATA, + NULL); + TEST_ASSERT (jerry_value_is_exception (result)); + jerry_value_free (result); + + + jerry_cleanup (); +} /* test_malformed_snapshot */ + int main (void) { TEST_INIT (); + test_malformed_snapshot (); + test_static_snapshot (); test_merge_snapshot ();