Skip to content

Commit 1cad9d9

Browse files
authored
Merge pull request #445 from kzys/jailer-patch-drive
Fix bind-mount for additional drive files
2 parents db10c6e + b931912 commit 1cad9d9

File tree

4 files changed

+120
-8
lines changed

4 files changed

+120
-8
lines changed

runtime/jailer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@ func newJailer(
105105
CgroupPath: request.JailerConfig.CgroupPath,
106106
DriveExposePolicy: request.JailerConfig.DriveExposePolicy,
107107
}
108-
return newRuncJailer(ctx, l, service.vmID, config)
108+
return newRuncJailer(ctx, l, service.vmID, config, request.DriveMounts)
109109
}

runtime/jailer_integ_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import (
2525
"github.com/containerd/containerd/namespaces"
2626
"github.com/containerd/containerd/oci"
2727
"github.com/containerd/containerd/pkg/ttrpcutil"
28+
"github.com/opencontainers/runtime-spec/specs-go"
2829
"github.com/stretchr/testify/assert"
2930
"github.com/stretchr/testify/require"
3031

3132
_ "github.com/firecracker-microvm/firecracker-containerd/firecracker-control"
33+
"github.com/firecracker-microvm/firecracker-containerd/internal"
3234
"github.com/firecracker-microvm/firecracker-containerd/proto"
3335
fccontrol "github.com/firecracker-microvm/firecracker-containerd/proto/service/fccontrol/ttrpc"
3436
"github.com/firecracker-microvm/firecracker-containerd/runtime/cpuset"
@@ -55,6 +57,10 @@ func TestJailer_Isolated(t *testing.T) {
5557
})
5658
}
5759

