Skip to content

Commit 555e10d

Browse files
thaJeztahAdamKorcz
authored andcommitted
capabilities: WARN, not ERROR, for unknown / unavailable capabilities
This updates handling of capabilities to match the updated runtime specification, in opencontainers/runtime-spec#1094. Prior to that change, the specification required runtimes to produce a (fatal) error if a container configuration requested capabilities that could not be granted (either the capability is "unknown" to the runtime, not supported by the kernel version in use, or not available in the environment that the runtime operates in). This caused problems in situations where the runtime was running in a restricted environment (for example, docker-in-docker), or if there is a mismatch between the list of capabilities known by higher-level runtimes and the OCI runtime. Some examples: - Kernel 5.8 introduced CAP_PERFMON, CAP_BPF, and CAP_CHECKPOINT_RESTORE capabilities. Docker 20.10.0 ("higher level runtime") shipped with an updated list of capabilities, and when creating a "privileged" container, would determine what capabilities are known by the kernel in use, and request all those capabilities (by including them in the container config). However, runc did not yet have an updated list of capabilities, and therefore reject the container specification, producing an error because the new capabilities were "unknown". - When running nested containers, for example, when running docker-in-docker, the "inner" container may be using a more recent version of docker than the "outer" container. In this situation, the "outer" container may be missing capabilities that the inner container expects to be supported (based on kernel version). However, starting the container would fail, because the OCI runtime could not grant those capabilities (them not being available in the environment it's running in). WARN (but otherwise ignore) capabilities that cannot be granted -------------------------------------------------------------------------------- This patch changes the handling to WARN (but otherwise ignore) capabilities that are requested in the container config, but cannot be granted, alleviating higher level runtimes to detect what capabilities are supported (by the kernel, and in the current environment), as well as avoiding failures in situations where the higher-level runtime is aware of capabilities that are not (yet) supported by runc. Impact on security -------------------------------------------------------------------------------- Given that `capabilities` is an "allow-list", ignoring unknown capabilities does not impose a security risk; worst case, a container does not get all requested capabilities granted and, as a result, some actions may fail. Backward-compatibility -------------------------------------------------------------------------------- This change should be fully backward compatible. Higher-level runtimes that already dynamically adjust the list of requested capabilities can continue to do so. Runtimes that do not adjust will see an improvement (containers can start even if some of the requested capabilities are not granted). Container processes MAY fail (as described in "impact on security"), but users can debug this situation either by looking at the warnings produces by the OCI runtime, or using tools such as `capsh` / `libcap` to get the list of actual capabilities in the container. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 665bb19 commit 555e10d

File tree

4 files changed

+159
-27
lines changed

4 files changed

+159
-27
lines changed

libcontainer/capabilities/capabilities.go

+34-26
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
package capabilities
44

55
import (
6-
"fmt"
6+
"sort"
77
"strings"
88

99
"github.com/opencontainers/runc/libcontainer/configs"
10+
"github.com/sirupsen/logrus"
1011
"github.com/syndtr/gocapability/capability"
1112
)
1213

@@ -34,50 +35,57 @@ func init() {
3435
}
3536

