reflect/protodesc: fix panic when working with dynamicpb

Improves on CL 642575 with a few additional checks to make
sure a panic doesn't occur even under unexpected conditions.

Fixes golang/protobuf#1671

Change-Id: I25bf1cfdf0b35b381cab603f9e06581b3b630d73
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/642975
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Stapelberg <stapelberg@google.com>
diff --git a/reflect/protodesc/editions.go b/reflect/protodesc/editions.go
index f55b036..697a61b 100644
--- a/reflect/protodesc/editions.go
+++ b/reflect/protodesc/editions.go
@@ -11,6 +11,7 @@
 
 	"google.golang.org/protobuf/internal/editiondefaults"
 	"google.golang.org/protobuf/internal/filedesc"
+	"google.golang.org/protobuf/internal/genid"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/types/descriptorpb"
@@ -128,23 +129,39 @@
 	// We must not use proto.GetExtension(child, gofeaturespb.E_Go)
 	// because that only works for messages we generated, but not for
 	// dynamicpb messages. See golang/protobuf#1669.
+	//
+	// Further, we harden this code against adversarial inputs: a
+	// service which accepts descriptors from a possibly malicious
+	// source shouldn't crash.
 	goFeatures := child.ProtoReflect().Get(gofeaturespb.E_Go.TypeDescriptor())
 	if !goFeatures.IsValid() {
 		return parentFS
 	}
+	gf, ok := goFeatures.Interface().(protoreflect.Message)
+	if !ok {
+		return parentFS
+	}
 	// gf.Interface() could be *dynamicpb.Message or *gofeaturespb.GoFeatures.
-	gf := goFeatures.Message()
 	fields := gf.Descriptor().Fields()
 
