Skip to content

Commit 75c2646

Browse files
authored
Merge pull request #4902 from mxpv/losetup
[Carry] mount: handle loopback mount
2 parents d7ec6e9 + eb16492 commit 75c2646

8 files changed

Lines changed: 341 additions & 236 deletions

File tree

mount/losetup_linux.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mount
18+
19+
import (
20+
"fmt"
21+
"math/rand"
22+
"os"
23+
"strings"
24+
"syscall"
25+
"time"
26+
"unsafe"
27+
28+
"github.com/pkg/errors"
29+
"golang.org/x/sys/unix"
30+
)
31+
32+
const (
33+
loopControlPath = "/dev/loop-control"
34+
loopDevFormat = "/dev/loop%d"
35+
36+
ebusyString = "device or resource busy"
37+
)
38+
39+
// LoopParams parameters to control loop device setup
40+
type LoopParams struct {
41+
// Loop device should forbid write
42+
Readonly bool
43+
// Loop device is automatically cleared by kernel when the
44+
// last opener closes it
45+
Autoclear bool
46+
// Use direct IO to access the loop backing file
47+
Direct bool
48+
}
49+
50+
func ioctl(fd, req, args uintptr) (uintptr, uintptr, error) {
51+
r1, r2, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, args)
52+
if errno != 0 {
53+
return 0, 0, errno
54+
}
55+
56+
return r1, r2, nil
57+
}
58+
59+
func getFreeLoopDev() (uint32, error) {
60+
ctrl, err := os.OpenFile(loopControlPath, os.O_RDWR, 0)
61+
if err != nil {
62+
return 0, errors.Errorf("could not open %v: %v", loopControlPath, err)
63+
}
64+
defer ctrl.Close()
65+
num, _, err := ioctl(ctrl.Fd(), unix.LOOP_CTL_GET_FREE, 0)
66+
if err != nil {
67+
return 0, errors.Wrap(err, "could not get free loop device")
68+
}
69+
return uint32(num), nil
70+
}
71+
72+
func setupLoopDev(backingFile, loopDev string, param LoopParams) error {
73+
// 1. Open backing file and loop device
74+
flags := os.O_RDWR
75+
if param.Readonly {
76+
flags = os.O_RDONLY
77+
}
78+
79+
back, err := os.OpenFile(backingFile, flags, 0)
80+
if err != nil {
81+
return errors.Wrapf(err, "could not open backing file: %s", backingFile)
82+
}
83+
defer back.Close()
84+
85+
loop, err := os.OpenFile(loopDev, flags, 0)
86+
if err != nil {
87+
return errors.Wrapf(err, "could not open loop device: %s", loopDev)
88+
}
89+
defer loop.Close()
90+
91+
// 2. Set FD
92+
if _, _, err = ioctl(loop.Fd(), unix.LOOP_SET_FD, back.Fd()); err != nil {
93+
return errors.Wrapf(err, "could not set loop fd for device: %s", loopDev)
94+
}
95+
96+
// 3. Set Info
97+
info := unix.LoopInfo64{}
98+
copy(info.File_name[:], backingFile)
99+
if param.Readonly {
100+
info.Flags |= unix.LO_FLAGS_READ_ONLY
101+
}
102+
103+
if param.Autoclear {
104+
info.Flags |= unix.LO_FLAGS_AUTOCLEAR
105+
}
106+
107+
if param.Direct {
108+
info.Flags |= unix.LO_FLAGS_DIRECT_IO
109+
}
110+
111+
_, _, err = ioctl(loop.Fd(), unix.LOOP_SET_STATUS64, uintptr(unsafe.Pointer(&info)))
112+
if err == nil {
113+
return nil
114+
}
115+
116+
if param.Direct {
117+
// Retry w/o direct IO flag in case kernel does not support it. The downside is that
118+
// it will suffer from double cache problem.
119+
info.Flags &= ^(uint32(unix.LO_FLAGS_DIRECT_IO))
120+
_, _, err = ioctl(loop.Fd(), unix.LOOP_SET_STATUS64, uintptr(unsafe.Pointer(&info)))
121+
if err == nil {
122+
return nil
123+
}
124+
}
125+
126+
// Cleanup loop fd and return error
127+
_, _, _ = ioctl(loop.Fd(), unix.LOOP_CLR_FD, 0)
128+
return errors.Errorf("failed to set loop device info: %v", err)
129+
}
130+
131+
// setupLoop looks for (and possibly creates) a free loop device, and
132+
// then attaches backingFile to it.
133+
//
134+
// When autoclear is true, caller should take care to close it when
135+
// done with the loop device. The loop device file handle keeps
136+
// loFlagsAutoclear in effect and we rely on it to clean up the loop
137+
// device. If caller closes the file handle after mounting the device,
138+
// kernel will clear the loop device after it is umounted. Otherwise
139+
// the loop device is cleared when the file handle is closed.
140+
//
141+
// When autoclear is false, caller should be responsible to remove
142+
// the loop device when done with it.
143+
//
144+
// Upon success, the file handle to the loop device is returned.
145+
func setupLoop(backingFile string, param LoopParams) (string, error) {
146+
for retry := 1; retry < 100; retry++ {
147+
num, err := getFreeLoopDev()
148+
if err != nil {
149+
return "", err
150+
}
151+
152+
loopDev := fmt.Sprintf(loopDevFormat, num)
153+
if err := setupLoopDev(backingFile, loopDev, param); err != nil {
154+
// Per util-linux/sys-utils/losetup.c:create_loop(),
155+
// free loop device can race and we end up failing
156+
// with EBUSY when trying to set it up.
157+
if strings.Contains(err.Error(), ebusyString) {
158+
// Fallback a bit to avoid live lock
159+
time.Sleep(time.Millisecond * time.Duration(rand.Intn(retry*10)))
160+
continue
161+
}
162+
return "", err
163+
}
164+
165+
return loopDev, nil
166+
}
167+
168+
return "", errors.New("timeout creating new loopback device")
169+
}
170+
171+
func removeLoop(loopdev string) error {
172+
file, err := os.Open(loopdev)
173+
if err != nil {
174+
return err
175+
}
176+
defer file.Close()
177+
178+
_, _, err = ioctl(file.Fd(), unix.LOOP_CLR_FD, 0)
179+
return err
180+
}
181+
182+
// Attach a specified backing file to a loop device
183+
func AttachLoopDevice(backingFile string) (string, error) {
184+
return setupLoop(backingFile, LoopParams{})
185+
}
186+
187+
// Detach a loop device
188+
func DetachLoopDevice(devices ...string) error {
189+
for _, dev := range devices {
190+
if err := removeLoop(dev); err != nil {
191+
return errors.Wrapf(err, "failed to remove loop device: %s", dev)
192+
}
193+
}
194+
195+
return nil
196+
}

