Skip to content

Commit 54e0edc

Browse files
committed
Fix mounting extra block device
The current implementation doesn't work for extra block device mounting. An error is returned "rpc error: code = Unknown desc = failed to create VM: failed to patch drive mount stub: failed to expose patched drive contents to jail: file exists". For more contexts, read this PR: firecracker-microvm#522 This commit is for fixing the above issue. Signed-off-by: Royce Zhao <[email protected]>
1 parent 3dd59cd commit 54e0edc

File tree

4 files changed

+191
-8
lines changed

4 files changed

+191
-8
lines changed

internal/fsutil.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os/exec"
2222
"path/filepath"
2323
"strings"
24+
"syscall"
2425
"testing"
2526

2627
"github.com/pkg/errors"
@@ -33,7 +34,7 @@ func CreateFSImg(ctx context.Context, t *testing.T, fsType string, testFiles ...
3334
t.Helper()
3435

3536
switch fsType {
36-
case "ext3", "ext4":
37+
case "ext4":
3738
return createTestExtImg(ctx, t, fsType, testFiles...)
3839
default:
3940
require.FailNowf(t, "unsupported fs type %q", fsType)
@@ -72,6 +73,48 @@ func createTestExtImg(ctx context.Context, t *testing.T, extName string, testFil
7273
return imgFile.Name()
7374
}
7475

76+
// BlockDeviceFile represents a block device
77+
type BlockDeviceFile struct {
78+
Subpath string
79+
UID int
80+
GID int
81+
Dev int // major number
82+
}
83+
84+
// CreateBlockDevice creates a block device, or block special file for testing
85+
func CreateBlockDevice(ctx context.Context, t *testing.T, fsType string, testFile BlockDeviceFile) string {
86+
t.Helper()
87+
88+
switch fsType {
89+
case "ext4":
90+
return createTestBlockDevice(ctx, t, fsType, testFile)
91+
default:
92+
require.FailNowf(t, "unsupported fs type %q", fsType)
93+
return ""
94+
}
95+
}
96+
97+
func createTestBlockDevice(ctx context.Context, t *testing.T, extName string, testFile BlockDeviceFile) string {
98+
t.Helper()
99+
100+
deviceFile := filepath.Join("/tmp/blockExample", testFile.Subpath)
101+
err := os.MkdirAll(filepath.Dir(deviceFile), 0750)
102+
require.NoError(t, err, "failed to mkdir for the block device")
103+
104+
err = syscall.Mknod(deviceFile, syscall.S_IFBLK, testFile.Dev)
105+
require.NoError(t, err, "failed to create a new block device")
106+
107+
err = os.Chmod(deviceFile, 0600)
108+
require.NoError(t, err, "failed to change file mode for the new created block device")
109+
110+
err = os.Chown(deviceFile, testFile.UID, testFile.GID)
111+
require.NoError(t, err, "failed to grant permission to the new created block device")
112+
113+
output, err := exec.CommandContext(ctx, "mkfs."+extName, "-v", deviceFile).CombinedOutput()
114+
require.NoErrorf(t, err, "failed to create ext img, command output:%s \n", string(output))
115+
return deviceFile
116+
}
117+
75118
// MountInfo holds data parsed from a line of /proc/mounts
76119
type MountInfo struct {
77120
SourcePath string

runtime/jailer_integ_test.go

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import (
3535
"github.com/firecracker-microvm/firecracker-containerd/runtime/firecrackeroci"
3636
)
3737

38+
const (
39+
jailerUID = 300001
40+
jailerGID = 300001
41+
)
42+
3843
func TestJailer_Isolated(t *testing.T) {
3944
prepareIntegTest(t)
4045
t.Run("Without Jailer", func(t *testing.T) {
@@ -44,15 +49,35 @@ func TestJailer_Isolated(t *testing.T) {
4449
t.Run("With Jailer", func(t *testing.T) {
4550
t.Parallel()
4651
testJailer(t, &proto.JailerConfig{
47-
UID: 300001,
48-
GID: 300001,
52+
UID: jailerUID,
53+
GID: jailerGID,
4954
})
5055
})
5156
t.Run("With Jailer and bind-mount", func(t *testing.T) {
5257
t.Parallel()
5358
testJailer(t, &proto.JailerConfig{
54-
UID: 300001,
55-
GID: 300001,
59+
UID: jailerUID,
60+
GID: jailerGID,
61+
DriveExposePolicy: proto.DriveExposePolicy_BIND,
62+
})
63+
})
64+
}
65+
66+
func TestAttachBlockDevice_Isolated(t *testing.T) {
67+
prepareIntegTest(t)
68+
t.Run("Without Jailer", func(t *testing.T) {
69+
testAttachBlockDevice(t, nil)
70+
})
71+
t.Run("With Jailer", func(t *testing.T) {
72+
testAttachBlockDevice(t, &proto.JailerConfig{
73+
UID: jailerUID,
74+
GID: jailerGID,
75+
})
76+
})
77+
t.Run("With Jailer and bind-mount", func(t *testing.T) {
78+
testAttachBlockDevice(t, &proto.JailerConfig{
79+
UID: jailerUID,
80+
GID: jailerGID,
5681
DriveExposePolicy: proto.DriveExposePolicy_BIND,
5782
})
5883
})
@@ -170,3 +195,110 @@ func TestJailerCPUSet_Isolated(t *testing.T) {
170195
}
171196
testJailer(t, config)
172197
}
198+
199+
func testAttachBlockDevice(t *testing.T, jailerConfig *proto.JailerConfig) {
200+
require := require.New(t)
201+
client, err := containerd.New(containerdSockPath, containerd.WithDefaultRuntime(firecrackerRuntime))
202+
require.NoError(err, "unable to create client to containerd service at %s, is containerd running?", containerdSockPath)
203+
defer client.Close()
204+
205+
ctx := namespaces.WithNamespace(context.Background(), "default")
206+
207+
image, err := alpineImage(ctx, client, defaultSnapshotterName)
208+
require.NoError(err, "failed to get alpine image")
209+
210+
fcClient, err := newFCControlClient(containerdSockPath)
211+
require.NoError(err)
212+
213+
vmID := testNameToVMID(t.Name())
214+
215+
// default BlockDeviceFile setup, will change UID & GID if a jailerConfig is passed
216+
deviceFile := internal.BlockDeviceFile{
217+
Subpath: "dir/block1",
218+
UID: 0,
219+
GID: 0,
220+
Dev: 259, // 259 is the major number for blkext which is one type of block devices
221+
}
222+
223+
if jailerConfig != nil {
224+
// cover "With Jailer" & "With Jailer and bind-mount" test cases
225+
deviceFile.UID = int(jailerConfig.UID)
226+
deviceFile.GID = int(jailerConfig.GID)
227+
}
228+
additionalBlockDevice := internal.CreateBlockDevice(ctx, t, "ext4", deviceFile)
229+
blockExampleDir := filepath.Dir(additionalBlockDevice)
230+
defer os.RemoveAll(filepath.Dir(blockExampleDir)) // clean up
231+
232+
request := proto.CreateVMRequest{
233+
VMID: vmID,
234+
JailerConfig: jailerConfig,
235+
DriveMounts: []*proto.FirecrackerDriveMount{
236+
{HostPath: additionalBlockDevice, VMPath: "/home/driveMount", FilesystemType: "ext4"},
237+
},
238+
}
239+
240+
// If the drive files are bind-mounted, the files must be readable from the jailer's user.
241+
if jailerConfig != nil && jailerConfig.DriveExposePolicy == proto.DriveExposePolicy_BIND {
242+
f, err := ioutil.TempFile("", fsSafeTestName(t)+"_rootfs")
243+
require.NoError(err)
244+
defer f.Close()
245+
246+
dst := f.Name()
247+
248+
// Copy the root drive before chown, since the file is used by other tests.
249+
err = copyFile(defaultRuntimeConfig.RootDrive, dst, 0400)
250+
require.NoErrorf(err, "failed to copy a rootfs as %q", dst)
251+
252+
err = os.Chown(dst, int(jailerConfig.UID), int(jailerConfig.GID))
253+
require.NoError(err, "failed to chown %q", dst)
254+
255+
request.RootDrive = &proto.FirecrackerRootDrive{HostPath: dst}
256+
257+
// The additional drive file is only used by this test.
258+
err = os.Chown(additionalBlockDevice, int(jailerConfig.UID), int(jailerConfig.GID))
259+
require.NoError(err, "failed to chown %q", additionalBlockDevice)
260+
}
261+
262+
_, err = fcClient.CreateVM(ctx, &request)
263+
require.NoError(err)
264+
265+
// create a container to test bind mount block device into the container
266+
c, err := client.NewContainer(ctx,
267+
vmID+"-container",
268+
containerd.WithSnapshotter(defaultSnapshotterName),
269+
containerd.WithNewSnapshot(vmID+"-snapshot", image),
270+
containerd.WithNewSpec(
271+
oci.WithProcessArgs(
272+
"/bin/sh", "-c", "echo heyhey && cd /mnt/blockDeviceTest",
273+
),
274+
firecrackeroci.WithVMID(vmID),
275+
oci.WithMounts([]specs.Mount{{
276+
Source: "/home/driveMount",
277+
Destination: "/mnt/blockDeviceTest",
278+
Options: []string{"bind"},
279+
}}),
280+
),
281+
)
282+
require.NoError(err)
283+
284+
stdout := startAndWaitTask(ctx, t, c)
285+
require.Equal("heyhey\n", stdout)
286+
287+
stat, err := os.Stat(filepath.Join(shimBaseDir(), "default#"+vmID))
288+
require.NoError(err)
289+
assert.True(t, stat.IsDir())
290+
291+
err = c.Delete(ctx, containerd.WithSnapshotCleanup)
292+
require.NoError(err, "failed to delete a container-block-device")
293+
294+
_, err = fcClient.StopVM(ctx, &proto.StopVMRequest{VMID: vmID})
295+
require.NoError(err)
296+
297+
_, err = os.Stat(filepath.Join(shimBaseDir(), "default#"+vmID))
298+
assert.Error(t, err)
299+
assert.True(t, os.IsNotExist(err))
300+
301+
shimContents, err := ioutil.ReadDir(shimBaseDir())
302+
require.NoError(err)
303+
assert.Len(t, shimContents, 0)
304+
}

runtime/runc_jailer.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,18 @@ func newRuncJailer(
138138

139139
func (j *runcJailer) prepareBindMounts(mounts []*proto.FirecrackerDriveMount) error {
140140
for _, m := range mounts {
141-
err := j.bindMountFileToJail(m.HostPath, filepath.Join(j.RootPath(), m.HostPath))
142-
if err != nil {
141+
stat := syscall.Stat_t{}
142+
if err := syscall.Stat(m.HostPath, &stat); err != nil {
143143
return err
144144
}
145+
// Only bindMount regular file.
146+
// To avoid duplicate files, for block device, we will use system call to create device file for it.
147+
if stat.Mode&syscall.S_IFMT == syscall.S_IFREG {
148+
err := j.bindMountFileToJail(m.HostPath, filepath.Join(j.RootPath(), m.HostPath))
149+
if err != nil {
150+
return err
151+
}
152+
}
145153
}
146154
return nil
147155
}

runtime/service_integ_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,7 @@ func TestDriveMount_Isolated(t *testing.T) {
10071007
},
10081008
{
10091009
VMPath: "/mnt",
1010-
FilesystemType: "ext3",
1010+
FilesystemType: "ext4",
10111011
// don't specify "ro" to validate it's automatically set via "IsWritable: false"
10121012
VMMountOptions: []string{"relatime"},
10131013
ContainerPath: "/bar",

0 commit comments

Comments
 (0)