...

Source file src/github.com/Microsoft/hcsshim/internal/jobcontainers/path.go

Documentation: github.com/Microsoft/hcsshim/internal/jobcontainers

     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  

View as plain text