| // Copyright 2018 The Goma Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package remoteexec |
| |
| import ( |
| "errors" |
| "fmt" |
| "strings" |
| |
| "go.chromium.org/goma/server/command/descriptor/posixpath" |
| "go.chromium.org/goma/server/command/descriptor/winpath" |
| gomapb "go.chromium.org/goma/server/proto/api" |
| ) |
| |
| func samePathElement(a, b []string) []string { |
| for i, p := range a { |
| if i >= len(b) { |
| return a[:i] |
| } |
| if p != b[i] { |
| return a[:i] |
| } |
| } |
| return a |
| } |
| |
| func commonDir(filepath clientFilePath, paths []string) string { |
| if len(paths) == 0 { |
| return "" |
| } |
| path := filepath.SplitElem(paths[0]) |
| for _, p := range paths[1:] { |
| path = samePathElement(path, filepath.SplitElem(p)) |
| } |
| return filepath.Join(path...) |
| } |
| |
| // argv0InInputRoot checks argv0 should be in input root or not. |
| // when argv0 is /usr/bin/*, it might use |
| // the command in platform container image, so not |
| // considered it as part of input root. |
| // TODO: non-linux platforms? |
| func argv0InInputRoot(argv0 string) bool { |
| for _, dir := range []string{"/bin/", "/sbin/", "/usr/bin/", "/usr/sbin/"} { |
| if strings.HasPrefix(argv0, dir) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func inputPaths(filepath clientFilePath, req *gomapb.ExecReq, argv0 string) ([]string, error) { |
| cwd := filepath.Clean(req.GetCwd()) |
| if !filepath.IsAbs(cwd) { |
| return nil, fmt.Errorf("cwd is not abs path: %s", cwd) |
| } |
| paths := []string{cwd} |
| for _, input := range req.Input { |
| filename := input.GetFilename() |
| if !filepath.IsAbs(filename) { |
| filename = filepath.Join(cwd, filename) |
| } |
| paths = append(paths, filepath.Clean(filename)) |
| } |
| if argv0InInputRoot(argv0) { |
| if !filepath.IsAbs(argv0) { |
| argv0 = filepath.Join(cwd, argv0) |
| } |
| paths = append(paths, filepath.Clean(argv0)) |
| } |
| return paths, nil |
| } |
| |
| // validCommonDir takes a path `dir` generated by commonDir() and returns |
| // true if it represents a valid common path. |
| func validCommonDir(filepath clientFilePath, dir string) bool { |
| if dir == "" { |
| return false |
| } |
| switch filepath.(type) { |
| case posixpath.FilePath: |
| if dir == "/" { |
| return false |
| } |
| case winpath.FilePath: |
| // TODO: check for win path. |
| return true |
| |
| default: |
| // unknown filepath? |
| return true |
| } |
| |
| return true |
| } |
| |
| // needChroot returns true if chroot is needed to run the task with |
| // given common `dir` |
| func needChroot(filepath clientFilePath, dir string) bool { |
| return !validCommonDir(filepath, dir) |
| } |
| |
| // getPathsWithNoCommonDir returns two or more paths from the input paths that |
| // don't have a common dir above the root directory. If there is a common dir, |
| // then returns an empty array. Does not guarantee any kind of order. |
| func getPathsWithNoCommonDir(filepath clientFilePath, paths []string) []string { |
| if len(paths) < 2 { |
| return nil |
| } |
| |
| p0 := paths[0] |
| // If there are any pairs of paths in `paths` that have no common directories, |
| // then there should be a mismatch between paths[0] and paths[i] for some i > 0. |
| for _, p1 := range paths[1:] { |
| pair := []string{p0, p1} |
| dir := commonDir(filepath, pair) |
| if !validCommonDir(filepath, dir) { |
| return pair |
| } |
| } |
| return nil |
| } |
| |
| func checkInputRootDir(filepath clientFilePath, dir string) error { |
| switch filepath.(type) { |
| case posixpath.FilePath: |
| // if dir covers these paths, command (e.g. clang) won't |
| // work because required *.so etc would not be accessible. |
| for _, p := range []string{ |
| "/lib/x86_64-linux-gnu/", |
| "/usr/lib/x86_64-linux-gnu/", |
| "/lib64/", |
| } { |
| if strings.HasPrefix(p, dir+"/") { |
| return fmt.Errorf("bad input root: %s", dir) |
| } |
| } |
| return nil |
| |
| case winpath.FilePath: |
| // TODO: check for win path. |
| return nil |
| |
| default: |
| // unknown filepath? |
| return nil |
| } |
| } |
| |
| // inputRootDir returns common root of paths. |
| // If second return value is true, chroot must be used. It become true only |
| // if `allowChroot` is true and common input root is "/". |
| func inputRootDir(filepath clientFilePath, paths []string, allowChroot bool) (string, bool, error) { |
| root := commonDir(filepath, paths) |
| if needChroot(filepath, root) && allowChroot { |
| switch filepath.(type) { |
| // TODO: support non-posix platform |
| case posixpath.FilePath: |
| return "/", true, nil |
| } |
| } |
| if !validCommonDir(filepath, root) { |
| pair := getPathsWithNoCommonDir(filepath, paths) |
| return "", false, fmt.Errorf("no common paths in inputs: %v", pair) |
| } |
| err := checkInputRootDir(filepath, root) |
| if err != nil { |
| return "", false, err |
| } |
| return root, false, nil |
| } |
| |
| var errOutOfRoot = errors.New("out of root") |
| |
| // rootRel returns relative path from rootDir for fname, |
| // which is relative path from cwd, or absolute path. |
| func rootRel(filepath clientFilePath, fname, cwd, rootDir string) (string, error) { |
| if filepath == nil { |
| return "", errors.New("rootRel: client filepath unknown") |
| } |
| if !filepath.IsAbs(fname) { |
| fname = filepath.Join(filepath.Clean(cwd), fname) |
| } |
| // filepath.Rel cleans paths, so we can't use it here. |
| // suppose rootdir and cwd are clean path, and |
| // cwd is under rootdir. |
| rootElems := filepath.SplitElem(filepath.Clean(rootDir)) |
| fileElems := filepath.SplitElem(fname) |
| |
| if len(fileElems) < len(rootElems) { |
| return "", errOutOfRoot |
| } |
| for i, elem := range rootElems { |
| if fileElems[i] != elem { |
| return "", errOutOfRoot |
| } |
| } |
| fileElems = fileElems[len(rootElems):] |
| relname := filepath.Join(fileElems...) |
| fileElems = filepath.SplitElem(filepath.Clean(relname)) |
| if len(fileElems) > 0 && fileElems[0] == ".." { |
| return "", errOutOfRoot |
| } |
| return relname, nil |
| } |
| |
| // hasPrefixDir returns true if p has prefix as a directory name. |
| // Note: it is case sensitive. |
| // TODO: support non-posix platforms. |
| func hasPrefixDir(p, prefix string) bool { |
| prefix = strings.TrimRight(prefix, "/") |
| if !strings.HasPrefix(p, prefix) { |
| return false |
| } |
| if len(p) == len(prefix) { |
| return true |
| } |
| return p[len(prefix)] == '/' |
| } |