Skip to content

Commit

Permalink
loopback: fix race condition opening loopback device
Browse files Browse the repository at this point in the history
the loopback device file could be already used/removed by another
process.  Since the process is inherently racy, just grab the next
available index and try again until it succeeds.

Closes: #2038

Signed-off-by: Giuseppe Scrivano <[email protected]>
  • Loading branch information
giuseppe committed Jul 23, 2024
1 parent b23e274 commit 01c633e
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 11 deletions.
30 changes: 19 additions & 11 deletions pkg/loopback/attach_loopback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
49 changes: 49 additions & 0 deletions pkg/loopback/attach_test.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 01c633e

Please sign in to comment.