Fortify image_helpers#15
Conversation
It was possible to crash AtomVM by using load_image with invalid data. Fortify it, so it returns an error instead. Signed-off-by: Davide Bettio <davide@uninstall.it>
PR Review — "Fortify image_helpers" (08bb49a)SummaryThe commit hardens The direction is correct and most of the new error handling is sound. However it does Verdict: changes requested — do not merge as-is. Findings🔴 Blocker — unchecked AtomVM binary allocation can still crash on large images
// image_helpers.c (send_decoded_image)
term_put_tuple_element(return_tuple, 1,
term_from_literal_binary(data, size, &heap, ctx->global));In AtomVM, for binaries larger than the heap-binary threshold, memcpy((void *) term_binary_data(binary), data, size); // term.h:1996-1998on that invalid term → dereference of garbage → crash. So a decoded RGBA8 image large Smallest fix — allocate explicitly and check before copying: BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE + term_binary_heap_size(size), heap);
term bin = term_create_uninitialized_binary(size, &heap, ctx->global);
if (UNLIKELY(term_is_invalid_term(bin))) {
END_WITH_STACK_HEAP(heap, ctx->global);
send_load_image_error(ref, pid, OUT_OF_MEMORY_ATOM, ctx);
return;
}
memcpy((void *) term_binary_data(bin), data, size);
term return_tuple = term_alloc_tuple(2, &heap);
term_put_tuple_element(return_tuple, 0, ref);
term_put_tuple_element(return_tuple, 1, bin);
...Better still: allocate the AtomVM binary first and decode Preferred fix — decode directly into a checked AtomVM binary: diff --git a/image_helpers.c b/image_helpers.c
index 0000000..0000000 100644
--- a/image_helpers.c
+++ b/image_helpers.c
@@
-static void send_decoded_image(term ref, term pid, const void *data, size_t size, Context *ctx)
-{
- // TODO: change return format to {ok, binary}
- BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE + term_binary_heap_size(size), heap);
-
- term return_tuple = term_alloc_tuple(2, &heap);
- term_put_tuple_element(return_tuple, 0, ref);
- term_put_tuple_element(return_tuple, 1, term_from_literal_binary(data, size, &heap, ctx->global));
-
- int local_process_id = term_to_local_process_id(pid);
- globalcontext_send_message(ctx->global, local_process_id, return_tuple);
-
- END_WITH_STACK_HEAP(heap, ctx->global)
-}
-
static void send_load_image_error(term ref, term pid, term reason, Context *ctx)
{
@@
- void *out = malloc(out_size);
- if (UNLIKELY(!out)) {
- fprintf(stderr, "handle_load_image: cannot allocate %zu bytes for decoded image\n", out_size);
- spng_ctx_free(png_ctx);
- send_load_image_error(ref, pid, OUT_OF_MEMORY_ATOM, ctx);
- return;
- }
+ BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2) + REF_SIZE + term_binary_heap_size(out_size), heap);
- ret = spng_decode_image(png_ctx, out, out_size, SPNG_FMT_RGBA8, 0);
+ term out_bin = term_create_uninitialized_binary(out_size, &heap, ctx->global);
+ if (UNLIKELY(term_is_invalid_term(out_bin))) {
+ END_WITH_STACK_HEAP(heap, ctx->global);
+ fprintf(stderr, "handle_load_image: cannot allocate %zu bytes for decoded image\n", out_size);
+ spng_ctx_free(png_ctx);
+ send_load_image_error(ref, pid, OUT_OF_MEMORY_ATOM, ctx);
+ return;
+ }
+
+ ret = spng_decode_image(png_ctx, (void *) term_binary_data(out_bin), out_size, SPNG_FMT_RGBA8, 0);
spng_ctx_free(png_ctx);
if (UNLIKELY(ret != SPNG_OK)) {
fprintf(stderr, "handle_load_image: spng_decode_image failed: %s\n", spng_strerror(ret));
- free(out);
+ END_WITH_STACK_HEAP(heap, ctx->global);
send_load_image_error(ref, pid, invalid_image_reason(ctx), ctx);
return;
}
- send_decoded_image(ref, pid, out, out_size, ctx);
+ term return_tuple = term_alloc_tuple(2, &heap);
+ term_put_tuple_element(return_tuple, 0, ref);
+ term_put_tuple_element(return_tuple, 1, out_bin);
- free(out);
+ int local_process_id = term_to_local_process_id(pid);
+ globalcontext_send_message(ctx->global, local_process_id, return_tuple);
+
+ END_WITH_STACK_HEAP(heap, ctx->global)
}🟡 Should-fix — no decoded-size / dimension cap before allocating attacker-controlled memory
ret = spng_decoded_image_size(png_ctx, SPNG_FMT_RGBA8, &out_size);
...
void *out = malloc(out_size);spng rejects zero dimensions and arithmetic overflow, but its defaults allow very large Smallest fix — after spng_set_image_limits(png_ctx, MAX_IMG_W, MAX_IMG_H); // appropriate to the display
spng_set_chunk_limits(png_ctx, MAX_CHUNK, MAX_TOTAL); // if untrusted ancillary chunks possible
// and/or:
if (UNLIKELY(out_size > MAX_DECODED_IMAGE_BYTES)) {
spng_ctx_free(png_ctx);
send_load_image_error(ref, pid, OUT_OF_MEMORY_ATOM, ctx);
return;
}Verified non-issues (checked against AtomVM source)
RecommendationFix the Blocker (unchecked |
It was possible to crash AtomVM by using
load_imagewith invalid data.Fortify it, so it returns an error instead of crashing.