Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions command_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package runc

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
Expand All @@ -29,7 +30,21 @@ func (r *Runc) command(context context.Context, args ...string) *exec.Cmd {
if command == "" {
command = DefaultCommand
}
if r.CommandFile != nil {
// Use the file's name as argv[0] and for the initial path; it may be
// a path that no longer exists on disk. finalizeCommand replaces
// cmd.Path with /proc/self/fd/<n> before the process is started.
if name := r.CommandFile.Name(); name != "" {
command = name
}
}
cmd := exec.CommandContext(context, command, append(r.args(), args...)...)
if r.CommandFile != nil {
// Suppress any path-lookup error: the binary will be exec'd via
// /proc/self/fd/<n> (set by finalizeCommand) so accessibility of the
// original path at this point doesn't matter.
cmd.Err = nil
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: r.Setpgid,
}
Expand All @@ -41,6 +56,26 @@ func (r *Runc) command(context context.Context, args ...string) *exec.Cmd {
return cmd
}

// finalizeCommand sets cmd.Path to /proc/self/fd/<n> and appends
// r.CommandFile to cmd.ExtraFiles so that the runc binary is executed
// from the open file descriptor rather than by filesystem path. This is
// semantically equivalent to execveat(fd, "", argv, env, AT_EMPTY_PATH).
//
// Appending the file last preserves the FD positions of any ExtraFiles
// already present (e.g. for --preserve-fds or --status-fd), which are
// therefore unaffected by use of CommandFile.
//
// Must be called after all modifications to cmd.ExtraFiles are complete
// and before cmd.Start(). startCommand calls it automatically.
func (r *Runc) finalizeCommand(cmd *exec.Cmd) {
if r.CommandFile == nil {
return
}
fdNum := 3 + len(cmd.ExtraFiles)
cmd.ExtraFiles = append(cmd.ExtraFiles, r.CommandFile)
cmd.Path = fmt.Sprintf("/proc/self/fd/%d", fdNum)
}

func filterEnv(in []string, names ...string) []string {
out := make([]string, 0, len(in))
loop0:
Expand Down
186 changes: 186 additions & 0 deletions command_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package runc

import (
"context"
"os"
"testing"
)

// TestCommandFileRun verifies that CommandFile can be used in place of
// Command to execute a binary by open file descriptor.
func TestCommandFileRun(t *testing.T) {
ctx := context.Background()

f, err := os.Open("/bin/true")
if err != nil {
t.Fatalf("open /bin/true: %v", err)
}
defer f.Close()

r := &Runc{CommandFile: f}
status, err := r.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != 0 {
t.Fatalf("want exit 0, got %d", status)
}

f2, err := os.Open("/bin/false")
if err != nil {
t.Fatalf("open /bin/false: %v", err)
}
defer f2.Close()

r2 := &Runc{CommandFile: f2}
status, err = r2.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{})
if err == nil {
t.Fatal("expected non-nil error from /bin/false, got nil")
}
if status != 1 {
t.Fatalf("want exit 1, got %d", status)
}
}

// TestCommandFileRunAfterUnlink is the key "path never needed" test: it copies
// a binary to a temporary file, opens it, removes the path, then verifies that
// execution still succeeds via the open file descriptor alone. This mirrors
// the execveat(fd, "", argv, env, AT_EMPTY_PATH) semantics.
func TestCommandFileRunAfterUnlink(t *testing.T) {
tmp, err := copyBinary(t, "/bin/true")
if err != nil {
t.Fatalf("copy /bin/true: %v", err)
}

f, err := os.Open(tmp)
if err != nil {
t.Fatalf("open temp binary: %v", err)
}
defer f.Close()

// Remove the path — the binary is now only reachable through the FD.
if err := os.Remove(tmp); err != nil {
t.Fatalf("remove temp binary: %v", err)
}

ctx := context.Background()
r := &Runc{CommandFile: f}
status, err := r.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{})
if err != nil {
t.Fatalf("unexpected error after unlink: %v", err)
}
if status != 0 {
t.Fatalf("want exit 0 after unlink, got %d", status)
}
}

