Skip to content

Commit f1e265b

Browse files
committed
core/runtime: Check shim PluginInfo to enforce idmap support
This commit gets rid of the TODO by moving the check to use the pluginInfo() infrastructure. The check is only enforced for shims that return info that can be read as type runtime.Features. For shims that don't provide that, we just ignore it, as those shims might not be affected by this. Signed-off-by: Rodrigo Campos <[email protected]>
1 parent 53160fb commit f1e265b

File tree

3 files changed

+79
-94
lines changed

3 files changed

+79
-94
lines changed

cmd/containerd-shim-runc-v2/process/init.go

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ package process
2121
import (
2222
"context"
2323
"encoding/json"
24-
"errors"
2524
"fmt"
2625
"io"
2726
"os"
@@ -142,12 +141,6 @@ func (p *Init) Create(ctx context.Context, r *CreateConfig) error {
142141
opts.ConsoleSocket = socket
143142
}
144143

145-
// runc ignores silently features it doesn't know about, so for things that this is
146-
// problematic let's check if this runc version supports them.
147-
if err := p.validateRuncFeatures(ctx, r.Bundle); err != nil {
148-
return fmt.Errorf("failed to detect OCI runtime features: %w", err)
149-
}
150-
151144
if err := p.runtime.Create(ctx, r.ID, r.Bundle, opts); err != nil {
152145
return p.runtimeError(err, "OCI runtime create failed")
153146
}
@@ -181,60 +174,6 @@ func (p *Init) Create(ctx context.Context, r *CreateConfig) error {
181174
return nil
182175
}
183176

184-
func (p *Init) validateRuncFeatures(ctx context.Context, bundle string) error {
185-
// TODO: We should remove the logic from here and rebase on #8509.
186-
// This way we can avoid the call to readConfig() here and the call to p.runtime.Features()
187-
// in validateIDMapMounts().
188-
// But that PR is not yet merged nor it is clear if it will be refactored.
189-
// Do this contained hack for now.
190-
spec, err := readConfig(bundle)
191-
if err != nil {
192-
return fmt.Errorf("failed to read config: %w", err)
193-
}
194-
195-
if err := p.validateIDMapMounts(ctx, spec); err != nil {
196-
return fmt.Errorf("OCI runtime doesn't support idmap mounts: %w", err)
197-
}
198-
199-
return nil
200-
}
201-
202-
func (p *Init) validateIDMapMounts(ctx context.Context, spec *specs.Spec) error {
203-
var used bool
204-
for _, m := range spec.Mounts {
205-
if m.UIDMappings != nil || m.GIDMappings != nil {
206-
used = true
207-
break
208-
}
209-
if sliceContainsStr(m.Options, "idmap") || sliceContainsStr(m.Options, "ridmap") {
210-
used = true
211-
break
212-
}
213-
}
214-
215-
if !used {
216-
return nil
217-
}
218-
219-
// From here onwards, we require idmap mounts. So if we fail to check, we return an error.
220-
features, err := p.runtime.Features(ctx)
221-
if err != nil {
222-
// If the features command is not implemented, then runc is too old.
223-
return fmt.Errorf("features command failed: %w", err)
224-
225-
}
226-
227-
if features.Linux.MountExtensions == nil || features.Linux.MountExtensions.IDMap == nil {
228-
return errors.New("missing `mountExtensions.idmap` entry in `features` command")
229-
}
230-
231-
if enabled := features.Linux.MountExtensions.IDMap.Enabled; enabled == nil || !*enabled {
232-
return errors.New("idmap mounts not supported")
233-
}
234-
235-
return nil
236-
}
237-
238177
func (p *Init) openStdin(path string) error {
239178
sc, err := fifo.OpenFifo(context.Background(), path, unix.O_WRONLY|unix.O_NONBLOCK, 0)
240179
if err != nil {
@@ -556,12 +495,3 @@ func withConditionalIO(c stdio.Stdio) runc.IOOpt {
556495
o.OpenStderr = c.Stderr != ""
557496
}
558497
}
559-
560-
func sliceContainsStr(s []string, str string) bool {
561-
for _, s := range s {
562-
if s == str {
563-
return true
564-
}
565-
}
566-
return false
567-
}

cmd/containerd-shim-runc-v2/process/utils.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ package process
2121
import (
2222
"context"
2323
"encoding/json"
24-
"errors"
2524
"fmt"
2625
"io"
2726
"os"
@@ -32,7 +31,6 @@ import (
3231

3332
"github.com/containerd/errdefs"
3433
runc "github.com/containerd/go-runc"
35-
specs "github.com/opencontainers/runtime-spec/specs-go"
3634
"golang.org/x/sys/unix"
3735
)
3836

@@ -41,8 +39,6 @@ const (
4139
RuncRoot = "/run/containerd/runc"
4240
// InitPidFile name of the file that contains the init pid
4341
InitPidFile = "init.pid"
44-
// configFile is the name of the runc config file
45-
configFile = "config.json"
4642
)
4743

4844
// safePid is a thread safe wrapper for pid.
@@ -188,23 +184,3 @@ func stateName(v interface{}) string {
188184
}
189185
panic(fmt.Errorf("invalid state %v", v))
190186
}
191-
192-
func readConfig(path string) (spec *specs.Spec, err error) {
193-
cfg := filepath.Join(path, configFile)
194-
f, err := os.Open(cfg)
195-
if err != nil {
196-
if os.IsNotExist(err) {
197-
return nil, fmt.Errorf("JSON specification file %s not found", cfg)
198-
}
199-
return nil, err
200-
}
201-
defer f.Close()
202-
203-
if err = json.NewDecoder(f).Decode(&spec); err != nil {
204-
return nil, fmt.Errorf("failed to parse config: %w", err)
205-
}
206-
if spec == nil {
207-
return nil, errors.New("config cannot be null")
208-
}
209-
return spec, nil
210-
}

core/runtime/v2/task_manager.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ package v2
1919
import (
2020
"bytes"
2121
"context"
22+
"errors"
2223
"fmt"
2324
"os"
2425
"os/exec"
26+
"slices"
2527

2628
"github.com/containerd/errdefs"
2729
"github.com/containerd/plugin"
2830
"github.com/containerd/plugin/registry"
31+
"github.com/containerd/typeurl/v2"
32+
"github.com/opencontainers/runtime-spec/specs-go"
33+
"github.com/opencontainers/runtime-spec/specs-go/features"
2934

3035
apitypes "github.com/containerd/containerd/v2/api/types"
3136
"github.com/containerd/containerd/v2/core/runtime"
@@ -111,6 +116,12 @@ func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.Cr
111116
return nil, err
112117
}
113118

119+
// runc ignores silently features it doesn't know about, so for things that this is
120+
// problematic let's check if this runc version supports them.
121+
if err := m.validateRuntimeFeatures(ctx, opts); err != nil {
122+
return nil, fmt.Errorf("failed to validate OCI runtime features: %w", err)
123+
}
124+
114125
t, err := shimTask.Create(ctx, opts)
115126
if err != nil {
116127
// NOTE: ctx contains required namespace information.
@@ -224,3 +235,71 @@ func (m *TaskManager) PluginInfo(ctx context.Context, request interface{}) (inte
224235
}
225236
return &info, nil
226237
}
238+
239+
func (m *TaskManager) validateRuntimeFeatures(ctx context.Context, opts runtime.CreateOpts) error {
240+
var spec specs.Spec
241+
if err := typeurl.UnmarshalTo(opts.Spec, &spec); err != nil {
242+
return fmt.Errorf("unmarshal spec: %w", err)
243+
}
244+
245+
// Only ask for the PluginInfo if idmap mounts are used.
246+
if !usesIDMapMounts(spec) {
247+
return nil
248+
}
249+
250+
pInfo, err := m.PluginInfo(ctx, &apitypes.RuntimeRequest{RuntimePath: opts.Runtime})
251+
if err != nil {
252+
return fmt.Errorf("runtime info: %w", err)
253+
}
254+
255+
pluginInfo, ok := pInfo.(*apitypes.RuntimeInfo)
256+
if !ok {
257+
return fmt.Errorf("invalid runtime info type: %T", pInfo)
258+
}
259+
260+
feat, err := typeurl.UnmarshalAny(pluginInfo.Features)
261+
if err != nil {
262+
return fmt.Errorf("unmarshal runtime features: %w", err)
263+
}
264+
265+
// runc-compatible runtimes silently ignores features it doesn't know about. But ignoring
266+
// our request to use idmap mounts can break permissions in the volume, so let's make sure
267+
// it supports it. For more info, see:
268+
// https://github.com/opencontainers/runtime-spec/pull/1219
269+
//
270+
features, ok := feat.(*features.Features)
271+
if !ok {
272+
// Leave alone non runc-compatible runtimes that don't provide the features info,
273+
// they might not be affected by this.
274+
return nil
275+
}
276+
277+
if err := supportsIDMapMounts(features); err != nil {
278+
return fmt.Errorf("idmap mounts not supported: %w", err)
279+
}
280+
281+
return nil
282+
}
283+
284+
func usesIDMapMounts(spec specs.Spec) bool {
285+
for _, m := range spec.Mounts {
286+
if m.UIDMappings != nil || m.GIDMappings != nil {
287+
return true
288+
}
289+
if slices.Contains(m.Options, "idmap") || slices.Contains(m.Options, "ridmap") {
290+
return true
291+
}
292+
293+
}
294+
return false
295+
}
296+
297+
func supportsIDMapMounts(features *features.Features) error {
298+
if features.Linux.MountExtensions == nil || features.Linux.MountExtensions.IDMap == nil {
299+
return errors.New("missing `mountExtensions.idmap` entry in `features` command")
300+
}
301+
if enabled := features.Linux.MountExtensions.IDMap.Enabled; enabled == nil || !*enabled {
302+
return errors.New("entry `mountExtensions.idmap.Enabled` in `features` command not present or disabled")
303+
}
304+
return nil
305+
}

0 commit comments

Comments
 (0)