From b0b90adf15a74da61bd6e62dbecf46632d0f01b7 Mon Sep 17 00:00:00 2001
From: htuch <htuch@users.noreply.github.com>
Date: Mon, 16 Nov 2020 18:16:22 -0500
Subject: [PATCH] config: fix crash when type URL doesn't match proto. (#14031)

Fixes #13681.

Risk level: Low
Testing: Unit and integration regression tests added.

Signed-off-by: Harvey Tuch <htuch@google.com>
---
 source/common/config/version_converter.cc    | 20 ++++++++++++++------
 test/common/config/version_converter_test.cc | 12 ++++++++++++
 test/integration/ads_integration_test.cc     | 17 +++++++++++++++++
 3 files changed, 43 insertions(+), 6 deletions(-)

diff --git a/source/common/config/version_converter.cc b/source/common/config/version_converter.cc
index db2bd1cfc216..0477c29bcf86 100644
--- a/source/common/config/version_converter.cc
+++ b/source/common/config/version_converter.cc
@@ -78,20 +78,28 @@ void VersionConverter::annotateWithOriginalType(const Protobuf::Descriptor& prev
     void onMessage(Protobuf::Message& message, const void* ctxt) override {
       const Protobuf::Descriptor* descriptor = message.GetDescriptor();
       const Protobuf::Reflection* reflection = message.GetReflection();
-      const Protobuf::Descriptor& prev_descriptor = *static_cast<const Protobuf::Descriptor*>(ctxt);
+      const Protobuf::Descriptor* prev_descriptor = static_cast<const Protobuf::Descriptor*>(ctxt);
+      // If there is no previous descriptor for this message, we don't need to annotate anything.
+      if (prev_descriptor == nullptr) {
+        return;
+      }
       // If they are the same type, there's no possibility of any different type
       // further down, so we're done.
-      if (descriptor->full_name() == prev_descriptor.full_name()) {
+      if (descriptor->full_name() == prev_descriptor->full_name()) {
         return;
       }
       auto* unknown_field_set = reflection->MutableUnknownFields(&message);
       unknown_field_set->AddLengthDelimited(ProtobufWellKnown::OriginalTypeFieldNumber,
-                                            prev_descriptor.full_name());
+                                            prev_descriptor->full_name());
     }
 
     const void* onField(Protobuf::Message&, const Protobuf::FieldDescriptor& field,
                         const void* ctxt) override {
-      const Protobuf::Descriptor& prev_descriptor = *static_cast<const Protobuf::Descriptor*>(ctxt);
+      const Protobuf::Descriptor* prev_descriptor = static_cast<const Protobuf::Descriptor*>(ctxt);
+      // If there is no previous descriptor for this field, we don't need to annotate anything.
+      if (prev_descriptor == nullptr) {
+        return nullptr;
+      }
       // TODO(htuch): This is a terrible hack, there should be no per-resource
       // business logic in this file. The reason this is required is that
       // endpoints, when captured in configuration such as inlined hosts in
@@ -101,13 +109,13 @@ void VersionConverter::annotateWithOriginalType(const Protobuf::Descriptor& prev
       // In theory, we should be able to just clean up these annotations in
       // ClusterManagerImpl with type erasure, but protobuf doesn't free up memory
       // as expected, we probably need some arena level trick to address this.
-      if (prev_descriptor.full_name() == "envoy.api.v2.Cluster" &&
+      if (prev_descriptor->full_name() == "envoy.api.v2.Cluster" &&
           (field.name() == "hidden_envoy_deprecated_hosts" || field.name() == "load_assignment")) {
         // This will cause the sub-message visit to abort early.
         return field.message_type();
       }
       const Protobuf::FieldDescriptor* prev_field =
-          prev_descriptor.FindFieldByNumber(field.number());
+          prev_descriptor->FindFieldByNumber(field.number());
       return prev_field != nullptr ? prev_field->message_type() : nullptr;
     }
   };
diff --git a/test/common/config/version_converter_test.cc b/test/common/config/version_converter_test.cc
index 65bb66145fc7..1c7e949fb625 100644
--- a/test/common/config/version_converter_test.cc
+++ b/test/common/config/version_converter_test.cc
@@ -71,6 +71,18 @@ TEST(VersionConverterProto, UpgradeNextVersion) {
   VersionConverter::upgrade(source, dst);
 }
 
+// Validate that even if we pass in a newer proto version that is being passed off as an older
+// version (e.g. via a type URL mistake), we don't crash. This is a regression test for
+// https://github.com/envoyproxy/envoy/issues/13681.
+TEST(VersionConverterProto, UpgradeWithConfusedTypes) {
+  test::common::config::NextVersion source_next;
+  source_next.mutable_new_message_in_this_version();
+  test::common::config::PreviousVersion source;
+  ASSERT_TRUE(source.ParseFromString(source_next.SerializeAsString()));
+  test::common::config::NextVersion dst;
+  VersionConverter::upgrade(source, dst);
+}
+
 // Bad UTF-8 can fail wire cast during upgrade.
 TEST(VersionConverterTest, UpgradeException) {
   API_NO_BOOST(envoy::api::v2::Cluster) source;
diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc
index a9d96e44a8a2..24468b45f20f 100644
--- a/test/integration/ads_integration_test.cc
+++ b/test/integration/ads_integration_test.cc
@@ -1521,4 +1521,21 @@ TEST_P(AdsClusterV2Test, XdsBatching) {
   initialize();
 }
 
+// Regression test for https://github.com/envoyproxy/envoy/issues/13681.
+TEST_P(AdsClusterV2Test, TypeUrlAnnotationRegression) {
+  initialize();
+  const auto cds_type_url = Config::getTypeUrl<envoy::config::cluster::v3::Cluster>(
+      envoy::config::core::v3::ApiVersion::V2);
+
+  EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true));
+  auto cluster = buildCluster("cluster_0");
+  auto* bias = cluster.mutable_least_request_lb_config()->mutable_active_request_bias();
+  bias->set_default_value(1.1);
+  bias->set_runtime_key("foo");
+  sendDiscoveryResponse<envoy::config::cluster::v3::Cluster>(cds_type_url, {cluster}, {cluster}, {},
+                                                             "1", true);
+
+  test_server_->waitForCounterGe("cluster_manager.cds.update_rejected", 1);
+}
+
 } // namespace Envoy