Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec: allow running commands from host volume #14851

Merged
merged 3 commits into from
Nov 11, 2022
Merged

exec: allow running commands from host volume #14851

merged 3 commits into from
Nov 11, 2022

Conversation

tgross
Copy link
Member

@tgross tgross commented Oct 7, 2022

Fixes #11781

The exec driver and other drivers derived from the shared executor check the path of the command before handing off to libcontainer to ensure that the command doesn't escape the sandbox. But we don't check any host volume mounts, which should be safe to use as a source for executables if we're letting the user mount them to the container in the first place.

Check the mount config to verify the executable lives in the mount's host path, but then return an absolute path within the mount's task path so that we can hand that off to libcontainer to run.

Includes a good bit of refactoring here because the anchoring of the final task path has different code paths for inside the task dir vs inside a mount. But I've fleshed out the test coverage of this a good bit to ensure we haven't created any regressions in the process.


Longer-term with some sort of exec v2 I'd like to switch all of this to an overlay FS, but we'll need a new FSIsolation mode so as not to break any 3rd-party drivers relying on the existing alloc dir behavior.

Example jobspec that this enables, assuming a binary named hello at the host volume named shared_data:

job "example" {
  datacenters = ["dc1"]

  group "hello" {

    volume "host_data" {
      type      = "host"
      read_only = false
      source    = "shared_data"
    }

    task "hello" {

      driver = "exec"

      config {
        command = "/host_data/hello"
      }

      volume_mount {
        volume      = "host_data"
        destination = "/host_data"
        read_only   = true
      }

    }
  }
}

@mikenomitch
Copy link
Contributor

Would close #11781

@tgross tgross added this to the 1.4.x milestone Oct 7, 2022
@schmichael
Copy link
Member

schmichael commented Oct 7, 2022

The exec driver and other drivers derived from the shared executor check the path of the command before handing off to libcontainer to ensure that the command doesn't escape the sandbox

Is the only reason for this to "speed up" errors due to the executable not being reachable to pre-chroot instead of post? Seems like just letting the command fail to execute in the chroot would fix this surprising behavior and simplify code at the cost of "slower" task failures?

If we used an overlayfs we could remove that slowdown for the failure case as well as the happy path.

@tgross
Copy link
Member Author

tgross commented Nov 9, 2022

Is the only reason for this to "speed up" errors due to the executable not being reachable to pre-chroot instead of post?

It turns out we also set the executable bit on the command binary for the user. For the exec driver (or any driver with FSIsolationChroot instead of FSIsolationImage) the task dir includes copies of the client.chroot_env. So it's safe to set the executable bit... so long as we make sure that we're doing so to one of those files copied into the task dir sandbox. Or now, in the mounted host volume.

The exec driver and other drivers derived from the shared executor check the
path of the command before handing off to libcontainer to ensure that the
command doesn't escape the sandbox. But we don't check any host volume mounts,
which should be safe to use as a source for executables if we're letting the
user mount them to the container in the first place.

Check the mount config to verify the executable lives in the mount's host path,
but then return an absolute path within the mount's task path so that we can hand
that off to libcontainer to run.

Includes a good bit of refactoring here because the anchoring of the final task
path has different code paths for inside the task dir vs inside a mount. But
I've fleshed out the test coverage of this a good bit to ensure we haven't
created any regressions in the process.
Comment on lines -133 to -146
path := absPath

// Ensure that the path is contained in the chroot, and find it relative to the container
rel, err := filepath.Rel(command.TaskDir, path)
if err != nil {
return nil, fmt.Errorf("failed to determine relative path base=%q target=%q: %v", command.TaskDir, path, err)
}

// Turn relative-to-chroot path into absolute path to avoid
// libcontainer trying to resolve the binary using $PATH.
// Do *not* use filepath.Join as it will translate ".."s returned by
// filepath.Rel. Prepending "/" will cause the path to be rooted in the
// chroot which is the desired behavior.
path = "/" + rel
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: this has been folded in to lookupTaskBin's inner functions. The alternative was to have lookupTaskBin pass back flags to the caller about where it got the paths, and that was all pretty gross.

Comment on lines -836 to -849
// lookPathIn looks for a file with PATH inside the directory root. Like exec.LookPath
func lookPathIn(path string, root string, bin string) (string, error) {
// exec.LookPath(file string)
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// match unix shell behavior, empty path element == .
dir = "."
}
path := filepath.Join(root, dir, bin)
f, err := os.Stat(path)
if err != nil {
continue
}
if m := f.Mode(); !m.IsDir() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: this was copied almost verbatim out of the stdlib but passed a mock PATH. But since we control the mock path there's no need to do any PATH parsing and we can just iterate over a known set of directories instead. That lets us use getPathInTaskDir for searching the task dir, the local dir, and these paths rather than having special cases.

Copy link
Member

@shoenig shoenig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; see thoughts

drivers/shared/executor/executor_linux.go Outdated Show resolved Hide resolved
drivers/shared/executor/executor_linux.go Outdated Show resolved Hide resolved
// identical for binaries that don't come from mounts).
//
// See also executor.lookupBin for a version used by non-isolated drivers.
func lookupTaskBin(command *ExecCommand) (string, string, error) {
taskDir := command.TaskDir
bin := command.Cmd
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to search task/local/ and task/ if the bin is given as absolute? don't think i've ever tried that, but it sounds confusing if it works

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does sound confusing, and I spent much of yesterday puzzled over that! But it's the existing behavior (ref executor_linux.go#L814-L819) so changing it now will break backwards compatibility. Let's save changing it for exec v2.

@github-actions
Copy link

I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to run the command directly from a mounted hosted volume
4 participants