mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
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>
169 lines
3.7 KiB
Go
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)
|
|
}
|