Browse code

seccomp: remove dependency on pkg/parsers/kernel

This removes the dependency on the `pkg/parsers/kernel` package, because secomp
only needs to consider Linux (and no parsing is needed for Windows or Darwin kernel
versions).

This patch implements the minimum requirements for this implementation:

- only `kernel` and `major` versions are considered
- `minor` version, `flavor`, and `-rcXX` suffixes are ignored

So, for example:

- `3.4.54.longterm-1` => `kernel: 3`, `major: 4`
- `3.8.0-19-generic` => `kernel: 3`, `major: 8`
- `3.10.0-862.2.3.el7.x86_64` => `kernel: 3`, `major: 10`

Some systems also omit the `minor` and/or have odd-formatted versions. In context
of generating seccomp profiles, both versions below are considered equal;

- `3.12.25-gentoo` => `kernel: 3`, `major: 12`
- `3.12-1-amd64` => `kernel: 3`, `major: 12`

Note that `-rcX` suffixes are also not considered, and thus (e.g.) kernel `5.9-rc1`,
`5.9-rc6` and `5.9` are all considered equal.

The motivation for ignoring "minor" versions and "flavors" is that;

- The upstream kernel only does "kernel.major" releases
- While release-candidates exists for kernel (e.g. 5.9-rc5), we don't expect users
to write profiles that target a specific release-candidate, and therefore consider
(e.g.) kernel `5.9-rc1`, `5.9-rc6` and `5.9` to be equal.
- Generally, a seccomp-profile should either be portable, or written for a specific
infrastructure (in which case the writer of the profile would know if the kernel-flavors
used does/does not support certain things.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2020/09/25 22:06:25
Showing 4 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,69 @@
0
+package seccomp
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"sync"
6
+
7
+	"golang.org/x/sys/unix"
8
+)
9
+
10
+// kernelVersion holds information about the kernel.
11
+type kernelVersion struct {
12
+	kernel uint // Version of the kernel (i.e., the "4" in "4.1.2-generic")
13
+	major  uint // Major revision of the kernel (i.e., the "1" in "4.1.2-generic")
14
+}
15
+
16
+var (
17
+	currentKernelVersion *kernelVersion
18
+	kernelVersionError   error
19
+	once                 sync.Once
20
+)
21
+
22
+// getKernelVersion gets the current kernel version.
23
+func getKernelVersion() (*kernelVersion, error) {
24
+	once.Do(func() {
25
+		var uts unix.Utsname
26
+		if err := unix.Uname(&uts); err != nil {
27
+			return
28
+		}
29
+		// Remove the \x00 from the release for Atoi to parse correctly
30
+		currentKernelVersion, kernelVersionError = parseRelease(string(uts.Release[:bytes.IndexByte(uts.Release[:], 0)]))
31
+	})
32
+	return currentKernelVersion, kernelVersionError
33
+}
34
+
35
+// parseRelease parses a string and creates a kernelVersion based on it.
36
+func parseRelease(release string) (*kernelVersion, error) {
37
+	var version = kernelVersion{}
38
+
39
+	// We're only make sure we get the "kernel" and "major revision". Sometimes we have
40
+	// 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64.
41
+	_, err := fmt.Sscanf(release, "%d.%d", &version.kernel, &version.major)
42
+	if err != nil {
43
+		return nil, fmt.Errorf("failed to parse kernel version %q: %w", release, err)
44
+	}
45
+	return &version, nil
46
+}
47
+
48
+// kernelGreaterEqualThan checks if the host's kernel version is greater than, or
49
+// equal to the given kernel version v. Only "kernel version" and "major revision"
50
+// can be specified (e.g., "3.12") and will be taken into account, which means
51
+// that 3.12.25-gentoo and 3.12-1-amd64 are considered equal (kernel: 3, major: 12).
52
+func kernelGreaterEqualThan(v string) (bool, error) {
53
+	minVersion, err := parseRelease(v)
54
+	if err != nil {
55
+		return false, err
56
+	}
57
+	kv, err := getKernelVersion()
58
+	if err != nil {
59
+		return false, err
60
+	}
61
+	if kv.kernel > minVersion.kernel {
62
+		return true, nil
63
+	}
64
+	if kv.kernel == minVersion.kernel && kv.major >= minVersion.major {
65
+		return true, nil
66
+	}
67
+	return false, nil
68
+}
0 69
new file mode 100644
... ...
@@ -0,0 +1,121 @@
0
+package seccomp
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+)
6
+
7
+func TestGetKernelVersion(t *testing.T) {
8
+	version, err := getKernelVersion()
9
+	if err != nil {
10
+		t.Fatal(err)
11
+	}
12
+	if version == nil {
13
+		t.Fatal("version is nil")
14
+	}
15
+	if version.kernel == 0 {
16
+		t.Fatal("no kernel version")
17
+	}
18
+}
19
+
20
+// TestParseRelease tests the ParseRelease() function
21
+func TestParseRelease(t *testing.T) {
22
+	tests := []struct {
23
+		in          string
24
+		out         kernelVersion
25
+		expectedErr error
26
+	}{
27
+		{in: "3.8", out: kernelVersion{kernel: 3, major: 8}},
28
+		{in: "3.8.0", out: kernelVersion{kernel: 3, major: 8}},
29
+		{in: "3.8.0-19-generic", out: kernelVersion{kernel: 3, major: 8}},
30
+		{in: "3.4.54.longterm-1", out: kernelVersion{kernel: 3, major: 4}},
31
+		{in: "3.10.0-862.2.3.el7.x86_64", out: kernelVersion{kernel: 3, major: 10}},
32
+		{in: "3.12.8tag", out: kernelVersion{kernel: 3, major: 12}},
33
+		{in: "3.12-1-amd64", out: kernelVersion{kernel: 3, major: 12}},
34
+		{in: "3.12foobar", out: kernelVersion{kernel: 3, major: 12}},
35
+		{in: "99.999.999-19-generic", out: kernelVersion{kernel: 99, major: 999}},
36
+		{in: "3", expectedErr: fmt.Errorf(`failed to parse kernel version "3": unexpected EOF`)},
37
+		{in: "3.", expectedErr: fmt.Errorf(`failed to parse kernel version "3.": EOF`)},
38
+		{in: "3a", expectedErr: fmt.Errorf(`failed to parse kernel version "3a": input does not match format`)},
39
+		{in: "3.a", expectedErr: fmt.Errorf(`failed to parse kernel version "3.a": expected integer`)},
40
+		{in: "a", expectedErr: fmt.Errorf(`failed to parse kernel version "a": expected integer`)},
41
+		{in: "a.a", expectedErr: fmt.Errorf(`failed to parse kernel version "a.a": expected integer`)},
42
+		{in: "a.a.a-a", expectedErr: fmt.Errorf(`failed to parse kernel version "a.a.a-a": expected integer`)},
43
+		{in: "-3", expectedErr: fmt.Errorf(`failed to parse kernel version "-3": expected integer`)},
44
+		{in: "-3.", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.": expected integer`)},
45
+		{in: "-3.8", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.8": expected integer`)},
46
+		{in: "-3.-8", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.-8": expected integer`)},
47
+		{in: "3.-8", expectedErr: fmt.Errorf(`failed to parse kernel version "3.-8": expected integer`)},
48
+	}
49
+	for _, tc := range tests {
50
+		tc := tc
51
+		t.Run(tc.in, func(t *testing.T) {
52
+			version, err := parseRelease(tc.in)
53
+			if tc.expectedErr != nil {
54
+				if err == nil {
55
+					t.Fatal("expected an error")
56
+				}
57
+				if err.Error() != tc.expectedErr.Error() {
58
+					t.Fatalf("expected: %s, got: %s", tc.expectedErr, err)
59
+				}
60
+				return
61
+			}
62
+			if err != nil {
63
+				t.Fatal("unexpected error:", err)
64
+			}
65
+			if version == nil {
66
+				t.Fatal("version is nil")
67
+			}
68
+			if version.kernel != tc.out.kernel || version.major != tc.out.major {
69
+				t.Fatalf("expected: %d.%d, got: %d.%d", tc.out.kernel, tc.out.major, version.kernel, version.major)
70
+			}
71
+		})
72
+	}
73
+}
74
+
75
+func TestKernelGreaterEqualThan(t *testing.T) {
76
+	// Get the current kernel version, so that we can make test relative to that
77
+	v, err := getKernelVersion()
78
+	if err != nil {
79
+		t.Fatal(err)
80
+	}
81
+
82
+	tests := []struct {
83
+		doc      string
84
+		in       string
85
+		expected bool
86
+	}{
87
+		{
88
+			doc:      "same version",
89
+			in:       fmt.Sprintf("%d.%d", v.kernel, v.major),
90
+			expected: true,
91
+		},
92
+		{
93
+			doc:      "kernel minus one",
94
+			in:       fmt.Sprintf("%d.%d", v.kernel-1, v.major),
95
+			expected: true,
96
+		},
97
+		{
98
+			doc:      "kernel plus one",
99
+			in:       fmt.Sprintf("%d.%d", v.kernel+1, v.major),
100
+			expected: false,
101
+		},
102
+		{
103
+			doc:      "major plus one",
104
+			in:       fmt.Sprintf("%d.%d", v.kernel, v.major+1),
105
+			expected: false,
106
+		},
107
+	}
108
+	for _, tc := range tests {
109
+		tc := tc
110
+		t.Run(tc.doc+": "+tc.in, func(t *testing.T) {
111
+			ok, err := kernelGreaterEqualThan(tc.in)
112
+			if err != nil {
113
+				t.Fatal("unexpected error:", err)
114
+			}
115
+			if ok != tc.expected {
116
+				t.Fatalf("expected: %v, got: %v", tc.expected, ok)
117
+			}
118
+		})
119
+	}
120
+}
... ...
@@ -21,9 +21,16 @@ type Architecture struct {
21 21
 
22 22
 // Filter is used to conditionally apply Seccomp rules
23 23
 type Filter struct {
24
-	Caps      []string `json:"caps,omitempty"`
25
-	Arches    []string `json:"arches,omitempty"`
26
-	MinKernel string   `json:"minKernel,omitempty"`
24
+	Caps   []string `json:"caps,omitempty"`
25
+	Arches []string `json:"arches,omitempty"`
26
+
27
+	// MinKernel describes the minimum kernel version the rule must be applied
28
+	// on, in the format "<kernel version>.<major revision>" (e.g. "3.12").
29
+	//
30
+	// When matching the kernel version of the host, minor revisions, and distro-
31
+	// specific suffixes are ignored, which means that "3.12.25-gentoo", "3.12-1-amd64",
32
+	// "3.12", and "3.12-rc5" are considered equal (kernel 3, major revision 12).
33
+	MinKernel string `json:"minKernel,omitempty"`
27 34
 }
28 35
 
29 36
 // Syscall is used to match a group of syscalls in Seccomp
... ...
@@ -8,7 +8,6 @@ import (
8 8
 	"fmt"
9 9
 	"runtime"
10 10
 
11
-	"github.com/docker/docker/pkg/parsers/kernel"
12 11
 	specs "github.com/opencontainers/runtime-spec/specs-go"
13 12
 )
14 13
 
... ...
@@ -177,19 +176,3 @@ func createSpecsSyscall(names []string, action specs.LinuxSeccompAction, args []
177 177
 	}
178 178
 	return newCall
179 179
 }
180
-
181
-var currentKernelVersion *kernel.VersionInfo
182
-
183
-func kernelGreaterEqualThan(v string) (bool, error) {
184
-	version, err := kernel.ParseRelease(v)
185
-	if err != nil {
186
-		return false, err
187
-	}
188
-	if currentKernelVersion == nil {
189
-		currentKernelVersion, err = kernel.GetKernelVersion()
190
-		if err != nil {
191
-			return false, err
192
-		}
193
-	}
194
-	return kernel.CompareKernelVersion(*version, *currentKernelVersion) <= 0, nil
195
-}