...

Source file src/k8s.io/kubernetes/pkg/volume/util/subpath/subpath_windows.go

Documentation: k8s.io/kubernetes/pkg/volume/util/subpath

     1  //go:build windows
     2  // +build windows
     3  
     4  /*
     5  Copyright 2017 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package subpath
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"syscall"
    29  
    30  	"k8s.io/klog/v2"
    31  	"k8s.io/mount-utils"
    32  	"k8s.io/utils/nsenter"
    33  )
    34  
    35  // MaxPathLength is the maximum length of Windows path. Normally, it is 260, but if long path is enable,
    36  // the max number is 32,767
    37  const MaxPathLength = 32767
    38  
    39  type subpath struct{}
    40  
    41  // New returns a subpath.Interface for the current system
    42  func New(mount.Interface) Interface {
    43  	return &subpath{}
    44  }
    45  
    46  // NewNSEnter is to satisfy the compiler for having NewSubpathNSEnter exist for all
    47  // OS choices. however, NSEnter is only valid on Linux
    48  func NewNSEnter(mounter mount.Interface, ne *nsenter.Nsenter, rootDir string) Interface {
    49  	return nil
    50  }
    51  
    52  // isDriveLetterPath returns true if the given path is empty or it ends with ":" or ":\" or ":\\"
    53  func isDriveLetterorEmptyPath(path string) bool {
    54  	if path == "" || strings.HasSuffix(path, ":\\\\") || strings.HasSuffix(path, ":") || strings.HasSuffix(path, ":\\") {
    55  		return true
    56  	}
    57  	return false
    58  }
    59  
    60  // isVolumePrefix returns true if the given path name starts with "Volume" or volume prefix including
    61  // "\\.\", "\\?\" for device path or "UNC" or "\\" for UNC path. Otherwise, it returns false.
    62  func isDeviceOrUncPath(path string) bool {
    63  	if strings.HasPrefix(path, "Volume") || strings.HasPrefix(path, "\\\\?\\") || strings.HasPrefix(path, "\\\\.\\") || strings.HasPrefix(path, "UNC") {
    64  		return true
    65  	}
    66  	return false
    67  }
    68  
    69  // getUpperPath removes the last level of directory.
    70  func getUpperPath(path string) string {
    71  	sep := fmt.Sprintf("%c", filepath.Separator)
    72  	upperpath := strings.TrimSuffix(path, sep)
    73  	return filepath.Dir(upperpath)
    74  }
    75  
    76  // Check whether a directory/file is a link type or not
    77  // LinkType could be SymbolicLink, Junction, or HardLink
    78  func isLinkPath(path string) (bool, error) {
    79  	cmd := exec.Command("powershell", "/c", "$ErrorActionPreference = 'Stop'; (Get-Item -Force -LiteralPath $env:linkpath).LinkType")
    80  	cmd.Env = append(os.Environ(), fmt.Sprintf("linkpath=%s", path))
    81  	klog.V(8).Infof("Executing command: %q", cmd.String())
    82  	output, err := cmd.CombinedOutput()
    83  	if err != nil {
    84  		return false, err
    85  	}
    86  	if strings.TrimSpace(string(output)) != "" {
    87  		return true, nil
    88  	}
    89  	return false, nil
    90  }
    91  
    92  // evalSymlink returns the path name after the evaluation of any symbolic links.
    93  // If the path after evaluation is a device path or network connection, the original path is returned
    94  func evalSymlink(path string) (string, error) {
    95  	path = mount.NormalizeWindowsPath(path)
    96  	if isDeviceOrUncPath(path) || isDriveLetterorEmptyPath(path) {
    97  		klog.V(4).Infof("Path '%s' is not a symlink, return its original form.", path)
    98  		return path, nil
    99  	}
   100  	upperpath := path
   101  	base := ""
   102  	for i := 0; i < MaxPathLength; i++ {
   103  		isLink, err := isLinkPath(upperpath)
   104  		if err != nil {
   105  			return "", err
   106  		}
   107  		if isLink {
   108  			break
   109  		}
   110  		// continue to check next layer
   111  		base = filepath.Join(filepath.Base(upperpath), base)
   112  		upperpath = getUpperPath(upperpath)
   113  		if isDriveLetterorEmptyPath(upperpath) {
   114  			klog.V(4).Infof("Path '%s' is not a symlink, return its original form.", path)
   115  			return path, nil
   116  		}
   117  	}
   118  	// This command will give the target path of a given symlink
   119  	// The -Force parameter will allow Get-Item to also evaluate hidden folders, like AppData.
   120  	cmd := exec.Command("powershell", "/c", "$ErrorActionPreference = 'Stop'; (Get-Item -Force -LiteralPath $env:linkpath).Target")
   121  	cmd.Env = append(os.Environ(), fmt.Sprintf("linkpath=%s", upperpath))
   122  	klog.V(8).Infof("Executing command: %q", cmd.String())
   123  	output, err := cmd.CombinedOutput()
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	klog.V(4).Infof("evaluate path %s: symlink from %s to %s", path, upperpath, string(output))
   128  	linkedPath := strings.TrimSpace(string(output))
   129  	if linkedPath == "" || isDeviceOrUncPath(linkedPath) {
   130  		klog.V(4).Infof("Path '%s' has a target %s. Return its original form.", path, linkedPath)
   131  		return path, nil
   132  	}
   133  	// If the target is not an absolute path, join iit with the current upperpath
   134  	if !filepath.IsAbs(linkedPath) {
   135  		linkedPath = filepath.Join(getUpperPath(upperpath), linkedPath)
   136  	}
   137  	nextLink, err := evalSymlink(linkedPath)
   138  	if err != nil {
   139  		return path, err
   140  	}
   141  	return filepath.Join(nextLink, base), nil
   142  }
   143  
   144  // check whether hostPath is within volume path
   145  // this func will lock all intermediate subpath directories, need to close handle outside of this func after container started
   146  func lockAndCheckSubPath(volumePath, hostPath string) ([]uintptr, error) {
   147  	if len(volumePath) == 0 || len(hostPath) == 0 {
   148  		return []uintptr{}, nil
   149  	}
   150  
   151  	finalSubPath, err := evalSymlink(hostPath)
   152  	if err != nil {
   153  		return []uintptr{}, fmt.Errorf("cannot evaluate link %s: %s", hostPath, err)
   154  	}
   155  
   156  	finalVolumePath, err := evalSymlink(volumePath)
   157  	if err != nil {
   158  		return []uintptr{}, fmt.Errorf("cannot read link %s: %s", volumePath, err)
   159  	}
   160  
   161  	return lockAndCheckSubPathWithoutSymlink(finalVolumePath, finalSubPath)
   162  }
   163  
   164  // lock all intermediate subPath directories and check they are all within volumePath
   165  // volumePath & subPath should not contain any symlink, otherwise it will return error
   166  func lockAndCheckSubPathWithoutSymlink(volumePath, subPath string) ([]uintptr, error) {
   167  	if len(volumePath) == 0 || len(subPath) == 0 {
   168  		return []uintptr{}, nil
   169  	}
   170  
   171  	// get relative path to volumePath
   172  	relSubPath, err := filepath.Rel(volumePath, subPath)
   173  	if err != nil {
   174  		return []uintptr{}, fmt.Errorf("Rel(%s, %s) error: %v", volumePath, subPath, err)
   175  	}
   176  	if mount.StartsWithBackstep(relSubPath) {
   177  		return []uintptr{}, fmt.Errorf("SubPath %q not within volume path %q", subPath, volumePath)
   178  	}
   179  
   180  	if relSubPath == "." {
   181  		// volumePath and subPath are equal
   182  		return []uintptr{}, nil
   183  	}
   184  
   185  	fileHandles := []uintptr{}
   186  	var errorResult error
   187  
   188  	currentFullPath := volumePath
   189  	dirs := strings.Split(relSubPath, string(os.PathSeparator))
   190  	for _, dir := range dirs {
   191  		// lock intermediate subPath directory first
   192  		currentFullPath = filepath.Join(currentFullPath, dir)
   193  		handle, err := lockPath(currentFullPath)
   194  		if err != nil {
   195  			errorResult = fmt.Errorf("cannot lock path %s: %s", currentFullPath, err)
   196  			break
   197  		}
   198  		fileHandles = append(fileHandles, handle)
   199  
   200  		// make sure intermediate subPath directory does not contain symlink any more
   201  		stat, err := os.Lstat(currentFullPath)
   202  		if err != nil {
   203  			errorResult = fmt.Errorf("Lstat(%q) error: %v", currentFullPath, err)
   204  			break
   205  		}
   206  		if stat.Mode()&os.ModeSymlink != 0 {
   207  			errorResult = fmt.Errorf("subpath %q is an unexpected symlink after EvalSymlinks", currentFullPath)
   208  			break
   209  		}
   210  
   211  		if !mount.PathWithinBase(currentFullPath, volumePath) {
   212  			errorResult = fmt.Errorf("SubPath %q not within volume path %q", currentFullPath, volumePath)
   213  			break
   214  		}
   215  	}
   216  
   217  	return fileHandles, errorResult
   218  }
   219  
   220  // unlockPath unlock directories
   221  func unlockPath(fileHandles []uintptr) {
   222  	if fileHandles != nil {
   223  		for _, handle := range fileHandles {
   224  			syscall.CloseHandle(syscall.Handle(handle))
   225  		}
   226  	}
   227  }
   228  
   229  // lockPath locks a directory or symlink, return handle, exec "syscall.CloseHandle(handle)" to unlock the path
   230  func lockPath(path string) (uintptr, error) {
   231  	if len(path) == 0 {
   232  		return uintptr(syscall.InvalidHandle), syscall.ERROR_FILE_NOT_FOUND
   233  	}
   234  	pathp, err := syscall.UTF16PtrFromString(path)
   235  	if err != nil {
   236  		return uintptr(syscall.InvalidHandle), err
   237  	}
   238  	access := uint32(syscall.GENERIC_READ)
   239  	sharemode := uint32(syscall.FILE_SHARE_READ)
   240  	createmode := uint32(syscall.OPEN_EXISTING)
   241  	flags := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT)
   242  	fd, err := syscall.CreateFile(pathp, access, sharemode, nil, createmode, flags, 0)
   243  	return uintptr(fd), err
   244  }
   245  
   246  // Lock all directories in subPath and check they're not symlinks.
   247  func (sp *subpath) PrepareSafeSubpath(subPath Subpath) (newHostPath string, cleanupAction func(), err error) {
   248  	handles, err := lockAndCheckSubPath(subPath.VolumePath, subPath.Path)
   249  
   250  	// Unlock the directories when the container starts
   251  	cleanupAction = func() {
   252  		unlockPath(handles)
   253  	}
   254  	return subPath.Path, cleanupAction, err
   255  }
   256  
   257  // No bind-mounts for subpaths are necessary on Windows
   258  func (sp *subpath) CleanSubPaths(podDir string, volumeName string) error {
   259  	return nil
   260  }
   261  
   262  // SafeMakeDir makes sure that the created directory does not escape given base directory mis-using symlinks.
   263  func (sp *subpath) SafeMakeDir(subdir string, base string, perm os.FileMode) error {
   264  	realBase, err := evalSymlink(base)
   265  	if err != nil {
   266  		return fmt.Errorf("error resolving symlinks in %s: %s", base, err)
   267  	}
   268  
   269  	realFullPath := filepath.Join(realBase, subdir)
   270  	return doSafeMakeDir(realFullPath, realBase, perm)
   271  }
   272  
   273  func doSafeMakeDir(pathname string, base string, perm os.FileMode) error {
   274  	klog.V(4).Infof("Creating directory %q within base %q", pathname, base)
   275  
   276  	if !mount.PathWithinBase(pathname, base) {
   277  		return fmt.Errorf("path %s is outside of allowed base %s", pathname, base)
   278  	}
   279  
   280  	// Quick check if the directory already exists
   281  	s, err := os.Stat(pathname)
   282  	if err == nil {
   283  		// Path exists
   284  		if s.IsDir() {
   285  			// The directory already exists. It can be outside of the parent,
   286  			// but there is no race-proof check.
   287  			klog.V(4).Infof("Directory %s already exists", pathname)
   288  			return nil
   289  		}
   290  		return &os.PathError{Op: "mkdir", Path: pathname, Err: syscall.ENOTDIR}
   291  	}
   292  
   293  	// Find all existing directories
   294  	existingPath, toCreate, err := findExistingPrefix(base, pathname)
   295  	if err != nil {
   296  		return fmt.Errorf("error opening directory %s: %s", pathname, err)
   297  	}
   298  	if len(toCreate) == 0 {
   299  		return nil
   300  	}
   301  
   302  	// Ensure the existing directory is inside allowed base
   303  	fullExistingPath, err := evalSymlink(existingPath)
   304  	if err != nil {
   305  		return fmt.Errorf("error opening existing directory %s: %s", existingPath, err)
   306  	}
   307  	fullBasePath, err := evalSymlink(base)
   308  	if err != nil {
   309  		return fmt.Errorf("cannot read link %s: %s", base, err)
   310  	}
   311  	if !mount.PathWithinBase(fullExistingPath, fullBasePath) {
   312  		return fmt.Errorf("path %s is outside of allowed base %s", fullExistingPath, err)
   313  	}
   314  
   315  	// lock all intermediate directories from fullBasePath to fullExistingPath (top to bottom)
   316  	fileHandles, err := lockAndCheckSubPathWithoutSymlink(fullBasePath, fullExistingPath)
   317  	defer unlockPath(fileHandles)
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	klog.V(4).Infof("%q already exists, %q to create", fullExistingPath, filepath.Join(toCreate...))
   323  	currentPath := fullExistingPath
   324  	// create the directories one by one, making sure nobody can change
   325  	// created directory into symlink by lock that directory immediately
   326  	for _, dir := range toCreate {
   327  		currentPath = filepath.Join(currentPath, dir)
   328  		klog.V(4).Infof("Creating %s", dir)
   329  		if err := os.Mkdir(currentPath, perm); err != nil {
   330  			return fmt.Errorf("cannot create directory %s: %s", currentPath, err)
   331  		}
   332  		handle, err := lockPath(currentPath)
   333  		if err != nil {
   334  			return fmt.Errorf("cannot lock path %s: %s", currentPath, err)
   335  		}
   336  		defer syscall.CloseHandle(syscall.Handle(handle))
   337  		// make sure newly created directory does not contain symlink after lock
   338  		stat, err := os.Lstat(currentPath)
   339  		if err != nil {
   340  			return fmt.Errorf("Lstat(%q) error: %v", currentPath, err)
   341  		}
   342  		if stat.Mode()&os.ModeSymlink != 0 {
   343  			return fmt.Errorf("subpath %q is an unexpected symlink after Mkdir", currentPath)
   344  		}
   345  	}
   346  
   347  	return nil
   348  }
   349  
   350  // findExistingPrefix finds prefix of pathname that exists. In addition, it
   351  // returns list of remaining directories that don't exist yet.
   352  func findExistingPrefix(base, pathname string) (string, []string, error) {
   353  	rel, err := filepath.Rel(base, pathname)
   354  	if err != nil {
   355  		return base, nil, err
   356  	}
   357  
   358  	if mount.StartsWithBackstep(rel) {
   359  		return base, nil, fmt.Errorf("pathname(%s) is not within base(%s)", pathname, base)
   360  	}
   361  
   362  	if rel == "." {
   363  		// base and pathname are equal
   364  		return pathname, []string{}, nil
   365  	}
   366  
   367  	dirs := strings.Split(rel, string(filepath.Separator))
   368  
   369  	var parent string
   370  	currentPath := base
   371  	for i, dir := range dirs {
   372  		parent = currentPath
   373  		currentPath = filepath.Join(parent, dir)
   374  		if _, err := os.Lstat(currentPath); err != nil {
   375  			if os.IsNotExist(err) {
   376  				return parent, dirs[i:], nil
   377  			}
   378  			return base, nil, err
   379  		}
   380  	}
   381  
   382  	return pathname, []string{}, nil
   383  }
   384  

View as plain text