// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This test tests that races on lazy fields in opaque protos are detected by the race detector, // even though the plain code uses atomic variables in a manner that would hide data races. // This is essential, as concurrent writes or read-writes on a lazy field can cause undefined // behaviours. // // Using exectest with the race detector to check that the code fails did not work, // as the race error got propagated from the subprocess and failed the test case in the parent process. // Instead we create the subprocess where the test is supposed to fail by ourselves. // Lazy decoding is only available in the fast path, which the protoreflect tag disables. //go:build !protoreflect package lazy_race_test import ( "fmt" "os" "os/exec" "reflect" "sync" "testing" "unsafe" "google.golang.org/protobuf/internal/impl" "google.golang.org/protobuf/internal/test/race" mixedpb "google.golang.org/protobuf/internal/testprotos/mixed" testopaquepb "google.golang.org/protobuf/internal/testprotos/testeditions/testeditions_opaque" "google.golang.org/protobuf/proto" ) // To get some output from the subprocess, set this to true const debug = false func makeM2() *testopaquepb.TestAllTypes { return testopaquepb.TestAllTypes_builder{ OptionalLazyNestedMessage: testopaquepb.TestAllTypes_NestedMessage_builder{ A: proto.Int32(1), Corecursive: testopaquepb.TestAllTypes_builder{ OptionalBool: proto.Bool(true), }.Build(), }.Build(), RepeatedNestedMessage: []*testopaquepb.TestAllTypes_NestedMessage{ testopaquepb.TestAllTypes_NestedMessage_builder{ A: proto.Int32(2), Corecursive: testopaquepb.TestAllTypes_builder{ OptionalInt32: proto.Int32(32), }.Build(), }.Build(), }, }.Build() } type testC struct { name string l1 func() l2 func() } const envVar = "GO_TESTING_IN_SUBPROCESS" // TestRaceDetectionOnWrite tests that any combination involving concurrent // read-write or write-write will trigger the race detector. func TestRaceDetectionOnWrite(t *testing.T) { var x *testopaquepb.TestAllTypes var y *testopaquepb.TestAllTypes_NestedMessage var z int32 // A table of test cases to expose to the race detector. // The name will be set in an environment variable, so don't use special characters or spaces. // Each entry in the table will be spawned into a sub process, where the actual execution will happen. cases := []testC{ { name: "TestSetSet", l1: func() { x.SetOptionalLazyNestedMessage(y) }, l2: func() { x.SetOptionalLazyNestedMessage(y) }, }, { name: "TestClearClear", l1: func() { x.ClearOptionalLazyNestedMessage() }, l2: func() { x.ClearOptionalLazyNestedMessage() }, }, { name: "TestSetClear", l1: func() { x.SetOptionalLazyNestedMessage(y) }, l2: func() { x.ClearOptionalLazyNestedMessage() }, }, { name: "TestSetGet", l1: func() { x.SetOptionalLazyNestedMessage(y) }, l2: func() { if x.GetOptionalLazyNestedMessage().GetCorecursive().GetOptionalBool() { z++ } }, }, { name: "TestSetHas", l1: func() { x.SetOptionalLazyNestedMessage(y) }, l2: func() { if x.HasOptionalLazyNestedMessage() { z++ } }, }, { name: "TestClearGet", l1: func() { x.ClearOptionalLazyNestedMessage() }, l2: func() { if x.GetOptionalLazyNestedMessage().GetCorecursive().GetOptionalBool() { z++ } }, }, { name: "TestClearHas", l1: func() { x.ClearOptionalLazyNestedMessage() }, l2: func() { if x.HasOptionalLazyNestedMessage() { z++ } }, }, } e := os.Getenv(envVar) if e != "" { // We're in the subprocess. As spawnCase will add filter for the subtest, // we will actually only execute one test in this subprocess even though // we call t.Run for all cases. for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { x = makeM2() y = x.GetOptionalLazyNestedMessage() z = 0 execCase(t, tc) return }) } return } // If we're not in a subprocess, spawn and check one for each entry in the table for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { spawnCase(t) }) } } // execCase actually executes the testcase when we're in a subprocess, it executes // the two operations of tc in parallel and make sure tsan sees this as parallel // execution. func execCase(t *testing.T, tc testC) { t.Helper() c1 := make(chan struct{}) wg := sync.WaitGroup{} wg.Add(2) // This is a very complicated but stable way of telling tsan that the // two operations are executed in parallel. I can only guess why this // works so I'll leave my speculations out of the comment but // experiments suggest that it works reliably. go func() { c1 <- struct{}{} tc.l1() <-c1 tc.l1() wg.Done() }() go func() { <-c1 tc.l2() c1 <- struct{}{} tc.l2() wg.Done() }() wg.Wait() } // spawnCase reruns this executable to execute t.Name() with the sub-case tn in the environment variable func spawnCase(t *testing.T) { // If we get here, we are in the parent process and should execute ourselves, but filter on the test that called us. ep, err := os.Executable() if err != nil { t.Fatalf("Failed to find my own executable: %v", err) } c := exec.Command(ep, "--test.run="+t.Name()) // Set the environment variable so that we know we're in a subproceess when re-executed c.Env = append(c.Env, envVar+"=true") out, err := c.CombinedOutput() // If we do not get an error, we fail in the parent process, otherwise we're good if race.Enabled && err == nil { t.Errorf("Got success, want error under race detector:\n-----------\n%s\n-------------\n", string(out)) } if !race.Enabled && err != nil { t.Errorf("Got error, want success without race detector:\n-----------\n%s\n-------------\n", string(out)) } if debug { fmt.Fprintf(os.Stderr, "Subprocess output:\n-----------\n%s\n-------------\n", string(out)) } } // TestNoRaceDetection should not fail under race detector (or otherwise) func TestNoRaceDetection(t *testing.T) { x := makeM2() var y int32 var z int32 c := make(chan struct{}) go func() { for i := 0; i < 10000; i++ { y += x.GetRepeatedNestedMessage()[0].GetA() } close(c) }() for i := 0; i < 10000; i++ { z += x.GetRepeatedNestedMessage()[0].GetA() } <-c if z != y { t.Errorf("The two go-routines did not calculate the same: %d != %d", z, y) } } func TestNoRaceOnGetsOfSlices(t *testing.T) { x := makeM2() b, err := proto.Marshal(x) if err != nil { t.Fatalf("Error while marshaling: %v", err) } var y int32 var z int32 d := make(chan int) // Check that there are no races when we do concurrent lazy gets of a field // containing a slice of message pointers. for i := 0; i < 10000; i++ { err := proto.Unmarshal(b, x) if err != nil { t.Fatalf("Error while unmarshaling: %v", err) } go func() { y += x.GetRepeatedNestedMessage()[0].GetA() d <- 1 }() go func() { z += x.GetRepeatedNestedMessage()[0].GetA() d <- 1 }() <-d <-d } if z != y { t.Errorf("The two go-routines did not calculate the same: %d != %d", z, y) } close(d) } func TestNoRaceOnGetsOfMessages(t *testing.T) { x := makeM2() b, err := proto.Marshal(x) if err != nil { t.Fatalf("Error while marshaling: %v", err) } var y int32 var z int32 d := make(chan int) // Check that there is no race when we do concurrent lazy gets of a field // pointing to a sub-message. for i := 0; i < 10000; i++ { err := proto.Unmarshal(b, x) if err != nil { t.Fatalf("Error while unmarshaling: %v", err) } go func() { if x.GetOptionalLazyNestedMessage().GetA() > 0 { y++ } d <- 1 }() go func() { if x.GetOptionalLazyNestedMessage().GetA() > 0 { z++ } d <- 1 }() <-d <-d } if z != y { t.Errorf("The two go-routines did not calculate the same: %d != %d", z, y) } close(d) } func fillRequiredLazy() *testopaquepb.TestRequiredLazy { return testopaquepb.TestRequiredLazy_builder{ OptionalLazyMessage: testopaquepb.TestRequired_builder{ RequiredField: proto.Int32(23), }.Build(), }.Build() } func expandedLazy(m *testopaquepb.TestRequiredLazy) bool { v := reflect.ValueOf(m).Elem() rf := v.FieldByName("xxx_hidden_OptionalLazyMessage") rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() return rf.Pointer() != 0 } // This test verifies all assumptions of TestParallellMarshalWithRequired // are (still) valid, to prevent the test from becoming a no-op (again). func TestParallellMarshalWithRequiredAssumptions(t *testing.T) { if !impl.LazyEnabled() { t.Skipf("this test requires lazy decoding to be enabled") } b, err := proto.Marshal(fillRequiredLazy()) if err != nil { t.Fatal(err) } ml := &testopaquepb.TestRequiredLazy{} // Specifying AllowPartial: true at unmarshal time is required, otherwise // the Marshal call will skip the required field check. if err := (proto.UnmarshalOptions{AllowPartial: true}).Unmarshal(b, ml); err != nil { t.Fatal(err) } if expandedLazy(ml) { t.Fatalf("lazy message unexpectedly decoded") } // Marshaling with AllowPartial: true means the no decoding is needed, // because no required field checks are done. if _, err := (proto.MarshalOptions{AllowPartial: true}).Marshal(ml); err != nil { t.Fatal(err) } if expandedLazy(ml) { t.Fatalf("lazy message unexpectedly decoded") } // Whereas marshaling with AllowPartial: false (default) means the message // will be decoded to check if any required fields are not set. if _, err := (proto.MarshalOptions{AllowPartial: false}).Marshal(ml); err != nil { t.Fatal(err) } if !expandedLazy(ml) { t.Fatalf("lazy message unexpectedly not decoded") } } // TestParallellMarshalWithRequired runs two goroutines that marshal the same // message. Marshaling a message can result in lazily decoding said message, // provided the message contains any required fields. This test ensures that // said lazy decoding can happen without causing races in the other goroutine // that marshals the same message. func TestParallellMarshalWithRequired(t *testing.T) { m := fillRequiredLazy() b, err := proto.MarshalOptions{}.Marshal(m) if err != nil { t.Fatal(err) } partial := false for i := 0; i < 1000; i++ { partial = !partial ml := &testopaquepb.TestRequiredLazy{} d := make(chan bool) err := proto.UnmarshalOptions{AllowPartial: true}.Unmarshal(b, ml) if err != nil { t.Fatalf("Error while unmarshaling: %v", err) } go func() { b2, err := proto.MarshalOptions{AllowPartial: partial}.Marshal(ml) if err != nil { t.Errorf("Marshal error: %v", err) d <- false return } m := &testopaquepb.TestRequiredLazy{} if err := (proto.UnmarshalOptions{}).Unmarshal(b2, m); err != nil { t.Errorf("Unmarshal error: %v", err) d <- false return } if !proto.Equal(ml, m) { t.Errorf("Unmarshal roundtrip - protos not equal") d <- false return } d <- true }() go func() { b2, err := proto.MarshalOptions{AllowPartial: partial}.Marshal(ml) if err != nil { t.Errorf("Marshal error: %v", err) d <- false return } m := &testopaquepb.TestRequiredLazy{} if err := (proto.UnmarshalOptions{}).Unmarshal(b2, m); err != nil { if !proto.Equal(ml, m) { t.Errorf("Unmarshal roundtrip - protos not equal") d <- false return } if !proto.Equal(ml, m) { t.Errorf("Unmarshal roundtrip - protos not equal") d <- false return } } d <- true }() x := <-d y := <-d if !x || !y { t.Fatalf("Worker reported error") } } } func fillMixedOpaqueLazy() *mixedpb.OpaqueLazy { return mixedpb.OpaqueLazy_builder{ Opaque: mixedpb.OpaqueLazy_builder{ OptionalInt32: proto.Int32(23), Hybrid: mixedpb.HybridLazy_builder{ OptionalInt32: proto.Int32(42), }.Build(), }.Build(), Hybrid: mixedpb.HybridLazy_builder{ OptionalInt32: proto.Int32(5), }.Build(), }.Build() } func TestParallellMarshalMixed(t *testing.T) { m := fillMixedOpaqueLazy() b, err := proto.Marshal(m) if err != nil { t.Fatal(err) } for i := 0; i < 10000; i++ { ml := &mixedpb.OpaqueLazy{} d := make(chan bool) if err := proto.Unmarshal(b, ml); err != nil { t.Fatalf("Error while unmarshaling: %v", err) } go func() { b2, err := proto.Marshal(ml) if err != nil { t.Errorf("Marshal error: %v", err) d <- false return } m := &mixedpb.OpaqueLazy{} if err := proto.Unmarshal(b2, m); err != nil { t.Errorf("Unmarshal error: %v", err) d <- false return } if !proto.Equal(ml, m) { // This is what expands all fields of ml t.Errorf("Unmarshal roundtrip - protos not equal") d <- false return } d <- true }() go func() { b2, err := proto.Marshal(ml) if err != nil { t.Errorf("Marshal error: %v", err) d <- false return } m := &mixedpb.OpaqueLazy{} if err := proto.Unmarshal(b2, m); err != nil { t.Errorf("Unmarshal error: %v", err) d <- false return } if !proto.Equal(ml, m) { // This is what expands all fields of ml t.Errorf("Unmarshal roundtrip - protos not equal") d <- false return } d <- true }() x := <-d y := <-d if !x || !y { t.Fatalf("Worker reported error") } } }