@@ -3,7 +3,12 @@ package runner
33import (
44 "context"
55 "errors"
6+ "math"
7+ "os"
68 "os/exec"
9+ "path/filepath"
10+ "strings"
11+ "syscall"
712
813 "github.com/buildbarn/bb-remote-execution/pkg/proto/runner"
914 "github.com/buildbarn/bb-storage/pkg/filesystem"
@@ -88,6 +93,37 @@ func (r *localRunner) openLog(logPath string) (filesystem.FileAppender, error) {
8893// on whether the action needs to be run in a chroot() or not.
8994type CommandCreator func (ctx context.Context , arguments []string , inputRootDirectory * path.Builder , workingDirectoryParser path.Parser , pathVariable string ) (* exec.Cmd , error )
9095
96+ // NewPlainCommandCreator returns a CommandCreator for cases where we don't
97+ // need to chroot into the input root directory.
98+ func NewPlainCommandCreator (sysProcAttr * syscall.SysProcAttr ) CommandCreator {
99+ return func (ctx context.Context , arguments []string , inputRootDirectory * path.Builder , workingDirectoryParser path.Parser , pathVariable string ) (* exec.Cmd , error ) {
100+ workingDirectory , scopeWalker := inputRootDirectory .Join (path .VoidScopeWalker )
101+ if err := path .Resolve (workingDirectoryParser , scopeWalker ); err != nil {
102+ return nil , util .StatusWrap (err , "Failed to resolve working directory" )
103+ }
104+ workingDirectoryStr , err := path .GetLocalString (workingDirectory )
105+ if err != nil {
106+ return nil , util .StatusWrap (err , "Failed to create local representation of working directory" )
107+ }
108+ executablePath , err := lookupExecutable (workingDirectory , pathVariable , arguments [0 ])
109+ if err != nil {
110+ return nil , err
111+ }
112+
113+ // exec.CommandContext() has some smartness to call
114+ // exec.LookPath() under the hood, which we don't want.
115+ // Call it with a placeholder path, followed by setting
116+ // cmd.Path and cmd.Args manually. This ensures that our
117+ // own values remain respected.
118+ cmd := exec .CommandContext (ctx , "/nonexistent" )
119+ cmd .Args = arguments
120+ cmd .Dir = workingDirectoryStr
121+ cmd .Path = executablePath
122+ cmd .SysProcAttr = sysProcAttr
123+ return cmd , nil
124+ }
125+ }
126+
91127// NewLocalRunner returns a Runner capable of running commands on the
92128// local system directly.
93129func NewLocalRunner (buildDirectory filesystem.Directory , buildDirectoryPath * path.Builder , commandCreator CommandCreator , setTmpdirEnvironmentVariable bool ) runner.RunnerServer {
@@ -201,3 +237,63 @@ func (r *localRunner) CheckReadiness(ctx context.Context, request *runner.CheckR
201237
202238 return & emptypb.Empty {}, nil
203239}
240+
241+ // getExecutablePath returns the path of an executable within a given
242+ // search path that is part of the PATH environment variable.
243+ func getExecutablePath (baseDirectory * path.Builder , searchPathStr , argv0 string ) (string , error ) {
244+ searchPath , scopeWalker := baseDirectory .Join (path .VoidScopeWalker )
245+ if err := path .Resolve (path .NewLocalParser (searchPathStr ), scopeWalker ); err != nil {
246+ return "" , err
247+ }
248+
249+ executablePath , scopeWalker := searchPath .Join (path .VoidScopeWalker )
250+ if err := path .Resolve (path .NewLocalParser (argv0 ), scopeWalker ); err != nil {
251+ return "" , err
252+ }
253+ return path .GetLocalString (executablePath )
254+ }
255+
256+ // lookupExecutable returns the path of an executable, taking the PATH
257+ // environment variable into account.
258+ func lookupExecutable (workingDirectory * path.Builder , pathVariable , argv0 string ) (string , error ) {
259+ if strings .ContainsFunc (argv0 , func (r rune ) bool {
260+ return r <= math .MaxUint8 && os .IsPathSeparator (uint8 (r ))
261+ }) {
262+ // No PATH processing needs to be performed.
263+ return argv0 , nil
264+ }
265+
266+ // Executable path does not contain any slashes. Perform PATH
267+ // lookups.
268+ //
269+ // We cannot use exec.LookPath() directly, as that function
270+ // disregards the working directory of the action. It also uses
271+ // the PATH environment variable of the current process, as
272+ // opposed to respecting the value that is provided as part of
273+ // the action. Do call into this function to validate the
274+ // existence of the executable.
275+ for _ , searchPathStr := range filepath .SplitList (pathVariable ) {
276+ executablePathAbs , err := getExecutablePath (workingDirectory , searchPathStr , argv0 )
277+ if err != nil {
278+ return "" , util .StatusWrapf (err , "Failed to resolve executable %#v in search path %#v" , argv0 , searchPathStr )
279+ }
280+ if _ , err := exec .LookPath (executablePathAbs ); err == nil {
281+ // Regular compiled executables will receive the
282+ // argv[0] that we provide, but scripts starting
283+ // with '#!' will receive the literal executable
284+ // path.
285+ //
286+ // Most shells seem to guarantee that if argv[0]
287+ // is relative, the executable path is relative
288+ // as well. Prevent these scripts from breaking
289+ // by recomputing the executable path once more,
290+ // but relative.
291+ executablePathRel , err := getExecutablePath (& path .EmptyBuilder , searchPathStr , argv0 )
292+ if err != nil {
293+ return "" , util .StatusWrapf (err , "Failed to resolve executable %#v in search path %#v" , argv0 , searchPathStr )
294+ }
295+ return executablePathRel , nil
296+ }
297+ }
298+ return "" , status .Errorf (codes .InvalidArgument , "Cannot find executable %#v in search paths %#v" , argv0 , pathVariable )
299+ }
0 commit comments