Merge branch 'proto_bounds_check_fix' into integration

This commit is contained in:
J. Nick Koston
2026-01-16 14:59:59 -10:00
7 changed files with 90 additions and 33 deletions
+8 -4
View File
@@ -48,14 +48,16 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
// Use subtraction to avoid integer overflow on 32-bit systems
if (field_length > end - ptr) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
// Use subtraction to avoid integer overflow on 32-bit systems
if (end - ptr < 4) {
return count;
}
ptr += 4;
@@ -110,7 +112,8 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
// Use subtraction to avoid integer overflow on 32-bit systems
if (field_length > end - ptr) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +124,8 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
// Use subtraction to avoid integer overflow on 32-bit systems
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
+18 -9
View File
@@ -19,12 +19,12 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr;
this->base85_ptr_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) {
this->base85_ptr_ = &base85;
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this;
@@ -35,7 +35,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr;
this->base85_ptr_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
@@ -101,13 +101,22 @@ void Infrared::control(const InfraredCall &call) {
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base85()) {
// Decode base85 directly into transmit buffer (zero heap allocations)
if (!transmit_data->set_data_from_base85(call.get_base85_data())) {
ESP_LOGE(TAG, "Invalid base85 data");
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
ESP_LOGD(TAG, "Transmitting base85 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
call.get_repeat_count());
} else {
// From vector (lambdas/automations)
+12 -12
View File
@@ -40,11 +40,11 @@ class InfraredCall {
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from base85-encoded int32 data
/// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server where the encoded string is on the stack.
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base85(const std::string &base85);
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
@@ -59,18 +59,18 @@ class InfraredCall {
/// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings, not packed or base85)
/// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (vector, packed, or base85)
/// Check if raw timings have been set (any format)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base85_ptr_ != nullptr;
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base85 data format
bool is_base85() const { return this->base85_ptr_ != nullptr; }
/// Get the base85 data string
const std::string &get_base85_data() const { return *this->base85_ptr_; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
@@ -84,8 +84,8 @@ class InfraredCall {
optional<uint32_t> carrier_frequency_;
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base85-encoded string (caller-owned, must outlive perform())
const std::string *base85_ptr_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
@@ -2,8 +2,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
@@ -160,8 +158,8 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
}
}
bool RemoteTransmitData::set_data_from_base85(const std::string &base85) {
return base85_decode_int32_vector(base85, this->data_);
bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
return base64_decode_int32_vector(base64url, this->data_);
}
/* RemoteTransmitterBase */
+4 -4
View File
@@ -36,11 +36,11 @@ class RemoteTransmitData {
/// @param len Length of the buffer in bytes
/// @param count Number of values (for reserve optimization)
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
/// Set data from base85-encoded int32 values
/// Decodes directly into internal buffer (zero heap allocations)
/// @param base85 Base85-encoded string (5 chars per int32 value)
/// Set data from base64url-encoded little-endian int32 values
/// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/'
/// @param base64url Base64url-encoded string of little-endian int32 values
/// @return true if successful, false if decode failed or invalid size
bool set_data_from_base85(const std::string &base85);
bool set_data_from_base64url(const std::string &base64url);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;
+40
View File
@@ -652,6 +652,46 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
return ret;
}
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
/// @param out Output vector (cleared and filled with decoded int32 values)
/// @return true if successful, false if decode failed or invalid size
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out) {
// Decode in chunks to minimize stack usage
constexpr size_t chunk_bytes = 48; // 12 int32 values
constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars
uint8_t chunk[chunk_bytes];
out.clear();
const uint8_t *input = reinterpret_cast<const uint8_t *>(base64.data());
size_t remaining = base64.size();
size_t pos = 0;
while (remaining > 0) {
size_t chars_to_decode = std::min(remaining, chunk_chars);
size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes);
if (decoded_len == 0)
return false;
// Parse little-endian int32 values
for (size_t i = 0; i + 3 < decoded_len; i += 4) {
int32_t timing = static_cast<int32_t>(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i]));
out.push_back(timing);
}
// Check for incomplete int32 in last chunk
if (remaining <= chunk_chars && (decoded_len % 4) != 0)
return false;
pos += chars_to_decode;
remaining -= chars_to_decode;
}
return !out.empty();
}
/// Encode int32 to 5 base85 characters + null terminator
/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84
inline void base85_encode_int32(int32_t value, std::span<char, BASE85_INT32_ENCODED_SIZE> output) {
+6
View File
@@ -1302,6 +1302,12 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string);
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
/// @param out Output vector (cleared and filled with decoded int32 values)
/// @return true if successful, false if decode failed or invalid size
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out);
/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator)
static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6;