Skip to content

Commit 9e42070

Browse files
bergwolfmxpv
authored andcommitted
mount: handle loopback mount
If a mount has specified `loop` option, we need to handle it on our own instead of passing it to the kernel. In such case, create a loopback device, attach the mount source to it, and mount the loopback device rather than the mount source. Signed-off-by: Peng Tao <bergwolf@hyper.sh>
1 parent 602af6f commit 9e42070

File tree

3 files changed

+370
-7
lines changed

3 files changed

+370
-7
lines changed

mount/losetup_linux.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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+
)
30+
31+
const (
32+
loopControlPath = "/dev/loop-control"
33+
loopDevFormat = "/dev/loop%d"
34+
35+
// According to util-linux/include/loopdev.h
36+
ioctlSetFd = 0x4C00
37+
ioctlClrFd = 0x4C01
38+
ioctlSetStatus64 = 0x4C04
39+
ioctlGetFree = 0x4C82
40+
41+
loFlagsReadonly = 1
42+
//loFlagsUseAops = 2
43+
loFlagsAutoclear = 4
44+
//loFlagsPartScan = 8
45+
loFlagsDirectIO = 16
46+
47+
ebusyString = "device or resource busy"
48+
)
49+
50+
// parameters to control loop device setup
51+
type LoopParams struct {
52+
// Loop device should forbid write
53+
Readonly bool
54+
// Loop device is automatically cleared by kernel when the
55+
// last opener closes it
56+
Autoclear bool
57+
// Use direct IO to access the loop backing file
58+
Direct bool
59+
}
60+
61+
// struct loop_info64 in util-linux/include/loopdev.h
62+
type loopInfo struct {
63+
/*
64+
device uint64
65+
inode uint64
66+
rdevice uint64
67+
offset uint64
68+
sizelimit uint64
69+
number uint32
70+
encryptType uint32
71+
encryptKeySize uint32
72+
*/
73+
_ [13]uint32
74+
flags uint32
75+
fileName [64]byte
76+
/*
77+
cryptName [64]byte
78+
encryptKey [32]byte
79+
init [2]uint64
80+
*/
81+
_ [112]byte
82+
}
83+
84+
func ioctl(fd, req, args uintptr) (uintptr, uintptr, error) {
85+
r1, r2, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, args)
86+
if errno != 0 {
87+
return 0, 0, errno
88+
}
89+
90+
return r1, r2, nil
91+
}
92+
93+
func getFreeLoopDev() (uint32, error) {
94+
ctrl, err := os.OpenFile(loopControlPath, os.O_RDWR, 0)
95+
if err != nil {
96+
return 0, errors.Errorf("could not open %v: %v", loopControlPath, err)
97+
}
98+
defer ctrl.Close()
99+
num, _, err := ioctl(ctrl.Fd(), ioctlGetFree, 0)
100+
if err != nil {
101+
return 0, errors.Wrap(err, "could not get free loop device")
102+
}
103+
return uint32(num), nil
104+
}
105+
106+
func setupLoopDev(backingFile, loopDev string, param LoopParams) (devFile *os.File, err error) {
107+
// 1. Open backing file and loop device
108+
oflags := os.O_RDWR
109+
if param.Readonly {
110+
oflags = os.O_RDONLY
111+
}
112+
back, err := os.OpenFile(backingFile, oflags, 0)
113+
if err != nil {
114+
return nil, errors.Errorf("could not open backing file: %v", err)
115+
}
116+
defer back.Close()
117+
118+
loopFile, err := os.OpenFile(loopDev, oflags, 0)
119+
if err != nil {
120+
return nil, errors.Errorf("could not open loop device: %v", err)
121+
}
122+
defer func() {
123+
if err != nil {
124+
loopFile.Close()
125+
}
126+
}()
127+
128+
// 2. Set FD
129+
if _, _, err = ioctl(loopFile.Fd(), ioctlSetFd, back.Fd()); err != nil {
130+
return nil, errors.Errorf("could not set loop fd: %v", err)
131+
}
132+
133+
// 3. Set Info
134+
info := loopInfo{}
135+
copy(info.fileName[:], []byte(backingFile))
136+
if param.Readonly {
137+
info.flags |= loFlagsReadonly
138+
}
139+
if param.Autoclear {
140+
info.flags |= loFlagsAutoclear
141+
}
142+
if param.Direct {
143+
info.flags |= loFlagsAutoclear
144+
}
145+
if _, _, err := ioctl(loopFile.Fd(), ioctlSetStatus64, uintptr(unsafe.Pointer(&info))); err != nil {
146+
// Retry w/o direct IO flag in case kernel does not support it. The downside is that
147+
// it will suffer from double cache problem.
148+
info.flags &= ^(uint32(loFlagsDirectIO))
149+
if _, _, err := ioctl(loopFile.Fd(), ioctlSetStatus64, uintptr(unsafe.Pointer(&info))); err != nil {
150+
ioctl(loopFile.Fd(), ioctlClrFd, 0)
151+
return nil, errors.Errorf("cannot set loop info:%v", err)
152+
}
153+
}
154+
155+
return loopFile, nil
156+
}
157+
158+
// setupLoop looks for (and possibly creates) a free loop device, and
159+
// then attaches backingFile to it.
160+
//
161+
// When autoclear is true, caller should take care to close it when
162+
// done with the loop device. The loop device file handle keeps
163+
// loFlagsAutoclear in effect and we rely on it to clean up the loop
164+
// device. If caller closes the file handle after mounting the device,
165+
// kernel will clear the loop device after it is umounted. Otherwise
166+
// the loop device is cleared when the file handle is closed.
167+
//
168+
// When autoclear is false, caller should be responsible to remove
169+
// the loop device when done with it.
170+
//
171+
// Upon success, the file handle to the loop device is returned.
172+
func setupLoop(backingFile string, param LoopParams) (*os.File, error) {
173+
var loopDev string
174+
175+
for retry := 1; retry < 200; retry++ {
176+
num, err := getFreeLoopDev()
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
loopDev = fmt.Sprintf(loopDevFormat, num)
182+
loopFile, err := setupLoopDev(backingFile, loopDev, param)
183+
if err != nil {
184+
// Per util-linux/sys-utils/losetup.c:create_loop(),
185+
// free loop device can race and we end up failing
186+
// with EBUSY when trying to set it up.
187+
if strings.Contains(err.Error(), ebusyString) {
188+
// Fallback a bit to avoid live lock
189+
time.Sleep(time.Millisecond * time.Duration(rand.Intn(retry*10)))
190+
continue
191+
}
192+
return nil, err
193+
}
194+
return loopFile, nil
195+
}
196+
197+
return nil, errors.New("Timeout creating new loopback device")
198+
}
199+
200+
func removeLoop(loopdev string) error {
201+
dev, err := os.Open(loopdev)
202+
if err != nil {
203+
return err
204+
}
205+
_, _, err = ioctl(dev.Fd(), ioctlClrFd, 0)
206+
dev.Close()
207+
return err
208+
}
209+
210+
// Attach a specified backing file to a loop device
211+
func AttachLoopDevice(backingFile string) (string, error) {
212+
dev, err := setupLoop(backingFile, LoopParams{})
213+
if err != nil {
214+
return "", err
215+
}
216+
dev.Close()
217+
218+
return dev.Name(), nil
219+
}
220+
221+
// Detach a loop device
222+
func DetachLoopDevice(devices ...string) error {
223+
for _, dev := range devices {
224+
if err := removeLoop(dev); err != nil {
225+
return err
226+
}
227+
}
228+
229+
return nil
230+
}

