1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package ociimage
import (
"context"
"errors"
"fmt"
"os"
apexlog "github.com/apex/log"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci"
umocilayer "github.com/opencontainers/umoci/oci/layer"
"github.com/opencontainers/umoci/pkg/idtools"
"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
"github.com/sylabs/singularity/v4/pkg/sylog"
)
// UnpackRootfs extracts all of the layers of the given image manifest from an
// OCI layout into rootfsDir.
func UnpackRootfs(ctx context.Context, layoutDir string, manifest imgspecv1.Manifest, destDir string) (err error) {
var mapOptions umocilayer.MapOptions
loggerLevel := sylog.GetLevel()
// set the apex log level, for umoci
if loggerLevel <= int(sylog.ErrorLevel) {
// silent option
apexlog.SetLevel(apexlog.ErrorLevel)
} else if loggerLevel <= int(sylog.LogLevel) {
// quiet option
apexlog.SetLevel(apexlog.WarnLevel)
} else if loggerLevel < int(sylog.DebugLevel) {
// verbose option(s) or default
apexlog.SetLevel(apexlog.InfoLevel)
} else {
// debug option
apexlog.SetLevel(apexlog.DebugLevel)
}
// Allow unpacking as non-root
if os.Geteuid() != 0 {
mapOptions.Rootless = true
uidMap, err := idtools.ParseMapping(fmt.Sprintf("0:%d:1", os.Geteuid()))
if err != nil {
return fmt.Errorf("error parsing uidmap: %s", err)
}
mapOptions.UIDMappings = append(mapOptions.UIDMappings, uidMap)
gidMap, err := idtools.ParseMapping(fmt.Sprintf("0:%d:1", os.Getegid()))
if err != nil {
return fmt.Errorf("error parsing gidmap: %s", err)
}
mapOptions.GIDMappings = append(mapOptions.GIDMappings, gidMap)
}
engineExt, err := umoci.OpenLayout(layoutDir)
if err != nil {
return fmt.Errorf("error opening layout: %s", err)
}
// UnpackRootfs from umoci v0.4.2 expects a path to a non-existing directory
os.RemoveAll(destDir)
// Unpack root filesystem
unpackOptions := umocilayer.UnpackOptions{MapOptions: mapOptions}
err = umocilayer.UnpackRootfs(ctx, engineExt, destDir, manifest, &unpackOptions)
if err != nil {
return fmt.Errorf("error unpacking rootfs: %s", err)
}
// No `--fix-perms` and no sandbox... we are fine
return err
}
// FixPerms will work through the rootfs of this bundle, making sure that all
// files and directories have permissions set such that the owner can read,
// modify, delete. This brings us to the situation of <=3.4
func FixPerms(rootfs string) (err error) {
errors := 0
err = fs.PermWalk(rootfs, func(path string, f os.FileInfo, err error) error {
if err != nil {
sylog.Errorf("Unable to access rootfs path %s: %s", path, err)
errors++
return nil
}
switch mode := f.Mode(); {
// Directories must have the owner 'rx' bits to allow traversal and reading on move, and the 'w' bit
// so their content can be deleted by the user when the rootfs/sandbox is deleted
case mode.IsDir():
if err := os.Chmod(path, f.Mode().Perm()|0o700); err != nil {
sylog.Errorf("Error setting permission for %s: %s", path, err)
errors++
}
case mode.IsRegular():
// Regular files must have the owner 'r' bit so that everything can be read in order to
// copy or move the rootfs/sandbox around. Also, the `w` bit as the build does write into
// some files (e.g. resolv.conf) in the container rootfs.
if err := os.Chmod(path, f.Mode().Perm()|0o600); err != nil {
sylog.Errorf("Error setting permission for %s: %s", path, err)
errors++
}
}
return nil
})
if errors > 0 {
err = fmt.Errorf("%d errors were encountered when setting permissions", errors)
}
return err
}
// CheckPerms will work through the rootfs of this bundle, and find if any
// directory does not have owner rwX - which may cause unexpected issues for a
// user trying to look through, or delete a sandbox
func CheckPerms(rootfs string) (err error) {
// This is a locally defined error we can bubble up to cancel our recursive
// structure.
errRestrictivePerm := errors.New("restrictive file permission found")
err = fs.PermWalkRaiseError(rootfs, func(path string, f os.FileInfo, err error) error {
if err != nil {
// If the walk function cannot access a directory at all, that's an
// obvious restrictive permission we need to warn on
if os.IsPermission(err) {
sylog.Debugf("Path %q has restrictive permissions", path)
return errRestrictivePerm
}
return fmt.Errorf("unable to access rootfs path %s: %s", path, err)
}
// Warn on any directory not `rwX` - technically other combinations may
// be traversable / removable... but are confusing to the user vs
// the Singularity 3.4 behavior.
if f.Mode().IsDir() && f.Mode().Perm()&0o700 != 0o700 {
sylog.Debugf("Path %q has restrictive permissions", path)
return errRestrictivePerm
}
return nil
})
if errors.Is(err, errRestrictivePerm) {
sylog.Warningf("The sandbox contain files/dirs that cannot be removed with 'rm'.")
sylog.Warningf("Use 'chmod -R u+rwX' to set permissions that allow removal.")
sylog.Warningf("Use the '--fix-perms' option to 'singularity build' to modify permissions at build time.")
// It's not an error any further up... the rootfs is still usable
return nil
}
return err
}
|