k8s.io/kubernetes@v1.29.3/pkg/volume/util/subpath/subpath_windows.go (about) 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 }