1 //go:build windows 2 3 package jobcontainers 4 5 import ( 6 "fmt" 7 "os" 8 "strings" 9 10 "github.com/Microsoft/hcsshim/internal/winapi" 11 "github.com/pkg/errors" 12 "golang.org/x/sys/windows" 13 ) 14 15 // This file emulates the path resolution logic that is used for launching regular 16 // process and hypervisor isolated Windows containers. 17 18 // getApplicationName resolves a given command line string and returns the path to the executable that should be launched, and 19 // an adjusted commandline if needed. The resolution logic may appear overcomplicated but is designed to match the logic used by 20 // Windows Server containers, as well as that used by CreateProcess (see notes for the lpApplicationName parameter). 21 // 22 // The logic follows this set of steps: 23 // 24 // - Construct a list of searchable paths to find the application. This includes the standard Windows system paths 25 // which are generally located at C:\Windows, C:\Windows\System32 and C:\Windows\System. If a working directory or path is specified 26 // via the `workingDirectory` or `pathEnv` parameters then these will be appended to the paths to search from as well. The 27 // searching logic is handled by the Windows API function `SearchPathW` which accepts a semicolon separated list of paths to search 28 // in. 29 // https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-searchpathw 30 // 31 // - If the commandline is quoted, simply grab whatever is in the quotes and search for this directly. 32 // We don't try any other logic here, if the application can't be found from the quoted contents we return an error. 33 // 34 // - If the commandline is not quoted, we iterate over each possible application name by splitting the arguments and iterating 35 // over them one by one while appending the last search each time until we either find a match or don't and return 36 // an error. If we don't find the application on the first try, this means that the application name has a space in it 37 // and we must adjust the commandline to add quotes around the application name. 38 // 39 // - If the application is found, we return the fullpath to the executable and the adjusted commandline (if needed). 40 // 41 // Examples: 42 // 43 // - Input: "C:\Program Files\sub dir\program name" 44 // Search order: 45 // 1. C:\Program.exe 46 // 2. C:\Program Files\sub.exe 47 // 3. C:\Program Files\sub dir\program.exe 48 // 4. C:\Program Files\sub dir\program name.exe 49 // 50 // Returned commandline: "\"C:\Program Files\sub dir\program name\"" 51 // 52 // - Input: "\"program name\"" 53 // Search order: 54 // 1. program name.exe 55 // 56 // Returned commandline: "\"program name\" 57 // 58 // - Input: "\"program name\" -flags -for -program" 59 // Search order: 60 // 1. program.exe 61 // 2. program name.exe 62 // 63 // Returned commandline: "\"program name\" -flags -for -program" 64 // 65 // - Input: "\"C:\path\to\program name\"" 66 // Search Order: 67 // 1. "C:\path\to\program name.exe" 68 // 69 // Returned commandline: "\"C:\path\to\program name"" 70 // 71 // - Input: "C:\path\to\program" 72 // Search Order: 73 // 1. "C:\path\to\program.exe" 74 // 75 // Returned commandline: "C:\path\to\program" 76 // 77 // CreateProcess documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa 78 func getApplicationName(commandLine, workingDirectory, pathEnv string) (string, string, error) { 79 var ( 80 searchPath string 81 result string 82 ) 83 84 // First we get the system paths concatenated with semicolons (C:\windows;C:\windows\system32;C:\windows\system;) 85 // and use this as the basis for the directories to search for the application. 86 systemPaths, err := getSystemPaths() 87 if err != nil { 88 return "", "", err 89 } 90 91 // If there's a working directory we should also add this to the list of directories to search. 92 if workingDirectory != "" { 93 searchPath += workingDirectory + ";" 94 } 95 96 // Append the path environment to the list of directories to search. 97 if pathEnv != "" { 98 searchPath += pathEnv + ";" 99 } 100 searchPath += systemPaths 101 102 if searchPath[len(searchPath)-1] == ';' { 103 searchPath = searchPath[:len(searchPath)-1] 104 } 105 106 // Application name was quoted, just search directly. 107 // 108 // For example given the commandline: "hello goodbye" -foo -bar -baz 109 // we would search for the executable 'hello goodbye.exe' 110 if commandLine != "" && commandLine[0] == '"' { 111 index := strings.Index(commandLine[1:], "\"") 112 if index == -1 { 113 return "", "", errors.New("no ending quotation mark found in command") 114 } 115 path, err := searchPathForExe(commandLine[1:index+1], searchPath) 116 if err != nil { 117 return "", "", err 118 } 119 return path, commandLine, nil 120 } 121 122 // Application name wasn't quoted, try each possible application name. 123 // For example given the commandline: hello goodbye, we would first try 124 // to find 'hello.exe' and then 'hello goodbye.exe' 125 var ( 126 trialName string 127 quoteCmdLine bool 128 argsIndex int 129 ) 130 args := splitArgs(commandLine) 131 132 // Loop through each element of the commandline and try and determine if any of them are executables. 133 // 134 // For example given the commandline: foo bar baz 135 // if foo.exe is successfully found we will stop and return with the full path to 'foo.exe'. If foo doesn't succeed we 136 // then try 'foo bar.exe' and 'foo bar baz.exe'. 137 for argsIndex < len(args) { 138 trialName += args[argsIndex] 139 fullPath, err := searchPathForExe(trialName, searchPath) 140 if err == nil { 141 result = fullPath 142 break 143 } 144 trialName += " " 145 quoteCmdLine = true 146 argsIndex++ 147 } 148 149 // If we searched through every argument and didn't find an executable, we need to error out. 150 if argsIndex == len(args) { 151 return "", "", fmt.Errorf("failed to find executable %q", commandLine) 152 } 153 154 // If we found an executable but after we concatenated two arguments together, 155 // we need to adjust the commandline to be quoted. 156 // 157 // For example given the commandline: foo bar 158 // if 'foo bar.exe' is found, we need to adjust the commandline to 159 // be quoted as this is what the platform expects (CreateProcess call). 160 adjustedCommandLine := commandLine 161 if quoteCmdLine { 162 trialName = "\"" + trialName + "\"" 163 trialName += " " + strings.Join(args[argsIndex+1:], " ") 164 adjustedCommandLine = strings.TrimSpace(trialName) // Take off trailing space at beginning and end. 165 } 166 167 return result, adjustedCommandLine, nil 168 } 169 170 // searchPathForExe calls the Windows API function `SearchPathW` to try and locate 171 // `fileName` by searching in `pathsToSearch`. `pathsToSearch` is generally a semicolon 172 // separated string of paths to search that `SearchPathW` will iterate through one by one. 173 // If the path resolved for `fileName` ends up being a directory, this function will return an 174 // error. 175 func searchPathForExe(fileName, pathsToSearch string) (string, error) { 176 fileNamePtr, err := windows.UTF16PtrFromString(fileName) 177 if err != nil { 178 return "", err 179 } 180 181 pathsToSearchPtr, err := windows.UTF16PtrFromString(pathsToSearch) 182 if err != nil { 183 return "", err 184 } 185 186 extension, err := windows.UTF16PtrFromString(".exe") 187 if err != nil { 188 return "", err 189 } 190 191 path := make([]uint16, windows.MAX_PATH) 192 _, err = winapi.SearchPath( 193 pathsToSearchPtr, 194 fileNamePtr, 195 extension, 196 windows.MAX_PATH, 197 &path[0], 198 nil, 199 ) 200 if err != nil { 201 return "", err 202 } 203 204 exePath := windows.UTF16PtrToString(&path[0]) 205 // Need to check if we just found a directory with the name of the executable and 206 // .exe at the end. ping.exe is a perfectly valid directory name for example. 207 attrs, err := os.Stat(exePath) 208 if err != nil { 209 return "", err 210 } 211 212 if attrs.IsDir() { 213 return "", fmt.Errorf("found directory instead of executable %q", exePath) 214 } 215 216 return exePath, nil 217 } 218 219 // Returns the system paths (system32, system, and windows) as a search path, 220 // including a terminating ;. 221 // 222 // Typical output would be `C:\WINDOWS\system32;C:\WINDOWS\System;C:\WINDOWS;` 223 func getSystemPaths() (string, error) { 224 var searchPath string 225 systemDir, err := windows.GetSystemDirectory() 226 if err != nil { 227 return "", errors.Wrap(err, "failed to get system directory") 228 } 229 searchPath += systemDir + ";" 230 231 windowsDir, err := windows.GetWindowsDirectory() 232 if err != nil { 233 return "", errors.Wrap(err, "failed to get Windows directory") 234 } 235 236 searchPath += windowsDir + "\\System;" + windowsDir + ";" 237 return searchPath, nil 238 } 239