-	if fd := fields.ByName("legacy_unmarshal_json_enum"); gf.Has(fd) {
+	if fd := fields.ByNumber(genid.GoFeatures_LegacyUnmarshalJsonEnum_field_number); fd != nil &&
+		!fd.IsList() &&
+		fd.Kind() == protoreflect.BoolKind &&
+		gf.Has(fd) {
 		parentFS.GenerateLegacyUnmarshalJSON = gf.Get(fd).Bool()
 	}
 
-	if fd := fields.ByName("strip_enum_prefix"); gf.Has(fd) {
+	if fd := fields.ByNumber(genid.GoFeatures_StripEnumPrefix_field_number); fd != nil &&
+		!fd.IsList() &&
+		fd.Kind() == protoreflect.EnumKind &&
+		gf.Has(fd) {
 		parentFS.StripEnumPrefix = int(gf.Get(fd).Enum())
 	}
 
-	if fd := fields.ByName("api_level"); gf.Has(fd) {
+	if fd := fields.ByNumber(genid.GoFeatures_ApiLevel_field_number); fd != nil &&
+		!fd.IsList() &&
+		fd.Kind() == protoreflect.EnumKind &&
+		gf.Has(fd) {
 		parentFS.APILevel = int(gf.Get(fd).Enum())
 	}
 
diff --git a/reflect/protodesc/editions_test.go b/reflect/protodesc/editions_test.go
index 91c5f38..9d9b769 100644
--- a/reflect/protodesc/editions_test.go
+++ b/reflect/protodesc/editions_test.go
@@ -7,41 +7,106 @@
 import (
 	"testing"
 
+	"google.golang.org/protobuf/internal/genid"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/reflect/protoregistry"
 	"google.golang.org/protobuf/types/descriptorpb"
 	"google.golang.org/protobuf/types/dynamicpb"
 	"google.golang.org/protobuf/types/gofeaturespb"
 )
 
-func TestGoFeaturesDynamic(t *testing.T) {
+func TestGoFeatures_NotExpectedType(t *testing.T) {
 	md := (*gofeaturespb.GoFeatures)(nil).ProtoReflect().Descriptor()
 	gf := dynamicpb.NewMessage(md)
 	opaque := protoreflect.ValueOfEnum(gofeaturespb.GoFeatures_API_OPAQUE.Number())
-	gf.Set(md.Fields().ByName("api_level"), opaque)
-	featureSet := &descriptorpb.FeatureSet{}
+	gf.Set(md.Fields().ByNumber(genid.GoFeatures_ApiLevel_field_number), opaque)
 	dynamicExt := dynamicpb.NewExtensionType(gofeaturespb.E_Go.TypeDescriptor().Descriptor())
-	proto.SetExtension(featureSet, dynamicExt, gf)
 
-	fd := &descriptorpb.FileDescriptorProto{
-		Name: proto.String("a.proto"),
-		Dependency: []string{
-			"google/protobuf/go_features.proto",
+	extFile, err := NewFile(&descriptorpb.FileDescriptorProto{
+		Name:       proto.String("test.proto"),
+		Dependency: []string{"google/protobuf/descriptor.proto"},
+		Extension: []*descriptorpb.FieldDescriptorProto{
+			{
+				Name:     proto.String("ext1002"),
+				Number:   proto.Int32(int32(gofeaturespb.E_Go.TypeDescriptor().Number())),
+				Type:     descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
+				Label:    descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
+				Extendee: proto.String(".google.protobuf.FeatureSet"),
+			},
 		},
-		Edition: descriptorpb.Edition_EDITION_2023.Enum(),
-		Syntax:  proto.String("editions"),
-		Options: &descriptorpb.FileOptions{
-			Features: featureSet,
-		},
-	}
-	fds := &descriptorpb.FileDescriptorSet{
-		File: []*descriptorpb.FileDescriptorProto{
-			ToFileDescriptorProto(descriptorpb.File_google_protobuf_descriptor_proto),
-			ToFileDescriptorProto(gofeaturespb.File_google_protobuf_go_features_proto),
-			fd,
-		},
-	}
-	if _, err := NewFiles(fds); err != nil {
+	}, protoregistry.GlobalFiles)
+	if err != nil {
 		t.Fatal(err)
 	}
+	nonMessageExt := dynamicpb.NewExtensionType(extFile.Extensions().Get(0))
+
+	extFdProto := ToFieldDescriptorProto(gofeaturespb.E_Go.TypeDescriptor().Descriptor())
+	extMsgProto := ToDescriptorProto(gofeaturespb.E_Go.TypeDescriptor().Descriptor().Message())
+	extMsgProto.Field = extMsgProto.Field[:1] // remove all but first field from the message
+	extFile, err = NewFile(&descriptorpb.FileDescriptorProto{
+		Name:        proto.String("google/protobuf/go_features.proto"),
+		Package:     proto.String("pb"),
+		Dependency:  []string{"google/protobuf/descriptor.proto"},
+		Extension:   []*descriptorpb.FieldDescriptorProto{extFdProto},
+		MessageType: []*descriptorpb.DescriptorProto{extMsgProto},
+	}, protoregistry.GlobalFiles)
+	if err != nil {
+		t.Fatal(err)
+	}
+	missingFieldsExt := dynamicpb.NewExtensionType(extFile.Extensions().Get(0))
+	gfMissingFields := dynamicpb.NewMessage(extFile.Messages().Get(0))
+	gfPresentField := gfMissingFields.Descriptor().Fields().Get(0)
+	gfMissingFields.Set(gfPresentField, gfMissingFields.NewField(gfPresentField))
+
+	testCases := []struct {
+		name string
+		ext  protoreflect.ExtensionType
+		val  any
+	}{
+		{
+			name: "dynamic_message",
+			ext:  dynamicExt,
+			val:  gf,
+		},
+		{
+			name: "not_a_message",
+			ext:  nonMessageExt,
+			val:  "abc",
+		},
+		{
+			name: "message_missing_fields",
+			ext:  missingFieldsExt,
+			val:  gfMissingFields,
+		},
+	}
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			featureSet := &descriptorpb.FeatureSet{}
+			proto.SetExtension(featureSet, tc.ext, tc.val)
+
+			fd := &descriptorpb.FileDescriptorProto{
+				Name: proto.String("a.proto"),
+				Dependency: []string{
+					"google/protobuf/go_features.proto",
+				},
+				Edition: descriptorpb.Edition_EDITION_2023.Enum(),
+				Syntax:  proto.String("editions"),
+				Options: &descriptorpb.FileOptions{
+					Features: featureSet,
+				},
+			}
+			fds := &descriptorpb.FileDescriptorSet{
+				File: []*descriptorpb.FileDescriptorProto{
+					ToFileDescriptorProto(descriptorpb.File_google_protobuf_descriptor_proto),
+					ToFileDescriptorProto(gofeaturespb.File_google_protobuf_go_features_proto),
+					fd,
+				},
+			}
+			if _, err := NewFiles(fds); err != nil {
+				t.Fatal(err)
+			}
+		})
+	}
 }