From 2acd02cc0c463a71fc6ec4204f6eaafc1b47c6c6 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Fri, 31 Jul 2026 18:36:28 -0700 Subject: [PATCH] Add defensive checks against accidentally depending on non-hermetic message factories. Avoids unexpected use-after-free bugs when messages are extended by an overlaid descriptor pool. PiperOrigin-RevId: 957432161 --- common/values/struct_value_builder.cc | 14 +- eval/eval/select_step.cc | 112 ++++++++++---- extensions/protobuf/BUILD | 1 + extensions/protobuf/value.h | 112 ++++++++++---- extensions/protobuf/value_test.cc | 146 ++++++++++++++++++ internal/well_known_types.cc | 6 + runtime/runtime.h | 17 +- runtime/runtime_builder_factory.h | 10 +- testutil/BUILD | 46 ++++++ testutil/test_external_extensions.proto | 42 +++++ ...test_external_extensions_descriptor_set.cc | 62 ++++++++ .../test_external_extensions_descriptor_set.h | 28 ++++ ...external_extensions_descriptor_set_test.cc | 56 +++++++ 13 files changed, 577 insertions(+), 75 deletions(-) create mode 100644 testutil/test_external_extensions.proto create mode 100644 testutil/test_external_extensions_descriptor_set.cc create mode 100644 testutil/test_external_extensions_descriptor_set.h create mode 100644 testutil/test_external_extensions_descriptor_set_test.cc diff --git a/common/values/struct_value_builder.cc b/common/values/struct_value_builder.cc index f43369e33..cafdbeb54 100644 --- a/common/values/struct_value_builder.cc +++ b/common/values/struct_value_builder.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -84,8 +85,17 @@ absl::StatusOr> ProtoMessageCopy( const google::protobuf::Message* absl_nonnull from_message) { CEL_ASSIGN_OR_RETURN(const auto* from_descriptor, GetDescriptor(*from_message)); - if (to_descriptor == from_descriptor) { - // Same. + if (to_descriptor == from_descriptor && + to_message->GetReflection()->GetMessageFactory() == + from_message->GetReflection()->GetMessageFactory()) { + // Same type, use proto reflection copy. + // + // We use the slower serialization copy if the factory is different to avoid + // adding an implicit lifetime dependency on the other factory. + // + // This should only happen if the embedding application is calling the + // builder directly or attempting to set the field from an unsafe wrapped + // message. to_message->CopyFrom(*from_message); return std::nullopt; } diff --git a/eval/eval/select_step.cc b/eval/eval/select_step.cc index e7974496b..0adb77fd6 100644 --- a/eval/eval/select_step.cc +++ b/eval/eval/select_step.cc @@ -36,11 +36,9 @@ namespace { using ::cel::BoolValue; using ::cel::ErrorValue; -using ::cel::MapValue; using ::cel::OptionalValue; using ::cel::ProtoWrapperTypeOptions; using ::cel::StringValue; -using ::cel::StructValue; using ::cel::Value; using ::cel::ValueKind; @@ -424,6 +422,42 @@ class DirectSelectStep : public DirectExpressionStep { bool enable_optional_types_; }; +bool CheckAttributeTrail(const std::string& field, ExecutionFrame* frame) { + if (!frame->attribute_tracking_enabled()) { + return false; + } + AttributeTrail& attr = frame->value_stack().PeekAttribute(); + attr = attr.Step(&field); + + absl::optional marked_attribute_check = + CheckForMarkedAttributes(attr, *frame); + if (marked_attribute_check.has_value()) { + frame->value_stack().Peek() = std::move(marked_attribute_check).value(); + return true; + } + + return false; +} + +bool SupportsCachedFieldDescriptor( + const cel::ParsedMessageValue& parsed_message, + const google::protobuf::Descriptor* descriptor, + const google::protobuf::FieldDescriptor* field_descriptor) { + ABSL_DCHECK_EQ(field_descriptor->containing_type(), descriptor); + const google::protobuf::Descriptor* rt_descriptor = parsed_message.GetDescriptor(); + + if (rt_descriptor != descriptor) { + return false; + } + + // Caller should have already checked this. Crash here if not instead of + // making proto crash. + ABSL_DCHECK_EQ(rt_descriptor->file()->pool(), + field_descriptor->file()->pool()); + + return true; +} + class ProtoSelectStep : public SelectStep { public: ProtoSelectStep(StringValue value, int64_t expr_id, @@ -447,15 +481,21 @@ class ProtoSelectStep : public SelectStep { const Value& arg = frame->value_stack().Peek(); if (auto unwrapped = arg.AsParsedMessage(); - unwrapped.has_value() && unwrapped->GetDescriptor() == descriptor_) { - return EvaluateModernMessageGetField(frame, *unwrapped); + unwrapped.has_value() && + SupportsCachedFieldDescriptor(*unwrapped, descriptor_, + field_descriptor_)) { + return EvaluateMessageFieldGet(frame, *unwrapped); } else if (const google::protobuf::Message* legacy_message = cel::interop_internal::GetLegacyMessage(arg); - legacy_message != nullptr && - legacy_message->GetDescriptor() == descriptor_) { + frame->options().enable_use_new_field_select_implementation && + legacy_message != nullptr) { + auto parsed_message = cel::UnsafeParsedMessageValue(legacy_message); // A little unfortunate, but need to special case for legacy values so we // can minimize back and forth interop conversions. - return EvaluateLegacyMessageGetField(frame, legacy_message); + if (SupportsCachedFieldDescriptor(parsed_message, descriptor_, + field_descriptor_)) { + return EvaluateMessageFieldGet(frame, legacy_message); + } } // If we get an unexpected value type, fall back to the generic // implementation. @@ -463,34 +503,18 @@ class ProtoSelectStep : public SelectStep { } private: - absl::Status EvaluateModernMessageGetField( + absl::Status EvaluateMessageFieldGet( ExecutionFrame* frame, const cel::ParsedMessageValue& parsed_message) const; - absl::Status EvaluateLegacyMessageGetField( - ExecutionFrame* frame, const google::protobuf::Message* legacy_message) const; + absl::Status EvaluateMessageFieldGet( + ExecutionFrame* frame, + const google::protobuf::Message* absl_nonnull legacy_message) const; const google::protobuf::Descriptor* descriptor_; const google::protobuf::FieldDescriptor* field_descriptor_; }; -bool CheckAttributeTrail(const std::string& field, ExecutionFrame* frame) { - if (!frame->attribute_tracking_enabled()) { - return false; - } - AttributeTrail& attr = frame->value_stack().PeekAttribute(); - attr = attr.Step(&field); - - absl::optional marked_attribute_check = - CheckForMarkedAttributes(attr, *frame); - if (marked_attribute_check.has_value()) { - frame->value_stack().Peek() = std::move(marked_attribute_check).value(); - return true; - } - - return false; -} - -absl::Status ProtoSelectStep::EvaluateModernMessageGetField( +absl::Status ProtoSelectStep::EvaluateMessageFieldGet( ExecutionFrame* frame, const cel::ParsedMessageValue& parsed_message) const { if (CheckAttributeTrail(field_, frame)) { @@ -501,8 +525,10 @@ absl::Status ProtoSelectStep::EvaluateModernMessageGetField( frame->message_factory(), frame->arena(), &frame->value_stack().Peek()); } -absl::Status ProtoSelectStep::EvaluateLegacyMessageGetField( - ExecutionFrame* frame, const google::protobuf::Message* legacy_message) const { +absl::Status ProtoSelectStep::EvaluateMessageFieldGet( + ExecutionFrame* frame, + const google::protobuf::Message* absl_nonnull legacy_message) const { + ABSL_DCHECK(legacy_message != nullptr); if (CheckAttributeTrail(field_, frame)) { return absl::OkStatus(); } @@ -534,15 +560,19 @@ class ProtoHasStep : public SelectStep { const Value& arg = frame->value_stack().Peek(); if (auto unwrapped = arg.AsParsedMessage(); - unwrapped.has_value() && unwrapped->GetDescriptor() == descriptor_) { + unwrapped.has_value() && + SupportsCachedFieldDescriptor(*unwrapped, descriptor_, + field_descriptor_)) { return EvaluateHas(frame, *unwrapped); } else if (const google::protobuf::Message* legacy_message = cel::interop_internal::GetLegacyMessage(arg); - legacy_message != nullptr && - legacy_message->GetDescriptor() == descriptor_) { + legacy_message != nullptr) { cel::ParsedMessageValue parsed_message = cel::UnsafeParsedMessageValue(legacy_message); - return EvaluateHas(frame, parsed_message); + if (SupportsCachedFieldDescriptor(parsed_message, descriptor_, + field_descriptor_)) { + return EvaluateHas(frame, parsed_message); + } } // If we get an unexpected value type, fall back to the generic // implementation. @@ -608,6 +638,20 @@ absl::StatusOr> CreateTypedSelectStep( const google::protobuf::FieldDescriptor* field_descriptor = resolved_field.GetMessage().descriptor(); + if (field_descriptor->file()->pool() != descriptor->file()->pool()) { + // The field descriptor is not in the same pool as the operand type. + // (this should only happen if an overlay extends the type). + // + // We don't have a way to determine if a runtime message is compatible + // with the resolved extension and proto's reflection implementation may + // crash. + // + // Fallback to the generic implementation. + return CreateSelectStep(std::move(field), test_only, expr_id, + enable_wrapper_type_null_unboxing, + enable_optional_types); + } + if (test_only) { return std::make_unique( std::move(field), expr_id, enable_wrapper_type_null_unboxing, diff --git a/extensions/protobuf/BUILD b/extensions/protobuf/BUILD index d814057e7..002da4764 100644 --- a/extensions/protobuf/BUILD +++ b/extensions/protobuf/BUILD @@ -123,6 +123,7 @@ cc_test( "//common:value_kind", "//common:value_testing", "//internal:testing", + "//testutil:test_external_extensions_descriptor_set", "@com_google_absl//absl/log:absl_check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", diff --git a/extensions/protobuf/value.h b/extensions/protobuf/value.h index b7a654064..68049787f 100644 --- a/extensions/protobuf/value.h +++ b/extensions/protobuf/value.h @@ -40,6 +40,70 @@ namespace cel::extensions { +namespace extensions_internal { +template +absl::Status ProtoMessageFromValue(const cel::Value& value, + google::protobuf::Message& dest_message) { + const auto* dest_descriptor = dest_message.GetDescriptor(); + const google::protobuf::Message* src_message = nullptr; + if (auto legacy_struct_value = + cel::common_internal::AsLegacyStructValue(value); + legacy_struct_value) { + src_message = legacy_struct_value->message_ptr(); + } + if (auto parsed_message_value = value.AsParsedMessage(); + parsed_message_value) { + src_message = cel::to_address(*parsed_message_value); + } + + if (src_message == nullptr) { + return TypeConversionError(value.GetRuntimeType(), + MessageType(dest_descriptor)) + .NativeValue(); + } + + const auto* src_descriptor = src_message->GetDescriptor(); + if (dest_descriptor != src_descriptor) { + goto slow_path; + } + + if constexpr (!SkipCopyCheck::value) { + // Try to catch cases where we'll take an implicit dependency on a + // dynamic message factory. + // + // This isn't exhaustive, but correctly checking requires fully + // traversing the source message which will approach the cost of the + // serialization round trip. + if (dest_message.GetReflection()->GetMessageFactory() != + src_message->GetReflection()->GetMessageFactory()) { + goto slow_path; + } + } + + dest_message.CopyFrom(*src_message); + return absl::OkStatus(); + +slow_path: + if (dest_descriptor->full_name() != src_descriptor->full_name()) { + return TypeConversionError(value.GetRuntimeType(), + MessageType(dest_descriptor)) + .NativeValue(); + } + + absl::Cord serialized; + if (!src_message->SerializePartialToCord(&serialized)) { + return absl::UnknownError(absl::StrCat("failed to serialize message: ", + src_descriptor->full_name())); + } + if (!dest_message.ParsePartialFromCord(serialized)) { + return absl::UnknownError(absl::StrCat("failed to parse message: ", + dest_descriptor->full_name())); + } + return absl::OkStatus(); +} + +} // namespace extensions_internal + // Adapt a protobuf message to a cel::Value. // // Handles unwrapping message types with special meanings in CEL (WKTs). @@ -56,41 +120,23 @@ ProtoMessageToValue(T&& value, message_factory, arena); } +// Unwraps a protobuf message from a cel::Value. inline absl::Status ProtoMessageFromValue(const Value& value, google::protobuf::Message& dest_message) { - const auto* dest_descriptor = dest_message.GetDescriptor(); - const google::protobuf::Message* src_message = nullptr; - if (auto legacy_struct_value = - cel::common_internal::AsLegacyStructValue(value); - legacy_struct_value) { - src_message = legacy_struct_value->message_ptr(); - } - if (auto parsed_message_value = value.AsParsedMessage(); - parsed_message_value) { - src_message = cel::to_address(*parsed_message_value); - } - if (src_message != nullptr) { - const auto* src_descriptor = src_message->GetDescriptor(); - if (dest_descriptor == src_descriptor) { - dest_message.CopyFrom(*src_message); - return absl::OkStatus(); - } - if (dest_descriptor->full_name() == src_descriptor->full_name()) { - absl::Cord serialized; - if (!src_message->SerializePartialToCord(&serialized)) { - return absl::UnknownError(absl::StrCat("failed to serialize message: ", - src_descriptor->full_name())); - } - if (!dest_message.ParsePartialFromCord(serialized)) { - return absl::UnknownError(absl::StrCat("failed to parse message: ", - dest_descriptor->full_name())); - } - return absl::OkStatus(); - } - } - return TypeConversionError(value.GetRuntimeType(), - MessageType(dest_descriptor)) - .NativeValue(); + return extensions_internal::ProtoMessageFromValue( + value, dest_message); +} + +// Unwraps a protobuf message from a cel::Value without checking for the +// presence of extensions. +// +// Warning: This function can lead to subtle use after free bugs if the caller +// is not careful to ensure that the source and destination message were created +// in a compatible way and do not outlive any implicit dependencies. +inline absl::Status ProtoMessageFromValueUnsafe(const Value& value, + google::protobuf::Message& dest_message) { + return extensions_internal::ProtoMessageFromValue( + value, dest_message); } } // namespace cel::extensions diff --git a/extensions/protobuf/value_test.cc b/extensions/protobuf/value_test.cc index 20d9dce2f..cc89588f7 100644 --- a/extensions/protobuf/value_test.cc +++ b/extensions/protobuf/value_test.cc @@ -37,7 +37,11 @@ #include "common/value_kind.h" #include "common/value_testing.h" #include "internal/testing.h" +#include "testutil/test_external_extensions_descriptor_set.h" #include "cel/expr/conformance/proto2/test_all_types.pb.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/dynamic_message.h" +#include "google/protobuf/message.h" #include "google/protobuf/text_format.h" namespace cel::extensions { @@ -796,5 +800,147 @@ TEST_F(ProtoValueWrapTest, ProtoListForEachWithIndex) { ElementsAre(Pair(0, IntValueIs(1)), Pair(1, IntValueIs(2)))); } +class ProtoValueUnwrapTest : public ProtoValueTest {}; + +TEST_F(ProtoValueUnwrapTest, SameDescriptor) { + TestAllTypes src = ParseTextOrDie( + R"pb(single_int32: 1 single_int64: 2 single_string: "hello")pb"); + ASSERT_OK_AND_ASSIGN( + auto value, + ProtoMessageToValue(src, descriptor_pool(), message_factory(), arena())); + TestAllTypes dest; + ASSERT_THAT(ProtoMessageFromValue(value, dest), IsOk()); + EXPECT_EQ(dest.single_int32(), 1); + EXPECT_EQ(dest.single_int64(), 2); + EXPECT_EQ(dest.single_string(), "hello"); +} + +TEST_F(ProtoValueUnwrapTest, DifferentDescriptorSameFullName) { + auto* dynamic_message = + DynamicParseTextProto(R"pb(single_int32: 42 + single_string: "dynamic")pb"); + Value value = Value::WrapMessage(dynamic_message, descriptor_pool(), + message_factory(), arena()); + TestAllTypes dest; + ASSERT_THAT(ProtoMessageFromValue(value, dest), IsOk()); + EXPECT_EQ(dest.single_int32(), 42); + EXPECT_EQ(dest.single_string(), "dynamic"); +} + +TEST_F(ProtoValueUnwrapTest, TypeMismatch) { + TestAllTypes::NestedMessage nested; + nested.set_bb(100); + Value value = Value::WrapMessage(&nested, descriptor_pool(), + message_factory(), arena()); + TestAllTypes dest; + EXPECT_THAT(ProtoMessageFromValue(value, dest), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(ProtoValueUnwrapTest, NonMessageValue) { + TestAllTypes dest; + EXPECT_THAT(ProtoMessageFromValue(IntValue(42), dest), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(ProtoMessageFromValue(StringValue("not a message"), dest), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(ProtoMessageFromValue(NullValue(), dest), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +const google::protobuf::DescriptorPool& +GetTestExternalExtensionsDescriptorPoolUnderlay() { + static const google::protobuf::DescriptorPool* pool = []() { + auto pool = std::make_unique( + google::protobuf::DescriptorPool::generated_pool()); + google::protobuf::LinkMessageReflection(); + ABSL_CHECK( + pool->BuildFile(test::GetTestExternalExtensionsFileDescriptor())); + return pool.release(); + }(); + return *pool; +} + +std::unique_ptr MakeTestExtendedMessage( + const google::protobuf::DescriptorPool& pool, google::protobuf::MessageFactory& factory) { + const google::protobuf::Descriptor* desc = + pool.FindMessageTypeByName("cel.expr.conformance.proto2.TestAllTypes"); + ABSL_CHECK(desc != nullptr); + const google::protobuf::FieldDescriptor* ext_msg_field = + pool.FindExtensionByPrintableName( + desc, "cel.cpp.testutil.test_external_extensions_message"); + ABSL_CHECK(ext_msg_field != nullptr); + const google::protobuf::Message* prototype = factory.GetPrototype(desc); + ABSL_CHECK(prototype != nullptr); + std::unique_ptr dynamic_message(prototype->New()); + + const auto* reflection = dynamic_message->GetReflection(); + reflection->SetInt32(dynamic_message.get(), + desc->FindFieldByName("single_int32"), 42); + reflection->SetString(dynamic_message.get(), + desc->FindFieldByName("single_string"), "dynamic"); + google::protobuf::Message* ext = reflection->MutableMessage(dynamic_message.get(), + ext_msg_field, &factory); + ext->GetReflection()->SetString( + ext, ext->GetDescriptor()->FindFieldByName("string_field"), + "external_string"); + return dynamic_message; +} + +TEST_F(ProtoValueUnwrapTest, DynamicMessageFromUnderlayDescriptorPool) { + const auto& pool = GetTestExternalExtensionsDescriptorPoolUnderlay(); + TestAllTypes dest; + { + google::protobuf::DynamicMessageFactory factory(&pool); + factory.SetDelegateToGeneratedFactory(false); + std::unique_ptr dynamic_message = + MakeTestExtendedMessage(pool, factory); + + Value value = + Value::WrapMessage(dynamic_message.get(), &pool, &factory, arena()); + ASSERT_THAT(ProtoMessageFromValue(value, dest), IsOk()); + } + EXPECT_EQ(dest.single_int32(), 42); + EXPECT_EQ(dest.single_string(), "dynamic"); + EXPECT_EQ(dest.unknown_fields().field_count(), 1); +} + +using ProtoValueUnwrapTestDeathTest = ProtoValueUnwrapTest; + +TEST_F(ProtoValueUnwrapTestDeathTest, + DynamicMessageFromUnderlayDescriptorPoolDelegateToGeneratedFactory) { +#ifndef ADDRESS_SANITIZER + GTEST_SKIP() << "Test requires ASAN enabled"; +#else + EXPECT_DEATH_IF_SUPPORTED( + { + const auto& pool = GetTestExternalExtensionsDescriptorPoolUnderlay(); + auto dest = std::make_unique(); + { + // The CEL runtime should not create a factory like this. + // To get this to work, the caller would need to manage the + // message factory, provide it to the runtime, and guarantee that it + // outlives the CEL value or any derived messages. + google::protobuf::DynamicMessageFactory factory(&pool); + factory.SetDelegateToGeneratedFactory(true); + + std::unique_ptr dynamic_message = + MakeTestExtendedMessage(pool, factory); + + Value value = Value::WrapMessage(dynamic_message.get(), &pool, + &factory, arena()); + ASSERT_THAT(ProtoMessageFromValue(value, *dest), IsOk()); + } + + EXPECT_EQ(dest->single_int32(), 42); + EXPECT_EQ(dest->single_string(), "dynamic"); + EXPECT_TRUE(dest->GetReflection()->HasField( + *dest, pool.FindExtensionByName( + "cel.cpp.testutil.test_external_extensions_message"))); + dest.reset(); + }, + "AddressSanitizer: heap-use-after-free"); +#endif +} + } // namespace } // namespace cel::extensions diff --git a/internal/well_known_types.cc b/internal/well_known_types.cc index 175f978d4..5232bb6ad 100644 --- a/internal/well_known_types.cc +++ b/internal/well_known_types.cc @@ -1980,6 +1980,12 @@ absl::StatusOr> AdaptAny( } BytesValue value = reflection.GetValue(*to_unwrap, value_scratch); Unique unpacked = WrapUnique(prototype->New(arena), arena); + // TODO(b/557267722): Extensions that are not included in the same + // descriptor pool as the resolved descriptor will be treated as unknown + // fields. Extending messages like this should be exceedingly rare and is + // hard to support safely. + // + // See CodedInputStream::SetExtensionRegistry. const bool ok = absl::visit(absl::Overload( [&](absl::string_view string) -> bool { return unpacked->ParseFromString(string); diff --git a/runtime/runtime.h b/runtime/runtime.h index 2db39b0e3..8c76236dd 100644 --- a/runtime/runtime.h +++ b/runtime/runtime.h @@ -76,11 +76,20 @@ class Program { // Activation manages instances of variables available in the cel expression's // environment. // - // The arena will be used to as necessary to allocate values and must outlive - // the returned value, as must this program. + // Notes on lifetimes: // - // For consistency, users should use the same arena to create values - // in the activation and for Program evaluation. + // The provided arena will be used as necessary to allocate complex values + // and must outlive any returned value. Values created by the program may + // depend on internal state in the runtime. In particular protobuf messages + // may depend on the descriptor pool and message factory managed by the + // runtime or program. + // + // Programs implicitly keep shared state in the runtime object alive so it + // is sufficient to ensure that any cel::Value result is destroyed before the + // cel::Program that created it. + // + // For consistency, users should use the same arena to create values placed in + // the activation for calls to Program::Evaluate. absl::StatusOr Evaluate( google::protobuf::Arena* absl_nonnull arena ABSL_ATTRIBUTE_LIFETIME_BOUND, const ActivationInterface& activation, diff --git a/runtime/runtime_builder_factory.h b/runtime/runtime_builder_factory.h index 0cb35d62a..2efe88465 100644 --- a/runtime/runtime_builder_factory.h +++ b/runtime/runtime_builder_factory.h @@ -48,8 +48,14 @@ namespace cel { // - google.protobuf.Duration // - google.protobuf.Timestamp // -// This is provided for environments that only use a subset of the CEL standard -// builtins. Most users should prefer CreateStandardRuntimeBuilder. +// Warning: It is best to use a hermetic descriptor pool (i.e. not created with +// the "underlay" constructor). There is not an efficient way to consistently +// detect whether a given message definitely has no dependency on an overlaid +// descriptor pool and associated message factory. This can hide use-after-free +// type bugs. These issues are mitigated by using a hermetic descriptor pool. +// +// This function is provided for environments that only use a subset of the CEL +// standard builtins. Most users should prefer CreateStandardRuntimeBuilder. // // Callers must register appropriate builtins. absl::StatusOr CreateRuntimeBuilder( diff --git a/testutil/BUILD b/testutil/BUILD index 3c1832652..a8bc214c9 100644 --- a/testutil/BUILD +++ b/testutil/BUILD @@ -15,6 +15,8 @@ load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") +load("//bazel:cel_cc_embed.bzl", "cel_cc_embed") +load("//bazel:cel_proto_transitive_descriptor_set.bzl", "cel_proto_transitive_descriptor_set") package(default_visibility = ["//visibility:public"]) @@ -79,3 +81,47 @@ proto_library( name = "test_json_names_proto", srcs = ["test_json_names.proto"], ) + +proto_library( + name = "test_external_extensions_proto", + srcs = ["test_external_extensions.proto"], + deps = ["@com_google_cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +cel_proto_transitive_descriptor_set( + name = "test_external_extensions_transitive_descriptor_set", + testonly = True, + deps = [ + ":test_external_extensions_proto", + ], +) + +cel_cc_embed( + name = "test_external_extensions_descriptor_set_embed", + testonly = True, + src = ":test_external_extensions_transitive_descriptor_set", +) + +cc_library( + name = "test_external_extensions_descriptor_set", + testonly = True, + srcs = ["test_external_extensions_descriptor_set.cc"], + hdrs = ["test_external_extensions_descriptor_set.h"], + textual_hdrs = [":test_external_extensions_descriptor_set_embed"], + deps = [ + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/base:no_destructor", + "@com_google_absl//absl/log:absl_check", + "@com_google_protobuf//:protobuf", + ], +) + +cc_test( + name = "test_external_extensions_descriptor_set_test", + srcs = ["test_external_extensions_descriptor_set_test.cc"], + deps = [ + ":test_external_extensions_descriptor_set", + "//internal:testing", + "@com_google_protobuf//:protobuf", + ], +) diff --git a/testutil/test_external_extensions.proto b/testutil/test_external_extensions.proto new file mode 100644 index 000000000..b78827b67 --- /dev/null +++ b/testutil/test_external_extensions.proto @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Tests for external extensions added in an overlay. + +edition = "2024"; + +package cel.cpp.testutil; + +import "cel/expr/conformance/proto2/test_all_types.proto"; + +// Test message for external extensions. +message TestExternalExtensions { + string string_field = 1; + int64 int64_field = 2; +} + +extend cel.expr.conformance.proto2.TestAllTypes { + TestExternalExtensions test_external_extensions_message = 2001; + string test_external_extensions_string = 2002; + int64 test_external_extensions_int64 = 2003; +} + +// Message scoped extension. +message ScopedTestExternalExtensions { + extend cel.expr.conformance.proto2.TestAllTypes { + TestExternalExtensions scoped_ext_message = 3001; + string scoped_ext_string = 3002; + int64 scoped_ext_int64 = 3003; + } +} diff --git a/testutil/test_external_extensions_descriptor_set.cc b/testutil/test_external_extensions_descriptor_set.cc new file mode 100644 index 000000000..d18633a03 --- /dev/null +++ b/testutil/test_external_extensions_descriptor_set.cc @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "testutil/test_external_extensions_descriptor_set.h" + +#include +#include + +#include "google/protobuf/descriptor.pb.h" +#include "absl/base/attributes.h" +#include "absl/base/no_destructor.h" +#include "absl/base/optimization.h" +#include "absl/log/absl_check.h" + +namespace cel::test { + +namespace { + +ABSL_CONST_INIT const uint8_t kTestExternalExtensionsDescriptorSet[] = { +#include "testutil/test_external_extensions_descriptor_set_embed.inc" +}; + +} // namespace + +const google::protobuf::FileDescriptorSet& GetTestExternalExtensionsFileDescriptorSet() { + static const absl::NoDestructor + file_descriptor_set([]() { + google::protobuf::FileDescriptorSet file_desc_set; + ABSL_CHECK(file_desc_set.ParseFromArray( // Crash OK + kTestExternalExtensionsDescriptorSet, + std::size(kTestExternalExtensionsDescriptorSet))); + return file_desc_set; + }()); + return *file_descriptor_set; +} + +const google::protobuf::FileDescriptorProto& GetTestExternalExtensionsFileDescriptor() { + static const google::protobuf::FileDescriptorProto* const file_desc = []() { + const auto& file_desc_set = GetTestExternalExtensionsFileDescriptorSet(); + for (const auto& file_desc : file_desc_set.file()) { + if (file_desc.name() == + "testutil/test_external_extensions.proto") { + return &file_desc; + } + } + ABSL_UNREACHABLE(); + }(); + return *file_desc; +} + +} // namespace cel::test diff --git a/testutil/test_external_extensions_descriptor_set.h b/testutil/test_external_extensions_descriptor_set.h new file mode 100644 index 000000000..a5302e1bb --- /dev/null +++ b/testutil/test_external_extensions_descriptor_set.h @@ -0,0 +1,28 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_CEL_CPP_TESTUTIL_TEST_EXTERNAL_EXTENSIONS_DESCRIPTOR_SET_H_ +#define THIRD_PARTY_CEL_CPP_TESTUTIL_TEST_EXTERNAL_EXTENSIONS_DESCRIPTOR_SET_H_ + +#include "google/protobuf/descriptor.pb.h" + +namespace cel::test { + +const google::protobuf::FileDescriptorSet& GetTestExternalExtensionsFileDescriptorSet(); + +const google::protobuf::FileDescriptorProto& GetTestExternalExtensionsFileDescriptor(); + +} // namespace cel::test + +#endif // THIRD_PARTY_CEL_CPP_TESTUTIL_TEST_EXTERNAL_EXTENSIONS_DESCRIPTOR_SET_H_ diff --git a/testutil/test_external_extensions_descriptor_set_test.cc b/testutil/test_external_extensions_descriptor_set_test.cc new file mode 100644 index 000000000..de4b61cba --- /dev/null +++ b/testutil/test_external_extensions_descriptor_set_test.cc @@ -0,0 +1,56 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "testutil/test_external_extensions_descriptor_set.h" + +#include "internal/testing.h" +#include "google/protobuf/descriptor.h" + +namespace cel::test { +namespace { + +using ::testing::NotNull; + +TEST(GetTestExternalExtensionsFileDescriptorSet, BuildsPool) { + const auto& fds = GetTestExternalExtensionsFileDescriptorSet(); + EXPECT_GT(fds.file_size(), 0); + + google::protobuf::DescriptorPool pool; + for (const auto& file : fds.file()) { + EXPECT_THAT(pool.BuildFile(file), NotNull()); + } + + EXPECT_THAT( + pool.FindMessageTypeByName("cel.cpp.testutil.TestExternalExtensions"), + NotNull()); + EXPECT_THAT(pool.FindExtensionByName( + "cel.cpp.testutil.test_external_extensions_message"), + NotNull()); + EXPECT_THAT( + pool.FindExtensionByName( + "cel.cpp.testutil.ScopedTestExternalExtensions.scoped_ext_string"), + NotNull()); +} + +TEST(GetTestExternalExtensionsFileDescriptor, CorrectFileDescriptor) { + const auto& fd = GetTestExternalExtensionsFileDescriptor(); + EXPECT_EQ(fd.name(), + "testutil/test_external_extensions.proto"); + EXPECT_EQ(fd.package(), "cel.cpp.testutil"); + EXPECT_GT(fd.message_type_size(), 0); + EXPECT_GT(fd.extension_size(), 0); +} + +} // namespace +} // namespace cel::test