Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,21 @@ jobs:
ulimit -c unlimited
./tests/test-structs

- name: "Test: test-avmpack with valgrind"
if: matrix.library-arch == ''
timeout-minutes: 10
working-directory: build
run: |
ulimit -c unlimited
valgrind --error-exitcode=1 ./tests/test-avmpack

- name: "Test: test-avmpack"
timeout-minutes: 10
working-directory: build
run: |
ulimit -c unlimited
./tests/test-avmpack

- name: "Test: test_etest.avm with valgrind"
if: matrix.library-arch == ''
timeout-minutes: 5
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build-linux-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ jobs:
make test-heap &&
make test-mailbox &&
make test-structs &&
make test-avmpack &&
file ./tests/test-erlang &&
./tests/test-erlang -s prime_smp &&
file ./tests/test-enif &&
Expand All @@ -246,6 +247,8 @@ jobs:
./tests/test-mailbox &&
file ./tests/test-structs &&
./tests/test-structs &&
file ./tests/test-avmpack &&
./tests/test-avmpack &&
file ./src/AtomVM &&
./src/AtomVM tests/libs/etest/test_etest.avm &&
./src/AtomVM tests/libs/estdlib/test_estdlib.avm &&
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
from ref ticks, `TERM_BOXED_REFERENCE_PROCESS_SIZE` for process references (aliases), or
`TERM_BOXED_REFERENCE_MAX_SIZE` to fit any reference. `REF_SIZE` still expands to the short
reference size, but now emits a compiler warning
- ESP32 builds now fail at configure time when `boot.avm` does not fit its partition, instead of
producing an image that is silently truncated at flash time
- `atomvm:read_priv/2` now raises `invalid_avm` when the requested resource is present but its
recorded size does not fit the AVM pack section holding it, instead of reporting the resource as
missing. A resource that is genuinely absent still returns `undefined`

### Removed
- Removed `ahttp_client` support for obsolete line folding (RFC 9112 §5.2); folded header and
Expand Down Expand Up @@ -86,6 +91,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a JIT crash (`EXC_BAD_ACCESS`/SIGBUS) on Apple Silicon
- Fixed the ESP32 event poller re-blocking after running a listener, which could delay a process
readied by a driver (e.g. an active-mode socket message) until the next event or timer tick
- Fixed AVM pack lookups not finding sections placed after a data file
- Fixed an out-of-bounds read crashing the VM on an AVM pack section lookup miss (e.g. `atomvm:read_priv/2` for an unpacked file)
- Fixed truncated or oversized AVM packs being accepted; they are now rejected at load instead of failing late
- Fixed generic_unix leaking the file mapping when rejecting an invalid `.avm` file

## [0.7.0-alpha.1] - 2026-04-06

Expand Down
2 changes: 2 additions & 0 deletions doc/src/packbeam-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ requirement that the `Path` component of a normal file be a simple file name.

Packbeam files end with a special `end` header. The `size` field of the `end` header is 0 bytes.

The `end` header is mandatory: the runtime uses it to confirm a pack is complete, and rejects a pack whose terminator is missing (e.g. a truncated flash image) instead of scanning past its data.

#### Example `end` header

The following sequence of bytes encodes the `end` header:
Expand Down
7 changes: 5 additions & 2 deletions libs/eavmlib/src/atomvm.erl
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,14 @@ rand_bytes(_Len) ->
%%-----------------------------------------------------------------------------
%% @param App application name.
%% @param Path path to the resource.
%% @returns Binary containing the resource content.
%% @returns Binary containing the resource content, or `undefined' if it is not in any loaded
%% AVM pack.
%% @doc This function allows to fetch priv/ resources content.
%% Raises `invalid_avm' if the resource is present but its recorded size does not fit
%% the AVM pack section holding it.
%% @end
%%-----------------------------------------------------------------------------
-spec read_priv(App :: atom(), Path :: list()) -> binary().
-spec read_priv(App :: atom(), Path :: list()) -> binary() | undefined.
read_priv(_App, _Path) ->
erlang:nif_error(undefined).

Expand Down
192 changes: 139 additions & 53 deletions src/libAtomVM/avmpack.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
#include <stdio.h>

