Skip to content
Closed
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
12 changes: 12 additions & 0 deletions internal/imagecache/bundle_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ func SetupBundleRootfs(bundlePath string) error {
// means nothing was mounted there.
_ = unix.Unmount(rootfs, unix.MNT_DETACH)

// These supply the container's "/": upper with an overlay (it shadows every
// lower root, same as implicitdirs.go one level down), rootfs without one.
// At 0700 a non-root container cannot search its own root, so it can exec
// nothing. 0755 matches what unpackLayerToPool gives a layer root. Set it
// explicitly (MkdirAll applies the umask and skips existing dirs), and
// after the unmount, or it chmods through a stale mount.
for _, d := range []string{rootfs, upper} {
if err := os.Chmod(d, 0o755); err != nil {
return fmt.Errorf("while setting mode of %q: %w", d, err)
}
}

if len(spec.Layers) == 0 {
// Degenerate zero-layer image: the empty rootfs dir plus ExtraDirs is
// all there is.
Expand Down
45 changes: 45 additions & 0 deletions internal/imagecache/bundle_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,51 @@ func TestSetupBundleRootfs_ImplicitDirMetadataRepair(t *testing.T) {
}
}

// The merged rootfs root is the container's "/". Left owner-only, a container
// declaring a non-root USER cannot exec anything at all — not because of the
// binary's own mode, but because it cannot search its own root directory. The
// attributes come from the bundle's upper with an overlay and from the rootfs
// dir itself without one, and both are created 0700.
func TestSetupBundleRootfs_RootIsSearchableByNonRoot(t *testing.T) {
assertSearchable := func(t *testing.T, rootfs string) {
t.Helper()
fi, err := os.Stat(rootfs)
if err != nil {
t.Fatalf("stat merged rootfs root: %v", err)
}
if perm := fi.Mode().Perm(); perm&0o005 != 0o005 {
t.Errorf("merged rootfs root mode = %04o, want world read+search so a non-root container can reach its own binaries", perm)
}
}

t.Run("zero layers", func(t *testing.T) {
bundle := t.TempDir()
if err := WriteSpec(bundle, &OverlaySpec{Layers: nil}); err != nil {
t.Fatalf("WriteSpec: %v", err)
}
if err := SetupBundleRootfs(bundle); err != nil {
t.Fatalf("SetupBundleRootfs: %v", err)
}
assertSearchable(t, filepath.Join(bundle, "rootfs"))
})

t.Run("overlay", func(t *testing.T) {
roottest.Require(t, "mount/unmount")
layer := t.TempDir()
writeLayer(t, layer, map[string]string{"bin/app": "x"}, nil)

bundle := t.TempDir()
if err := WriteSpec(bundle, &OverlaySpec{Layers: []string{layer}}); err != nil {
t.Fatalf("WriteSpec: %v", err)
}
if err := SetupBundleRootfs(bundle); err != nil {
t.Fatalf("SetupBundleRootfs: %v", err)
}
t.Cleanup(func() { _ = UnmountAllUnder(bundle) })
assertSearchable(t, filepath.Join(bundle, "rootfs"))
})
}

// Full overlay mount + UnmountAllUnder round trip; needs CAP_SYS_ADMIN.
func TestSetupBundleRootfs_MountAndUnmount(t *testing.T) {
roottest.Require(t, "mount/unmount")
Expand Down
Loading