@@ -3,7 +3,12 @@ package runner
3
3
import (
4
4
"context"
5
5
"errors"
6
+ "math"
7
+ "os"
6
8
"os/exec"
9
+ "path/filepath"
10
+ "strings"
11
+ "syscall"
7
12
8
13
"github.com/buildbarn/bb-remote-execution/pkg/proto/runner"
9
14
"github.com/buildbarn/bb-storage/pkg/filesystem"
@@ -88,6 +93,37 @@ func (r *localRunner) openLog(logPath string) (filesystem.FileAppender, error) {
88
93
// on whether the action needs to be run in a chroot() or not.
89
94
type CommandCreator func (ctx context.Context , arguments []string , inputRootDirectory * path.Builder , workingDirectoryParser path.Parser , pathVariable string ) (* exec.Cmd , error )
90
95
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
+
91
127
// NewLocalRunner returns a Runner capable of running commands on the
92
128
// local system directly.
93
129
func 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
201
237
202
238
return & emptypb.Empty {}, nil
203
239
}
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