mount/losetup_linux_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
func TestSetupLoop(t *testing.T) {
30+
testutil.RequiresRoot(t)
31+
const randomdata = "randomdata"
32+
33+
/* Non-existing loop */
34+
backingFile := "setup-loop-test-no-such-file"
35+
_, err := setupLoop(backingFile, LoopParams{})
36+
if err == nil {
37+
t.Fatalf("setupLoop with non-existing file should fail")
38+
}
39+
40+
f, err := ioutil.TempFile("", "losetup")
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
if err = f.Truncate(512); err != nil {
45+
t.Fatal(err)
46+
}
47+
backingFile = f.Name()
48+
f.Close()
49+
defer func() {
50+
if err := os.Remove(backingFile); err != nil {
51+
t.Fatal(err)
52+
}
53+
}()
54+
55+
/* RO loop */
56+
f, err = setupLoop(backingFile, LoopParams{Readonly: true, Autoclear: true})
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
ff, err := os.OpenFile(f.Name(), os.O_RDWR, 0)
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
if _, err = ff.Write([]byte(randomdata)); err == nil {
65+
t.Fatalf("writing to readonly loop device should fail")
66+
}
67+
if err = ff.Close(); err != nil {
68+
t.Fatal(err)
69+
}
70+
if err = f.Close(); err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
/* RW loop */
75+
f, err = setupLoop(backingFile, LoopParams{Autoclear: true})
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
ff, err = os.OpenFile(f.Name(), os.O_RDWR, 0)
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
if _, err = ff.Write([]byte(randomdata)); err != nil {
84+
t.Fatal(err)
85+
}
86+
if err = ff.Close(); err != nil {
87+
t.Fatal(err)
88+
}
89+
if err = f.Close(); err != nil {
90+
t.Fatal(err)
91+
}
92+
}
93+
94+
func TestAttachDetachLoopDevice(t *testing.T) {
95+
testutil.RequiresRoot(t)
96+
f, err := ioutil.TempFile("", "losetup")
97+
if err != nil {
98+
t.Fatal(err)
99+
}
100+
if err = f.Truncate(512); err != nil {
101+
t.Fatal(err)
102+
}
103+
f.Close()
104+
defer func() {
105+
if err := os.Remove(f.Name()); err != nil {
106+
t.Fatal(err)
107+
}
108+
}()
109+
110+
dev, err := AttachLoopDevice(f.Name())
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
if err = DetachLoopDevice(dev); err != nil {
115+
t.Fatal(err)
116+
}
117+
}

0 commit comments

Comments
 (0)