Skip to content

Commit 41a69eb

Browse files
committed
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:100: 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 <[email protected]> (cherry picked from commit a5c8402) Signed-off-by: Wei Fu <[email protected]>
1 parent 280dc16 commit 41a69eb

2 files changed

Lines changed: 45 additions & 13 deletions

File tree

core/mount/loopback_handler_linux.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package mount
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"os"
2223
"time"
2324

@@ -79,7 +80,7 @@ func (loopbackHandler) Unmount(ctx context.Context, path string) error {
7980
defer loop.Close()
8081

8182
if err := setLoopAutoclear(loop, true); err != nil {
82-
return err
83+
return fmt.Errorf("failed to set auto clear on loop device %q: %w", loopdev, err)
8384
}
8485

8586
if err := os.Remove(path); err != nil {

core/mount/losetup_linux_test.go

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
package mount
1818

1919
import (
20+
"errors"
21+
"fmt"
2022
"os"
2123
"path/filepath"
24+
"syscall"
2225
"testing"
2326
"time"
2427

2528
"github.com/containerd/continuity/testutil"
2629
"github.com/stretchr/testify/require"
30+
"golang.org/x/sys/unix"
2731
)
2832

2933
var randomData = []byte("randomdata")
@@ -100,23 +104,50 @@ func TestAttachDetachLoopDevice(t *testing.T) {
100104
func TestAutoclearTrueLoop(t *testing.T) {
101105
testutil.RequiresRoot(t)
102106

103-
dev := func() string {
104-
backingFile := createTempFile(t)
107+
backingFile := createTempFile(t)
108+
bInfo, err := os.Stat(backingFile)
109+
require.NoError(t, err)
110+
bInode := bInfo.Sys().(*syscall.Stat_t).Ino
111+
112+
file, err := SetupLoop(backingFile, LoopParams{Autoclear: true})
113+
require.NoError(t, err)
114+
dev := file.Name()
115+
file.Close()
116+
117+
var checkFn = func(loopDev string, expectedInode uint64) (_shouldRetry bool, _ error) {
118+
loop, err := os.Open(loopDev)
119+
if err != nil {
120+
if os.IsNotExist(err) {
121+
return false, nil
122+
}
123+
return false, fmt.Errorf("failed to open loop device: %w", err)
124+
}
125+
info, err := unix.IoctlLoopGetStatus64(int(loop.Fd()))
126+
loop.Close()
127+
if err != nil {
128+
if errors.Is(err, unix.ENXIO) {
129+
return false, nil
130+
}
131+
return false, fmt.Errorf("failed to get loop device info: %w", err)
132+
}
133+
134+
if info.Inode != expectedInode {
135+
return false, nil
136+
}
137+
138+
t.Logf("loop device %s still present with backing inode %d", loopDev, expectedInode)
139+
return true, nil
140+
}
105141

106-
file, err := SetupLoop(backingFile, LoopParams{Autoclear: true})
107-
require.NoError(t, err)
108-
dev := file.Name()
109-
file.Close()
110-
return dev
111-
}()
112142
for range 10 {
113-
if err := removeLoop(dev); err != nil {
114-
// Expected to fail as Autoclear should have already removed the loop device
143+
retry, err := checkFn(dev, bInode)
144+
require.NoError(t, err)
145+
if !retry {
115146
return
116147
}
117-
time.Sleep(20 * time.Millisecond)
148+
time.Sleep(100 * time.Millisecond)
118149
}
119-
t.Fatalf("removeLoop should fail if Autoclear is true")
150+
t.Fatalf("loop device %s still present after autoclear", dev)
120151
}
121152

122153
func TestAutoclearFalseLoop(t *testing.T) {

0 commit comments

Comments
 (0)