Fix atomvm:read_priv and detect truncated packs#2340
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
PR Review: AVM pack bounds and truncation hardeningRange reviewed: The bounded section walker is a strong improvement over the previous unbounded scans, and the new Findings1. [Blocker] Validation dereferences potentially unaligned binary dataFiles:
The Small fix: use alignment-safe loads for both section words and the priv length prefix. diff --git a/src/libAtomVM/avmpack.c b/src/libAtomVM/avmpack.c
@@
- const uint32_t *header = (const uint32_t *) ((const uint8_t *) avmpack_binary + offset);
- uint32_t section_size = ENDIAN_SWAP_32(header[0]);
- uint32_t flags = ENDIAN_SWAP_32(header[1]);
- const char *name = (const char *) (header + 3);
+ const uint8_t *header = (const uint8_t *) avmpack_binary + offset;
+ uint32_t section_size_be;
+ uint32_t flags_be;
+ memcpy(§ion_size_be, header, sizeof(section_size_be));
+ memcpy(&flags_be, header + sizeof(section_size_be), sizeof(flags_be));
+ uint32_t section_size = ENDIAN_SWAP_32(section_size_be);
+ uint32_t flags = ENDIAN_SWAP_32(flags_be);
+ const char *name = (const char *) header + AVMPACK_SECTION_HEADER_SIZE;
diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c
@@
- uint32_t file_size = READ_32_ALIGNED((uint32_t *) bin_data);
+ uint32_t file_size_be;
+ memcpy(&file_size_be, bin_data, sizeof(file_size_be));
+ uint32_t file_size = ENDIAN_SWAP_32(file_size_be);Add a regression using a valid pack at 2. [Blocker]
|
22d6864 to
2a6c79b
Compare
|
fresh AMP: your call on these PR review: AVM pack bounds and truncation handlingRange reviewed: SummaryThe new bounded section parser is a substantial safety improvement: it uses subtraction-based bounds checks, handles unaligned pack bases, validates section names before string operations, and propagates exact pack sizes through One issue should block merge: the JIT still locates the end marker with a flags-only lookup, but both ordinary data sections and the end marker use flags Findings[High] JIT end-marker lookup matches ordinary zero-flag data sectionsFiles:
(section.flags & flags_mask) == flags_valbefore checking whether the parsed section is the terminator. The JIT calls it with If such a section precedes the terminator:
This can reject caching, corrupt a resource name, or erase/write flash occupied by later pack sections. The flags ambiguity predates this range, but the PR modifies this exact lookup path and claims that scans now stop only at the end marker. The new bounded implementation should not preserve the ambiguity in the JIT safety path. Suggested fix: expose a terminator-specific lookup instead of encoding “end” as a flags query. For example: diff --git a/src/libAtomVM/avmpack.h b/src/libAtomVM/avmpack.h
@@
int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t avmpack_size,
uint32_t flags_mask, uint32_t flags_value, const void **ptr, uint32_t *size, const char **name);
+bool avmpack_find_end(
+ const void *avmpack_binary, uint32_t avmpack_size, const void **ptr, const char **name);
diff --git a/src/libAtomVM/avmpack.c b/src/libAtomVM/avmpack.c
@@
+bool avmpack_find_end(
+ const void *avmpack_binary, uint32_t avmpack_size, const void **ptr, const char **name)
+{
+ uint32_t offset = AVMPACK_SIZE;
+ struct AVMPackSection section;
+ enum AVMPackSectionKind kind;
+
+ while ((kind = read_section(avmpack_binary, avmpack_size, offset, §ion))
+ != AVMPackSectionInvalid) {
+ if (kind == AVMPackSectionEnd) {
+ *ptr = section.data;
+ *name = section.name;
+ return true;
+ }
+ offset += section.size;
+ }
+ return false;
+}
diff --git a/src/libAtomVM/jit_stream_flash.c b/src/libAtomVM/jit_stream_flash.c
@@
- if (!avmpack_find_section_by_flag(avmpack_data->data, avmpack_data->size,
- END_OF_FILE_MASK, END_OF_FILE, &end_offset, &end_size, &end_name)) {
+ if (!avmpack_find_end(
+ avmpack_data->data, avmpack_data->size, &end_offset, &end_name)) {Apply the JIT replacement at both call sites. Add a regression test with a zero-flag regular section followed by another section and the real terminator; assert that the returned end pointer is immediately after the real terminator and that JIT allocation starts after the complete pack. [Medium] Invalid ESP32 partition packs leak flash mappingsFile:
Truncated packs newly take the validation-failure path, so repeatedly opening one can exhaust mapping resources. Calling only The invalid-header failure path already leaked before this range; truncation validation broadens the set of inputs that take it. Suggested fix: separate mapping from registration/ownership. Map DATA first, validate the pack, allocate Add failure-path tests for both invalid pack data and metadata allocation failure, asserting that DATA and IBUS mappings and the registry entry are released. [Medium] Unix and Emscripten loaders narrow file sizes before validationFiles: The generic Unix mapped-file size is The binary NIF already handles this correctly by rejecting Suggested fix: follow the same pattern in both loaders: diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c
@@
if (IS_NULL_PTR(mapped)) {
return AVM_OPEN_CANNOT_OPEN;
}
- if (UNLIKELY(!avmpack_is_complete(mapped->mapped, (uint32_t) mapped->size))) {
+ if (UNLIKELY(mapped->size > UINT32_MAX
+ || !avmpack_is_complete(mapped->mapped, (uint32_t) mapped->size))) {
mapped_file_close(mapped);
return AVM_OPEN_INVALID;
}
diff --git a/src/platforms/emscripten/src/lib/sys.c b/src/platforms/emscripten/src/lib/sys.c
@@
- if (UNLIKELY(!avmpack_is_complete(data, (uint32_t) data_size))) {
+ if (UNLIKELY(data_size > UINT32_MAX
+ || !avmpack_is_complete(data, (uint32_t) data_size))) {A sparse-file test on generic Unix would cover this without allocating 4 GiB of physical storage. [Low] JIT compares unrelated pointers with relational operatorsFile:
This comparison predates the reviewed commits but remains part of the modified JIT end-boundary path. Suggested fix: compare integer addresses explicitly: diff --git a/src/libAtomVM/jit_stream_flash.c b/src/libAtomVM/jit_stream_flash.c
@@
- const void *max_end_offset = NULL;
+ uintptr_t max_end_addr = 0;
@@
- if (end_offset > max_end_offset) {
- max_end_offset = end_offset;
+ uintptr_t end_addr = (uintptr_t) end_offset;
+ if (end_addr > max_end_addr) {
+ max_end_addr = end_addr;
}
@@
- if (IS_NULL_PTR(max_end_offset)) {
+ if (max_end_addr == 0) {
@@
- uintptr_t max_end_offset_page = ((((uintptr_t) max_end_offset) - 1) & ~(FLASH_SECTOR_SIZE - 1));
+ uintptr_t max_end_offset_page = ((max_end_addr - 1) & ~(FLASH_SECTOR_SIZE - 1));Additional test gaps
Verification performed
These tests do not currently exercise the blocking zero-flag data-section/JIT interaction. Pre-existing issue noticed (not introduced by this range)
diff --git a/src/platforms/esp32/components/avm_sys/sys.c b/src/platforms/esp32/components/avm_sys/sys.c
@@
struct InMemoryAVMPack *in_memory_avm = malloc(sizeof(struct InMemoryAVMPack));
- if (IS_NULL_PTR(avmpack_data)) {
+ if (IS_NULL_PTR(in_memory_avm)) {
free(file_data);
return AVM_OPEN_FAILED_ALLOC;
} |
The packbeam scanners were unbounded, so a lookup miss (e.g. read_priv for an unpacked file) ran off the pack into erased flash and crashed. Bound and validate every scan against the stored pack size. The walk now stops only at the end marker: the old scanners stopped at the first data file (flags 0), hiding every section after it. Signed-off-by: Davide Bettio <davide@uninstall.it>
Reject truncated or incomplete packs (e.g. an image flashed to a too-small partition) at load instead of failing late, validating by tail check or bounded walk. Signed-off-by: Davide Bettio <davide@uninstall.it>
Fail at configure time when boot.avm does not fit its partition. Signed-off-by: Davide Bettio <davide@uninstall.it>
A priv entry whose length prefix does not fit its own section was skipped, so a corrupt pack was indistinguishable from a missing file. Raise invalid_avm instead. A genuine miss still returns undefined. Read the length prefix unaligned: an in-place pack may sit at any byte offset and the direct load faults Xtensa/Cortex-M0+. Signed-off-by: Davide Bettio <davide@uninstall.it>
The generic_unix invalid-pack rejection in sys_open_avm_from_file returned without releasing the mapping, leaking it and its file descriptor. Pre-existing, but truncated packs now take this path. Signed-off-by: Davide Bettio <davide@uninstall.it>
The XIP_SRAM_BASE bound spans past the flash, so a corrupt section size could steer bounded header reads into unmapped address space. Signed-off-by: Davide Bettio <davide@uninstall.it>
The packbeam (.avm) scanners were unbounded and trusted section sizes,
so a lookup miss (e.g. atomvm:read_priv/2 for an unpacked file) ran off
the pack into erased flash and crashed the VM. A truncated pack failed
the same way.
Bound and validate every scan, detect truncation at load, and add a
build-time guard for the one pack known at build time. No on-disk
format change.
Also user visible:
are found
of reporting a missing file
These changes are made under both the "Apache 2.0" and the "GNU Lesser
General Public License 2.1 or later" license terms (dual license).
SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later