Skip to content

Commit a2f27f0

Browse files
authored
Merge pull request #3588 from kolyshkin/seccomp-flags-rework
seccomp: refactor flags support; add flags to features, set SPEC_ALLOW by default
2 parents ba994dc + 19a9d9f commit a2f27f0

File tree

8 files changed

+194
-50
lines changed

8 files changed

+194
-50
lines changed

features.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ var featuresCommand = cli.Command{
5959

6060
if seccomp.Enabled {
6161
feat.Linux.Seccomp = &features.Seccomp{
62-
Enabled: &tru,
63-
Actions: seccomp.KnownActions(),
64-
Operators: seccomp.KnownOperators(),
65-
Archs: seccomp.KnownArchs(),
62+
Enabled: &tru,
63+
Actions: seccomp.KnownActions(),
64+
Operators: seccomp.KnownOperators(),
65+
Archs: seccomp.KnownArchs(),
66+
KnownFlags: seccomp.KnownFlags(),
67+
SupportedFlags: seccomp.SupportedFlags(),
6668
}
6769
major, minor, patch := seccomp.Version()
6870
feat.Annotations[features.AnnotationLibseccompVersion] = fmt.Sprintf("%d.%d.%d", major, minor, patch)

libcontainer/seccomp/config.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import (
55
"sort"
66

77
"github.com/opencontainers/runc/libcontainer/configs"
8+
"github.com/opencontainers/runtime-spec/specs-go"
89
)
910

11+
// flagTsync is recognized but ignored by runc, and it is not defined
12+
// in the runtime-spec.
13+
const flagTsync = "SECCOMP_FILTER_FLAG_TSYNC"
14+
1015
var operators = map[string]configs.Operator{
1116
"SCMP_CMP_NE": configs.NotEqualTo,
1217
"SCMP_CMP_LT": configs.LessThan,
@@ -111,3 +116,35 @@ func ConvertStringToArch(in string) (string, error) {
111116
}
112117
return "", fmt.Errorf("string %s is not a valid arch for seccomp", in)
113118
}
119+
120+
// List of flags known to this version of runc.
121+
var flags = []string{
122+
flagTsync,
123+
string(specs.LinuxSeccompFlagSpecAllow),
124+
string(specs.LinuxSeccompFlagLog),
125+
}
126+
127+
// KnownFlags returns the list of the known filter flags.
128+
// Used by `runc features`.
129+
func KnownFlags() []string {
130+
return flags
131+
}
132+
133+
// SupportedFlags returns the list of the supported filter flags.
134+
// This list may be a subset of one returned by KnownFlags due to
135+
// some flags not supported by the current kernel and/or libseccomp.
136+
// Used by `runc features`.
137+
func SupportedFlags() []string {
138+
if !Enabled {
139+
return nil
140+
}
141+
142+
var res []string
143+
for _, flag := range flags {
144+
if FlagSupported(specs.LinuxSeccompFlag(flag)) == nil {
145+
res = append(res, flag)
146+
}
147+
}
148+
149+
return res
150+
}

libcontainer/seccomp/patchbpf/enosys_linux.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ func filterFlags(config *configs.Seccomp, filter *libseccomp.ScmpFilter) (flags
643643
flags |= uint(C.C_FILTER_FLAG_SPEC_ALLOW)
644644
}
645645
}
646+
// XXX: add newly supported filter flags above this line.
646647

647648
for _, call := range config.Syscalls {
648649
if call.Action == configs.Notify {

libcontainer/seccomp/seccomp_linux.go

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,10 @@ func InitSeccomp(config *configs.Seccomp) (int, error) {
8787
}
8888
}
8989

90-
// Add extra flags
90+
// Add extra flags.
9191
for _, flag := range config.Flags {
92-
switch flag {
93-
case "SECCOMP_FILTER_FLAG_TSYNC":
94-
// libseccomp-golang always use filterAttrTsync when
95-
// possible so all goroutines will receive the same
96-
// rules, so there is nothing to do. It does not make
97-
// sense to apply the seccomp filter on only one
98-
// thread; other threads will be terminated after exec
99-
// anyway.
100-
case specs.LinuxSeccompFlagLog:
101-
if err := filter.SetLogBit(true); err != nil {
102-
return -1, fmt.Errorf("error adding log flag to seccomp filter: %w", err)
103-
}
104-
case specs.LinuxSeccompFlagSpecAllow:
105-
if err := filter.SetSSB(true); err != nil {
106-
return -1, fmt.Errorf("error adding SSB flag to seccomp filter: %w", err)
107-
}
108-
// NOTE when adding more flags, make sure to also modify filterFlags in patchbpf.
109-
default:
110-
return -1, fmt.Errorf("seccomp flags %q not yet supported by runc", flag)
92+
if err := setFlag(filter, flag); err != nil {
93+
return -1, err
11194
}
11295
}
11396

@@ -149,6 +132,67 @@ func InitSeccomp(config *configs.Seccomp) (int, error) {
149132
return seccompFd, nil
150133
}
151134

135+
type unknownFlagError struct {
136+
flag specs.LinuxSeccompFlag
137+
}
138+
139+
func (e *unknownFlagError) Error() string {
140+
return "seccomp flag " + string(e.flag) + " is not known to runc"
141+
}
142+
143+
func setFlag(filter *libseccomp.ScmpFilter, flag specs.LinuxSeccompFlag) error {
144+
switch flag {
145+
case flagTsync:
146+
// libseccomp-golang always use filterAttrTsync when
147+
// possible so all goroutines will receive the same
148+
// rules, so there is nothing to do. It does not make
149+
// sense to apply the seccomp filter on only one
150+
// thread; other threads will be terminated after exec
151+
// anyway.
152+
return nil
153+
case specs.LinuxSeccompFlagLog:
154+
if err := filter.SetLogBit(true); err != nil {
155+
return fmt.Errorf("error adding log flag to seccomp filter: %w", err)
156+
}
157+
return nil
158+
case specs.LinuxSeccompFlagSpecAllow:
159+
if err := filter.SetSSB(true); err != nil {
160+
return fmt.Errorf("error adding SSB flag to seccomp filter: %w", err)
161+
}
162+
return nil
163+
}
164+
// NOTE when adding more flags above, do not forget to also:
165+
// - add new flags to `flags` slice in config.go;
166+
// - add new flag values to flags_value() in tests/integration/seccomp.bats;
167+
// - modify func filterFlags in patchbpf/ accordingly.
168+
169+
return &unknownFlagError{flag: flag}
170+
}
171+
172+
// FlagSupported checks if the flag is known to runc and supported by
173+
// currently used libseccomp and kernel (i.e. it can be set).
174+
func FlagSupported(flag specs.LinuxSeccompFlag) error {
175+
filter := &libseccomp.ScmpFilter{}
176+
err := setFlag(filter, flag)
177+
178+
// For flags we don't know, setFlag returns unknownFlagError.
179+
var uf *unknownFlagError
180+
if errors.As(err, &uf) {
181+
return err
182+
}
183+
// For flags that are known to runc and libseccomp-golang but can not
184+
// be applied because either libseccomp or the kernel is too old,
185+
// seccomp.VersionError is returned.
186+
var verErr *libseccomp.VersionError
187+
if errors.As(err, &verErr) {
188+
// Not supported by libseccomp or the kernel.
189+
return err
190+
}
191+
192+
// All other flags are known and supported.
193+
return nil
194+
}
195+
152196
// Convert Libcontainer Action to Libseccomp ScmpAction
153197
func getAction(act configs.Action, errnoRet *uint) (libseccomp.ScmpAction, error) {
154198
switch act {

libcontainer/seccomp/seccomp_unsupported.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88

99
"github.com/opencontainers/runc/libcontainer/configs"
10+
"github.com/opencontainers/runtime-spec/specs-go"
1011
)
1112

1213
var ErrSeccompNotEnabled = errors.New("seccomp: config provided but seccomp not supported")
@@ -19,6 +20,11 @@ func InitSeccomp(config *configs.Seccomp) (int, error) {
1920
return -1, nil
2021
}
2122

23+
// FlagSupported tells if a provided seccomp flag is supported.
24+
func FlagSupported(_ specs.LinuxSeccompFlag) error {
25+
return ErrSeccompNotEnabled
26+
}
27+
2228
// Version returns major, minor, and micro.
2329
func Version() (uint, uint, uint) {
2430
return 0, 0, 0

libcontainer/specconv/spec_linux.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,15 +1024,21 @@ func SetupSeccomp(config *specs.LinuxSeccomp) (*configs.Seccomp, error) {
10241024
newConfig.Syscalls = []*configs.Syscall{}
10251025

10261026
// The list of flags defined in runtime-spec is a subset of the flags
1027-
// in the seccomp() syscall
1028-
for _, flag := range config.Flags {
1029-
switch flag {
1030-
case "SECCOMP_FILTER_FLAG_TSYNC":
1031-
// Tsync can be silently ignored
1032-
case specs.LinuxSeccompFlagLog, specs.LinuxSeccompFlagSpecAllow:
1027+
// in the seccomp() syscall.
1028+
if config.Flags == nil {
1029+
// No flags are set explicitly (not even the empty set);
1030+
// set the default of specs.LinuxSeccompFlagSpecAllow,
1031+
// if it is supported by the libseccomp and the kernel.
1032+
if err := seccomp.FlagSupported(specs.LinuxSeccompFlagSpecAllow); err == nil {
1033+
newConfig.Flags = []specs.LinuxSeccompFlag{specs.LinuxSeccompFlagSpecAllow}
1034+
}
1035+
} else {
1036+
// Fail early if some flags are unknown or unsupported.
1037+
for _, flag := range config.Flags {
1038+
if err := seccomp.FlagSupported(flag); err != nil {
1039+
return nil, err
1040+
}
10331041
newConfig.Flags = append(newConfig.Flags, flag)
1034-
default:
1035-
return nil, fmt.Errorf("seccomp flag %q not yet supported by runc", flag)
10361042
}
10371043
}
10381044

tests/integration/seccomp.bats

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,32 @@ function teardown() {
6666
[[ "$output" == *"Network is down"* ]]
6767
}
6868

69-
@test "runc run [seccomp] (SECCOMP_FILTER_FLAG_*)" {
70-
# Linux 4.14: SECCOMP_FILTER_FLAG_LOG
71-
# Linux 4.17: SECCOMP_FILTER_FLAG_SPEC_ALLOW
72-
requires_kernel 4.17
69+
# Prints the numeric value of provided seccomp flags combination.
70+
# The parameter is flags string, as supplied in OCI spec, for example
71+
# '"SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_LOG"'.
72+
function flags_value() {
73+
# Numeric values of seccomp flags.
74+
declare -A values=(
75+
['"SECCOMP_FILTER_FLAG_TSYNC"']=0 # Supported but ignored by runc, thus 0.
76+
['"SECCOMP_FILTER_FLAG_LOG"']=2
77+
['"SECCOMP_FILTER_FLAG_SPEC_ALLOW"']=4
78+
# XXX: add new values above this line.
79+
)
80+
# Split the flags.
81+
IFS=',' read -ra flags <<<"$1"
82+
83+
local flag v sum=0
84+
for flag in "${flags[@]}"; do
85+
# This will produce "values[$flag]: unbound variable"
86+
# error for a new flag yet unknown to the test.
87+
v=${values[$flag]}
88+
((sum += v)) || true
89+
done
90+
91+
echo $sum
92+
}
7393

94+
@test "runc run [seccomp] (SECCOMP_FILTER_FLAG_*)" {
7495
update_config ' .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
7596
| .process.noNewPrivileges = false
7697
| .linux.seccomp = {
@@ -79,18 +100,35 @@ function teardown() {
79100
"syscalls":[{"names":["mkdir", "mkdirat"], "action":"SCMP_ACT_ERRNO"}]
80101
}'
81102

82-
declare -A FLAGS=(
83-
['REMOVE']=0 # No setting, use built-in default.
84-
['EMPTY']=0 # Empty set of flags.
85-
['"SECCOMP_FILTER_FLAG_LOG"']=2
86-
['"SECCOMP_FILTER_FLAG_SPEC_ALLOW"']=4
87-
['"SECCOMP_FILTER_FLAG_TSYNC"']=0 # tsync flag is ignored.
88-
['"SECCOMP_FILTER_FLAG_LOG","SECCOMP_FILTER_FLAG_SPEC_ALLOW"']=6
89-
['"SECCOMP_FILTER_FLAG_LOG","SECCOMP_FILTER_FLAG_TSYNC"']=2
90-
['"SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_TSYNC"']=4
91-
['"SECCOMP_FILTER_FLAG_LOG","SECCOMP_FILTER_FLAG_SPEC_ALLOW","SECCOMP_FILTER_FLAG_TSYNC"']=6
103+
# Get the list of flags supported by runc/seccomp/kernel,
104+
# or "null" if no flags are supported or runc is too old.
105+
mapfile -t flags < <(__runc features | jq -c '.linux.seccomp.supportedFlags' |
106+
tr -d '[]\n' | tr ',' '\n')
107+
108+
# This is a set of all possible flag combinations to test.
109+
declare -A TEST_CASES=(
110+
['EMPTY']=0 # Special value: empty set of flags.
111+
['REMOVE']=0 # Special value: no flags set.
92112
)
93-
for key in "${!FLAGS[@]}"; do
113+
114+
# If supported, runc should set SPEC_ALLOW if no flags are set.
115+
if [[ " ${flags[*]} " == *' "SECCOMP_FILTER_FLAG_SPEC_ALLOW" '* ]]; then
116+
TEST_CASES['REMOVE']=$(flags_value '"SECCOMP_FILTER_FLAG_SPEC_ALLOW"')
117+
fi
118+
119+
# Add all possible combinations of seccomp flags
120+
# and their expected numeric values to TEST_CASES.
121+
if [ "${flags[0]}" != "null" ]; then
122+
# Use shell {a,}{b,}{c,} to generate the powerset.
123+
for fc in $(eval echo "$(printf "{'%s,',}" "${flags[@]}")"); do
124+
# Remove the last comma.
125+
fc="${fc/%,/}"
126+
TEST_CASES[$fc]=$(flags_value "$fc")
127+
done
128+
fi
129+
130+
# Finally, run the tests.
131+
for key in "${!TEST_CASES[@]}"; do
94132
case "$key" in
95133
'REMOVE')
96134
update_config ' del(.linux.seccomp.flags)'
@@ -108,7 +146,7 @@ function teardown() {
108146
[[ "$output" == *"mkdir:"*"/dev/shm/foo"*"Operation not permitted"* ]]
109147

110148
# Check the numeric flags value, as printed in the debug log, is as expected.
111-
exp="\"seccomp filter flags: ${FLAGS[$key]}\""
149+
exp="\"seccomp filter flags: ${TEST_CASES[$key]}\""
112150
echo "flags $key, expecting $exp"
113151
[[ "$output" == *"$exp"* ]]
114152
done

types/features/features.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,23 @@ type Seccomp struct {
5353
// Nil value means "unknown", not "no support for any action".
5454
Actions []string `json:"actions,omitempty"`
5555

56-
// Operators is the list of the recognized actions, e.g., "SCMP_CMP_NE".
56+
// Operators is the list of the recognized operators, e.g., "SCMP_CMP_NE".
5757
// Nil value means "unknown", not "no support for any operator".
5858
Operators []string `json:"operators,omitempty"`
5959

60-
// Operators is the list of the recognized archs, e.g., "SCMP_ARCH_X86_64".
60+
// Archs is the list of the recognized archs, e.g., "SCMP_ARCH_X86_64".
6161
// Nil value means "unknown", not "no support for any arch".
6262
Archs []string `json:"archs,omitempty"`
63+
64+
// KnownFlags is the list of the recognized filter flags, e.g., "SECCOMP_FILTER_FLAG_LOG".
65+
// Nil value means "unknown", not "no flags are recognized".
66+
KnownFlags []string `json:"knownFlags,omitempty"`
67+
68+
// SupportedFlags is the list of the supported filter flags, e.g., "SECCOMP_FILTER_FLAG_LOG".
69+
// This list may be a subset of KnownFlags due to some flags
70+
// not supported by the current kernel and/or libseccomp.
71+
// Nil value means "unknown", not "no flags are supported".
72+
SupportedFlags []string `json:"supportedFlags,omitempty"`
6373
}
6474

6575
// Apparmor represents the "apparmor" field.

0 commit comments

Comments
 (0)