Files
containerd/core/mount/loopback_handler_linux.go
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

105 lines
2.5 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 (
"context"
"fmt"
"os"
"time"
"github.com/containerd/errdefs"
"github.com/containerd/log"
)
func LoopbackHandler() Handler {
return loopbackHandler{}
}
type loopbackHandler struct {
}
func (loopbackHandler) Mount(ctx context.Context, m Mount, mp string, _ []ActiveMount) (ActiveMount, error) {
if m.Type != "loop" {
return ActiveMount{}, errdefs.ErrNotImplemented
}
params := LoopParams{
Autoclear: true,
}
// TODO: Handle readonly
// TODO: Handle direct io
t := time.Now()
loop, err := SetupLoop(m.Source, params)
if err != nil {
return ActiveMount{}, err
}
defer loop.Close()
if err := os.Symlink(loop.Name(), mp); err != nil {
return ActiveMount{}, err
}
if err := setLoopAutoclear(loop, false); err != nil {
return ActiveMount{}, err
}
return ActiveMount{
Mount: m,
MountedAt: &t,
MountPoint: mp,
}, nil
}
func (loopbackHandler) Unmount(ctx context.Context, path string) error {
loopdev, err := os.Readlink(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
loop, err := os.Open(loopdev)
if err != nil {
return err
}
defer loop.Close()
if err := setLoopAutoclear(loop, true); err != nil {
return fmt.Errorf("failed to set auto clear on loop device %q: %w", loopdev, err)
}
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
return nil
}
// if removal of the symlink has failed, its possible for the loop device to get cleaned
// up and re-used. Leave the loop device around to prevent re-use and let a retry of
// Unmount clear it.`
if err := setLoopAutoclear(loop, false); err != nil {
// Very unlikely but log to track in case there is a problem with
// the loop being cleared and re-used.
log.G(ctx).WithError(err).Errorf("Failed to unset auto clear flag on symlink removal failure, loopback %q may be cleaned up while still being tracked", loopdev)
}
return err
}
return nil
}