Skip to content

Commit 26065e7

Browse files
committed
btf: fix race due to concurrent read access
Up until the introduction of lazy copying, reading from a Spec concurrently was safe. Now a read may trigger a copy and a write into the Spec, therefore causing a race on mutableTypes. Fix this by introducing a mutex which protects access to the mutable state. We need to be a bit careful here: copying in mutableTypes.add happens piecemeal, so we need to take a lock for the whole duration of modifyGraph. Signed-off-by: Lorenz Bauer <[email protected]>
1 parent b24722c commit 26065e7

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

btf/btf.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,30 @@ func (s *immutableTypes) typeByID(id TypeID) (Type, bool) {
6666
// mutableTypes is a set of types which may be changed.
6767
type mutableTypes struct {
6868
imm immutableTypes
69+
mu *sync.RWMutex // protects copies below
6970
copies map[Type]Type // map[orig]copy
70-
copiedTypeIDs map[Type]TypeID //map[copy]origID
71+
copiedTypeIDs map[Type]TypeID // map[copy]origID
7172
}
7273

7374
// add a type to the set of mutable types.
7475
//
7576
// Copies type and all of its children once. Repeated calls with the same type
7677
// do not copy again.
7778
func (mt *mutableTypes) add(typ Type, typeIDs map[Type]TypeID) Type {
79+
mt.mu.RLock()
80+
cpy, ok := mt.copies[typ]
81+
mt.mu.RUnlock()
82+
83+
if ok {
84+
// Fast path: the type has been copied before.
85+
return cpy
86+
}
87+
88+
// modifyGraphPreorder copies the type graph node by node, so we can't drop
89+
// the lock in between.
90+
mt.mu.Lock()
91+
defer mt.mu.Unlock()
92+
7893
return modifyGraphPreorder(typ, func(t Type) (Type, bool) {
7994
cpy, ok := mt.copies[t]
8095
if ok {
@@ -98,6 +113,7 @@ func (mt *mutableTypes) add(typ Type, typeIDs map[Type]TypeID) Type {
98113
func (mt *mutableTypes) copy() mutableTypes {
99114
mtCopy := mutableTypes{
100115
mt.imm,
116+
&sync.RWMutex{},
101117
make(map[Type]Type, len(mt.copies)),
102118
make(map[Type]TypeID, len(mt.copiedTypeIDs)),
103119
}
@@ -122,6 +138,9 @@ func (mt *mutableTypes) typeID(typ Type) (TypeID, error) {
122138
return 0, nil
123139
}
124140

141+
mt.mu.RLock()
142+
defer mt.mu.RUnlock()
143+
125144
id, ok := mt.copiedTypeIDs[typ]
126145
if !ok {
127146
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
@@ -343,6 +362,7 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error
343362
typesByName,
344363
bo,
345364
},
365+
&sync.RWMutex{},
346366
make(map[Type]Type),
347367
make(map[Type]TypeID),
348368
},

btf/btf_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"fmt"
88
"io"
99
"os"
10+
"runtime"
1011
"sync"
12+
"sync/atomic"
1113
"testing"
1214

1315
"github.com/go-quicktest/qt"
@@ -525,6 +527,35 @@ func TestFixupDatasecLayout(t *testing.T) {
525527
qt.Assert(t, qt.Equals(ds.Vars[5].Offset, 32))
526528
}
527529

530+
func TestSpecConcurrentAccess(t *testing.T) {
531+
spec := vmlinuxTestdataSpec(t)
532+
533+
maxprocs := runtime.GOMAXPROCS(0)
534+
if maxprocs < 2 {
535+
t.Error("GOMAXPROCS is lower than 2:", maxprocs)
536+
}
537+
538+
var cond atomic.Int64
539+
var wg sync.WaitGroup
540+
for i := 0; i < maxprocs; i++ {
541+
wg.Add(1)
542+
go func() {
543+
defer wg.Done()
544+
545+
cond.Add(1)
546+
for cond.Load() != int64(maxprocs) {
547+
// Spin to increase the chances of a race.
548+
}
549+
550+
_, _ = spec.AnyTypeByName("gov_update_cpu_data")
551+
}()
552+
553+
// Try to get the Goroutines scheduled and spinning.
554+
runtime.Gosched()
555+
}
556+
wg.Wait()
557+
}
558+
528559
func BenchmarkSpecCopy(b *testing.B) {
529560
spec := vmlinuxTestdataSpec(b)
530561
b.ResetTimer()

run-tests.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ if [[ "${1:-}" = "--exec-vm" ]]; then
6868
fi
6969

7070
for ((i = 0; i < 3; i++)); do
71-
if ! $sudo virtme-run --kimg "${input}/boot/vmlinuz" --memory 768M --pwd \
71+
if ! $sudo virtme-run --kimg "${input}/boot/vmlinuz" --cpus 2 --memory 768M --pwd \
7272
--rwdir="${testdir}=${testdir}" \
7373
--rodir=/run/input="${input}" \
7474
--rwdir=/run/output="${output}" \
75-
--script-sh "$(quote_env "${preserved_env[@]}") \"$script\" --exec-test $cmd" \
76-
--kopt possible_cpus=2; then # need at least two CPUs for some tests
75+
--script-sh "$(quote_env "${preserved_env[@]}") \"$script\" \
76+
--exec-test $cmd"; then
7777
exit 23
7878
fi
7979

0 commit comments

Comments
 (0)