Skip to content

Mount '/proc' into chrooted actions #115

@stagnation

Description

@stagnation

Problem Description / Feature Request

Using chrooted actions is great for fully hermetic input roots,
but some tools rely on the /proc filesystem to be mounted and will not work without it.
The block devices from /dev can all be mounted (or created) into the input root,
but full filesystems are trickier.

This lists some tools that do not work without /proc,
shows a patch I used to work around it and discusses the limitations of the patch
and what we need before we can merge it.

Problems

In building bb-deployments I found the following issues::

 ERROR: /home/spill/.cache/bazel/_bazel_spill/627a8b419c21f55bea98864c74ae71f0/external/go_sdk/BUILD.bazel:46:15: GoToolchainBinary external/go_sdk/builder [for host] failed: (Exit 2): go failed: error executing command external/go_sdk/bin/go tool link -o bazel-out/host/bin/external/go_sdk/builder bazel-out/host/bin/external/go_sdk/builder.a
 Action details (uncached result): http://localhost:7984/remote-execution/blobs/historical_execute_response/73ce201a6e9757c300fdc64a213b855f63f796813c33f4b2274715c9dae9e43d-550/
 go: cannot find GOROOT directory: /usr/local/go

and::

 ERROR: /home/spill/.cache/bazel/_bazel_spill/627a8b419c21f55bea98864c74ae71f0/external/com_github_buildbarn_bb_storage/pkg/otel/BUILD.bazel:62:8: Executing genrule @com_github_buildbarn_bb_storage//pkg/otel:stylesheet failed: (Exit 1): bash failed: error executing command /bin/bash -c ... (remaining 1 argument skipped)
 Action details (uncached result): http://localhost:7984/remote-execution/blobs/historical_execute_response/e709ec1e8614565d1a6c7a1248ca167361280bd6404edd286e45213811dce6f4-561/
 bazel-out/host/bin/external/npm/purgecss/bin/purgecss.sh.runfiles/build_bazel_rules_nodejs/third_party/github.com/bazelbuild/bazel/tools/bash/runfiles/runfiles.bash: line 170: RUNFILES_MANIFEST_FILE: unbound variable
 bazel-out/host/bin/external/npm/purgecss/bin/purgecss.sh.runfiles/build_bazel_rules_nodejs/third_party/github.com/bazelbuild/bazel/tools/bash/runfiles/runfiles.bash: line 176: RUNFILES_MANIFEST_FILE: unbound variable

 >>>> FAIL: The node binary 'nodejs_linux_amd64/bin/nodejs/bin/node' not found in runfiles.
 This node toolchain was chosen based on your uname 'Linux x86_64'.
 Please file an issue to https://github.com/bazelbuild/rules_nodejs/issues if
 you would like to add your platform to the supported rules_nodejs node platforms. <<<<

 ERROR: /home/spill/.cache/bazel/_bazel_spill/627a8b419c21f55bea98864c74ae71f0/external/bazel_tools/tools/jdk/BUILD:336:14: JavaToolchainCompileClasses external/bazel_tools/tools/jdk/platformclasspath_classes [for host] failed: (Exit 127): javac failed: error executing command external/remotejdk11_linux/bin/javac -source 8 -target 8 -Xlint:-options -cp external/remotejdk11_linux/lib/tools.jar -d bazel-out/host/bin/external/bazel_tools/tools/jdk/platformclasspath_classes ... (remaining 1 argument skipped)
 Action details (uncached result): http://localhost:7984/remote-execution/blobs/historical_execute_response/788fa4f412c6b68678ec8c72d0bdfe63b766386c6011e672f997a81c2fbf40f5-704/
 external/remotejdk11_linux/bin/javac: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory

It was easy to find the root cause for the go problem::

 3367 28    readlinkat(AT_FDCWD, "/proc/self/exe",  <unfinished ...>
 3370 28    <... readlinkat resumed>0xc000126000, 128) = -1 ENOENT (No such      file or directory)

It tries to find the go binary by looking at itself through /proc,
and then find the default GOROOT standard library relative to the compiler.
The proc filesystem is core to Unix/Posix, or some such standard execution environment specification
and for chrooted actions to work for all tools we should have it.

This is also a problem for rust, which has the best error message of the bunch
https://buildteamworld.slack.com/archives/CD6HZC750/p1692391421607939

::

thread 'main' panicked at 'failed to get current_exe: no /proc/self/exe available. Is /proc mounted?'

Proof of Concept Patch

Note, this was developed in February, and has not been rebased on recent work,
1524fef
which adds PATH lookup to the same file. Though the patch is orthogonal.
::

