Files
containerd/core/mount/losetup_linux_test.go
T
Wei Fu a5c84021c8 core/mount: should not call removeLoop when set autoclear
In CI we run make root-test via gotestsum, which executes multiple
package tests concurrently. TestAutoclearTrueLoop attempts to invoke
LOOP_CLR_FD using a device name, which introduces a race condition.

Example race:

Process P1 represents mount.test which runs TestAutoclearTrueLoop
Process P2 represents manager.test which runs TestLoopbackMount

T1: P1 closes fd of loop-device (loop3) (kernel unsets backing-file on close)
T2: P2 gets loop3 from /dev/loop-control
T3: P2 configures loop3 with backing file successfully
T4: P1 invokes removeLoop to clear backing file for loop3

You might see that failure like this

```
=== FAIL: core/mount/manager TestLoopbackMount (0.05s)
    log_hook.go:47: time="2025-10-23T21:49:22.532811960Z" level=debug msg="activating mount" func="manager.(*mountManager).Activate" file="/home/runner/work/containerd/containerd/core/mount/manager/manager.go:134" mounts="[{loop /tmp/TestLoopbackMount989607109/001/fs-1621892597  []} {format/ext4 {{ mount 0 }}  []}]" name=id1 testcase=TestLoopbackMount
    helpers.go💯 unmount /tmp/TestLoopbackMount989607109/001/test-mount-3030342351
    manager_linux_test.go:80:
        	Error Trace:	/home/runner/work/containerd/containerd/core/mount/manager/manager_linux_test.go:80
        	            				/home/runner/work/containerd/containerd/core/mount/manager/manager_linux_test.go:105
        	Error:      	Received unexpected error:
        	            	failed to get loop device info: no such device or address
        	Test:       	TestLoopbackMount
```

To fix this, the test now compares backing-file's inode directly and does
not call removeLoop when autoclear is set.

Signed-off-by: Wei Fu <fuweid89@gmail.com>
2025-11-23 21:31:08 -05:00

169 lines
3.7 KiB
Go

/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mount
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/containerd/continuity/testutil"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
var randomData = []byte("randomdata")
func createTempFile(t *testing.T) string {
t.Helper()
f, err := os.Create(filepath.Join(t.TempDir(), "losetup"))
require.NoError(t, err)
t.Cleanup(func() {
f.Close()
})
err = f.Truncate(512)
require.NoError(t, err)
return f.Name()
}
func TestNonExistingLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := "setup-loop-test-no-such-file"
_, err := SetupLoop(backingFile, LoopParams{})
if err == nil {
t.Fatalf("setupLoop with non-existing file should fail")
}
}
func TestRoLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := createTempFile(t)
file, err := SetupLoop(backingFile, LoopParams{Readonly: true, Autoclear: true})
require.NoError(t, err)
t.Cleanup(func() {
file.Close()
})
if _, err := file.Write(randomData); err == nil {
t.Fatalf("writing to readonly loop device should fail")
}
}
func TestRwLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := createTempFile(t)
file, err := SetupLoop(backingFile, LoopParams{Autoclear: true})
require.NoError(t, err)
t.Cleanup(func() {
file.Close()
})
if _, err := file.Write(randomData); err != nil {
t.Fatal(err)
}
}
func TestAttachDetachLoopDevice(t *testing.T) {
testutil.RequiresRoot(t)
path := createTempFile(t)
dev, err := AttachLoopDevice(path)
require.NoError(t, err)
err = DetachLoopDevice(dev)
require.NoError(t, err)
}
func TestAutoclearTrueLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := createTempFile(t)
bInfo, err := os.Stat(backingFile)
require.NoError(t, err)
bInode := bInfo.Sys().(*syscall.Stat_t).Ino
file, err := SetupLoop(backingFile, LoopParams{Autoclear: true})
require.NoError(t, err)
dev := file.Name()
file.Close()
var checkFn = func(loopDev string, expectedInode uint64) (_shouldRetry bool, _ error) {
loop, err := os.Open(loopDev)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("failed to open loop device: %w", err)
}
info, err := unix.IoctlLoopGetStatus64(int(loop.Fd()))
loop.Close()
if err != nil {
if errors.Is(err, unix.ENXIO) {
return false, nil
}
return false, fmt.Errorf("failed to get loop device info: %w", err)
}
if info.Inode != expectedInode {
return false, nil
}
t.Logf("loop device %s still present with backing inode %d", loopDev, expectedInode)
return true, nil
}
for range 10 {
retry, err := checkFn(dev, bInode)
require.NoError(t, err)
if !retry {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("loop device %s still present after autoclear", dev)
}
func TestAutoclearFalseLoop(t *testing.T) {
testutil.RequiresRoot(t)
dev := func() string {
backingFile := createTempFile(t)
file, err := SetupLoop(backingFile, LoopParams{Autoclear: false})
require.NoError(t, err)
dev := file.Name()
file.Close()
return dev
}()
time.Sleep(100 * time.Millisecond)
err := removeLoop(dev)
require.NoError(t, err)
}