Skip to content
Open
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
1 change: 1 addition & 0 deletions features.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ var featuresCommand = &cli.Command{
"bundle",
"org.systemd.property.", // prefix form
"org.criu.config",
"org.opencontainers.runc.clone-self-exe",
},
}

Expand Down
3 changes: 3 additions & 0 deletions libcontainer/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ type Config struct {
// Labels are user defined metadata that is stored in the config and populated on the state
Labels []string `json:"labels"`

// CloneSelfExe selects how runc protects runc binary against tampering.
CloneSelfExe string `json:"clone_self_exe,omitempty"`

// NoNewKeyring will not allocated a new session keyring for the container. It will use the
// callers keyring in this case.
NoNewKeyring bool `json:"no_new_keyring,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions libcontainer/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
exePath string
safeExe *os.File
)
if exeseal.IsSelfExeCloned() {
if c.config.CloneSelfExe == exeseal.ModeUnset && exeseal.IsSelfExeCloned() {
// /proc/self/exe is already a cloned binary -- no need to do anything
logrus.Debug("skipping binary cloning -- /proc/self/exe is already cloned!")
// We don't need to use /proc/thread-self here because the exe mm of a
Expand All @@ -563,7 +563,7 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
exePath = "/proc/self/exe"
} else {
var err error
safeExe, err = exeseal.CloneSelfExe(c.stateDir)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think if this was explicitly set, we probably want to skip the exeseal.IsSelfExeCloned check -- at the very least, someone asking for independent-data-copy doesn't want to share for any reason, even if they used memfd-bind.

safeExe, err = exeseal.CloneSelfExe(c.stateDir, c.config.CloneSelfExe)
if err != nil {
return nil, fmt.Errorf("unable to create safe /proc/self/exe clone for runc init: %w", err)
}
Expand Down
43 changes: 15 additions & 28 deletions libcontainer/exeseal/cloned_binary_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,37 +215,24 @@
// /proc/self/exe). This binary can then be used for "runc init" in order to
// make sure the container process can never resolve the original runc binary.
// For more details on why this is necessary, see CVE-2019-5736.
func CloneSelfExe(tmpDir string) (*os.File, error) {
// Try to create a temporary overlayfs to produce a readonly version of
// /proc/self/exe that cannot be "unwrapped" by the container. In contrast
// to CloneBinary, this technique does not require any extra memory usage
// and does not have the (fairly noticeable) performance impact of copying
// a large binary file into a memfd.
//
// Based on some basic performance testing, the overlayfs approach has
// effectively no performance overhead (it is on par with both
// MS_BIND+MS_RDONLY and no binary cloning at all) while memfd copying adds
// around ~60% overhead during container startup.
overlayFile, err := sealedOverlayfs("/proc/self/exe", tmpDir)
if err == nil {
logrus.Debug("runc exeseal: using overlayfs for sealed /proc/self/exe") // used for tests
return overlayFile, nil
}
logrus.WithError(err).Debugf("could not use overlayfs for /proc/self/exe sealing -- falling back to making a temporary copy")

selfExe, err := os.Open("/proc/self/exe")
if err != nil {
return nil, fmt.Errorf("opening current binary: %w", err)
func CloneSelfExe(tmpDir string, mode string) (*os.File, error) {

Check failure on line 218 in libcontainer/exeseal/cloned_binary_linux.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
strategies := strategiesFor(mode)
if len(strategies) == 0 {
return nil, fmt.Errorf("internal: no strategies for clone-self-exe mode %q", mode)
}
defer selfExe.Close()

stat, err := selfExe.Stat()
if err != nil {
return nil, fmt.Errorf("checking /proc/self/exe size: %w", err)
var lastErr error
for i, strategy := range strategies {
f, err := strategy(tmpDir)
if err == nil {
return f, nil
}
lastErr = err
if i < len(strategies)-1 {
logrus.WithError(err).Debugf("clone-self-exe strategy %d/%d failed, trying next", i+1, len(strategies))
}
}
size := stat.Size()

return CloneBinary(selfExe, size, "/proc/self/exe", tmpDir)
return nil, fmt.Errorf("clone-self-exe failed (mode=%q): %w", mode, lastErr)
}

// IsSelfExeCloned returns whether /proc/self/exe is a cloned binary that can
Expand Down
85 changes: 85 additions & 0 deletions libcontainer/exeseal/mode_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package exeseal

import (
"fmt"
"os"

"github.com/sirupsen/logrus"
)

// AnnotationKey is the OCI annotation that selects how runc protects
// the host runc binary against tampering. See ValidateMode for the
// recognized values.
const AnnotationKey = "org.opencontainers.runc.clone-self-exe"

const (
ModeUnset = ""
ModeIndependentDataCopy = "independent-data-copy"
ModeROSharedPage = "ro-shared-page"
)

// ValidateMode reports whether value is a recognized annotation value.
//
// Recognized values:
// - "": annotation absent; use the default fallback chain.
// - "independent-data-copy": use the clone-binary path (memfd, with
// an internal fallback to a classic unlinked tmpfile on older
// kernels). Sealed overlayfs is not attempted.
// - "ro-shared-page": use sealed overlayfs only; fail
// container creation if it is not available.
func ValidateMode(value string) error {
switch value {
case ModeUnset, ModeIndependentDataCopy, ModeROSharedPage:
return nil
default:
return fmt.Errorf("invalid %s value %q (want %q or %q)",
AnnotationKey, value,
ModeIndependentDataCopy, ModeROSharedPage)
}
}

// strategy produces a sealed /proc/self/exe handle by one specific
// mechanism. Returns the file on success, or an error on failure.
type strategy func(tmpDir string) (*os.File, error)

func overlayfsStrategy(tmpDir string) (*os.File, error) {
f, err := sealedOverlayfs("/proc/self/exe", tmpDir)
if err != nil {
return nil, err
}
logrus.Debug("runc exeseal: using overlayfs for sealed /proc/self/exe") // used for tests
return f, nil
}

func cloneBinaryStrategy(tmpDir string) (*os.File, error) {
selfExe, err := os.Open("/proc/self/exe")
if err != nil {
return nil, fmt.Errorf("opening current binary: %w", err)
}
defer selfExe.Close()

stat, err := selfExe.Stat()
if err != nil {
return nil, fmt.Errorf("checking /proc/self/exe size: %w", err)
}
logrus.Debug("runc exeseal: using clone-binary path") // used for tests
return CloneBinary(selfExe, stat.Size(), "/proc/self/exe", tmpDir)
}

// strategiesFor returns the ordered list of strategies to try for a
// given annotation value. The first successful strategy wins; if all
// fail, the last error is returned to the caller.
func strategiesFor(mode string) []strategy {
switch mode {
case ModeROSharedPage:
return []strategy{overlayfsStrategy}
case ModeIndependentDataCopy:
return []strategy{cloneBinaryStrategy}
case ModeUnset:
// Historical default: overlayfs first, clone-binary fallback.
// The order may be reversed in a future release.
return []strategy{overlayfsStrategy, cloneBinaryStrategy}
default:
return nil
}
}
26 changes: 26 additions & 0 deletions libcontainer/exeseal/mode_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package exeseal

import "testing"

func TestValidateMode(t *testing.T) {
cases := []struct {
desc string
in string
wantErr bool
}{
{"absent (empty string) is valid", "", false},
{"recognized: independent-data-copy", "independent-data-copy", false},
{"recognized: ro-shared-page", "ro-shared-page", false},
{"unrecognized errors", "not-a-real-mode", true},
{"case-sensitive", "INDEPENDENT-DATA-COPY", true},
{"no whitespace trimming", " independent-data-copy ", true},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := ValidateMode(tc.in)
if (err != nil) != tc.wantErr {
t.Errorf("ValidateMode(%q) err=%v, wantErr=%v", tc.in, err, tc.wantErr)
}
})
}
}
7 changes: 7 additions & 0 deletions libcontainer/specconv/spec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/opencontainers/runc/internal/linux"
"github.com/opencontainers/runc/internal/pathrs"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/exeseal"
"github.com/opencontainers/runc/libcontainer/internal/userns"
"github.com/opencontainers/runc/libcontainer/seccomp"
)
Expand Down Expand Up @@ -432,6 +433,12 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
}

config.Cgroups = c

config.CloneSelfExe = spec.Annotations[exeseal.AnnotationKey]
if err := exeseal.ValidateMode(config.CloneSelfExe); err != nil {
return nil, err
}

// set linux-specific config
if spec.Linux != nil {
initMaps()
Expand Down
Loading