From 6a7925b3cea591e8a22f92ab99a4da7542436cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 17:36:56 -0600 Subject: [PATCH] [json] Fix SerializationBuffer truncation when payload exceeds stack buffer ArduinoJson's serializeJson() with a bounded buffer returns the actual bytes written (truncated count), NOT the would-be size like snprintf(). When the payload exceeded the 512-byte stack buffer, the return value was used as the exact size for heap reallocation, but this was only the truncated count. The heap buffer was allocated too small, the second serialization was also truncated, and no NUL terminator was written. This caused corrupted JSON in SSE streams for entities with payloads exceeding 512 bytes (e.g., climate DETAIL_ALL with many features). Fix: use measureJson() in the heap fallback path to get the exact untruncated size before allocating. Also add the missing set_size_() call after the second serialization to ensure proper NUL termination. --- esphome/components/json/json_util.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index cb5dfe6e2d..6583e55e38 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -88,10 +88,14 @@ SerializationBuffer<> JsonBuilder::serialize() { // - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer" // Should show only destructor, NOT move constructor // - // Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash. - // Instead, try stack buffer first. 512 bytes covers 99.9% of JSON payloads (sensors ~200B, + // Try stack buffer first. 512 bytes covers 99.9% of JSON payloads (sensors ~200B, // lights ~170B, climate ~700B). Only entities with 40+ options exceed this. // + // IMPORTANT: ArduinoJson's serializeJson() with a bounded buffer returns the actual + // bytes written (truncated count), NOT the would-be size like snprintf(). When the + // payload exceeds the buffer, the return value equals the buffer capacity. The heap + // fallback uses measureJson() to get the exact size for allocation. + // // =========================================================================================== constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE; SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null) @@ -113,9 +117,13 @@ SerializationBuffer<> JsonBuilder::serialize() { return result; } - // Needs heap allocation - reallocate and serialize again with exact size + // Payload exceeded stack buffer. ArduinoJson's serializeJson() with a bounded + // buffer returns the actual bytes written (truncated), NOT the would-be size. + // Use measureJson() to get the exact size for heap allocation. + size = measureJson(doc_); result.reallocate_heap_(size); serializeJson(doc_, result.data_writable_(), size + 1); + result.set_size_(size); return result; }