Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/linters/urunc-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ LOGPIPE
MANDLOCK
Mainas
Mewz
cgroupfs
SECURITYFS
DEBUGFS
TRACEFS
EFIVARFS
PSTOREFS
BINFMTFS
NSFS
Mirred
Mkdev
Mountpoint
Expand Down
116 changes: 116 additions & 0 deletions pkg/unikontainers/bind_mount_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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 unikontainers

import (
"os"
"path/filepath"
"testing"

"github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

func TestIsBindMount(t *testing.T) {
t.Parallel()

// Docker spells a volume as type bind with an rbind option.
assert.True(t, isBindMount(specs.Mount{Type: "bind", Options: []string{"rbind", "ro"}}))
// nerdctl and containerd spell a volume as type none with an rbind option.
assert.True(t, isBindMount(specs.Mount{Type: "none", Options: []string{"rbind", "rprivate", "rw"}}))
assert.True(t, isBindMount(specs.Mount{Options: []string{"bind"}}))
// The options alone decide: a bind or rbind Type without such an option is
// not a bind mount.
assert.False(t, isBindMount(specs.Mount{Type: "bind"}))
assert.False(t, isBindMount(specs.Mount{Type: "rbind", Options: []string{"rw"}}))
assert.False(t, isBindMount(specs.Mount{Type: "tmpfs", Options: []string{"nosuid"}}))
assert.False(t, isBindMount(specs.Mount{Type: "proc"}))

// filterBindMounts keeps such volumes and re-roots them under the shared rootfs.
dir := t.TempDir()
vol := t.TempDir()
mounts, err := filterBindMounts(dir, []specs.Mount{
{Type: "none", Source: vol, Destination: "/data", Options: []string{"rbind", "rw"}},
{Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
})
require.NoError(t, err)
require.Len(t, mounts, 1)
assert.Equal(t, vol, mounts[0].Source)
assert.Equal(t, containerRootfsMountPath+"/data", mounts[0].Destination)
}

func TestOnlyBindMounts(t *testing.T) {
t.Parallel()

vol := t.TempDir()
file := filepath.Join(vol, "file")
require.NoError(t, os.WriteFile(file, []byte("data"), 0o600))

mounts := []specs.Mount{
{Type: "bind", Source: vol, Destination: "/data", Options: []string{"rbind"}},
{Type: "none", Source: file, Destination: "/etc/file", Options: []string{"bind", "ro"}},
{Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
{Type: "sysfs", Source: "sysfs", Destination: "/sys"},
// Rootless runtimes emit an rbind of the host's /sys in place of sysfs.
{Type: "none", Source: "/sys", Destination: "/sys", Options: []string{"rbind", "nosuid", "noexec", "nodev", "ro"}},
{Type: "bind", Source: "/proc", Destination: "/host/proc", Options: []string{"rbind"}},
{Type: "bind", Source: "/proc/cpuinfo", Destination: "/cpuinfo", Options: []string{"bind"}},
}
// cgroupfs is a separate filesystem mounted under /sys.
if kernelFS, err := isHostKernelFS("/sys/fs/cgroup"); err == nil && kernelFS {
mounts = append(mounts, specs.Mount{Type: "bind", Source: "/sys/fs/cgroup", Destination: "/cgroup", Options: []string{"rbind"}})
}

result, err := onlyBindMounts(mounts)
require.NoError(t, err)
require.Len(t, result, 2)
assert.Equal(t, vol, result[0].Source)
assert.Equal(t, file, result[1].Source)

_, err = onlyBindMounts([]specs.Mount{
{Type: "bind", Source: filepath.Join(vol, "missing"), Destination: "/missing", Options: []string{"rbind"}},
})
assert.Error(t, err)
}

func TestCopyMountfilesSkipsSpecialFiles(t *testing.T) {
t.Parallel()

src := t.TempDir()
file := filepath.Join(src, "file")
require.NoError(t, os.WriteFile(file, []byte("data"), 0o600))
fifo := filepath.Join(src, "fifo")
require.NoError(t, unix.Mkfifo(fifo, 0o600))

assert.ErrorIs(t, fileFromHost(t.TempDir(), fifo, "/fifo"), ErrNotRegularFile)
assert.ErrorIs(t, fileFromHost(t.TempDir(), "/dev/null", "/dev/null"), ErrNotRegularFile)
assert.ErrorIs(t, fileFromHost(t.TempDir(), src, "/data"), ErrNotRegularFile)

target := t.TempDir()
require.NoError(t, copyMountfiles(target, []specs.Mount{
{Type: "none", Source: file, Destination: "/etc/file", Options: []string{"rbind"}},
{Type: "none", Source: fifo, Destination: "/run/fifo", Options: []string{"rbind"}},
{Type: "bind", Source: "/dev/null", Destination: "/dev/null", Options: []string{"rbind"}},
{Type: "none", Source: src, Destination: "/data", Options: []string{"rbind"}},
}))

content, err := os.ReadFile(filepath.Join(target, "etc/file"))
require.NoError(t, err)
assert.Equal(t, "data", string(content))
assert.NoFileExists(t, filepath.Join(target, "run/fifo"))
assert.NoFileExists(t, filepath.Join(target, "dev/null"))
}
24 changes: 13 additions & 11 deletions pkg/unikontainers/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,13 @@ func extractBootFiles(rootfsPath string, newRootfsPath string, unikernel string,
}

func copyMountfiles(targetPath string, mounts []specs.Mount) error {
for _, m := range mounts {
if m.Type != "bind" {
continue
}
err := fileFromHost(targetPath, m.Source, m.Destination)
if (err != nil) && !errors.Is(err, ErrCopyDir) {
bindMounts, err := onlyBindMounts(mounts)
if err != nil {
return err
}
for _, m := range bindMounts {
err = fileFromHost(targetPath, m.Source, m.Destination)
if (err != nil) && !errors.Is(err, ErrNotRegularFile) {
return err
}
}
Expand Down Expand Up @@ -210,11 +211,12 @@ func handleExplicitBlockImage(blockImg string, mountPoint string) (types.BlockDe
// find the ones that come from a block.
func getBlockVolumes(mounts []specs.Mount, ukernel types.Unikernel) ([]types.BlockDevParams, error) {
blkImgs := []types.BlockDevParams{}
for i, m := range mounts {
// We check only bind mounts
if m.Type != "bind" {
continue
}
// We check only bind mounts
bindMounts, err := onlyBindMounts(mounts)
if err != nil {
return nil, err
}
for i, m := range bindMounts {
// Get the information of the source path
// from /proc/thread-self/mountinfo
mInfo, err := getMountInfo(m.Source)
Expand Down
10 changes: 3 additions & 7 deletions pkg/unikontainers/initrd/initrd.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ func copyFileToInitrdWithInode(w *cpio.Writer, srcFile string, destFile string,
return nil
}

// CopyFileMountsToInitrd appends the files of the given mounts to the initrd
// as a new archive member. The caller should pass only bind mounts.
func CopyFileMountsToInitrd(oldInitrd string, mounts []specs.Mount) error {
f, err := os.OpenFile(oldInitrd, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
Expand All @@ -125,9 +127,6 @@ func CopyFileMountsToInitrd(oldInitrd string, mounts []specs.Mount) error {

w := cpio.NewWriter(f)
for _, m := range mounts {
if m.Type != "bind" {
continue
}
err = CopyFileToInitrd(w, m.Source, m.Destination)
if err != nil {
return fmt.Errorf("could not add file %s to initrd: %w", m.Source, err)
Expand All @@ -143,7 +142,7 @@ func CopyFileMountsToInitrd(oldInitrd string, mounts []specs.Mount) error {

// MergeFileMountsIntoInitrd adds regular-file bind mounts before the trailer
// of an uncompressed newc archive. The archive is fully parsed before the
// disposable initrd is updated in place.
// disposable initrd is updated in place. The caller should pass only bind mounts.
func MergeFileMountsIntoInitrd(oldInitrd string, mounts []specs.Mount) (retErr error) {
fileMounts, err := regularFileBindMounts(mounts)
if err != nil {
Expand Down Expand Up @@ -208,9 +207,6 @@ func MergeFileMountsIntoInitrd(oldInitrd string, mounts []specs.Mount) (retErr e
func regularFileBindMounts(mounts []specs.Mount) ([]specs.Mount, error) {
var fileMounts []specs.Mount
for _, mount := range mounts {
if mount.Type != "bind" {
continue
}
info, err := os.Stat(mount.Source)
if err != nil {
return nil, fmt.Errorf("could not stat file %s: %w", mount.Source, err)
Expand Down
1 change: 0 additions & 1 deletion pkg/unikontainers/initrd/initrd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ func TestMergeFileMountsIntoInitrdWithoutRegularBindMountsDoesNotRewrite(t *test
require.NoError(t, err)

require.NoError(t, MergeFileMountsIntoInitrd(initrdPath, []specs.Mount{
{Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
{Type: "bind", Source: dir, Destination: "/directory"},
}))

Expand Down
9 changes: 7 additions & 2 deletions pkg/unikontainers/initrd_rootfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ func (i initrdRootfs) postSetup() error {
return fmt.Errorf("failed to resolve guest's initrd path: %w", err)
}

// The initrd package expects only bind mounts.
mounts, err := onlyBindMounts(i.mounts)
if err != nil {
return err
}
if i.guestType == unikernels.UnikraftUnikernel {
err = initrd.MergeFileMountsIntoInitrd(initrdHostFullPath, i.mounts)
err = initrd.MergeFileMountsIntoInitrd(initrdHostFullPath, mounts)
} else {
err = initrd.CopyFileMountsToInitrd(initrdHostFullPath, i.mounts)
err = initrd.CopyFileMountsToInitrd(initrdHostFullPath, mounts)
}
if err != nil {
return fmt.Errorf("failed to update guest's initrd: %w", err)
Expand Down
3 changes: 2 additions & 1 deletion pkg/unikontainers/initrd_rootfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ func TestInitrdRootfsPostSetupSelectsUpdateByGuestType(t *testing.T) {
initrdPath: "initrd.cpio",
guestType: tt.guestType,
mounts: []specs.Mount{
{Type: "bind", Source: source, Destination: "/mounted"},
{Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
{Type: "none", Source: source, Destination: "/mounted", Options: []string{"rbind", "rw"}},
},
}

Expand Down
84 changes: 76 additions & 8 deletions pkg/unikontainers/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
"github.com/opencontainers/runtime-spec/specs-go"
)

var ErrCopyDir = errors.New("can not copy a directory")
var ErrNotRegularFile = errors.New("not a regular file")

// setupDevices creates every device from the list inside monitor's rootfs
func setupDevices(monRootfs string, devices []specs.LinuxDevice, needsTAP bool) error {
Expand Down Expand Up @@ -138,8 +138,8 @@ func setupDev(monRootfs string, dev specs.LinuxDevice) error {
// is placed at the same path it has on the host.
// A copy is preferred for monitor binaries: none of the monitor processes share
// memory with other processes of the same monitor, at the cost of being slower
// and consuming more space. Copying a directory is not supported and returns
// ErrCopyDir.
// and consuming more space. Only regular files can be copied; for anything
// else (a directory, socket, FIFO or device node) it returns ErrNotRegularFile.
func fileFromHost(monRootfs string, hostPath string, target string) error {
// Get the info of the original file
var fileInfo unix.Stat_t
Expand All @@ -148,8 +148,11 @@ func fileFromHost(monRootfs string, hostPath string, target string) error {
return err
}

if (fileInfo.Mode & unix.S_IFMT) == unix.S_IFDIR {
return ErrCopyDir
// Only a regular file has content to copy: a directory would need a
// recursive copy, opening a FIFO blocks, opening a socket fails with ENXIO
// and reading a device like /dev/urandom never ends.
if (fileInfo.Mode & unix.S_IFMT) != unix.S_IFREG {
return ErrNotRegularFile
}

if target == "" {
Expand Down Expand Up @@ -389,7 +392,7 @@ func applyMount(rootfsPath string, m specs.Mount) error {
// containerd's parser cannot translate propagation tokens, and the kernel
// ignores per-mount VFS flags (nosuid, nodev, ...) when a bind is created, so
// parse the options once and apply them manually later.
containerdOpts, propagation, vfsFlags := splitMountOptions(m.Options, m.Type == "bind")
containerdOpts, propagation, vfsFlags := splitMountOptions(m.Options, isBindMount(m))

cm := mount.Mount{
Type: m.Type,
Expand All @@ -405,7 +408,7 @@ func applyMount(rootfsPath string, m specs.Mount) error {
// them with a remount. In a user namespace the remount must preserve the
// flags locked on the source mount, otherwise the kernel rejects it with
// EPERM (this is what containerd's own bind remount does for us elsewhere).
if m.Type == "bind" && vfsFlags != 0 {
if isBindMount(m) && vfsFlags != 0 {
remount := vfsFlags | unix.MS_BIND | unix.MS_REMOUNT
if userns.RunningInUserNS() {
locked, err := getUnprivilegedMountFlags(m.Source)
Expand Down Expand Up @@ -441,9 +444,74 @@ func applyMount(rootfsPath string, m specs.Mount) error {
return nil
}

// isBindMount reports whether m is a bind mount. As the OCI runtime spec
// defines it, a mount is a bind mount if its options include "bind" or
// "rbind". Its Type is then a dummy and is ignored, as runc does.
// See the Mounts section of runtime-spec v1.2.1:
// https://github.com/opencontainers/runtime-spec/blob/524fc0e1b8ab0180e2fc9abd31837a0f4ed1fd6b/config.md#configMounts
func isBindMount(m specs.Mount) bool {
return slices.Contains(m.Options, "bind") || slices.Contains(m.Options, "rbind")
}

// isHostKernelFS reports whether path resides on one of the filesystems
// through which the host kernel exposes its state and configuration (sysfs,
// procfs, cgroupfs, ...).
func isHostKernelFS(path string) (bool, error) {
var st unix.Statfs_t
err := unix.Statfs(path, &st)
if err != nil {
return false, fmt.Errorf("failed to statfs %s: %w", path, err)
}

// Statfs_t.Type is an int64 on amd64 and arm64; the conversion only
// matters on architectures where it is narrower.
switch int64(st.Type) {
case unix.SYSFS_MAGIC,
unix.PROC_SUPER_MAGIC,
unix.CGROUP_SUPER_MAGIC,
unix.CGROUP2_SUPER_MAGIC,
unix.SECURITYFS_MAGIC,
unix.DEBUGFS_MAGIC,
unix.TRACEFS_MAGIC,
unix.BPF_FS_MAGIC,
unix.EFIVARFS_MAGIC,
unix.PSTOREFS_MAGIC,
unix.BINFMTFS_MAGIC,
unix.NSFS_MAGIC:
return true, nil
}

return false, nil
}

// onlyBindMounts returns the bind mounts among mounts, as recognized by
// isBindMount, which urunc passes to the guest. It leaves out bind mounts
// whose source resides on a host kernel filesystem, which the guest should not
// have access to. The guest has its own kernel and its own instances of these
// filesystems.
func onlyBindMounts(mounts []specs.Mount) ([]specs.Mount, error) {
var result []specs.Mount
for _, m := range mounts {
if !isBindMount(m) {
continue
}
kernelFS, err := isHostKernelFS(m.Source)
if err != nil {
return nil, err
}
if kernelFS {
uniklog.Warnf("skipping bind mount of %s to %s: its source is a host kernel filesystem", m.Source, m.Destination)
continue
}
result = append(result, m)
}

return result, nil
}

// createMountPoint creates the mountpoint of a mount.
func createMountPoint(target string, m specs.Mount) error {
if m.Type == "bind" {
if isBindMount(m) {
var st unix.Stat_t
err := unix.Stat(m.Source, &st)
if err != nil {
Expand Down
14 changes: 7 additions & 7 deletions pkg/unikontainers/shared_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,14 @@ func chooseTmpfsSize(sfsType string, mem uint64) string {
// bind mounts and adjusts the Destination path to the mountpoint of the
// container's rootfs inside the monitor rootfs.
func filterBindMounts(containerRootfs string, mounts []specs.Mount) ([]specs.Mount, error) {
var result []specs.Mount
for _, m := range mounts {
// Skip non-bind mounts
// TODO handle other types of mounts too
if m.Type != "bind" {
continue
}
// TODO handle other types of mounts too
bindMounts, err := onlyBindMounts(mounts)
if err != nil {
return nil, err
}

var result []specs.Mount
for _, m := range bindMounts {
// Resolve the destination against the real container rootfs,
// and then re-root the resolved path under the container
// rootfs mount inside the monitor rootfs, where the mount is
Expand Down
Loading