"time"
 )
 
-func cpuHogger(f func(), dur time.Duration) {
+func cpuHogger(f func() int, dur time.Duration) {
        // We only need to get one 100 Hz clock tick, so we've got
        // a large safety buffer.
        // But do at least 500 iterations (which should take about 100ms),
 // The actual CPU hogging function.
 // Must not call other functions nor access heap/globals in the loop,
 // otherwise under race detector the samples will be in the race runtime.
-func cpuHog1() {
+func cpuHog1() int {
        foo := salt1
        for i := 0; i < 1e5; i++ {
                if foo > 0 {
                        foo *= foo + 1
                }
        }
-       salt1 = foo
+       return foo
 }
 
-func cpuHog2() {
+func cpuHog2() int {
        foo := salt2
        for i := 0; i < 1e5; i++ {
                if foo > 0 {
                        foo *= foo + 2
                }
        }
-       salt2 = foo
+       return foo
 }
 
 func TestCPUProfile(t *testing.T) {
        })
 }
 
-func inlinedCaller() {
+func inlinedCaller() int {
        inlinedCallee()
+       return 0
 }
 
 func inlinedCallee() {
        })
 }
 
+func TestLabelRace(t *testing.T) {
+       // Test the race detector annotations for synchronization
+       // between settings labels and consuming them from the
+       // profile.
+       testCPUProfile(t, []string{"runtime/pprof.cpuHogger;key=value"}, func(dur time.Duration) {
+               start := time.Now()
+               var wg sync.WaitGroup
+               for time.Since(start) < dur {
+                       for i := 0; i < 10; i++ {
+                               wg.Add(1)
+                               go func() {
+                                       Do(context.Background(), Labels("key", "value"), func(context.Context) {
+                                               cpuHogger(cpuHog1, time.Millisecond)
+                                       })
+                                       wg.Done()
+                               }()
+                       }
+                       wg.Wait()
+               }
+       })
+}
+
 // Check that there is no deadlock when the program receives SIGPROF while in
 // 64bit atomics' critical section. Used to happen on mips{,le}. See #20146.
 func TestAtomicLoadStore64(t *testing.T) {
 
        b.rNext = br.addCountsAndClearFlags(skip+di, ti)
 
        if raceenabled {
-               // Match racewritepc in runtime_setProfLabel,
+               // Match racereleasemerge in runtime_setProfLabel,
                // so that the setting of the labels in runtime_setProfLabel
                // is treated as happening before any use of the labels
                // by our caller. The synchronization on labelSync itself is a fiction
 
        // Introduce race edge for read-back via profile.
        // This would more properly use &getg().labels as the sync address,
        // but we do the read in a signal handler and can't call the race runtime then.
+       //
+       // This uses racereleasemerge rather than just racerelease so
+       // the acquire in profBuf.read synchronizes with *all* prior
+       // setProfLabel operations, not just the most recent one. This
+       // is important because profBuf.read will observe different
+       // labels set by different setProfLabel operations on
+       // different goroutines, so it needs to synchronize with all
+       // of them (this wouldn't be an issue if we could synchronize
+       // on &getg().labels since we would synchronize with each
+       // most-recent labels write separately.)
+       //
+       // racereleasemerge is like a full read-modify-write on
+       // labelSync, rather than just a store-release, so it carries
+       // a dependency on the previous racereleasemerge, which
+       // ultimately carries forward to the acquire in profBuf.read.
        if raceenabled {
-               racerelease(unsafe.Pointer(&labelSync))
+               racereleasemerge(unsafe.Pointer(&labelSync))
        }
        getg().labels = labels
 }