// TestFinalizeCommandFDNumbering verifies that finalizeCommand appends the
// binary FD after any pre-existing ExtraFiles, keeping their FD positions
// intact. This ensures --preserve-fds and --status-fd accounting is correct.
func TestFinalizeCommandFDNumbering(t *testing.T) {
f, err := os.Open("/bin/true")
if err != nil {
t.Fatalf("open /bin/true: %v", err)
}
defer f.Close()

r := &Runc{CommandFile: f}
cmd := r.command(context.Background(), "run")

// Simulate two pre-existing ExtraFiles (e.g. from --preserve-fds).
d1, err := os.Open("/dev/null")
if err != nil {
t.Fatalf("open /dev/null: %v", err)
}
defer d1.Close()
d2, err := os.Open("/dev/null")
if err != nil {
t.Fatalf("open /dev/null: %v", err)
}
defer d2.Close()
cmd.ExtraFiles = []*os.File{d1, d2}

r.finalizeCommand(cmd)

// d1 → FD 3, d2 → FD 4, binary → FD 5.
const wantPath = "/proc/self/fd/5"
if cmd.Path != wantPath {
t.Errorf("cmd.Path = %q; want %q", cmd.Path, wantPath)
}
if n := len(cmd.ExtraFiles); n != 3 {
t.Errorf("len(ExtraFiles) = %d; want 3", n)
}
if cmd.ExtraFiles[2] != f {
t.Error("CommandFile not appended last to ExtraFiles")
}
}

// TestFinalizeCommandNoExtraFiles verifies that finalizeCommand places the
// binary at FD 3 when there are no other ExtraFiles.
func TestFinalizeCommandNoExtraFiles(t *testing.T) {
f, err := os.Open("/bin/true")
if err != nil {
t.Fatalf("open /bin/true: %v", err)
}
defer f.Close()

r := &Runc{CommandFile: f}
cmd := r.command(context.Background(), "run")

r.finalizeCommand(cmd)

const wantPath = "/proc/self/fd/3"
if cmd.Path != wantPath {
t.Errorf("cmd.Path = %q; want %q", cmd.Path, wantPath)
}
if n := len(cmd.ExtraFiles); n != 1 {
t.Errorf("len(ExtraFiles) = %d; want 1", n)
}
if cmd.ExtraFiles[0] != f {
t.Error("CommandFile not placed at ExtraFiles[0]")
}
}

// copyBinary copies the ELF binary at src to a fresh temporary file and
// marks it executable. The caller is responsible for cleanup.
func copyBinary(t *testing.T, src string) (string, error) {
t.Helper()
data, err := os.ReadFile(src)
if err != nil {
return "", err
}
tmp, err := os.CreateTemp("", "runc-cmdfile-test-*")
if err != nil {
return "", err
}
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmp.Name())
return "", err
}
if err := tmp.Close(); err != nil {
os.Remove(tmp.Name())
return "", err
}
if err := os.Chmod(tmp.Name(), 0o755); err != nil {
os.Remove(tmp.Name())
return "", err
}
return tmp.Name(), nil
}
4 changes: 4 additions & 0 deletions command_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ func (r *Runc) command(context context.Context, args ...string) *exec.Cmd {
cmd.Env = os.Environ()
return cmd
}

// finalizeCommand is a no-op on non-Linux platforms.
// CommandFile-based execution via /proc/self/fd/<n> is only supported on Linux.
func (r *Runc) finalizeCommand(_ *exec.Cmd) {}
15 changes: 14 additions & 1 deletion runc.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var DefaultCommand = "runc"
// Runc is the client to the runc cli
type Runc struct {
// Command overrides the name of the runc binary. If empty, DefaultCommand
// is used.
// is used. On Linux, CommandFile takes precedence over this field when set.
Command string
Root string
Debug bool
Expand Down Expand Up @@ -91,6 +91,18 @@ type Runc struct {
SystemdCgroup bool
Rootless *bool // nil stands for "auto"
ExtraArgs []string

// CommandFile is an open file for the runc binary. On Linux, when set,
// each invocation executes the binary via /proc/self/fd/<n> so the
// original file path need not remain accessible after the file is opened.
// This is semantically equivalent to execveat(fd, "", argv, env,
// AT_EMPTY_PATH): the binary is identified solely by its open file
// descriptor, not by any filesystem path.
//
// CommandFile takes precedence over Command and DefaultCommand on Linux.
// The caller is responsible for keeping the file open for the lifetime of
// the Runc instance. Ignored on non-Linux platforms.
CommandFile *os.File
}

// List returns all containers created inside the provided runc root directory
Expand Down Expand Up @@ -170,6 +182,7 @@ func (o *CreateOpts) args() (out []string, err error) {
}

func (r *Runc) startCommand(cmd *exec.Cmd) (chan Exit, error) {
r.finalizeCommand(cmd)
if r.PdeathSignal != 0 {
return Monitor.StartLocked(cmd)
}
Expand Down
Loading