3637
// New creates a new Caps from the given Capabilities config. Unknown Capabilities
37-
// or Capabilities that are unavailable in the current environment produce an error.
38+
// or Capabilities that are unavailable in the current environment are ignored,
39+
// printing a warning instead.
3840
func New(capConfig *configs.Capabilities) (*Caps, error) {
3941
var (
4042
err error
41-
c = Caps{caps: make(map[capability.CapType][]capability.Cap, len(capTypes))}
43+
c Caps
4244
)
4345

44-
if c.caps[capability.BOUNDING], err = capSlice(capConfig.Bounding); err != nil {
45-
return nil, err
46-
}
47-
if c.caps[capability.EFFECTIVE], err = capSlice(capConfig.Effective); err != nil {
48-
return nil, err
49-
}
50-
if c.caps[capability.INHERITABLE], err = capSlice(capConfig.Inheritable); err != nil {
51-
return nil, err
52-
}
53-
if c.caps[capability.PERMITTED], err = capSlice(capConfig.Permitted); err != nil {
54-
return nil, err
55-
}
56-
if c.caps[capability.AMBIENT], err = capSlice(capConfig.Ambient); err != nil {
57-
return nil, err
46+
unknownCaps := make(map[string]struct{})
47+
c.caps = map[capability.CapType][]capability.Cap{
48+
capability.BOUNDING: capSlice(capConfig.Bounding, unknownCaps),
49+
capability.EFFECTIVE: capSlice(capConfig.Effective, unknownCaps),
50+
capability.INHERITABLE: capSlice(capConfig.Inheritable, unknownCaps),
51+
capability.PERMITTED: capSlice(capConfig.Permitted, unknownCaps),
52+
capability.AMBIENT: capSlice(capConfig.Ambient, unknownCaps),
5853
}
5954
if c.pid, err = capability.NewPid2(0); err != nil {
6055
return nil, err
6156
}
6257
if err = c.pid.Load(); err != nil {
6358
return nil, err
6459
}
60+
if len(unknownCaps) > 0 {
61+
logrus.Warn("ignoring unknown or unavailable capabilities: ", mapKeys(unknownCaps))
62+
}
6563
return &c, nil
6664
}
6765

6866
// capSlice converts the slice of capability names in caps, to their numeric
6967
// equivalent, and returns them as a slice. Unknown or unavailable capabilities
70-
// produce an error.
71-
func capSlice(caps []string) ([]capability.Cap, error) {
72-
out := make([]capability.Cap, len(caps))
73-
for i, c := range caps {
74-
v, ok := capabilityMap[c]
75-
if !ok {
76-
return nil, fmt.Errorf("unknown capability %q", c)
68+
// are not returned, but appended to unknownCaps.
69+
func capSlice(caps []string, unknownCaps map[string]struct{}) []capability.Cap {
70+
var out []capability.Cap
71+
for _, c := range caps {
72+
if v, ok := capabilityMap[c]; !ok {
73+
unknownCaps[c] = struct{}{}
74+
} else {
75+
out = append(out, v)
7776
}
78-
out[i] = v
7977
}
80-
return out, nil
78+
return out
79+
}
80+
81+
// mapKeys returns the keys of input in sorted order
82+
func mapKeys(input map[string]struct{}) []string {
83+
var keys []string
84+
for c := range input {
85+
keys = append(keys, c)
86+
}
87+
sort.Strings(keys)
88+
return keys
8189
}
8290

8391
// Caps holds the capabilities for a container.

libcontainer/capabilities/capabilities_linux_test.go

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package capabilities
22

33
import (
4+
"io/ioutil"
5+
"os"
46
"testing"
57

68
"github.com/opencontainers/runc/libcontainer/configs"
9+
"github.com/sirupsen/logrus"
10+
"github.com/sirupsen/logrus/hooks/test"
711
"github.com/syndtr/gocapability/capability"
812
)
913

1014
func TestNew(t *testing.T) {
11-
cs := []string{"CAP_CHOWN"}
15+
cs := []string{"CAP_CHOWN", "CAP_UNKNOWN", "CAP_UNKNOWN2"}
1216
conf := configs.Capabilities{
1317
Bounding: cs,
1418
Effective: cs,
@@ -17,10 +21,36 @@ func TestNew(t *testing.T) {
1721
Ambient: cs,
1822
}
1923

24+
hook := test.NewGlobal()
25+
defer hook.Reset()
26+
27+
logrus.SetOutput(ioutil.Discard)
2028
caps, err := New(&conf)
29+
logrus.SetOutput(os.Stderr)
30+
2131
if err != nil {
2232
t.Error(err)
2333
}
34+
e := hook.AllEntries()
35+
if len(e) != 1 {
36+
t.Errorf("expected 1 warning, got %d", len(e))
37+
}
38+
39+
expectedLogs := logrus.Entry{
40+
Level: logrus.WarnLevel,
41+
Message: "ignoring unknown or unavailable capabilities: [CAP_UNKNOWN CAP_UNKNOWN2]",
42+
}
43+
44+
l := hook.LastEntry()
45+
if l == nil {
46+
t.Fatal("expected a warning, but got none")
47+
}
48+
if l.Level != expectedLogs.Level {
49+
t.Errorf("expected %q, got %q", expectedLogs.Level, l.Level)
50+
}
51+
if l.Message != expectedLogs.Message {
52+
t.Errorf("expected %q, got %q", expectedLogs.Message, l.Message)
53+
}
2454

2555
if len(caps.caps) != len(capTypes) {
2656
t.Errorf("expected %d capability types, got %d: %v", len(capTypes), len(caps.caps), caps.caps)
@@ -36,4 +66,6 @@ func TestNew(t *testing.T) {
3666
continue
3767
}
3868
}
69+
70+
hook.Reset()
3971
}

vendor/github.com/sirupsen/logrus/hooks/test/test.go

+91
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/modules.txt

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ github.com/shurcooL/sanitized_anchor_name
6060
# github.com/sirupsen/logrus v1.7.0
6161
## explicit
6262
github.com/sirupsen/logrus
63+
github.com/sirupsen/logrus/hooks/test
6364
# github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
6465
## explicit
6566
github.com/syndtr/gocapability/capability

0 commit comments

Comments
 (0)