1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17// TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have 18// an alternate platform, we will need to abstract further. 19 20package mount 21 22import ( 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 28 utilexec "k8s.io/utils/exec" 29) 30 31const ( 32 // Default mount command if mounter path is not specified. 33 defaultMountCommand = "mount" 34 // Log message where sensitive mount options were removed 35 sensitiveOptionsRemoved = "<masked>" 36) 37 38// Interface defines the set of methods to allow for mount operations on a system. 39type Interface interface { 40 // Mount mounts source to target as fstype with given options. 41 // options MUST not contain sensitive material (like passwords). 42 Mount(source string, target string, fstype string, options []string) error 43 // MountSensitive is the same as Mount() but this method allows 44 // sensitiveOptions to be passed in a separate parameter from the normal 45 // mount options and ensures the sensitiveOptions are never logged. This 46 // method should be used by callers that pass sensitive material (like 47 // passwords) as mount options. 48 MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error 49 // Unmount unmounts given target. 50 Unmount(target string) error 51 // List returns a list of all mounted filesystems. This can be large. 52 // On some platforms, reading mounts directly from the OS is not guaranteed 53 // consistent (i.e. it could change between chunked reads). This is guaranteed 54 // to be consistent. 55 List() ([]MountPoint, error) 56 // IsLikelyNotMountPoint uses heuristics to determine if a directory 57 // is not a mountpoint. 58 // It should return ErrNotExist when the directory does not exist. 59 // IsLikelyNotMountPoint does NOT properly detect all mountpoint types 60 // most notably linux bind mounts and symbolic link. For callers that do not 61 // care about such situations, this is a faster alternative to calling List() 62 // and scanning that output. 63 IsLikelyNotMountPoint(file string) (bool, error) 64 // GetMountRefs finds all mount references to pathname, returning a slice of 65 // paths. Pathname can be a mountpoint path or a normal directory 66 // (for bind mount). On Linux, pathname is excluded from the slice. 67 // For example, if /dev/sdc was mounted at /path/a and /path/b, 68 // GetMountRefs("/path/a") would return ["/path/b"] 69 // GetMountRefs("/path/b") would return ["/path/a"] 70 // On Windows there is no way to query all mount points; as long as pathname is 71 // a valid mount, it will be returned. 72 GetMountRefs(pathname string) ([]string, error) 73} 74 75// Compile-time check to ensure all Mounter implementations satisfy 76// the mount interface. 77var _ Interface = &Mounter{} 78 79// MountPoint represents a single line in /proc/mounts or /etc/fstab. 80type MountPoint struct { 81 Device string 82 Path string 83 Type string 84 Opts []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged). 85 Freq int 86 Pass int 87} 88 89type MountErrorType string 90 91const ( 92 FilesystemMismatch MountErrorType = "FilesystemMismatch" 93 HasFilesystemErrors MountErrorType = "HasFilesystemErrors" 94 UnformattedReadOnly MountErrorType = "UnformattedReadOnly" 95 FormatFailed MountErrorType = "FormatFailed" 96 GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed" 97 UnknownMountError MountErrorType = "UnknownMountError" 98) 99 100type MountError struct { 101 Type MountErrorType 102 Message string 103} 104 105func (mountError MountError) String() string { 106 return mountError.Message 107} 108 109func (mountError MountError) Error() string { 110 return mountError.Message 111} 112 113func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error { 114 mountError := MountError{ 115 Type: mountErrorValue, 116 Message: fmt.Sprintf(format, args...), 117 } 118 return mountError 119} 120 121// SafeFormatAndMount probes a device to see if it is formatted. 122// Namely it checks to see if a file system is present. If so it 123// mounts it otherwise the device is formatted first then mounted. 124type SafeFormatAndMount struct { 125 Interface 126 Exec utilexec.Interface 127} 128 129// FormatAndMount formats the given disk, if needed, and mounts it. 130// That is if the disk is not formatted and it is not being mounted as 131// read-only it will format it first then mount it. Otherwise, if the 132// disk is already formatted or it is being mounted as read-only, it 133// will be mounted without formatting. 134// options MUST not contain sensitive material (like passwords). 135func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error { 136 return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) 137} 138 139// FormatAndMountSensitive is the same as FormatAndMount but this method allows 140// sensitiveOptions to be passed in a separate parameter from the normal mount 141// options and ensures the sensitiveOptions are never logged. This method should 142// be used by callers that pass sensitive material (like passwords) as mount 143// options. 144func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 145 return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions) 146} 147 148// getMountRefsByDev finds all references to the device provided 149// by mountPath; returns a list of paths. 150// Note that mountPath should be path after the evaluation of any symblolic links. 151func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) { 152 mps, err := mounter.List() 153 if err != nil { 154 return nil, err 155 } 156 157 // Finding the device mounted to mountPath. 158 diskDev := "" 159 for i := range mps { 160 if mountPath == mps[i].Path { 161 diskDev = mps[i].Device 162 break 163 } 164 } 165 166 // Find all references to the device. 167 var refs []string 168 for i := range mps { 169 if mps[i].Device == diskDev || mps[i].Device == mountPath { 170 if mps[i].Path != mountPath { 171 refs = append(refs, mps[i].Path) 172 } 173 } 174 } 175 return refs, nil 176} 177 178// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts 179// returns the device name, reference count, and error code. 180func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) { 181 mps, err := mounter.List() 182 if err != nil { 183 return "", 0, err 184 } 185 186 // Find the device name. 187 // FIXME if multiple devices mounted on the same mount path, only the first one is returned. 188 device := "" 189 // If mountPath is symlink, need get its target path. 190 slTarget, err := filepath.EvalSymlinks(mountPath) 191 if err != nil { 192 slTarget = mountPath 193 } 194 for i := range mps { 195 if mps[i].Path == slTarget { 196 device = mps[i].Device 197 break 198 } 199 } 200 201 // Find all references to the device. 202 refCount := 0 203 for i := range mps { 204 if mps[i].Device == device { 205 refCount++ 206 } 207 } 208 return device, refCount, nil 209} 210 211// IsNotMountPoint determines if a directory is a mountpoint. 212// It should return ErrNotExist when the directory does not exist. 213// IsNotMountPoint is more expensive than IsLikelyNotMountPoint. 214// IsNotMountPoint detects bind mounts in linux. 215// IsNotMountPoint enumerates all the mountpoints using List() and 216// the list of mountpoints may be large, then it uses 217// isMountPointMatch to evaluate whether the directory is a mountpoint. 218func IsNotMountPoint(mounter Interface, file string) (bool, error) { 219 // IsLikelyNotMountPoint provides a quick check 220 // to determine whether file IS A mountpoint. 221 notMnt, notMntErr := mounter.IsLikelyNotMountPoint(file) 222 if notMntErr != nil && os.IsPermission(notMntErr) { 223 // We were not allowed to do the simple stat() check, e.g. on NFS with 224 // root_squash. Fall back to /proc/mounts check below. 225 notMnt = true 226 notMntErr = nil 227 } 228 if notMntErr != nil { 229 return notMnt, notMntErr 230 } 231 // identified as mountpoint, so return this fact. 232 if notMnt == false { 233 return notMnt, nil 234 } 235 236 // Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts. 237 resolvedFile, err := filepath.EvalSymlinks(file) 238 if err != nil { 239 return true, err 240 } 241 242 // check all mountpoints since IsLikelyNotMountPoint 243 // is not reliable for some mountpoint types. 244 mountPoints, mountPointsErr := mounter.List() 245 if mountPointsErr != nil { 246 return notMnt, mountPointsErr 247 } 248 for _, mp := range mountPoints { 249 if isMountPointMatch(mp, resolvedFile) { 250 notMnt = false 251 break 252 } 253 } 254 return notMnt, nil 255} 256 257// MakeBindOpts detects whether a bind mount is being requested and makes the remount options to 258// use in case of bind mount, due to the fact that bind mount doesn't respect mount options. 259// The list equals: 260// options - 'bind' + 'remount' (no duplicate) 261func MakeBindOpts(options []string) (bool, []string, []string) { 262 bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */) 263 return bind, bindOpts, bindRemountOpts 264} 265 266// MakeBindOptsSensitive is the same as MakeBindOpts but this method allows 267// sensitiveOptions to be passed in a separate parameter from the normal mount 268// options and ensures the sensitiveOptions are never logged. This method should 269// be used by callers that pass sensitive material (like passwords) as mount 270// options. 271func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) { 272 // Because we have an FD opened on the subpath bind mount, the "bind" option 273 // needs to be included, otherwise the mount target will error as busy if you 274 // remount as readonly. 275 // 276 // As a consequence, all read only bind mounts will no longer change the underlying 277 // volume mount to be read only. 278 bindRemountOpts := []string{"bind", "remount"} 279 bindRemountSensitiveOpts := []string{} 280 bind := false 281 bindOpts := []string{"bind"} 282 283 // _netdev is a userspace mount option and does not automatically get added when 284 // bind mount is created and hence we must carry it over. 285 if checkForNetDev(options, sensitiveOptions) { 286 bindOpts = append(bindOpts, "_netdev") 287 } 288 289 for _, option := range options { 290 switch option { 291 case "bind": 292 bind = true 293 break 294 case "remount": 295 break 296 default: 297 bindRemountOpts = append(bindRemountOpts, option) 298 } 299 } 300 301 for _, sensitiveOption := range sensitiveOptions { 302 switch sensitiveOption { 303 case "bind": 304 bind = true 305 break 306 case "remount": 307 break 308 default: 309 bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption) 310 } 311 } 312 313 return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts 314} 315 316func checkForNetDev(options []string, sensitiveOptions []string) bool { 317 for _, option := range options { 318 if option == "_netdev" { 319 return true 320 } 321 } 322 for _, sensitiveOption := range sensitiveOptions { 323 if sensitiveOption == "_netdev" { 324 return true 325 } 326 } 327 return false 328} 329 330// PathWithinBase checks if give path is within given base directory. 331func PathWithinBase(fullPath, basePath string) bool { 332 rel, err := filepath.Rel(basePath, fullPath) 333 if err != nil { 334 return false 335 } 336 if StartsWithBackstep(rel) { 337 // Needed to escape the base path. 338 return false 339 } 340 return true 341} 342 343// StartsWithBackstep checks if the given path starts with a backstep segment. 344func StartsWithBackstep(rel string) bool { 345 // normalize to / and check for ../ 346 return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../") 347} 348 349// sanitizedOptionsForLogging will return a comma seperated string containing 350// options and sensitiveOptions. Each entry in sensitiveOptions will be 351// replaced with the string sensitiveOptionsRemoved 352// e.g. o1,o2,<masked>,<masked> 353func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string { 354 seperator := "" 355 if len(options) > 0 && len(sensitiveOptions) > 0 { 356 seperator = "," 357 } 358 359 sensitiveOptionsStart := "" 360 sensitiveOptionsEnd := "" 361 if len(sensitiveOptions) > 0 { 362 sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1) 363 sensitiveOptionsEnd = sensitiveOptionsRemoved 364 } 365 366 return strings.Join(options, ",") + 367 seperator + 368 sensitiveOptionsStart + 369 sensitiveOptionsEnd 370} 371