mount/losetup_linux_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// +build linux
2+
3+
/*
4+
Copyright The containerd Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package mount
20+
21+
import (
22+
"io/ioutil"
23+
"os"
24+
"testing"
25+
26+
"github.com/containerd/continuity/testutil"
27+
)
28+
29+
var randomData = []byte("randomdata")
30+
31+
func createTempFile(t *testing.T) string {
32+
t.Helper()
33+
34+
f, err := ioutil.TempFile("", "losetup")
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
defer f.Close()
39+
40+
if err = f.Truncate(512); err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
return f.Name()
45+
}
46+
47+
func TestNonExistingLoop(t *testing.T) {
48+
testutil.RequiresRoot(t)
49+
50+
backingFile := "setup-loop-test-no-such-file"
51+
_, err := setupLoop(backingFile, LoopParams{})
52+
if err == nil {
53+
t.Fatalf("setupLoop with non-existing file should fail")
54+
}
55+
}
56+
57+
func TestRoLoop(t *testing.T) {
58+
testutil.RequiresRoot(t)
59+
60+
backingFile := createTempFile(t)
61+
defer func() {
62+
if err := os.Remove(backingFile); err != nil {
63+
t.Fatal(err)
64+
}
65+
}()
66+
67+
path, err := setupLoop(backingFile, LoopParams{Readonly: true, Autoclear: true})
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
72+
if err := ioutil.WriteFile(path, randomData, os.ModePerm); err == nil {
73+
t.Fatalf("writing to readonly loop device should fail")
74+
}
75+
}
76+
77+
func TestRwLoop(t *testing.T) {
78+
testutil.RequiresRoot(t)
79+
80+
backingFile := createTempFile(t)
81+
defer func() {
82+
if err := os.Remove(backingFile); err != nil {
83+
t.Fatal(err)
84+
}
85+
}()
86+
87+
path, err := setupLoop(backingFile, LoopParams{Autoclear: true})
88+
if err != nil {
89+
t.Fatal(err)
90+
}
91+
92+
if err := ioutil.WriteFile(path, randomData, os.ModePerm); err != nil {
93+
t.Fatal(err)
94+
}
95+
}
96+
97+
func TestAttachDetachLoopDevice(t *testing.T) {
98+
testutil.RequiresRoot(t)
99+
100+
path := createTempFile(t)
101+
defer func() {
102+
if err := os.Remove(path); err != nil {
103+
t.Fatal(err)
104+
}
105+
}()
106+
107+
dev, err := AttachLoopDevice(path)
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
112+
if err = DetachLoopDevice(dev); err != nil {
113+
t.Fatal(err)
114+
}
115+
}

mount/mount_linux.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func init() {
4242
//
4343
// If m.Type starts with "fuse." or "fuse3.", "mount.fuse" or "mount.fuse3"
4444
// helper binary is called.
45-
func (m *Mount) Mount(target string) error {
45+
func (m *Mount) Mount(target string) (err error) {
4646
for _, helperBinary := range allowedHelperBinaries {
4747
// helperBinary = "mount.fuse", typePrefix = "fuse."
4848
typePrefix := strings.TrimPrefix(helperBinary, "mount.") + "."
@@ -62,7 +62,7 @@ func (m *Mount) Mount(target string) error {
6262
chdir, options = compactLowerdirOption(options)
6363
}
6464

65-
flags, data := parseMountOptions(options)
65+
flags, data, losetup := parseMountOptions(options)
6666
if len(data) > pagesize {
6767
return errors.Errorf("mount options is too long")
6868
}
@@ -77,7 +77,18 @@ func (m *Mount) Mount(target string) error {
7777
if flags&unix.MS_REMOUNT == 0 || data != "" {
7878
// Initial call applying all non-propagation flags for mount
7979
// or remount with changed data
80-
if err := mountAt(chdir, m.Source, target, m.Type, uintptr(oflags), data); err != nil {
80+
source := m.Source
81+
if losetup {
82+
devFile, err := setupLoop(m.Source, LoopParams{
83+
Readonly: oflags&unix.MS_RDONLY == unix.MS_RDONLY,
84+
Autoclear: true})
85+
if err != nil {
86+
return err
87+
}
88+
// Mount the loop device instead
89+
source = devFile
90+
}
91+
if err := mountAt(chdir, source, target, m.Type, uintptr(oflags), data); err != nil {
8192
return err
8293
}
8394
}
@@ -186,11 +197,13 @@ func UnmountAll(mount string, flags int) error {
186197

187198
// parseMountOptions takes fstab style mount options and parses them for
188199
// use with a standard mount() syscall
189-
func parseMountOptions(options []string) (int, string) {
200+
func parseMountOptions(options []string) (int, string, bool) {
190201
var (
191-
flag int
192-
data []string
202+
flag int
203+
losetup bool
204+
data []string
193205
)
206+
loopOpt := "loop"
194207
flags := map[string]struct {
195208
clear bool
196209
flag int
@@ -231,11 +244,13 @@ func parseMountOptions(options []string) (int, string) {
231244
} else {
232245
flag |= f.flag
233246
}
247+
} else if o == loopOpt {
248+
losetup = true
234249
} else {
235250
data = append(data, o)
236251
}
237252
}
238-
return flag, strings.Join(data, ",")
253+
return flag, strings.Join(data, ","), losetup
239254
}
240255

241256
// compactLowerdirOption updates overlay lowdir option and returns the common

0 commit comments

Comments
 (0)