Skip to content

proto: malformed oneof field values can make Marshal panic #478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
maljub01 opened this issue Dec 29, 2017 · 8 comments
Closed

proto: malformed oneof field values can make Marshal panic #478

maljub01 opened this issue Dec 29, 2017 · 8 comments

Comments

@maljub01
Copy link

proto.Marshal defines an errOneofHasNil error which it returns when it encounters oneof fields that are invalid due to the presence of unexpected nil values.

For an example of this working properly, see this test: https://github.com/golang/protobuf/blob/master/proto/all_test.go#L1356

As the test linked above illustrates, this works in some cases. However, here's a code snippet that illustrates a case where proto.Marshal fails to detect the nil values properly and instead panics:

package main

import (
        "fmt"

        "github.com/golang/protobuf/proto"
        "github.com/golang/protobuf/proto/testdata"
)

func main() {
        pbs := []proto.Message{
                &testdata.Oneof{Union: &testdata.Oneof_F_Message{nil}},  // Handled properly.
                &testdata.Oneof{Union: (*testdata.Oneof_F_String)(nil)},  // Panics.
        }

        for _, pb := range pbs {
                fmt.Println(pb)
                if _, err := proto.Marshal(pb); err != nil {
                        fmt.Println(err)
                }
                fmt.Println()
        }
}

When running this using the latest protobuf library, I get the following output:

F_Message:/* nil */ 
proto: oneof field has nil value

%!v(PANIC=reflect: call of reflect.Value.Type on zero Value)
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4db67a]

goroutine 1 [running]:
github.com/golang/protobuf/proto/testdata._Oneof_OneofMarshaler(0x5c8c20, 0xc420078080, 0xc420090680, 0x5c8c20, 0xc420078080)
        $GOPATH/src/github.com/golang/protobuf/proto/testdata/test.pb.go:2454 +0xd1a
github.com/golang/protobuf/proto.(*Buffer).enc_struct(0xc420090680, 0xc4200ac000, 0xc420078080, 0x5264a0, 0xc420078080)
        $GOPATH/src/github.com/golang/protobuf/proto/encode.go:1257 +0x6c6
github.com/golang/protobuf/proto.(*Buffer).Marshal(0xc420090680, 0x5c8c20, 0xc420078080, 0x0, 0x0)
        $GOPATH/src/github.com/golang/protobuf/proto/encode.go:274 +0x2ad
github.com/golang/protobuf/proto.Marshal(0x5c8c20, 0xc420078080, 0x1, 0x3d, 0x0, 0x0, 0xc42000e2f0)
        $GOPATH/src/github.com/golang/protobuf/proto/encode.go:236 +0x93
main.main()
        /tmp/tmp.sHbPYcz7QI/test.go:18 +0x1f1
exit status 2

The first two lines from the output show the behavior working properly for the first proto in the test, while the remaining lines show the bad case.

maljub01 added a commit to maljub01/objecthash-proto that referenced this issue Dec 29, 2017
- Make sure badly constructed oneof fields are handled properly.
- Add an additional test case for a non-empty proto extension.
- Add logic to make sure that the HashProto method recovers properly if
  the proto library panics (See golang/protobuf/issues/478 for an
  example).

  This case is included in the malformed oneof field tests as
  `&Singleton{Singleton: (*Singleton_TheString)(nil)}`.
@puellanivis
Copy link
Collaborator

The line %!v(PANIC=reflect: call of reflect.Value.Type on zero Value) is interesting, as this is fmt.Println swallowing a panic during the pb.String() call. Splitting the fmt.Println(pb) to s := pb.String(); fmt.Println(s), we get this output:

F_Message:/* nil */
proto: oneof field has nil value

panic: reflect: call of reflect.Value.Type on zero Value

goroutine 1 [running]:
reflect.Value.Type(0x0, 0x0, 0x0, 0x0, 0x0)
	/usr/local/go/src/reflect/value.go:1688 +0x1a3
github.com/golang/protobuf/proto.(*TextMarshaler).writeStruct(0x11c9006, 0xc42000a2e0, 0x1117d60, 0xc42004e100, 0x199, 0x199, 0x1238000)
	$GOPATH/src/github.com/golang/protobuf/proto/text.go:414 +0x544
