github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/layer/utils.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package layer
    19  
    20  import (
    21  	"archive/tar"
    22  	"os"
    23  	"path/filepath"
    24  	"syscall"
    25  
    26  	"github.com/apex/log"
    27  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    28  	"github.com/opencontainers/umoci/pkg/idtools"
    29  	"github.com/pkg/errors"
    30  	rootlesscontainers "github.com/rootless-containers/proto/go-proto"
    31  	"golang.org/x/sys/unix"
    32  	"google.golang.org/protobuf/proto"
    33  )
    34  
    35  // MapOptions specifies the UID and GID mappings used when unpacking and
    36  // repacking images.
    37  type MapOptions struct {
    38  	// UIDMappings and GIDMappings are the UID and GID mappings to apply when
    39  	// packing and unpacking image rootfs layers.
    40  	UIDMappings []rspec.LinuxIDMapping `json:"uid_mappings"`
    41  	GIDMappings []rspec.LinuxIDMapping `json:"gid_mappings"`
    42  
    43  	// Rootless specifies whether any to error out if chown fails.
    44  	Rootless bool `json:"rootless"`
    45  }
    46  
    47  // mapHeader maps a tar.Header generated from the filesystem so that it
    48  // describes the inode as it would be observed by a container process. In
    49  // particular this involves apply an ID mapping from the host filesystem to the
    50  // container mappings. Returns an error if it's not possible to map the given
    51  // UID.
    52  func mapHeader(hdr *tar.Header, mapOptions MapOptions) error {
    53  	var newUID, newGID int
    54  
    55  	// It only makes sense to do un-mapping if we're not rootless. If we're
    56  	// rootless then all of the files will be owned by us anyway.
    57  	if !mapOptions.Rootless {
    58  		var err error
    59  		newUID, err = idtools.ToContainer(hdr.Uid, mapOptions.UIDMappings)
    60  		if err != nil {
    61  			return errors.Wrap(err, "map uid to container")
    62  		}
    63  		newGID, err = idtools.ToContainer(hdr.Gid, mapOptions.GIDMappings)
    64  		if err != nil {
    65  			return errors.Wrap(err, "map gid to container")
    66  		}
    67  	}
    68  
    69  	// We have special handling for the "user.rootlesscontainers" xattr. If
    70  	// we're rootless then we override the owner of the file we're currently
    71  	// parsing (and then remove the xattr). If we're not rootless then the user
    72  	// is doing something strange, so we log a warning but just ignore the
    73  	// xattr otherwise.
    74  	//
    75  	// TODO: We should probably add a flag to opt-out of this (though I'm not
    76  	//       sure why anyone would intentionally use this incorrectly).
    77  	if value, ok := hdr.Xattrs[rootlesscontainers.Keyname]; !ok {
    78  		// noop
    79  	} else if !mapOptions.Rootless {
    80  		log.Warnf("suspicious filesystem: saw special rootless xattr %s in non-rootless invocation", rootlesscontainers.Keyname)
    81  	} else {
    82  		var payload rootlesscontainers.Resource
    83  		if err := proto.Unmarshal([]byte(value), &payload); err != nil {
    84  			return errors.Wrap(err, "unmarshal rootlesscontainers payload")
    85  		}
    86  
    87  		// If the payload isn't uint32(-1) we apply it. The xattr includes the
    88  		// *in-container* owner so we don't want to map it.
    89  		if uid := payload.GetUid(); uid != rootlesscontainers.NoopID {
    90  			newUID = int(uid)
    91  		}
    92  		if gid := payload.GetGid(); gid != rootlesscontainers.NoopID {
    93  			newGID = int(gid)
    94  		}
    95  
    96  		// Drop the xattr since it's just a marker for us and shouldn't be in
    97  		// layers. This is technically out-of-spec, but so is
    98  		// "user.rootlesscontainers".
    99  		delete(hdr.Xattrs, rootlesscontainers.Keyname)
   100  	}
   101  
   102  	hdr.Uid = newUID
   103  	hdr.Gid = newGID
   104  	return nil
   105  }
   106  
   107  // unmapHeader maps a tar.Header from a tar layer stream so that it describes
   108  // the inode as it would be exist on the host filesystem. In particular this
   109  // involves applying an ID mapping from the container filesystem to the host
   110  // mappings. Returns an error if it's not possible to map the given UID.
   111  func unmapHeader(hdr *tar.Header, mapOptions MapOptions) error {
   112  	// To avoid nil references.
   113  	if hdr.Xattrs == nil {
   114  		hdr.Xattrs = make(map[string]string)
   115  	}
   116  
   117  	// If there is already a "user.rootlesscontainers" we give a warning in
   118  	// both rootless and root cases -- but in rootless we explicitly delete the
   119  	// entry because we might replace it.
   120  	if _, ok := hdr.Xattrs[rootlesscontainers.Keyname]; ok {
   121  		if mapOptions.Rootless {
   122  			log.Warnf("rootless{%s} ignoring special xattr %s stored in layer", hdr.Name, rootlesscontainers.Keyname)
   123  			delete(hdr.Xattrs, rootlesscontainers.Keyname)
   124  		} else {
   125  			log.Warnf("suspicious layer: saw special xattr %s in non-rootless invocation", rootlesscontainers.Keyname)
   126  		}
   127  	}
   128  
   129  	// In rootless mode there are a few things we need to do. We need to map
   130  	// all of the files in the layer to have an owner of (0, 0) because we
   131  	// cannot lchown(2) anything -- and then if the owner was non-root we have
   132  	// to create a "user.rootlesscontainers" xattr for it.
   133  	if mapOptions.Rootless {
   134  		// Fill the rootlesscontainers payload with the original (uid, gid). If
   135  		// either is 0, we replace it with uint32(-1). Technically we could
   136  		// just leave it as 0 (since that is what the source of truth told us
   137  		// the owner was), but this would result in a massive increase in
   138  		// xattrs with no real benefit.
   139  		payload := &rootlesscontainers.Resource{
   140  			Uid: rootlesscontainers.NoopID,
   141  			Gid: rootlesscontainers.NoopID,
   142  		}
   143  		if uid := hdr.Uid; uid != 0 {
   144  			payload.Uid = uint32(uid)
   145  		}
   146  		if gid := hdr.Gid; gid != 0 {
   147  			payload.Gid = uint32(gid)
   148  		}
   149  
   150  		// Don't add the xattr if the owner isn't just (0, 0) because that's a
   151  		// waste of space.
   152  		if !rootlesscontainers.IsDefault(payload) {
   153  			valueBytes, err := proto.Marshal(payload)
   154  			if err != nil {
   155  				return errors.Wrap(err, "marshal rootlesscontainers payload")
   156  			}
   157  			// While the payload is almost certainly not UTF-8, Go strings can
   158  			// actually be arbitrary bytes (in case you didn't know this and
   159  			// were confused like me when this worked). See
   160  			// <https://blog.golang.org/strings> for more detail.
   161  			hdr.Xattrs[rootlesscontainers.Keyname] = string(valueBytes)
   162  		}
   163  
   164  		hdr.Uid = 0
   165  		hdr.Gid = 0
   166  	}
   167  
   168  	newUID, err := idtools.ToHost(hdr.Uid, mapOptions.UIDMappings)
   169  	if err != nil {
   170  		return errors.Wrap(err, "map uid to host")
   171  	}
   172  	newGID, err := idtools.ToHost(hdr.Gid, mapOptions.GIDMappings)
   173  	if err != nil {
   174  		return errors.Wrap(err, "map gid to host")
   175  	}
   176  
   177  	hdr.Uid = newUID
   178  	hdr.Gid = newGID
   179  	return nil
   180  }
   181  
   182  // CleanPath makes a path safe for use with filepath.Join. This is done by not
   183  // only cleaning the path, but also (if the path is relative) adding a leading
   184  // '/' and cleaning it (then removing the leading '/'). This ensures that a
   185  // path resulting from prepending another path will always resolve to lexically
   186  // be a subdirectory of the prefixed path. This is all done lexically, so paths
   187  // that include symlinks won't be safe as a result of using CleanPath.
   188  //
   189  // This function comes from runC (libcontainer/utils/utils.go).
   190  func CleanPath(path string) string {
   191  	// Deal with empty strings nicely.
   192  	if path == "" {
   193  		return ""
   194  	}
   195  
   196  	// Ensure that all paths are cleaned (especially problematic ones like
   197  	// "/../../../../../" which can cause lots of issues).
   198  	path = filepath.Clean(path)
   199  
   200  	// If the path isn't absolute, we need to do more processing to fix paths
   201  	// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
   202  	// paths to relative ones.
   203  	if !filepath.IsAbs(path) {
   204  		path = filepath.Clean(string(os.PathSeparator) + path)
   205  		// This can't fail, as (by definition) all paths are relative to root.
   206  		// #nosec G104
   207  		path, _ = filepath.Rel(string(os.PathSeparator), path)
   208  	}
   209  
   210  	// Clean the path again for good measure.
   211  	return filepath.Clean(path)
   212  }
   213  
   214  // InnerErrno returns the "real" system error from an error that originally
   215  // came from the "os" package. The returned error can be compared directly with
   216  // unix.* (or syscall.*) errno values. If the type could not be detected we just return
   217  func InnerErrno(err error) error {
   218  	// All of the os.* cases as well as an explicit
   219  	errno := errors.Cause(err)
   220  	switch err := errno.(type) {
   221  	case *os.PathError:
   222  		errno = err.Err
   223  	case *os.LinkError:
   224  		errno = err.Err
   225  	case *os.SyscallError:
   226  		errno = err.Err
   227  	}
   228  	return errno
   229  }
   230  
   231  // isOverlayWhiteout returns true if the FileInfo represents an overlayfs style
   232  // whiteout (i.e. mknod c 0 0) and false otherwise.
   233  func isOverlayWhiteout(info os.FileInfo) (bool, error) {
   234  	var major, minor uint32
   235  	switch stat := info.Sys().(type) {
   236  	case *unix.Stat_t:
   237  		major = unix.Major(uint64(stat.Rdev))
   238  		minor = unix.Minor(uint64(stat.Rdev))
   239  	case *syscall.Stat_t:
   240  		major = unix.Major(uint64(stat.Rdev))
   241  		minor = unix.Minor(uint64(stat.Rdev))
   242  	default:
   243  		return false, errors.Errorf("[internal error] unknown stat info type %T", info.Sys())
   244  	}
   245  
   246  	return major == 0 && minor == 0 &&
   247  		info.Mode()&os.ModeCharDevice != 0, nil
   248  }