From 0155769ffe1d236e2e5c54361ecdf1fe01a79981 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Jul 2025 23:01:24 -1000 Subject: [PATCH 1/2] [api] Fix string lifetime issue in Home Assistant service calls with templated values --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 4 +- esphome/components/api/api_pb2.h | 3 +- esphome/components/api/api_pb2_dump.cpp | 2 +- .../components/api/homeassistant_service.h | 9 ++-- script/api_protobuf/api_protobuf.py | 42 ++++++++++++++++--- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 5956c8b0b9..922fb691d6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -759,7 +759,7 @@ message SubscribeHomeassistantServicesRequest { message HomeassistantServiceMap { string key = 1; - string value = 2; + string value = 2 [(no_zero_copy) = true]; } message HomeassistantServiceResponse { diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index bb3947e8a3..4f0f52fc6f 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -27,4 +27,5 @@ extend google.protobuf.MessageOptions { extend google.protobuf.FieldOptions { optional string field_ifdef = 1042; optional uint32 fixed_array_size = 50007; + optional bool no_zero_copy = 50008 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e2d1cfebd4..c056892b6c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -845,11 +845,11 @@ void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { #endif void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key_ref_); - buffer.encode_string(2, this->value_ref_); + buffer.encode_string(2, this->value); } void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->key_ref_.size()); - ProtoSize::add_string_field(total_size, 1, this->value_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->value); } void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->service_ref_); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b7d8945e8e..4a6323b4a4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1061,8 +1061,7 @@ class HomeassistantServiceMap : public ProtoMessage { public: StringRef key_ref_{}; void set_key(const StringRef &ref) { this->key_ref_ = ref; } - StringRef value_ref_{}; - void set_value(const StringRef &ref) { this->value_ref_ = ref; } + std::string value{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d7f9f63f5f..2ea7ab616d 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1044,7 +1044,7 @@ void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { void HomeassistantServiceMap::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantServiceMap"); dump_field(out, "key", this->key_ref_); - dump_field(out, "value", this->value_ref_); + dump_field(out, "value", this->value); } void HomeassistantServiceResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantServiceResponse"); diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 212b3b22d6..7c294e1798 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -69,22 +69,19 @@ template class HomeAssistantServiceCallAction : public Actiondata_template_) { resp.data_template.emplace_back(); auto &kv = resp.data_template.back(); kv.set_key(StringRef(it.key)); - std::string value = it.value.value(x...); - kv.set_value(StringRef(value)); + kv.value = it.value.value(x...); } for (auto &it : this->variables_) { resp.variables.emplace_back(); auto &kv = resp.variables.back(); kv.set_key(StringRef(it.key)); - std::string value = it.value.value(x...); - kv.set_value(StringRef(value)); + kv.value = it.value.value(x...); } this->parent_->send_homeassistant_service_call(resp); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 92c85d2366..7a603a4670 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -562,11 +562,16 @@ class StringType(TypeInfo): @property def public_content(self) -> list[str]: content: list[str] = [] - # Add std::string storage if message needs decoding - if self._needs_decode: + + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # Add std::string storage if message needs decoding OR if no_zero_copy is set + if self._needs_decode or no_zero_copy: content.append(f"std::string {self.field_name}{{}};") - if self._needs_encode: + # Only add StringRef if encoding is needed AND no_zero_copy is not set + if self._needs_encode and not no_zero_copy: content.extend( [ # Add StringRef field if message needs encoding @@ -581,13 +586,28 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: - return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + if no_zero_copy: + # Use the std::string directly + return f"buffer.encode_string({self.number}, this->{self.field_name});" + else: + # Use the StringRef + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" def dump(self, name): + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + # If name is 'it', this is a repeated field element - always use string if name == "it": return "append_quoted_string(out, StringRef(it));" + # If no_zero_copy is set, always use std::string + if no_zero_copy: + return f'out.append("\'").append(this->{self.field_name}).append("\'");' + # For SOURCE_CLIENT only, always use std::string if not self._needs_encode: return f'out.append("\'").append(this->{self.field_name}).append("\'");' @@ -607,6 +627,13 @@ class StringType(TypeInfo): @property def dump_content(self) -> str: + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # If no_zero_copy is set, always use std::string + if no_zero_copy: + return f'dump_field(out, "{self.name}", this->{self.field_name});' + # For SOURCE_CLIENT only, use std::string if not self._needs_encode: return f'dump_field(out, "{self.name}", this->{self.field_name});' @@ -622,8 +649,11 @@ class StringType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - # For SOURCE_CLIENT only messages, use the string field directly - if not self._needs_encode: + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # For SOURCE_CLIENT only messages or no_zero_copy, use the string field directly + if not self._needs_encode or no_zero_copy: return self._get_simple_size_calculation(name, force, "add_string_field") # Check if this is being called from a repeated field context From 5feb891e973c50a3277043a2b9fd30af4c4f4e4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Jul 2025 23:08:22 -1000 Subject: [PATCH 2/2] fix --- esphome/components/api/api_pb2.cpp | 2 +- script/api_protobuf/api_protobuf.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c056892b6c..173b356cd2 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -849,7 +849,7 @@ void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { } void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->key_ref_.size()); - ProtoSize::add_string_field(total_size, 1, this->value); + ProtoSize::add_string_field(total_size, 1, this->value.size()); } void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->service_ref_); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7a603a4670..4b9a61383d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -654,6 +654,10 @@ class StringType(TypeInfo): # For SOURCE_CLIENT only messages or no_zero_copy, use the string field directly if not self._needs_encode or no_zero_copy: + # For no_zero_copy, we need to use .size() on the string + if no_zero_copy and name != "it": + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_string_field(total_size, {field_id_size}, this->{self.field_name}.size());" return self._get_simple_size_calculation(name, force, "add_string_field") # Check if this is being called from a repeated field context