bug: 32-bit overflow in stable-diffusion image_to_image resize allows a heap buffer overflow
Summary
SDImageToImage::body takes Width and Height as uint32_t host-function parameters. The only validation is parameterCheck, which requires each to be a multiple of 64 but sets no upper bound. The resize path then computes the output allocation as ResizedHeight * ResizedWidth * 3 in int arithmetic, so a guest can wrap the product to 0, get a non-NULL malloc(0) pointer past the null check, and reach stbir_resize with a zero-sized destination buffer.
The vendored stb_image_resize.h does not validate output dimensions, so it writes out of bounds. Reproduced with AddressSanitizer against the exact header the plugin builds with.
This is the same defect class as #4914 (ac37ae82a) and #5177 (678e63744), which fixed W * H * 3 wrapping past a bounds check in wasmedge_image.
Current State
plugins/wasmedge_stablediffusion/sd_func.cpp:519-533:
int ResizedHeight = Height;
int ResizedWidth = Width;
uint8_t *ResizedImageBuffer =
(uint8_t *)malloc(ResizedHeight * ResizedWidth * 3);
if (ResizedImageBuffer == nullptr) {
spdlog::error(
"[WasmEdge-StableDiffusion] Failed to allocate memory for resize input image."sv);
free(InputImageBuffer);
return static_cast<uint32_t>(ErrNo::InvalidArgument);
}
stbir_resize(InputImageBuffer, ImageWidth, ImageHeight, 0,
ResizedImageBuffer, ResizedWidth, ResizedHeight, 0, ...);Width and Height are uint32_t parameters of SDImageToImage::body (sd_func.cpp:437), registered as a guest-callable host function in sd_module.cpp:13 (addHostFunc("image_to_image", ...)).
The only validation between entry and line 522 is parameterCheck (sd_func.cpp:61):
if (Width % 64 != 0) { ... return false; }
if (Height % 64 != 0) { ... return false; }There is no upper bound, so Width = 0x40000000 passes. Computing the size in int:
| Width | Height | passes % 64 |
malloc arg |
true size |
|---|---|---|---|---|
| 0x40000000 | 64 | yes | 0 | 206158430208 |
| 65536 | 65536 | yes | 0 | 12884901888 |
| 0x20000000 | 128 | yes | 0 | 206158430208 |
| 1024 | 1024 | yes | 3145728 | 3145728 |
malloc(0) returns a valid non-NULL pointer, so the == nullptr guard does not catch it, and stbir_resize is handed a zero-sized destination.
The vendored stb_image_resize.h does not defend against this. Its entry point (stbir__resize_arbitrary) validates channels, filters, alpha and scratch memory, but performs no check on output_w / output_h — its only early return there is if (!extra_memory) return 0;.
Expected State
The required size should be computed so it cannot wrap, matching the fix already merged for wasmedge_image in ac37ae82a:
const uint64_t BytesPerPixel = 3;
const uint64_t NumPixels = static_cast<uint64_t>(Width) * Height;
if (unlikely(NumPixels > SomeSensibleLimit / BytesPerPixel)) {
spdlog::error(
"[WasmEdge-StableDiffusion] Requested output image size is too large."sv);
free(InputImageBuffer);
return static_cast<uint32_t>(ErrNo::InvalidArgument);
}An explicit upper bound in parameterCheck would also be reasonable, since Width/Height are used elsewhere in the same function. I've left the actual cap unspecified since that's your call.
I'm happy to send a PR for whichever shape you prefer.
Reproduction steps
The overflow was reproduced standalone against the exact header the plugin builds with, using the values from sd_func.cpp.
- Configure the plugin so the dependency is fetched:
cmake -S . -Bbuild-sd -GNinja -DWASMEDGE_PLUGIN_STABLEDIFFUSION=ONThe vendored header lands at
build-sd/_deps/stable-diffusion-src/thirdparty/stb_image_resize.h. It comes from
stable-diffusion.cpp at the commit pinned in
plugins/wasmedge_stablediffusion/CMakeLists.txt
(GIT_TAG dcf91f9e0f2cbf9da472ee2a556751ed4bab2d2a).
- Reproduce the same call
sd_func.cpp:522-533makes, withWidth = 0x40000000,Height = 64:
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h" // the vendored copy
int iw = 64, ih = 64;
unsigned char *in = calloc((size_t)iw * ih * 3, 1);
int RW = (int)0x40000000u, RH = 64; // guest Width/Height
int mallocarg = RH * RW * 3; // wraps to 0
unsigned char *out = malloc(mallocarg); // malloc(0) -> non-NULL
stbir_resize(in, iw, ih, 0, out, RW, RH, 0,
STBIR_TYPE_UINT8, 3, STBIR_ALPHA_CHANNEL_NONE, 0,
STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP,
STBIR_FILTER_BOX, STBIR_FILTER_BOX,
STBIR_COLORSPACE_SRGB, NULL);clang -O0 -g -w -fsanitize=address -o t t.c && ./tReaching line 522 from a guest additionally requires the "path:" input branch
(sd_func.cpp:490) and Width/Height differing from the decoded image's
dimensions (sd_func.cpp:517-518).
Any logs you want to share for showing the specific issue
malloc arg = 0 out ptr = 0x6020000000b0 (null-check PASSES)
==13645==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61c0000007a0 WRITE of size 4 at 0x61c0000007a0 thread T0 #0 stbir__calculate_coefficients_upsample stb_image_resize.h:1043 #1 stbir__calculate_filters stb_image_resize.h:1209 #2 stbir__resize_allocated stb_image_resize.h:2398 #3 stbir__resize_arbitrary stb_image_resize.h:2444 #4 stbir_resize stb_image_resize.h:2542 #5 main t.c:16
Without AddressSanitizer the same program terminates with SIGSEGV (exit 139).
Control - replacing the size computation with a 64-bit check: REJECTED: need 206158430208 bytes exit 0, no ASan report.
Components
Core, Plugins
WasmEdge Version or Commit you used
77a76b72f
Operating system information
macOS 15 (Darwin 24.6.0)
Hardware Architecture
arm64
Appendix
Scope and verification limits, stated up front:
- The overflow, the non-NULL
malloc(0), and the out-of-bounds write are all measured, using the exactstb_image_resize.hthatstable-diffusion.cppvendors. I did not drive this through a running WasmEdge guest — the plugin wiring (addHostFunc("image_to_image", ...)->parameterCheck-> line 522) is read from source rather than executed. - Reaching line 522 requires the
"path:"input branch, i.e. the guest supplies a filesystem path rather than raw image bytes. Thestbi_load_from_memorybranch performs no resize. - Verified on macOS/arm64 only.
Related prior fixes for the same defect class in wasmedge_image: ac37ae82a (#4914) and 678e63744 (#5177).
Source: WasmEdge/WasmEdge