#define AVMPACK_SIZE 24
#define AVMPACK_SECTION_HEADER_SIZE 12
#define AVMPACK_MIN_SECTION_SIZE 16
#define AVMPACK_END_MARKER_SIZE 16

static inline int pad(int size)
static inline size_t pad(size_t size)
{
return ((size + 4 - 1) >> 2) << 2;
}
Expand All @@ -52,83 +55,166 @@ bool avmpack_is_valid(const void *avmpack_binary, uint32_t size)
return memcmp(avmpack_binary, pack_header, AVMPACK_SIZE) == 0;
}

int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask, uint32_t flags_val, const void **ptr, uint32_t *size, const char **name)
enum AVMPackSectionKind
{
int offset = AVMPACK_SIZE;
const uint32_t *flags;
AVMPackSectionInvalid,
AVMPackSectionRegular,
AVMPackSectionEnd
};

do {
const uint32_t *sizes = ((const uint32_t *) (avmpack_binary)) + offset / sizeof(uint32_t);
flags = ((const uint32_t *) (avmpack_binary)) + 1 + offset / sizeof(uint32_t);
struct AVMPackSection
{
uint32_t size;
uint32_t data_size;
uint32_t flags;
const char *name;
const void *data;
};

if ((ENDIAN_SWAP_32(*flags) & flags_mask) == flags_val) {
const char *found_section_name = (const char *) (sizes + 3);
int section_name_len = pad(strlen(found_section_name) + 1);
static enum AVMPackSectionKind read_section(const void *avmpack_binary, uint32_t avmpack_size,
uint32_t offset, struct AVMPackSection *section)
{
if (offset > avmpack_size || avmpack_size - offset < AVMPACK_MIN_SECTION_SIZE) {
return AVMPackSectionInvalid;
}

*ptr = sizes + 3 + section_name_len / sizeof(uint32_t);
*size = ENDIAN_SWAP_32(*sizes);
*name = (const char *) (sizes + 3);
return 1;
// A pack may sit at any byte offset (e.g. atomvm:add_avm_pack_binary/2 with a sub-binary).
const uint8_t *header = (const uint8_t *) avmpack_binary + offset;
uint32_t section_size = READ_32_UNALIGNED(header);
uint32_t flags = READ_32_UNALIGNED(header + 4);
const char *name = (const char *) (header + AVMPACK_SECTION_HEADER_SIZE);

if (section_size == 0) {
// The min-size guard above keeps the terminator name bytes in bounds for strcmp.
if (flags == 0 && (strcmp(name, "end") == 0 || strcmp(name, "END") == 0)) {
section->size = 0;
section->data_size = 0;
section->flags = 0;
section->name = name;
section->data = (const uint8_t *) avmpack_binary + offset + AVMPACK_END_MARKER_SIZE;
return AVMPackSectionEnd;
}
return AVMPackSectionInvalid;
}

offset += ENDIAN_SWAP_32(*sizes);
if (section_size < AVMPACK_MIN_SECTION_SIZE || (section_size & 3) != 0
|| section_size > avmpack_size - offset) {
return AVMPackSectionInvalid;
}

} while (*flags);
size_t name_region = section_size - AVMPACK_SECTION_HEADER_SIZE;
const void *nul = memchr(name, '\0', name_region);
if (nul == NULL) {
return AVMPackSectionInvalid;
}
size_t padded_name_len = pad((size_t) ((const char *) nul - name) + 1);
if (padded_name_len > name_region) {
return AVMPackSectionInvalid;
}

return 0;
section->size = section_size;
section->data_size = section_size - AVMPACK_SECTION_HEADER_SIZE - (uint32_t) padded_name_len;
section->flags = flags;
section->name = name;
section->data
= (const uint8_t *) avmpack_binary + offset + AVMPACK_SECTION_HEADER_SIZE + padded_name_len;

return AVMPackSectionRegular;
}

int avmpack_find_section_by_name(const void *avmpack_binary, const char *name, const void **ptr, uint32_t *size)
int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t avmpack_size,
uint32_t flags_mask, uint32_t flags_val, const void **ptr, uint32_t *size, const char **name)
{
int offset = AVMPACK_SIZE;
const uint32_t *flags;
uint32_t offset = AVMPACK_SIZE;
struct AVMPackSection section;
enum AVMPackSectionKind kind;

while ((kind = read_section(avmpack_binary, avmpack_size, offset, &section))
!= AVMPackSectionInvalid) {
if ((section.flags & flags_mask) == flags_val) {
*ptr = section.data;
*size = section.data_size;
*name = section.name;
return 1;
}
if (kind == AVMPackSectionEnd) {
break;
}
offset += section.size;
}

do {
const uint32_t *sizes = ((const uint32_t *) (avmpack_binary)) + offset / sizeof(uint32_t);
flags = ((const uint32_t *) (avmpack_binary)) + 1 + offset / sizeof(uint32_t);
return 0;
}

const char *found_section_name = (const char *) (sizes + 3);
if (!strcmp(name, found_section_name)) {
int section_name_len = pad(strlen(found_section_name) + 1);
int avmpack_find_section_by_name(const void *avmpack_binary, uint32_t avmpack_size,
const char *name, const void **ptr, uint32_t *size)
{
uint32_t offset = AVMPACK_SIZE;
struct AVMPackSection section;

*ptr = sizes + 3 + section_name_len / sizeof(uint32_t);
*size = ENDIAN_SWAP_32(*sizes);
while (read_section(avmpack_binary, avmpack_size, offset, &section) == AVMPackSectionRegular) {
if (!strcmp(name, section.name)) {
*ptr = section.data;
*size = section.data_size;
return 1;
}

offset += ENDIAN_SWAP_32(*sizes);

} while (*flags);
offset += section.size;
}

return 0;
}

void *avmpack_fold(void *accum, const void *avmpack_binary, avmpack_fold_fun fold_fun)
void *avmpack_fold(
void *accum, const void *avmpack_binary, uint32_t avmpack_size, avmpack_fold_fun fold_fun)
{
int offset = AVMPACK_SIZE;
uint32_t size = 0;

do {
const uint32_t *size_ptr = ((const uint32_t *) (avmpack_binary)) + offset / sizeof(uint32_t);
size = ENDIAN_SWAP_32(*size_ptr);
if (size > 0) {
const uint32_t *flags_ptr = size_ptr + 1;
uint32_t flags = ENDIAN_SWAP_32(*flags_ptr);
const char *section_name = (const char *) (size_ptr + 3);
int section_name_len = pad(strlen(section_name) + 1);
accum = fold_fun(
accum,
size_ptr, size,
size_ptr + 3 + section_name_len / sizeof(uint32_t),
flags,
section_name);
offset += size;
}
} while (size > 0);
uint32_t offset = AVMPACK_SIZE;
struct AVMPackSection section;

while (read_section(avmpack_binary, avmpack_size, offset, &section) == AVMPackSectionRegular) {
accum = fold_fun(accum, (const uint8_t *) avmpack_binary + offset, section.size,
section.data, section.flags, section.name);
offset += section.size;
}

return accum;
}

bool avmpack_is_complete(const void *avmpack_binary, uint32_t exact_size)
{
// The header and every section are multiples of 4, so a complete pack's size must be too.
if (!avmpack_is_valid(avmpack_binary, exact_size)
|| exact_size < AVMPACK_SIZE + AVMPACK_END_MARKER_SIZE || (exact_size & 3) != 0) {
return false;
}

struct AVMPackSection section;

return read_section(avmpack_binary, exact_size, exact_size - AVMPACK_END_MARKER_SIZE, &section)
== AVMPackSectionEnd;
}

bool avmpack_compute_size(const void *avmpack_binary, uint32_t max_size, uint32_t *real_size)
{
if (!avmpack_is_valid(avmpack_binary, max_size)) {
return false;
}

uint32_t offset = AVMPACK_SIZE;
struct AVMPackSection section;
enum AVMPackSectionKind kind;
while ((kind = read_section(avmpack_binary, max_size, offset, &section))
== AVMPackSectionRegular) {
offset += section.size;
}

if (kind == AVMPackSectionEnd) {
*real_size = offset + AVMPACK_END_MARKER_SIZE;
return true;
}

return false;
}

static void in_memory_avm_pack_destructor(struct AVMPackData *obj, GlobalContext *global);

const struct AVMPackInfo in_memory_avm_pack_info = {
Expand Down
Loading
Loading