github.com/golang/protobuf/proto.(*TextMarshaler).Marshal(0x11c9006, 0x11ca1c0, 0xc42008e540, 0x11cbc80, 0xc42004e100, 0x0, 0x0)
	$GOPATH/src/github.com/golang/protobuf/proto/text.go:820 +0x351
github.com/golang/protobuf/proto.(*TextMarshaler).Text(0x11c9006, 0x11cbc80, 0xc42004e100, 0x11caa40, 0xc42000c018)
	$GOPATH/src/github.com/golang/protobuf/proto/text.go:832 +0x6e
github.com/golang/protobuf/proto.CompactTextString(0x11cbc80, 0xc42004e100, 0x0, 0x0)
	$GOPATH/src/github.com/golang/protobuf/proto/text.go:854 +0x41
github.com/golang/protobuf/proto/testdata.(*Oneof).String(0xc42004e100, 0x0, 0x0)
	$GOPATH/src/github.com/golang/protobuf/proto/testdata/test.pb.go:2169 +0x37
main.main()
	$CWD/niloneof.go:17 +0x18b

It looks, like using an actual nil pointer struct for the Oneof is violating a contract of how protobuf oneof data structures will be composed.

Of note, you construct the first as &testdata.Oneof_F_Message{nil} which is so strongly different from (*testdata.Oneof_F_Message)(nil) so as to break the expectation of equivalent behavior.(NOTE: (*testdata.Oneof_F_Message)(nil) panics the same as (*testdata.Oneof_F_String)(nil) The problem clearly being in the handling of a nil intermediate/wrapper struct, not the handling of a nil (zero-value) proto.Message in the intermediate/wrapper struct as is the case with &testdata.Oneof_F_Message{nil}).

This said, I recall a comment to a recent pull request, which is to check for required fields being set, noting the assumptions being made (that the intermediate/wrapper struct will not be nil) should not be made, and one should check the oneof struct to ensure that it is not nil before proceeding to take it’s Type().

The central question, I suppose, is how these things should be composed. Is it reasonable to expect that the intermediate/wrapper oneof structs shall always be non-nil, as they will be composed from a &pb.Oneof_Field{} literal, or should we actually handle the case where someone has intentionally made a typed pb.Oneof_Field pointer that is nil.

As the best-practices in Go protobufs is to compose messages with &MessageName{}, I think this expectation that it should not be nil might be reasonable. Really, it would require either very odd and explicit typed-nil pointer constructions, or zero-value typed pointers being assigned into a Oneof field.

But in general no one should be instantiating a pb.Message with a zero-value pointer (i.e. var msg *pb.Message) as this is known to throw errors and/or panics all over the protobuf code. So, assuming that people should be making these fields through either new(pb.Message) or &pb.Message{} shouldn’t be too entirely unreasonable.

@dsnet dsnet changed the title Malformed oneof field values can make proto.Marshal panic proto: malformed oneof field values can make Marshal panic Feb 14, 2018
@ryho
Copy link

ryho commented Jun 19, 2018

When dealing with interfaces in Go, there is this insane concept of typed nils. How I think of it is that an interface variable has a type and a value. The type being nil is independent from whether or not the value is nil. And it is very easy to accidentally type your nils by passing them around.

See more about it here: https://golang.org/doc/faq#nil_error

Here is an example of a marshaller that was generated by this library that can have the exception described above. SendEmailRequest may have a ReplyToOption that is one of ReplyTo or ReplyToAttributes.

func _SendEmailRequest_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
	m := msg.(*SendEmailRequest)
	// reply_to_option
	switch x := m.ReplyToOption.(type) {
	case *SendEmailRequest_ReplyTo:
		b.EncodeVarint(6<<3 | proto.WireBytes)
		if err := b.EncodeMessage(x.ReplyTo); err != nil {
			return err
		}
	case *SendEmailRequest_ReplyToAttributes:
		b.EncodeVarint(17<<3 | proto.WireBytes)
		if err := b.EncodeMessage(x.ReplyToAttributes); err != nil {
			return err
		}
	case nil:
	default:
		return fmt.Errorf("SendEmailRequest.ReplyToOption has unexpected type %T", x)
	}
	return nil
}

