blob: 33e657ab454bfa04b736c169ee544425697a5a5f [file] [log] [blame]
// 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)] == '/'
}