From 97535c6c2b8e0ace4fd0c3190a58cf42dd31a7dc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 25 Sep 2020 15:06:25 +0200 Subject: 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 --- profiles/seccomp/kernel_linux.go | 69 +++++++++++++++++++ profiles/seccomp/kernel_linux_test.go | 121 ++++++++++++++++++++++++++++++++++ profiles/seccomp/seccomp.go | 13 +++- profiles/seccomp/seccomp_linux.go | 17 ----- 4 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 profiles/seccomp/kernel_linux.go create mode 100644 profiles/seccomp/kernel_linux_test.go (limited to 'profiles') diff --git a/profiles/seccomp/kernel_linux.go b/profiles/seccomp/kernel_linux.go new file mode 100644 index 0000000000..c2ca03506c --- /dev/null +++ b/profiles/seccomp/kernel_linux.go @@ -0,0 +1,69 @@ +package seccomp + +import ( + "bytes" + "fmt" + "sync" + + "golang.org/x/sys/unix" +) + +// kernelVersion holds information about the kernel. +type kernelVersion struct { + kernel uint // Version of the kernel (i.e., the "4" in "4.1.2-generic") + major uint // Major revision of the kernel (i.e., the "1" in "4.1.2-generic") +} + +var ( + currentKernelVersion *kernelVersion + kernelVersionError error + once sync.Once +) + +// getKernelVersion gets the current kernel version. +func getKernelVersion() (*kernelVersion, error) { + once.Do(func() { + var uts unix.Utsname + if err := unix.Uname(&uts); err != nil { + return + } + // Remove the \x00 from the release for Atoi to parse correctly + currentKernelVersion, kernelVersionError = parseRelease(string(uts.Release[:bytes.IndexByte(uts.Release[:], 0)])) + }) + return currentKernelVersion, kernelVersionError +} + +// parseRelease parses a string and creates a kernelVersion based on it. +func parseRelease(release string) (*kernelVersion, error) { + var version = kernelVersion{} + + // We're only make sure we get the "kernel" and "major revision". Sometimes we have + // 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64. + _, err := fmt.Sscanf(release, "%d.%d", &version.kernel, &version.major) + if err != nil { + return nil, fmt.Errorf("failed to parse kernel version %q: %w", release, err) + } + return &version, nil +} + +// kernelGreaterEqualThan checks if the host's kernel version is greater than, or +// equal to the given kernel version v. Only "kernel version" and "major revision" +// can be specified (e.g., "3.12") and will be taken into account, which means +// that 3.12.25-gentoo and 3.12-1-amd64 are considered equal (kernel: 3, major: 12). +func kernelGreaterEqualThan(v string) (bool, error) { + minVersion, err := parseRelease(v) + if err != nil { + return false, err + } + kv, err := getKernelVersion() + if err != nil { + return false, err + } + if kv.kernel > minVersion.kernel { + return true, nil + } + if kv.kernel == minVersion.kernel && kv.major >= minVersion.major { + return true, nil + } + return false, nil +} diff --git a/profiles/seccomp/kernel_linux_test.go b/profiles/seccomp/kernel_linux_test.go new file mode 100644 index 0000000000..60fb55c416 --- /dev/null +++ b/profiles/seccomp/kernel_linux_test.go @@ -0,0 +1,121 @@ +package seccomp + +import ( + "fmt" + "testing" +) + +func TestGetKernelVersion(t *testing.T) { + version, err := getKernelVersion() + if err != nil { + t.Fatal(err) + } + if version == nil { + t.Fatal("version is nil") + } + if version.kernel == 0 { + t.Fatal("no kernel version") + } +} + +// TestParseRelease tests the ParseRelease() function +func TestParseRelease(t *testing.T) { + tests := []struct { + in string + out kernelVersion + expectedErr error + }{ + {in: "3.8", out: kernelVersion{kernel: 3, major: 8}}, + {in: "3.8.0", out: kernelVersion{kernel: 3, major: 8}}, + {in: "3.8.0-19-generic", out: kernelVersion{kernel: 3, major: 8}}, + {in: "3.4.54.longterm-1", out: kernelVersion{kernel: 3, major: 4}}, + {in: "3.10.0-862.2.3.el7.x86_64", out: kernelVersion{kernel: 3, major: 10}}, + {in: "3.12.8tag", out: kernelVersion{kernel: 3, major: 12}}, + {in: "3.12-1-amd64", out: kernelVersion{kernel: 3, major: 12}}, + {in: "3.12foobar", out: kernelVersion{kernel: 3, major: 12}}, + {in: "99.999.999-19-generic", out: kernelVersion{kernel: 99, major: 999}}, + {in: "3", expectedErr: fmt.Errorf(`failed to parse kernel version "3": unexpected EOF`)}, + {in: "3.", expectedErr: fmt.Errorf(`failed to parse kernel version "3.": EOF`)}, + {in: "3a", expectedErr: fmt.Errorf(`failed to parse kernel version "3a": input does not match format`)}, + {in: "3.a", expectedErr: fmt.Errorf(`failed to parse kernel version "3.a": expected integer`)}, + {in: "a", expectedErr: fmt.Errorf(`failed to parse kernel version "a": expected integer`)}, + {in: "a.a", expectedErr: fmt.Errorf(`failed to parse kernel version "a.a": expected integer`)}, + {in: "a.a.a-a", expectedErr: fmt.Errorf(`failed to parse kernel version "a.a.a-a": expected integer`)}, + {in: "-3", expectedErr: fmt.Errorf(`failed to parse kernel version "-3": expected integer`)}, + {in: "-3.", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.": expected integer`)}, + {in: "-3.8", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.8": expected integer`)}, + {in: "-3.-8", expectedErr: fmt.Errorf(`failed to parse kernel version "-3.-8": expected integer`)}, + {in: "3.-8", expectedErr: fmt.Errorf(`failed to parse kernel version "3.-8": expected integer`)}, + } + for _, tc := range tests { + tc := tc + t.Run(tc.in, func(t *testing.T) { + version, err := parseRelease(tc.in) + if tc.expectedErr != nil { + if err == nil { + t.Fatal("expected an error") + } + if err.Error() != tc.expectedErr.Error() { + t.Fatalf("expected: %s, got: %s", tc.expectedErr, err) + } + return + } + if err != nil { + t.Fatal("unexpected error:", err) + } + if version == nil { + t.Fatal("version is nil") + } + if version.kernel != tc.out.kernel || version.major != tc.out.major { + t.Fatalf("expected: %d.%d, got: %d.%d", tc.out.kernel, tc.out.major, version.kernel, version.major) + } + }) + } +} + +func TestKernelGreaterEqualThan(t *testing.T) { + // Get the current kernel version, so that we can make test relative to that + v, err := getKernelVersion() + if err != nil { + t.Fatal(err) + } + + tests := []struct { + doc string + in string + expected bool + }{ + { + doc: "same version", + in: fmt.Sprintf("%d.%d", v.kernel, v.major), + expected: true, + }, + { + doc: "kernel minus one", + in: fmt.Sprintf("%d.%d", v.kernel-1, v.major), + expected: true, + }, + { + doc: "kernel plus one", + in: fmt.Sprintf("%d.%d", v.kernel+1, v.major), + expected: false, + }, + { + doc: "major plus one", + in: fmt.Sprintf("%d.%d", v.kernel, v.major+1), + expected: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.doc+": "+tc.in, func(t *testing.T) { + ok, err := kernelGreaterEqualThan(tc.in) + if err != nil { + t.Fatal("unexpected error:", err) + } + if ok != tc.expected { + t.Fatalf("expected: %v, got: %v", tc.expected, ok) + } + }) + } +} diff --git a/profiles/seccomp/seccomp.go b/profiles/seccomp/seccomp.go index 441c37f648..a7a9c5446f 100644 --- a/profiles/seccomp/seccomp.go +++ b/profiles/seccomp/seccomp.go @@ -21,9 +21,16 @@ type Architecture struct { // Filter is used to conditionally apply Seccomp rules type Filter struct { - Caps []string `json:"caps,omitempty"` - Arches []string `json:"arches,omitempty"` - MinKernel string `json:"minKernel,omitempty"` + Caps []string `json:"caps,omitempty"` + Arches []string `json:"arches,omitempty"` + + // MinKernel describes the minimum kernel version the rule must be applied + // on, in the format "." (e.g. "3.12"). + // + // When matching the kernel version of the host, minor revisions, and distro- + // specific suffixes are ignored, which means that "3.12.25-gentoo", "3.12-1-amd64", + // "3.12", and "3.12-rc5" are considered equal (kernel 3, major revision 12). + MinKernel string `json:"minKernel,omitempty"` } // Syscall is used to match a group of syscalls in Seccomp diff --git a/profiles/seccomp/seccomp_linux.go b/profiles/seccomp/seccomp_linux.go index c2221115d1..cddf9c4214 100644 --- a/profiles/seccomp/seccomp_linux.go +++ b/profiles/seccomp/seccomp_linux.go @@ -8,7 +8,6 @@ import ( "fmt" "runtime" - "github.com/docker/docker/pkg/parsers/kernel" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -177,19 +176,3 @@ func createSpecsSyscall(names []string, action specs.LinuxSeccompAction, args [] } return newCall } - -var currentKernelVersion *kernel.VersionInfo - -func kernelGreaterEqualThan(v string) (bool, error) { - version, err := kernel.ParseRelease(v) - if err != nil { - return false, err - } - if currentKernelVersion == nil { - currentKernelVersion, err = kernel.GetKernelVersion() - if err != nil { - return false, err - } - } - return kernel.CompareKernelVersion(*version, *currentKernelVersion) <= 0, nil -} -- cgit v1.2.1