In this function, doing x.ReplyTo and x.ReplyToAttributes is a mistake. In the first two cases of the switch, you know what the type of x is, but you still don't know whether or not the value of x is nil. So grabbing a variable off the struct pointer can easily lead to nil pointer exceptions. Inside of both of these switches, the encoding should only be done if the value in x is non-nil.

But I have case nil:

Yeah, Go doesn't care. This is a type switch, and the type of the variable stored in the struct was not nil even though the value was nil.

Here is an example of a Getter for one of the possibilities in a oneof that also can encounter nil pointer exceptions:

func (m *SendEmailRequest) GetReplyToOption() isSendEmailRequest_ReplyToOption {
	if m != nil {
		return m.ReplyToOption
	}
	return nil
}
func (m *SendEmailRequest) GetReplyTo() *Personalization {
	if x, ok := m.GetReplyToOption().(*SendEmailRequest_ReplyTo); ok {
		return x.ReplyTo
	}
	return nil
}

Again, x.ReplyTo in the second function is a mistake. Just because you have typed the interface, does not mean that the value inside the the interface was not actually a typed nil. The if statement should actually be:
if x, ok := m.GetReplyToOption().(*SendEmailRequest_ReplyTo); ok && x != nil {

@puellanivis
Copy link
Collaborator

Nothing internally to the protobuf code would assign a typed nil to that interface anyways. Which is what the point of my discussion was about. Should we expect that someone might have put a typed nil into a field external to the protobuf code, or can we reasonably rely that one would not assign a typed nil into the oneof field?

Where does the equilibrium lie between “you’ve doing something out of spec, and your code is wrong“ and “always validate inputs”? This is not a trivial question, and any answer to it should be non-trivial as well.

Clearly misusing the API is “your code is wrong” but is managing to store a typed nil into this specific type of interface something that should be guarded against, so as to save developers, who are out of spec? 🤷‍♀️

@alecthomas
Copy link

@puellanivis I would normally agree, but the code is already checking for nil, it's just not doing it correctly.

I think the original (contrived) example is misleading in that it is "obviously" not something you would do normally. In reality this is most likely caused by situations where you have a zero value variable that is conditionally assigned, like so:

var str testdata.Oneof_F_String

if some_condition {
  str = &testdata.Oneof_F_String{"something"}
}

msg := &testdata.Oneof{Union: str},

In this case I think the API consumer would reasonably expect that to not panic.

@ryho
Copy link

ryho commented Jun 20, 2018 via email

@puellanivis
Copy link
Collaborator

@alecthomas but wouldn’t this code be better written as:

var val testdata.isOneof_Union

if some_condition {
  val =  &testdata.Oneof_F_String{"something"}
}

msg := &testdata.Oneof{Union: val}

or even better (because it does not rely upon an unexported type (see below)):

msg := &testdata.Oneof{}

if some_condition {
  val.Union =  &testdata.Oneof_F_String{"something"}
}

Now, I note that this will not work, because there is an unexported type used there. A different issue discusses how oneof union types should be exported. And its precisely in my opinion for this reason. The proper conditional store of this value should not be the concrete type, but the union.

This is also to address @ryho ’s point though as well, this exported interface may be exported, but its type is not. The whole oneof implementation in Go is generally required to be complicated because Go does not have a matching idiom, and so unions have to be cobbled together from imperfect idioms. This has led to just more complicated code to account for things that developers never expected API users to try and do. 😕

While typed nils are a part of the language, this does not necessarily mean that they need to be handled by this code. There are many functions that panic on fundamentally bad or broken inputs, and this is not automatically a bad design.

@puellanivis
Copy link
Collaborator

@alecthomas now that I look over your code, the val declaration needs to be made a pointer for your code to work. var val *testdata.Oneof_F_String.

@dsnet dsnet added this to the v2 release milestone Aug 21, 2019
@dsnet
Copy link
Member

dsnet commented Mar 3, 2020

@dsnet dsnet closed this as completed Mar 3, 2020
@golang golang locked and limited conversation to collaborators Sep 4, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants