Browse code

seccomp: implement marshal/unmarshall for MinVersion

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

Sebastiaan van Stijn authored on 2020/10/01 19:26:46
Showing 6 changed files
... ...
@@ -389,7 +389,7 @@ func DefaultProfile() *Seccomp {
389 389
 			Names:  []string{"ptrace"},
390 390
 			Action: specs.ActAllow,
391 391
 			Includes: Filter{
392
-				MinKernel: "4.8",
392
+				MinKernel: &KernelVersion{4, 8},
393 393
 			},
394 394
 		},
395 395
 		{
... ...
@@ -8,20 +8,14 @@ import (
8 8
 	"golang.org/x/sys/unix"
9 9
 )
10 10
 
11
-// kernelVersion holds information about the kernel.
12
-type kernelVersion struct {
13
-	kernel uint // Version of the kernel (i.e., the "4" in "4.1.2-generic")
14
-	major  uint // Major revision of the kernel (i.e., the "1" in "4.1.2-generic")
15
-}
16
-
17 11
 var (
18
-	currentKernelVersion *kernelVersion
12
+	currentKernelVersion *KernelVersion
19 13
 	kernelVersionError   error
20 14
 	once                 sync.Once
21 15
 )
22 16
 
23 17
 // getKernelVersion gets the current kernel version.
24
-func getKernelVersion() (*kernelVersion, error) {
18
+func getKernelVersion() (*KernelVersion, error) {
25 19
 	once.Do(func() {
26 20
 		var uts unix.Utsname
27 21
 		if err := unix.Uname(&uts); err != nil {
... ...
@@ -33,13 +27,13 @@ func getKernelVersion() (*kernelVersion, error) {
33 33
 	return currentKernelVersion, kernelVersionError
34 34
 }
35 35
 
36
-// parseRelease parses a string and creates a kernelVersion based on it.
37
-func parseRelease(release string) (*kernelVersion, error) {
38
-	var version = kernelVersion{}
36
+// parseRelease parses a string and creates a KernelVersion based on it.
37
+func parseRelease(release string) (*KernelVersion, error) {
38
+	var version = KernelVersion{}
39 39
 
40 40
 	// We're only make sure we get the "kernel" and "major revision". Sometimes we have
41 41
 	// 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64.
42
-	_, err := fmt.Sscanf(release, "%d.%d", &version.kernel, &version.major)
42
+	_, err := fmt.Sscanf(release, "%d.%d", &version.Kernel, &version.Major)
43 43
 	if err != nil {
44 44
 		return nil, fmt.Errorf("failed to parse kernel version %q: %w", release, err)
45 45
 	}
... ...
@@ -50,19 +44,15 @@ func parseRelease(release string) (*kernelVersion, error) {
50 50
 // equal to the given kernel version v. Only "kernel version" and "major revision"
51 51
 // can be specified (e.g., "3.12") and will be taken into account, which means
52 52
 // that 3.12.25-gentoo and 3.12-1-amd64 are considered equal (kernel: 3, major: 12).
53
-func kernelGreaterEqualThan(v string) (bool, error) {
54
-	minVersion, err := parseRelease(v)
55
-	if err != nil {
56
-		return false, err
57
-	}
53
+func kernelGreaterEqualThan(minVersion KernelVersion) (bool, error) {
58 54
 	kv, err := getKernelVersion()
59 55
 	if err != nil {
60 56
 		return false, err
61 57
 	}
62
-	if kv.kernel > minVersion.kernel {
58
+	if kv.Kernel > minVersion.Kernel {
63 59
 		return true, nil
64 60
 	}
65
-	if kv.kernel == minVersion.kernel && kv.major >= minVersion.major {
61
+	if kv.Kernel == minVersion.Kernel && kv.Major >= minVersion.Major {
66 62
 		return true, nil
67 63
 	}
68 64
 	return false, nil
... ...
@@ -13,7 +13,7 @@ func TestGetKernelVersion(t *testing.T) {
13 13
 	if version == nil {
14 14
 		t.Fatal("version is nil")
15 15
 	}
16
-	if version.kernel == 0 {
16
+	if version.Kernel == 0 {
17 17
 		t.Fatal("no kernel version")
18 18
 	}
19 19
 }
... ...
@@ -22,18 +22,19 @@ func TestGetKernelVersion(t *testing.T) {
22 22
 func TestParseRelease(t *testing.T) {
23 23
 	tests := []struct {
24 24
 		in          string
25
-		out         kernelVersion
25
+		out         KernelVersion
26 26
 		expectedErr error
27 27
 	}{
28
-		{in: "3.8", out: kernelVersion{kernel: 3, major: 8}},
29
-		{in: "3.8.0", out: kernelVersion{kernel: 3, major: 8}},
30
-		{in: "3.8.0-19-generic", out: kernelVersion{kernel: 3, major: 8}},
31
-		{in: "3.4.54.longterm-1", out: kernelVersion{kernel: 3, major: 4}},
32
-		{in: "3.10.0-862.2.3.el7.x86_64", out: kernelVersion{kernel: 3, major: 10}},
33
-		{in: "3.12.8tag", out: kernelVersion{kernel: 3, major: 12}},
34
-		{in: "3.12-1-amd64", out: kernelVersion{kernel: 3, major: 12}},
35
-		{in: "3.12foobar", out: kernelVersion{kernel: 3, major: 12}},
36
-		{in: "99.999.999-19-generic", out: kernelVersion{kernel: 99, major: 999}},
28
+		{in: "3.8", out: KernelVersion{Kernel: 3, Major: 8}},
29
+		{in: "3.8.0", out: KernelVersion{Kernel: 3, Major: 8}},
30
+		{in: "3.8.0-19-generic", out: KernelVersion{Kernel: 3, Major: 8}},
31
+		{in: "3.4.54.longterm-1", out: KernelVersion{Kernel: 3, Major: 4}},
32
+		{in: "3.10.0-862.2.3.el7.x86_64", out: KernelVersion{Kernel: 3, Major: 10}},
33
+		{in: "3.12.8tag", out: KernelVersion{Kernel: 3, Major: 12}},
34
+		{in: "3.12-1-amd64", out: KernelVersion{Kernel: 3, Major: 12}},
35
+		{in: "3.12foobar", out: KernelVersion{Kernel: 3, Major: 12}},
36
+		{in: "99.999.999-19-generic", out: KernelVersion{Kernel: 99, Major: 999}},
37
+		{in: "", expectedErr: fmt.Errorf(`failed to parse kernel version "": EOF`)},
37 38
 		{in: "3", expectedErr: fmt.Errorf(`failed to parse kernel version "3": unexpected EOF`)},
38 39
 		{in: "3.", expectedErr: fmt.Errorf(`failed to parse kernel version "3.": EOF`)},
39 40
 		{in: "3a", expectedErr: fmt.Errorf(`failed to parse kernel version "3a": input does not match format`)},
... ...
@@ -66,8 +67,8 @@ func TestParseRelease(t *testing.T) {
66 66
 			if version == nil {
67 67
 				t.Fatal("version is nil")
68 68
 			}
69
-			if version.kernel != tc.out.kernel || version.major != tc.out.major {
70
-				t.Fatalf("expected: %d.%d, got: %d.%d", tc.out.kernel, tc.out.major, version.kernel, version.major)
69
+			if version.Kernel != tc.out.Kernel || version.Major != tc.out.Major {
70
+				t.Fatalf("expected: %d.%d, got: %d.%d", tc.out.Kernel, tc.out.Major, version.Kernel, version.Major)
71 71
 			}
72 72
 		})
73 73
 	}
... ...
@@ -82,33 +83,33 @@ func TestKernelGreaterEqualThan(t *testing.T) {
82 82
 
83 83
 	tests := []struct {
84 84
 		doc      string
85
-		in       string
85
+		in       KernelVersion
86 86
 		expected bool
87 87
 	}{
88 88
 		{
89 89
 			doc:      "same version",
90
-			in:       fmt.Sprintf("%d.%d", v.kernel, v.major),
90
+			in:       KernelVersion{v.Kernel, v.Major},
91 91
 			expected: true,
92 92
 		},
93 93
 		{
94 94
 			doc:      "kernel minus one",
95
-			in:       fmt.Sprintf("%d.%d", v.kernel-1, v.major),
95
+			in:       KernelVersion{v.Kernel - 1, v.Major},
96 96
 			expected: true,
97 97
 		},
98 98
 		{
99 99
 			doc:      "kernel plus one",
100
-			in:       fmt.Sprintf("%d.%d", v.kernel+1, v.major),
100
+			in:       KernelVersion{v.Kernel + 1, v.Major},
101 101
 			expected: false,
102 102
 		},
103 103
 		{
104 104
 			doc:      "major plus one",
105
-			in:       fmt.Sprintf("%d.%d", v.kernel, v.major+1),
105
+			in:       KernelVersion{v.Kernel, v.Major + 1},
106 106
 			expected: false,
107 107
 		},
108 108
 	}
109 109
 	for _, tc := range tests {
110 110
 		tc := tc
111
-		t.Run(tc.doc+": "+tc.in, func(t *testing.T) {
111
+		t.Run(tc.doc+": "+tc.in.String(), func(t *testing.T) {
112 112
 			ok, err := kernelGreaterEqualThan(tc.in)
113 113
 			if err != nil {
114 114
 				t.Fatal("unexpected error:", err)
... ...
@@ -1,6 +1,13 @@
1 1
 package seccomp // import "github.com/docker/docker/profiles/seccomp"
2 2
 
3
-import "github.com/opencontainers/runtime-spec/specs-go"
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"strconv"
7
+	"strings"
8
+
9
+	"github.com/opencontainers/runtime-spec/specs-go"
10
+)
4 11
 
5 12
 // Seccomp represents the config for a seccomp profile for syscall restriction.
6 13
 type Seccomp struct {
... ...
@@ -30,7 +37,7 @@ type Filter struct {
30 30
 	// When matching the kernel version of the host, minor revisions, and distro-
31 31
 	// specific suffixes are ignored, which means that "3.12.25-gentoo", "3.12-1-amd64",
32 32
 	// "3.12", and "3.12-rc5" are considered equal (kernel 3, major revision 12).
33
-	MinKernel string `json:"minKernel,omitempty"`
33
+	MinKernel *KernelVersion `json:"minKernel,omitempty"`
34 34
 }
35 35
 
36 36
 // Syscall is used to match a group of syscalls in Seccomp
... ...
@@ -43,3 +50,52 @@ type Syscall struct {
43 43
 	Includes Filter                   `json:"includes"`
44 44
 	Excludes Filter                   `json:"excludes"`
45 45
 }
46
+
47
+// KernelVersion holds information about the kernel.
48
+type KernelVersion struct {
49
+	Kernel uint64 // Version of the Kernel (i.e., the "4" in "4.1.2-generic")
50
+	Major  uint64 // Major revision of the Kernel (i.e., the "1" in "4.1.2-generic")
51
+}
52
+
53
+// String implements fmt.Stringer for KernelVersion
54
+func (k *KernelVersion) String() string {
55
+	if k.Kernel > 0 || k.Major > 0 {
56
+		return fmt.Sprintf("%d.%d", k.Kernel, k.Major)
57
+	}
58
+	return ""
59
+}
60
+
61
+// MarshalJSON implements json.Unmarshaler for KernelVersion
62
+func (k *KernelVersion) MarshalJSON() ([]byte, error) {
63
+	return json.Marshal(k.String())
64
+}
65
+
66
+// UnmarshalJSON implements json.Marshaler for KernelVersion
67
+func (k *KernelVersion) UnmarshalJSON(version []byte) error {
68
+	var (
69
+		ver string
70
+		err error
71
+	)
72
+
73
+	// make sure we have a string
74
+	if err = json.Unmarshal(version, &ver); err != nil {
75
+		return fmt.Errorf(`invalid kernel version: %s, expected "<kernel>.<major>": %v`, string(version), err)
76
+	}
77
+	if ver == "" {
78
+		return nil
79
+	}
80
+	parts := strings.SplitN(ver, ".", 3)
81
+	if len(parts) != 2 {
82
+		return fmt.Errorf(`invalid kernel version: %s, expected "<kernel>.<major>"`, string(version))
83
+	}
84
+	if k.Kernel, err = strconv.ParseUint(parts[0], 10, 8); err != nil {
85
+		return fmt.Errorf(`invalid kernel version: %s, expected "<kernel>.<major>": %v`, string(version), err)
86
+	}
87
+	if k.Major, err = strconv.ParseUint(parts[1], 10, 8); err != nil {
88
+		return fmt.Errorf(`invalid kernel version: %s, expected "<kernel>.<major>": %v`, string(version), err)
89
+	}
90
+	if k.Kernel == 0 && k.Major == 0 {
91
+		return fmt.Errorf(`invalid kernel version: %s, expected "<kernel>.<major>": version cannot be 0.0`, string(version))
92
+	}
93
+	return nil
94
+}
... ...
@@ -123,8 +123,8 @@ Loop:
123 123
 				}
124 124
 			}
125 125
 		}
126
-		if call.Excludes.MinKernel != "" {
127
-			if ok, err := kernelGreaterEqualThan(call.Excludes.MinKernel); err != nil {
126
+		if call.Excludes.MinKernel != nil {
127
+			if ok, err := kernelGreaterEqualThan(*call.Excludes.MinKernel); err != nil {
128 128
 				return nil, err
129 129
 			} else if ok {
130 130
 				continue Loop
... ...
@@ -142,8 +142,8 @@ Loop:
142 142
 				}
143 143
 			}
144 144
 		}
145
-		if call.Includes.MinKernel != "" {
146
-			if ok, err := kernelGreaterEqualThan(call.Includes.MinKernel); err != nil {
145
+		if call.Includes.MinKernel != nil {
146
+			if ok, err := kernelGreaterEqualThan(*call.Includes.MinKernel); err != nil {
147 147
 				return nil, err
148 148
 			} else if !ok {
149 149
 				continue Loop
... ...
@@ -5,6 +5,7 @@ package seccomp // import "github.com/docker/docker/profiles/seccomp"
5 5
 import (
6 6
 	"encoding/json"
7 7
 	"io/ioutil"
8
+	"strings"
8 9
 	"testing"
9 10
 
10 11
 	"github.com/opencontainers/runtime-spec/specs-go"
... ...
@@ -67,6 +68,58 @@ func TestUnmarshalDefaultProfile(t *testing.T) {
67 67
 	assert.DeepEqual(t, expected.Syscalls, profile.Syscalls)
68 68
 }
69 69
 
70
+func TestMarshalUnmarshalFilter(t *testing.T) {
71
+	t.Parallel()
72
+	tests := []struct {
73
+		in    string
74
+		out   string
75
+		error bool
76
+	}{
77
+		{in: `{"arches":["s390x"],"minKernel":3}`, error: true},
78
+		{in: `{"arches":["s390x"],"minKernel":3.12}`, error: true},
79
+		{in: `{"arches":["s390x"],"minKernel":true}`, error: true},
80
+		{in: `{"arches":["s390x"],"minKernel":"0.0"}`, error: true},
81
+		{in: `{"arches":["s390x"],"minKernel":"3"}`, error: true},
82
+		{in: `{"arches":["s390x"],"minKernel":".3"}`, error: true},
83
+		{in: `{"arches":["s390x"],"minKernel":"3."}`, error: true},
84
+		{in: `{"arches":["s390x"],"minKernel":"true"}`, error: true},
85
+		{in: `{"arches":["s390x"],"minKernel":"3.12.1\""}`, error: true},
86
+		{in: `{"arches":["s390x"],"minKernel":"4.15abc"}`, error: true},
87
+		{in: `{"arches":["s390x"],"minKernel":null}`, out: `{"arches":["s390x"]}`},
88
+		{in: `{"arches":["s390x"],"minKernel":""}`, out: `{"arches":["s390x"],"minKernel":""}`}, // FIXME: try to fix omitempty for this
89
+		{in: `{"arches":["s390x"],"minKernel":"0.5"}`, out: `{"arches":["s390x"],"minKernel":"0.5"}`},
90
+		{in: `{"arches":["s390x"],"minKernel":"0.50"}`, out: `{"arches":["s390x"],"minKernel":"0.50"}`},
91
+		{in: `{"arches":["s390x"],"minKernel":"5.0"}`, out: `{"arches":["s390x"],"minKernel":"5.0"}`},
92
+		{in: `{"arches":["s390x"],"minKernel":"50.0"}`, out: `{"arches":["s390x"],"minKernel":"50.0"}`},
93
+		{in: `{"arches":["s390x"],"minKernel":"4.15"}`, out: `{"arches":["s390x"],"minKernel":"4.15"}`},
94
+	}
95
+	for _, tc := range tests {
96
+		tc := tc
97
+		t.Run(tc.in, func(t *testing.T) {
98
+			var filter Filter
99
+			err := json.Unmarshal([]byte(tc.in), &filter)
100
+			if tc.error {
101
+				if err == nil {
102
+					t.Fatal("expected an error")
103
+				} else if !strings.Contains(err.Error(), "invalid kernel version") {
104
+					t.Fatal("unexpected error:", err)
105
+				}
106
+				return
107
+			}
108
+			if err != nil {
109
+				t.Fatal(err)
110
+			}
111
+			out, err := json.Marshal(filter)
112
+			if err != nil {
113
+				t.Fatal(err)
114
+			}
115
+			if string(out) != tc.out {
116
+				t.Fatalf("expected %s, got %s", tc.out, string(out))
117
+			}
118
+		})
119
+	}
120
+}
121
+
70 122
 func TestLoadConditional(t *testing.T) {
71 123
 	f, err := ioutil.ReadFile("fixtures/conditional_include.json")
72 124
 	if err != nil {