Merge pull request #9781 from kinvolk/rata/userns-use-pluginInfo

core/runtime: Check shim PluginInfo to enforce idmap support
This commit is contained in:
Akihiro Suda 2024-05-03 16:07:50 +00:00 committed by GitHub
commit ef12da25e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 79 additions and 94 deletions

View File

@ -21,7 +21,6 @@ package process
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -142,12 +141,6 @@ func (p *Init) Create(ctx context.Context, r *CreateConfig) error {
opts.ConsoleSocket = socket opts.ConsoleSocket = socket
} }
// runc ignores silently features it doesn't know about, so for things that this is
// problematic let's check if this runc version supports them.
if err := p.validateRuncFeatures(ctx, r.Bundle); err != nil {
return fmt.Errorf("failed to detect OCI runtime features: %w", err)
}
if err := p.runtime.Create(ctx, r.ID, r.Bundle, opts); err != nil { if err := p.runtime.Create(ctx, r.ID, r.Bundle, opts); err != nil {
return p.runtimeError(err, "OCI runtime create failed") return p.runtimeError(err, "OCI runtime create failed")
} }
@ -181,60 +174,6 @@ func (p *Init) Create(ctx context.Context, r *CreateConfig) error {
return nil return nil
} }
func (p *Init) validateRuncFeatures(ctx context.Context, bundle string) error {
// TODO: We should remove the logic from here and rebase on #8509.
// This way we can avoid the call to readConfig() here and the call to p.runtime.Features()
// in validateIDMapMounts().
// But that PR is not yet merged nor it is clear if it will be refactored.
// Do this contained hack for now.
spec, err := readConfig(bundle)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
if err := p.validateIDMapMounts(ctx, spec); err != nil {
return fmt.Errorf("OCI runtime doesn't support idmap mounts: %w", err)
}
return nil
}
func (p *Init) validateIDMapMounts(ctx context.Context, spec *specs.Spec) error {
var used bool
for _, m := range spec.Mounts {
if m.UIDMappings != nil || m.GIDMappings != nil {
used = true
break
}
if sliceContainsStr(m.Options, "idmap") || sliceContainsStr(m.Options, "ridmap") {
used = true
break
}
}
if !used {
return nil
}
// From here onwards, we require idmap mounts. So if we fail to check, we return an error.
features, err := p.runtime.Features(ctx)
if err != nil {
// If the features command is not implemented, then runc is too old.
return fmt.Errorf("features command failed: %w", err)
}
if features.Linux.MountExtensions == nil || features.Linux.MountExtensions.IDMap == nil {
return errors.New("missing `mountExtensions.idmap` entry in `features` command")
}
if enabled := features.Linux.MountExtensions.IDMap.Enabled; enabled == nil || !*enabled {
return errors.New("idmap mounts not supported")
}
return nil
}
func (p *Init) openStdin(path string) error { func (p *Init) openStdin(path string) error {
sc, err := fifo.OpenFifo(context.Background(), path, unix.O_WRONLY|unix.O_NONBLOCK, 0) sc, err := fifo.OpenFifo(context.Background(), path, unix.O_WRONLY|unix.O_NONBLOCK, 0)
if err != nil { if err != nil {
@ -556,12 +495,3 @@ func withConditionalIO(c stdio.Stdio) runc.IOOpt {
o.OpenStderr = c.Stderr != "" o.OpenStderr = c.Stderr != ""
} }
} }
func sliceContainsStr(s []string, str string) bool {
for _, s := range s {
if s == str {
return true
}
}
return false
}

View File

@ -21,7 +21,6 @@ package process
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -32,7 +31,6 @@ import (
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
runc "github.com/containerd/go-runc" runc "github.com/containerd/go-runc"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -41,8 +39,6 @@ const (
RuncRoot = "/run/containerd/runc" RuncRoot = "/run/containerd/runc"
// InitPidFile name of the file that contains the init pid // InitPidFile name of the file that contains the init pid
InitPidFile = "init.pid" InitPidFile = "init.pid"
// configFile is the name of the runc config file
configFile = "config.json"
) )
// safePid is a thread safe wrapper for pid. // safePid is a thread safe wrapper for pid.
@ -188,23 +184,3 @@ func stateName(v interface{}) string {
} }
panic(fmt.Errorf("invalid state %v", v)) panic(fmt.Errorf("invalid state %v", v))
} }
func readConfig(path string) (spec *specs.Spec, err error) {
cfg := filepath.Join(path, configFile)
f, err := os.Open(cfg)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("JSON specification file %s not found", cfg)
}
return nil, err
}
defer f.Close()
if err = json.NewDecoder(f).Decode(&spec); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
if spec == nil {
return nil, errors.New("config cannot be null")
}
return spec, nil
}

