diff --git a/pkg/loopback/attach_loopback.go b/pkg/loopback/attach_loopback.go index bfdafcdf78..f9f0134984 100644 --- a/pkg/loopback/attach_loopback.go +++ b/pkg/loopback/attach_loopback.go @@ -40,7 +40,7 @@ func getNextFreeLoopbackIndex() (int, error) { return index, err } -func openNextAvailableLoopback(index int, sparseName string, sparseFile *os.File) (loopFile *os.File, err error) { +func openNextAvailableLoopback(sparseName string, sparseFile *os.File) (loopFile *os.File, err error) { // Read information about the loopback file. var st syscall.Stat_t err = syscall.Fstat(int(sparseFile.Fd()), &st) @@ -49,15 +49,31 @@ func openNextAvailableLoopback(index int, sparseName string, sparseFile *os.File return nil, ErrAttachLoopbackDevice } + // upper bound to avoid infinite loop + remaining := 1000 + // Start looking for a free /dev/loop for { + if remaining == 0 { + logrus.Errorf("No free loopback devices available") + return nil, ErrAttachLoopbackDevice + } + remaining-- + + index, err := getNextFreeLoopbackIndex() + if err != nil { + logrus.Debugf("Error retrieving the next available loopback: %s", err) + return nil, err + } + target := fmt.Sprintf("/dev/loop%d", index) - index++ // OpenFile adds O_CLOEXEC loopFile, err = os.OpenFile(target, os.O_RDWR, 0o644) if err != nil { if errors.Is(err, fs.ErrNotExist) { + // Another process could have taken the loopback device in the meantime. So repeat + // the process with the next loopback device. continue } logrus.Errorf("Opening loopback device: %s", err) @@ -127,14 +143,6 @@ func AttachLoopDeviceRO(sparseName string) (loop *os.File, err error) { } func attachLoopDevice(sparseName string, readonly bool) (loop *os.File, err error) { - // Try to retrieve the next available loopback device via syscall. - // If it fails, we discard error and start looping for a - // loopback from index 0. - startIndex, err := getNextFreeLoopbackIndex() - if err != nil { - logrus.Debugf("Error retrieving the next available loopback: %s", err) - } - var sparseFile *os.File // OpenFile adds O_CLOEXEC @@ -149,7 +157,7 @@ func attachLoopDevice(sparseName string, readonly bool) (loop *os.File, err erro } defer sparseFile.Close() - loopFile, err := openNextAvailableLoopback(startIndex, sparseName, sparseFile) + loopFile, err := openNextAvailableLoopback(sparseName, sparseFile) if err != nil { return nil, err } diff --git a/pkg/loopback/attach_test.go b/pkg/loopback/attach_test.go new file mode 100644 index 0000000000..8ae2f1143d --- /dev/null +++ b/pkg/loopback/attach_test.go @@ -0,0 +1,49 @@ +//go:build linux && cgo +// +build linux,cgo + +package loopback + +import ( + "os" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + maxDevicesPerGoroutine = 1000 + maxGoroutines = 10 +) + +func TestAttachLoopbackDeviceRace(t *testing.T) { + createLoopbackDevice := func() { + // Create a file to use as a backing file + f, err := os.CreateTemp(t.TempDir(), "loopback-test") + require.NoError(t, err) + defer f.Close() + + defer os.Remove(f.Name()) + + lp, err := AttachLoopDevice(f.Name()) + assert.NoError(t, err) + assert.NotNil(t, lp, "loopback device file should not be nil") + if lp != nil { + lp.Close() + } + } + + wg := sync.WaitGroup{} + + for i := 0; i < maxGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < maxDevicesPerGoroutine; i++ { + createLoopbackDevice() + } + }() + } + wg.Wait() +}