[runtime_image] Prevent integer overflow in image buffer size calculation (#17624)

This commit is contained in:
Jonathan Swoboda
2026-07-16 18:36:18 -04:00
committed by GitHub
parent 7a2d13da90
commit cc6392785f
4 changed files with 27 additions and 2 deletions
@@ -10,12 +10,16 @@ static const char *const TAG = "image_decoder";
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize(width, height) > 0;
this->size_valid_ = success;
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
return success;
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
if (!this->size_valid_) {
return;
}
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) {
@@ -108,6 +108,7 @@ class ImageDecoder {
size_t decoded_bytes_ = 0; // Bytes processed so far
double x_scale_ = 1.0;
double y_scale_ = 1.0;
bool size_valid_ = true; // Last set_size() result; draw() no-ops while false
};
} // namespace esphome::runtime_image
@@ -96,6 +96,8 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
} else if (!this->size_valid_) {
return DECODE_ERROR_OUT_OF_MEMORY;
} else {
this->decoded_bytes_ += fed;
}
@@ -3,7 +3,9 @@
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <limits>
#ifdef USE_RUNTIME_IMAGE_BMP
#include "bmp_decoder.h"
@@ -19,6 +21,13 @@ namespace esphome::runtime_image {
static const char *const TAG = "runtime_image";
// Widest supported format is 4 bytes/pixel, so 32767 * 32767 * 4 still fits a 32-bit size_t
static constexpr int MAX_IMAGE_DIMENSION = 32767;
static constexpr int MAX_IMAGE_BPP = 32;
static_assert((static_cast<uint64_t>(MAX_IMAGE_BPP) * MAX_IMAGE_DIMENSION + 7) / 8 * MAX_IMAGE_DIMENSION <=
std::numeric_limits<size_t>::max(),
"MAX_IMAGE_DIMENSION must keep the worst-case buffer size within size_t");
inline bool is_color_on(const Color &color) {
// This produces the most accurate monochrome conversion, but is slightly slower.
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
@@ -257,6 +266,11 @@ void RuntimeImage::release_buffer_() {
size_t RuntimeImage::resize_buffer_(int width, int height) {
size_t new_size = this->get_buffer_size_(width, height);
if (new_size == 0) {
ESP_LOGE(TAG, "Refusing to allocate buffer for invalid image dimensions %dx%d", width, height);
return 0;
}
if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
// Buffer already allocated with correct size
return new_size;
@@ -287,11 +301,15 @@ size_t RuntimeImage::resize_buffer_(int width, int height) {
}
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
// Dimensions come from a remote image header; reject absurd values so the size math cannot overflow
if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
return 0;
}
if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
// Add extra alpha channel for RGB565 with alpha
return width * height * 3;
return static_cast<size_t>(width) * height * 3;
}
return (this->get_bpp() * width + 7u) / 8u * height;
return (static_cast<size_t>(this->get_bpp()) * width + 7u) / 8u * height;
}
int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }