package engine import ( "os" "path" "path/filepath" "strings" "werkdock/internal/rootpath" ) // Docker's exit codes for a command that cannot be started at all. const ( ExitNotExecutable = 126 ExitNotFound = 127 ) // CommandError says the command cannot be started: Code is 127 when no // executable of that name exists in the sandbox, 126 when one exists // but cannot be executed. type CommandError struct { Code int Message string } func (e *CommandError) Error() string { return e.Message } // CheckCommand looks the command up the way execvp in the sandbox will: // the name itself when it holds a slash, else every PATH directory in // order, a candidate that is there but not executable skipped for a // later one. Without it bwrap answers a command it cannot execute with // its own exit code 1, which the caller cannot tell from the command's // own failure. What a mount brings in is not known before the sandbox // starts, so a candidate under a mount, or one the rootfs cannot // resolve, passes unchecked. func CheckCommand(spec RunSpec) error { name := spec.Command[0] denied := "" for _, candidate := range commandCandidates(spec, name) { switch commandStateOf(spec, candidate) { case commandRunnable, commandUnknown: return nil case commandNotExecutable: if denied == "" { denied = candidate } } } switch { case denied != "": return &CommandError{Code: ExitNotExecutable, Message: denied + ": permission denied"} case strings.Contains(name, "/"): return &CommandError{Code: ExitNotFound, Message: name + ": no such file or directory"} default: return &CommandError{Code: ExitNotFound, Message: name + ": executable file not found in $PATH"} } } type commandState int const ( commandMissing commandState = iota commandNotExecutable commandRunnable commandUnknown ) // commandCandidates are the absolute in-sandbox paths execvp tries for // name, relative ones taken from the working directory. func commandCandidates(spec RunSpec, name string) []string { workdir := spec.Workdir if workdir == "" { workdir = "/" } if strings.Contains(name, "/") { return []string{inSandbox(workdir, name)} } var candidates []string for _, dir := range filepath.SplitList(sandboxPATH(spec)) { candidates = append(candidates, inSandbox(workdir, path.Join(dir, name))) } return candidates } func inSandbox(workdir, name string) string { if path.IsAbs(name) { return path.Clean(name) } return path.Join(workdir, name) } // sandboxPATH is the PATH the command sees: werkdock's default, unless // an environment variable sets it; the last one wins, as in Argv. func sandboxPATH(spec RunSpec) string { value := DefaultPATH for _, e := range spec.Env { if e.Key == "PATH" { value = e.Value } } return value } // commandStateOf says what the sandbox will find at candidate. func commandStateOf(spec RunSpec, candidate string) commandState { if _, ok := coveringMount(spec, candidate); ok { return commandUnknown } host, err := rootpath.Resolve(spec.RootFS, candidate) if err != nil { return commandUnknown } // a symlink may have led into a mount if _, ok := coveringMount(spec, "/"+strings.TrimPrefix(strings.TrimPrefix(host, filepath.Clean(spec.RootFS)), "/")); ok { return commandUnknown } fi, err := os.Stat(host) switch { case err != nil: return commandMissing case fi.IsDir() || fi.Mode().Perm()&0o111 == 0: return commandNotExecutable default: return commandRunnable } }