diff --git a/pkg/loopback/attach_loopback.go b/pkg/loopback/attach_loopback.go index bfdafcdf78..92f2f2186a 100644 --- a/pkg/loopback/attach_loopback.go +++ b/pkg/loopback/attach_loopback.go @@ -58,6 +58,13 @@ func openNextAvailableLoopback(index int, sparseName string, sparseFile *os.File 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. + index, err = getNextFreeLoopbackIndex() + if err != nil { + logrus.Debugf("Error retrieving the next available loopback: %s", err) + return nil, err + } continue } logrus.Errorf("Opening loopback device: %s", 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() +}