60+
func fsSafeTestName(tb testing.TB) string {
61+
return strings.Replace(tb.Name(), "/", "-", -1)
62+
}
63+
5864
func testJailer(t *testing.T, jailerConfig *proto.JailerConfig) {
5965
require := require.New(t)
6066

@@ -72,26 +78,39 @@ func testJailer(t *testing.T, jailerConfig *proto.JailerConfig) {
7278

7379
vmID := testNameToVMID(t.Name())
7480

81+
additionalDrive := internal.CreateFSImg(ctx, t, "ext4", internal.FSImgFile{
82+
Subpath: "dir/hello",
83+
Contents: "additional drive\n",
84+
})
85+
7586
request := proto.CreateVMRequest{
7687
VMID: vmID,
7788
JailerConfig: jailerConfig,
89+
DriveMounts: []*proto.FirecrackerDriveMount{
90+
{HostPath: additionalDrive, VMPath: "/mnt", FilesystemType: "ext4"},
91+
},
7892
}
7993

8094
// If the drive files are bind-mounted, the files must be readable from the jailer's user.
8195
if jailerConfig != nil && jailerConfig.DriveExposePolicy == proto.DriveExposePolicy_BIND {
82-
f, err := ioutil.TempFile("", strings.Replace(t.Name(), "/", "-", -1))
96+
f, err := ioutil.TempFile("", fsSafeTestName(t)+"_rootfs")
8397
require.NoError(err)
8498
defer f.Close()
8599

86100
dst := f.Name()
87101

102+
// Copy the root drive before chown, since the file is used by other tests.
88103
err = copyFile(defaultRuntimeConfig.RootDrive, dst, 0400)
89104
require.NoErrorf(err, "failed to copy a rootfs as %q", dst)
90105

91106
err = os.Chown(dst, int(jailerConfig.UID), int(jailerConfig.GID))
92107
require.NoError(err, "failed to chown %q", dst)
93108

94109
request.RootDrive = &proto.FirecrackerRootDrive{HostPath: dst}
110+
111+
// The additional drive file is only used by this test.
112+
err = os.Chown(additionalDrive, int(jailerConfig.UID), int(jailerConfig.GID))
113+
require.NoError(err, "failed to chown %q", additionalDrive)
95114
}
96115

97116
fcClient := fccontrol.NewFirecrackerClient(pluginClient.Client())
@@ -102,12 +121,22 @@ func testJailer(t *testing.T, jailerConfig *proto.JailerConfig) {
102121
"container",
103122
containerd.WithSnapshotter(defaultSnapshotterName),
104123
containerd.WithNewSnapshot("snapshot", image),
105-
containerd.WithNewSpec(oci.WithProcessArgs("/bin/echo", "-n", "hello"), firecrackeroci.WithVMID(vmID)),
124+
containerd.WithNewSpec(
125+
oci.WithProcessArgs(
126+
"/bin/sh", "-c", "echo hello && cat /mnt/in-container/dir/hello",
127+
),
128+
firecrackeroci.WithVMID(vmID),
129+
oci.WithMounts([]specs.Mount{{
130+
Source: "/mnt",
131+
Destination: "/mnt/in-container",
132+
Options: []string{"bind"},
133+
}}),
134+
),
106135
)
107136
require.NoError(err)
108137

109138
stdout := startAndWaitTask(ctx, t, c)
110-
require.Equal("hello", stdout)
139+
require.Equal("hello\nadditional drive\n", stdout)
111140

112141
stat, err := os.Stat(filepath.Join(shimBaseDir(), "default", vmID))
113142
require.NoError(err)

runtime/runc_jailer.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type runcJailer struct {
5151
vmID string
5252
configSpec specs.Spec
5353
runcClient runc.Runc
54+
started bool
5455
}
5556

5657
const firecrackerFileName = "firecracker"
@@ -69,7 +70,10 @@ type runcJailerConfig struct {
6970
DriveExposePolicy proto.DriveExposePolicy
7071
}
7172

72-
func newRuncJailer(ctx context.Context, logger *logrus.Entry, vmID string, cfg runcJailerConfig) (*runcJailer, error) {
73+
func newRuncJailer(
74+
ctx context.Context, logger *logrus.Entry, vmID string, cfg runcJailerConfig,
75+
mounts []*proto.FirecrackerDriveMount,
76+
) (*runcJailer, error) {
7377
l := logger.WithField("ociBundlePath", cfg.OCIBundlePath).
7478
WithField("runcBinaryPath", cfg.RuncBinPath)
7579

@@ -102,9 +106,26 @@ func newRuncJailer(ctx context.Context, logger *logrus.Entry, vmID string, cfg r
102106
return nil, errors.Wrapf(err, "%s failed to mkdirAndChown", rootPath)
103107
}
104108

109+
if j.Config.DriveExposePolicy == proto.DriveExposePolicy_BIND {
110+
err := j.prepareBindMounts(mounts)
111+
if err != nil {
112+
return nil, err
113+
}
114+
}
115+
105116
return j, nil
106117
}
107118

119+
func (j *runcJailer) prepareBindMounts(mounts []*proto.FirecrackerDriveMount) error {
120+
for _, m := range mounts {
121+
err := j.bindMountFileToJail(m.HostPath, filepath.Join(j.RootPath(), m.HostPath))
122+
if err != nil {
123+
return err
124+
}
125+
}
126+
return nil
127+
}
128+
108129
// JailPath returns the base directory from where the jail binary will be ran
109130
// from
110131
func (j *runcJailer) OCIBundlePath() string {
@@ -242,6 +263,8 @@ func (j *runcJailer) BuildJailedRootHandler(cfg *config.Config, machineConfig *f
242263
}
243264

244265
j.logger.Info("Successfully ran jailer handler")
266+
j.started = true
267+
245268
return nil
246269
},
247270
}
@@ -341,7 +364,7 @@ func (j *runcJailer) ExposeFileToJail(srcPath string) error {
341364
// exposeFileToJail will make the file accessible from the jail.
342365
func (j *runcJailer) exposeFileToJail(src, dst string, mode os.FileMode) error {
343366
if j.Config.DriveExposePolicy == proto.DriveExposePolicy_BIND {
344-
return j.bindMountFileToJail(src, dst, mode)
367+
return j.bindMountFileToJail(src, dst)
345368
}
346369
return j.copyFileToJail(src, dst, mode)
347370
}
@@ -359,7 +382,28 @@ func (j *runcJailer) copyFileToJail(src, dst string, mode os.FileMode) error {
359382

360383
// bindMountFileToJail mounts a file from src to dst, and chown the new file to
361384
// the jail user. Note that actual mount is not happening until runc is invoked.
362-
func (j *runcJailer) bindMountFileToJail(src, dst string, _ os.FileMode) error {
385+
func (j *runcJailer) bindMountFileToJail(src, dst string) error {
386+
// Once runc has started, the jailer cannot mount any new files.
387+
if j.started {
388+
_, err := os.Stat(dst)
389+
if err != nil {
390+
return errors.Wrapf(err, "%q must be created by runc", dst)
391+
}
392+
return nil
393+
}
394+
395+
// The directory must be traversable from runc (running as root) and Firecracker (running as j.Config.UID).
396+
// These two users don't have a shared group. Hence the permission must be 701, not 700.
397+
err := os.MkdirAll(filepath.Dir(dst), 0701)
398+
if err != nil {
399+
return err
400+
}
401+
402+
err = os.Chown(filepath.Dir(dst), int(j.Config.UID), int(j.Config.GID))
403+
if err != nil {
404+
return err
405+
}
406+
363407
f, err := os.Create(dst)
364408
if err != nil {
365409
return err

runtime/runc_jailer_test.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"syscall"
2222
"testing"
2323

24+
"github.com/firecracker-microvm/firecracker-containerd/proto"
25+
2426
"github.com/firecracker-microvm/firecracker-go-sdk"
2527
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
2628
"github.com/sirupsen/logrus"
@@ -61,7 +63,7 @@ func TestBuildJailedRootHandler_Isolated(t *testing.T) {
6163
GID: 456,
6264
}
6365
vmID := "foo"
64-
jailer, err := newRuncJailer(context.Background(), l, vmID, runcConfig)
66+
jailer, err := newRuncJailer(context.Background(), l, vmID, runcConfig, []*proto.FirecrackerDriveMount{})
6567
require.NoError(t, err, "failed to create runc jailer")
6668

6769
cfg := config.Config{
@@ -132,3 +134,40 @@ func TestMkdirAllWithPermissions_Isolated(t *testing.T) {
132134
assert.Equal(t, newuid, newlyCreatedPathStat.Sys().(*syscall.Stat_t).Uid)
133135
assert.Equal(t, newgid, newlyCreatedPathStat.Sys().(*syscall.Stat_t).Gid)
134136
}
137+
138+
func TestBindMountToJail_Isolated(t *testing.T) {
139+
// The user must be root to call chown.
140+
internal.RequiresIsolation(t)
141+
142+
dir, err := ioutil.TempDir("", t.Name())
143+
require.NoError(t, err)
144+
defer os.RemoveAll(dir)
145+
146+
f, err := os.Create(filepath.Join(dir, "src1"))
147+
require.NoError(t, err)
148+
defer f.Close()
149+
150+
j := &runcJailer{
151+
started: false,
152+
Config: runcJailerConfig{OCIBundlePath: dir},
153+
}
154+
155+
// Create the mount point. The mount point will be used by runc later.
156+
err = j.bindMountFileToJail(
157+
filepath.Join(dir, "src1"),
158+
filepath.Join(dir, "dst1"),
159+
)
160+
require.NoError(t, err)
161+
162+
_, err = os.Stat(filepath.Join(dir, "dst1"))
163+
require.NoError(t, err)
164+
165+
// Once runc has been started, it doesn't create a mount point and
166+
// let the caller know the method cannot be used.
167+
j.started = true
168+
err = j.bindMountFileToJail(
169+
filepath.Join(dir, "src2"),
170+
filepath.Join(dir, "not-found"),
171+
)
172+
require.Error(t, err)
173+
}

0 commit comments

Comments
 (0)