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 }