View File

@ -19,13 +19,18 @@ package v2
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"slices"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/containerd/plugin" "github.com/containerd/plugin"
"github.com/containerd/plugin/registry" "github.com/containerd/plugin/registry"
"github.com/containerd/typeurl/v2"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-spec/specs-go/features"
apitypes "github.com/containerd/containerd/api/types" apitypes "github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/v2/core/runtime" "github.com/containerd/containerd/v2/core/runtime"
@ -111,6 +116,12 @@ func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.Cr
return nil, err return nil, err
} }
// runc ignores silently features it doesn't know about, so for things that this is
// problematic let's check if this runc version supports them.
if err := m.validateRuntimeFeatures(ctx, opts); err != nil {
return nil, fmt.Errorf("failed to validate OCI runtime features: %w", err)
}
t, err := shimTask.Create(ctx, opts) t, err := shimTask.Create(ctx, opts)
if err != nil { if err != nil {
// NOTE: ctx contains required namespace information. // NOTE: ctx contains required namespace information.
@ -224,3 +235,71 @@ func (m *TaskManager) PluginInfo(ctx context.Context, request interface{}) (inte
} }
return &info, nil return &info, nil
} }
func (m *TaskManager) validateRuntimeFeatures(ctx context.Context, opts runtime.CreateOpts) error {
var spec specs.Spec
if err := typeurl.UnmarshalTo(opts.Spec, &spec); err != nil {
return fmt.Errorf("unmarshal spec: %w", err)
}
// Only ask for the PluginInfo if idmap mounts are used.
if !usesIDMapMounts(spec) {
return nil
}
pInfo, err := m.PluginInfo(ctx, &apitypes.RuntimeRequest{RuntimePath: opts.Runtime})
if err != nil {
return fmt.Errorf("runtime info: %w", err)
}
pluginInfo, ok := pInfo.(*apitypes.RuntimeInfo)
if !ok {
return fmt.Errorf("invalid runtime info type: %T", pInfo)
}
feat, err := typeurl.UnmarshalAny(pluginInfo.Features)
if err != nil {
return fmt.Errorf("unmarshal runtime features: %w", err)
}
// runc-compatible runtimes silently ignores features it doesn't know about. But ignoring
// our request to use idmap mounts can break permissions in the volume, so let's make sure
// it supports it. For more info, see:
// https://github.com/opencontainers/runtime-spec/pull/1219
//
features, ok := feat.(*features.Features)
if !ok {
// Leave alone non runc-compatible runtimes that don't provide the features info,
// they might not be affected by this.
return nil
}
if err := supportsIDMapMounts(features); err != nil {
return fmt.Errorf("idmap mounts not supported: %w", err)
}
return nil
}
func usesIDMapMounts(spec specs.Spec) bool {
for _, m := range spec.Mounts {
if m.UIDMappings != nil || m.GIDMappings != nil {
return true
}
if slices.Contains(m.Options, "idmap") || slices.Contains(m.Options, "ridmap") {
return true
}
}
return false
}
func supportsIDMapMounts(features *features.Features) error {
if features.Linux.MountExtensions == nil || features.Linux.MountExtensions.IDMap == nil {
return errors.New("missing `mountExtensions.idmap` entry in `features` command")
}
if enabled := features.Linux.MountExtensions.IDMap.Enabled; enabled == nil || !*enabled {
return errors.New("entry `mountExtensions.idmap.Enabled` in `features` command not present or disabled")
}
return nil
}