diff --git pkg/runner/BUILD.bazel pkg/runner/BUILD.bazel
index f134f66..33d33f8 100644
--- pkg/runner/BUILD.bazel
+++ pkg/runner/BUILD.bazel
@@ -29,6 +29,7 @@ go_library(
         "@org_golang_google_protobuf//proto",
         "@org_golang_google_protobuf//types/known/anypb",
         "@org_golang_google_protobuf//types/known/emptypb",
+        "@org_golang_x_sys//unix",
     ] + select({
         "@io_bazel_rules_go//go/platform:android": [
             "//pkg/proto/resourceusage",
diff --git pkg/runner/local_runner.go pkg/runner/local_runner.go
index 34ac73a..74d1445 100644
--- pkg/runner/local_runner.go
+++ pkg/runner/local_runner.go
@@ -3,7 +3,11 @@ package runner
 import (
 >  "context"
 >  "errors"
+>  "fmt"
+>  "io/fs"
+>  "os"
 >  "os/exec"
+>  stdpath "path"
 >  "path/filepath"
 >  "syscall"
-
@@ -11,6 +15,7 @@ import (
 >  "github.com/buildbarn/bb-storage/pkg/filesystem"
 >  "github.com/buildbarn/bb-storage/pkg/filesystem/path"
 >  "github.com/buildbarn/bb-storage/pkg/util"
+>  "golang.org/x/sys/unix"
-
 >  "google.golang.org/grpc/codes"
 >  "google.golang.org/grpc/status"
@@ -122,7 +127,7 @@ func (r *localRunner) Run(ctx context.Context, request *runner.RunRequest) (*run
-
 >  cmd, workingDirectoryBase := r.commandCreator(ctx, request.Arguments, inputRootDirectory)
-
->  // Set the environment variable.
+>  // Set the environment variables.
 >  cmd.Env = make([]string, 0, len(request.EnvironmentVariables)+1)
 >  if r.setTmpdirEnvironmentVariable && request.TemporaryDirectory != "" {
 >  >   temporaryDirectory, scopeWalker := r.buildDirectoryPath.Join(path.VoidScopeWalker)
@@ -158,6 +163,43 @@ func (r *localRunner) Run(ctx context.Context, request *runner.RunRequest) (*run
 >  }
 >  cmd.Stderr = stderr
-
+>  if cmd.SysProcAttr != nil && cmd.SysProcAttr.Chroot != "" {
+
+>  >   rootdir := cmd.SysProcAttr.Chroot
+>  >   mode := fs.FileMode(0755)
+
+>  >   var err error
+>  >   err = os.MkdirAll(rootdir, mode)
+>  >   if err != nil {
+>  >   >   panic(err)
+>  >   }
+>  >   err = os.Chdir(rootdir)
+>  >   if err != nil {
+>  >   >   panic(err)
+>  >   }
+
+>  >   noOptions := ""
+>  >   noFlags := 0
+
+>  >   for _, mount := range []struct {
+>  >   >   point   string
+>  >   >   fstype  string
+>  >   >   options uintptr
+>  >   }{
+>  >   >   {"/proc", "proc", uintptr(0)},
+>  >   >   {"/sys", "sysfs", uintptr(0)},
+>  >   } {
+>  >   >   absolute := stdpath.Join(rootdir, mount.point)
+
+>  >   >   err = unix.Mount(mount.point, absolute, mount.fstype, mount.options, noOptions)
+>  >   >   if err != nil {
+>  >   >   >   fmt.Printf("Mount error %#v %#v", mount.point, err)
+>  >   >   >   panic(err)
+>  >   >   }
+>  >   >   defer unix.Unmount(absolute, noFlags)
+>  >   }
+>  }
+
 >  // Start the subprocess. We can already close the output files
 >  // while the process is running.
 >  err = cmd.Start()

Problems with merging

Note that the absolute path is constructed here.
Because the mount syscall does not work with relative paths.
So the Directory abstraction in the runner, where the directory is hidden,
cannot be used for these mounts.

Mountat

The solution is to use the newer mount syscall api to construct a "mountat" functionality, which can mount with relative filepaths. (What the "at" suffix typically means).
But when I prototyped this I could not find a way to unmount the paths.

::

 ...

 void
 mountat(const char *fstype, const char *source, const char *dirname)
 {
     int fd = fsopen(fstype, FSOPEN_CLOEXEC);
     fsconfig(fd, FSCONFIG_SET_STRING, "source", source, 0);
     fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
     int mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);
     move_mount(mfd, "", AT_FDCWD, dirname, MOVE_MOUNT_F_EMPTY_PATH);
 }

 int
 main()
 {
     mountat("proc", "/proc", "proc");
     mountat("sysfs", "/sys", "sys");
 }

This can be used in go as well, last I checked there was a PR in review for these new syscalls, but to create a wrapper for what we need is not hard.
The problem is how to unmount the "proc" and "sys" relative paths after the action.
According to the documentation it should work, but I could not piece it together.

TODO
++++

I have a lot more to say on this topic,
and should wrap up my investigation and publish that.
and should publish the scaffolding c and go code to use them, but the files are too long to paste in here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions