Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/build-and-test-macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ jobs:
run: |
./tests/test-term

- name: "Test: test-avmpack"
working-directory: build
run: |
./tests/test-avmpack

- name: "Test: test_etest.avm"
timeout-minutes: 5
working-directory: build
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/build-and-test-on-freebsd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ jobs:
cd build
./tests/test-term

- name: "Test: test-avmpack"
shell: freebsd {0}
run: |
cd $GITHUB_WORKSPACE;
cd build
./tests/test-avmpack

- name: "Test: test_etest.avm"
shell: freebsd {0}
run: |
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,21 @@ jobs:
ulimit -c unlimited
./tests/test-term

- 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 @@ -237,6 +237,7 @@ jobs:
make test-mailbox &&
make test-structs &&
make test-term &&
make test-avmpack &&
file ./tests/test-erlang &&
./tests/test-erlang -s prime_smp &&
file ./tests/test-enif &&
Expand All @@ -249,6 +250,8 @@ jobs:
./tests/test-structs &&
file ./tests/test-term &&
./tests/test-term &&
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Stopped CI testing on FreeBSD 13, which has reached end of life.
- `.avm` files, binaries and the pack embedded in an escript must now end exactly with the pack
terminator: data after the end of the pack (for example a padded partition dump passed to
`atomvm:add_avm_pack_binary/2`) is rejected when loaded

### Fixed
- Fixed maps decoded by `binary_to_term/1,2` and received over distribution keeping the
Expand All @@ -28,6 +31,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
bytes that the other scheduler then took
- Fixed ESP32 SPI pin typespecs to allow `-1` for unused pins.
- Fixed ESP32 double free crash when closing SPI ports
- Fixed the VM crashing (`LoadProhibited` on ESP32) when `atomvm:read_priv/2` or a module load
looked up a name that is not in a truncated or corrupt AVM pack, such as an image larger than
its flash partition
- Fixed `code:get_object_code/1` returning extra bytes after the module (the start of the next
pack entry) for modules loaded from an AVM pack
- Fixed truncated AVM packs, such as an image larger than its flash partition, being accepted at
load and failing later in unpredictable ways; they are now rejected when loaded, with an error
at boot or from `atomvm:add_avm_pack_file/2` and `atomvm:add_avm_pack_binary/2`
- Fixed `atomvm:read_priv/2` returning a binary that extends past its pack entry, reading other
entries or memory after the pack, when the entry's recorded size is corrupt; it now raises
`invalid_avm`. A resource that is genuinely absent still returns `undefined`

## [0.7.0-beta.0] - 2026-09-28

Expand Down
2 changes: 2 additions & 0 deletions doc/src/apidocs/libatomvm/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ Functions
.. doxygenfunction:: atom_string_len
.. doxygenfunction:: atom_string_to_c
.. doxygenfunction:: atom_write_mfa
.. doxygenfunction:: avmpack_compute_size
.. doxygenfunction:: avmpack_find_section_by_flag
.. doxygenfunction:: avmpack_find_section_by_name
.. doxygenfunction:: avmpack_fold
.. doxygenfunction:: avmpack_is_complete
.. doxygenfunction:: avmpack_is_valid
.. doxygenfunction:: bitstring_copy_bits
.. doxygenfunction:: bitstring_copy_bits_incomplete_bytes
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 @@ -184,11 +184,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
199 changes: 146 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,173 @@ 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
{
const char *name;
const void *data;
uint32_t size;
uint32_t data_size;
uint32_t flags;
};

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 bool is_end_marker_name(const char *name)
{
return (name[0] == 'e' || name[0] == 'E') && (name[1] == 'n' || name[1] == 'N')
&& (name[2] == 'd' || name[2] == 'D') && name[3] == '\0';
}

*ptr = sizes + 3 + section_name_len / sizeof(uint32_t);
*size = ENDIAN_SWAP_32(*sizes);
*name = (const char *) (sizes + 3);
return 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;
}

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) {
if (flags == 0 && is_end_marker_name(name)) {
section->size = 0;
section->data_size = 0;
section->flags = 0;
section->name = name;
section->data = header + AVMPACK_END_MARKER_SIZE;
return AVMPackSectionEnd;
}
return AVMPackSectionInvalid;
}

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

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

} while (*flags);
section->size = section_size;
section->data_size = section_size - AVMPACK_SECTION_HEADER_SIZE - padded_name_len;
section->flags = flags;
section->name = name;
section->data = header + AVMPACK_SECTION_HEADER_SIZE + padded_name_len;

return 0;
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_value, 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_value) {
*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, size_t exact_size)
{
#if SIZE_MAX > UINT32_MAX
if (exact_size > UINT32_MAX) {
return false;
}
#endif
uint32_t size = 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, size)
|| size < AVMPACK_SIZE + AVMPACK_END_MARKER_SIZE || (size & 3) != 0) {
return false;
}

struct AVMPackSection section;

return read_section(avmpack_binary, size, 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