Skip to content

Commit d1df97f

Browse files
committed
dlopen: make error handling thread safe
The dlerror() value is stored in thread local storage and thus checking the error from go code is not safe because go can schedule goroutines on any other thread at any time. This means it is possible that between the dlsym() or dlclose() call the code get moved to another thread and thus the dlerror() value is not what we expect. Instead because we read now from another thread it will be an error from a different call. A test is added which without this fix is able to reproduce the problem somewhat reliably. This issue was observed in the podman CI[1] which uses the sdjournal API. [1] containers/podman#20569 Signed-off-by: Paul Holzinger <[email protected]>
1 parent d843340 commit d1df97f

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

internal/dlopen/dlopen.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import "C"
2323
import (
2424
"errors"
2525
"fmt"
26+
"runtime"
2627
"unsafe"
2728
)
2829

@@ -56,6 +57,10 @@ func GetHandle(libs []string) (*LibHandle, error) {
5657

5758
// GetSymbolPointer takes a symbol name and returns a pointer to the symbol.
5859
func (l *LibHandle) GetSymbolPointer(symbol string) (unsafe.Pointer, error) {
60+
// Locking the thread is critical here as the dlerror() is thread local so
61+
// go should not reschedule this onto another thread.
62+
runtime.LockOSThread()
63+
defer runtime.UnlockOSThread()
5964
sym := C.CString(symbol)
6065
defer C.free(unsafe.Pointer(sym))
6166

@@ -71,6 +76,10 @@ func (l *LibHandle) GetSymbolPointer(symbol string) (unsafe.Pointer, error) {
7176

7277
// Close closes a LibHandle.
7378
func (l *LibHandle) Close() error {
79+
// Locking the thread is critical here as the dlerror() is thread local so
80+
// go should not reschedule this onto another thread.
81+
runtime.LockOSThread()
82+
defer runtime.UnlockOSThread()
7483
C.dlerror()
7584
C.dlclose(l.Handle)
7685
e := C.dlerror()

internal/dlopen/dlopen_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package dlopen
1616

1717
import (
1818
"fmt"
19+
"sync"
1920
"testing"
2021
)
2122

@@ -62,3 +63,42 @@ func TestDlopen(t *testing.T) {
6263
}
6364
}
6465
}
66+
67+
// Note this is not a reliable reproducer for the problem.
68+
// It depends on the fact the it first generates some dlerror() errors
69+
// by using non existent libraries.
70+
func TestDlopenThreadSafety(t *testing.T) {
71+
libs := []string{
72+
"libstrange1.so",
73+
"libstrange2.so",
74+
"libstrange3.so",
75+
"libstrange4.so",
76+
"libstrange5.so",
77+
"libstrange6.so",
78+
"libstrange7.so",
79+
"libstrange8.so",
80+
"libc.so.6",
81+
"libc.so",
82+
}
83+
84+
// 10000 is the default golang thread limit, so adding more will likely fail
85+
// but this number is enough to reproduce the issue most of the time for me.
86+
count := 10000
87+
wg := sync.WaitGroup{}
88+
wg.Add(count)
89+
90+
for i := 0; i < count; i++ {
91+
go func() {
92+
defer wg.Done()
93+
lib, err := GetHandle(libs)
94+
if err != nil {
95+
t.Errorf("GetHandle failed unexpectedly: %v", err)
96+
}
97+
_, err = lib.GetSymbolPointer("strlen")
98+
if err != nil {
99+
t.Errorf("GetSymbolPointer strlen failed unexpectedly: %v", err)
100+
}
101+
}()
102+
}
103+
wg.Wait()
104+
}

0 commit comments

Comments
 (0)