diff --git a/kernelversion/LICENSE b/kernelversion/LICENSE new file mode 100644 index 0000000..f60dfc5 --- /dev/null +++ b/kernelversion/LICENSE @@ -0,0 +1,29 @@ +Copyright (C) 2022 The Go Authors. All rights reserved. +Copyright (C) 2024-2025 SUSE LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/kernelversion/go.mod b/kernelversion/go.mod new file mode 100644 index 0000000..a7a0c3c --- /dev/null +++ b/kernelversion/go.mod @@ -0,0 +1,14 @@ +module github.com/moby/sys/kernelversion + +go 1.18 + +require ( + github.com/stretchr/testify v1.7.1 + golang.org/x/sys v0.18.0 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/kernelversion/go.sum b/kernelversion/go.sum new file mode 100644 index 0000000..940f907 --- /dev/null +++ b/kernelversion/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kernelversion/internal/gocompat/README.md b/kernelversion/internal/gocompat/README.md new file mode 100644 index 0000000..c3d1865 --- /dev/null +++ b/kernelversion/internal/gocompat/README.md @@ -0,0 +1,8 @@ +## gocompat ## + +This directory contains backports of stdlib functions from later Go versions so +that `github.com/moby/sys/kernelversion` can continue to be used by projects +that are stuck with Go 1.18 support. + +The source code is licensed under the same license as the Go stdlib. See the +source files for the precise license information. diff --git a/kernelversion/internal/gocompat/doc.go b/kernelversion/internal/gocompat/doc.go new file mode 100644 index 0000000..4b1803f --- /dev/null +++ b/kernelversion/internal/gocompat/doc.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build linux && go1.20 + +// Copyright (C) 2025 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gocompat includes compatibility shims (backported from future Go +// stdlib versions) to permit filepath-securejoin to be used with older Go +// versions (often filepath-securejoin is added in security patches for old +// releases, so avoiding the need to bump Go compiler requirements is a huge +// plus to downstreams). +package gocompat diff --git a/kernelversion/internal/gocompat/gocompat_generics_go121.go b/kernelversion/internal/gocompat/gocompat_generics_go121.go new file mode 100644 index 0000000..f6bb5eb --- /dev/null +++ b/kernelversion/internal/gocompat/gocompat_generics_go121.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && go1.21 + +// Copyright (C) 2024-2025 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocompat + +import ( + "cmp" + "sync" +) + +// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues. +func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { + return sync.OnceValues(f) +} + +// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition. +type CmpOrdered = cmp.Ordered + +// CmpCompare is equivalent to Go 1.21's cmp.Compare. +func CmpCompare[T CmpOrdered](x, y T) int { + return cmp.Compare(x, y) +} + +// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters). +func Max2[T CmpOrdered](x, y T) T { + return max(x, y) +} diff --git a/kernelversion/internal/gocompat/gocompat_generics_unsupported.go b/kernelversion/internal/gocompat/gocompat_generics_unsupported.go new file mode 100644 index 0000000..1dfabf3 --- /dev/null +++ b/kernelversion/internal/gocompat/gocompat_generics_unsupported.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && !go1.21 + +// Copyright (C) 2021, 2022 The Go Authors. All rights reserved. +// Copyright (C) 2024-2025 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +package gocompat + +import ( + "sync" +) + +// These are very minimal implementations of functions that appear in Go 1.21's +// stdlib, included so that we can build on older Go versions. Most are +// borrowed directly from the stdlib, and a few are modified to be "obviously +// correct" without needing to copy too many other helpers. + +// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues. +// Copied from the Go 1.25 stdlib implementation. +func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { + // Use a struct so that there's a single heap allocation. + d := struct { + f func() (T1, T2) + once sync.Once + valid bool + p any + r1 T1 + r2 T2 + }{ + f: f, + } + return func() (T1, T2) { + d.once.Do(func() { + defer func() { + d.f = nil + d.p = recover() + if !d.valid { + panic(d.p) + } + }() + d.r1, d.r2 = d.f() + d.valid = true + }) + if !d.valid { + panic(d.p) + } + return d.r1, d.r2 + } +} + +// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition. +// Copied from the Go 1.25 stdlib implementation. +type CmpOrdered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +// Copied from the Go 1.25 stdlib implementation. +func isNaN[T CmpOrdered](x T) bool { + return x != x +} + +// CmpCompare is equivalent to Go 1.21's cmp.Compare. +// Copied from the Go 1.25 stdlib implementation. +func CmpCompare[T CmpOrdered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN { + if yNaN { + return 0 + } + return -1 + } + if yNaN { + return +1 + } + if x < y { + return -1 + } + if x > y { + return +1 + } + return 0 +} + +// Max2 is equivalent to Go 1.21's max builtin for two parameters. +func Max2[T CmpOrdered](x, y T) T { + m := x + if y > m { + m = y + } + return m +} diff --git a/kernelversion/kernel_linux.go b/kernelversion/kernel_linux.go new file mode 100644 index 0000000..3a1f8ed --- /dev/null +++ b/kernelversion/kernel_linux.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: BSD-3-Clause + +// Copyright (C) 2022 The Go Authors. All rights reserved. +// Copyright (C) 2025 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +// The parsing logic is very loosely based on the Go stdlib's +// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks +// a bit like runc's libcontainer/system/kernelversion. + +// Package kernelversion provides a simple mechanism for checking whether the +// running kernel is at least as new as some baseline kernel version. This is +// often useful when checking for features that would be too complicated to +// test support for (or in cases where we know that some kernel features in +// backport-heavy kernels are broken and need to be avoided). +package kernelversion + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/sys/unix" + + "github.com/moby/sys/kernelversion/internal/gocompat" +) + +// KernelVersion is a numeric representation of the key numerical elements of a +// kernel version (for instance, "4.1.2-default-1" would be represented as +// KernelVersion{4, 1, 2}). +type KernelVersion []uint64 + +func (kver KernelVersion) String() string { + var str strings.Builder + for idx, elem := range kver { + if idx != 0 { + _, _ = str.WriteRune('.') + } + _, _ = str.WriteString(strconv.FormatUint(elem, 10)) + } + return str.String() +} + +var errInvalidKernelVersion = errors.New("invalid kernel version") + +// parseKernelVersion parses a string and creates a KernelVersion based on it. +func parseKernelVersion(kverStr string) (KernelVersion, error) { + kver := make(KernelVersion, 1, 3) + for idx, ch := range kverStr { + if '0' <= ch && ch <= '9' { + v := &kver[len(kver)-1] + *v = (*v * 10) + uint64(ch-'0') + } else { + if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] { + // "." must be preceded by a digit while in version section + return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr) + } + if ch != '.' { + break + } + kver = append(kver, 0) + } + } + if len(kver) < 2 { + return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr) + } + return kver, nil +} + +// getKernelVersion gets the current kernel version. +var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) { + var uts unix.Utsname + if err := unix.Uname(&uts); err != nil { + return nil, err + } + // Remove the \x00 from the release. + release := uts.Release[:] + return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)])) +}) + +// GreaterEqualThan returns true if the the host kernel version is greater than +// or equal to the provided [KernelVersion]. When doing this comparison, any +// non-numerical suffixes of the host kernel version are ignored. +// +// If the number of components provided is not equal to the number of numerical +// components of the host kernel version, any missing components are treated as +// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the +// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the +// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will +// return false (because the host version will be treated as "4.0"). +func GreaterEqualThan(wantKver KernelVersion) (bool, error) { + hostKver, err := getKernelVersion() + if err != nil { + return false, err + } + + // Pad out the kernel version lengths to match one another. + cmpLen := gocompat.Max2(len(hostKver), len(wantKver)) + hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...) + wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...) + + for i := 0; i < cmpLen; i++ { + switch gocompat.CmpCompare(hostKver[i], wantKver[i]) { + case -1: + // host < want + return false, nil + case +1: + // host > want + return true, nil + case 0: + continue + } + } + // equal version values + return true, nil +} diff --git a/kernelversion/kernel_linux_test.go b/kernelversion/kernel_linux_test.go new file mode 100644 index 0000000..694e2e7 --- /dev/null +++ b/kernelversion/kernel_linux_test.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSD-3-Clause + +// Copyright (C) 2025 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +package kernelversion + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetKernelVersion(t *testing.T) { + version, err := getKernelVersion() + require.NoError(t, err) + assert.GreaterOrEqualf(t, len(version), 2, "KernelVersion %#v must have at least 2 elements", version) +} + +func TestParseKernelVersion(t *testing.T) { + for _, test := range []struct { + kverStr string + expected KernelVersion + expectedErr error + }{ + // <2 components + {"", nil, errInvalidKernelVersion}, + {"dummy", nil, errInvalidKernelVersion}, + {"1", nil, errInvalidKernelVersion}, + {"420", nil, errInvalidKernelVersion}, + // >=2 components + {"3.7", KernelVersion{3, 7}, nil}, + {"3.8", KernelVersion{3, 8}, nil}, + {"3.8.0", KernelVersion{3, 8, 0}, nil}, + {"3.8.12", KernelVersion{3, 8, 12}, nil}, + {"3.8.12.10.0.2", KernelVersion{3, 8, 12, 10, 0, 2}, nil}, + {"42.12.1000", KernelVersion{42, 12, 1000}, nil}, + // suffix + {"2.6.16foobar", KernelVersion{2, 6, 16}, nil}, + {"2.6.16f00b4r", KernelVersion{2, 6, 16}, nil}, + {"3.8.16-generic", KernelVersion{3, 8, 16}, nil}, + {"6.12.0-1-default", KernelVersion{6, 12, 0}, nil}, + {"4.9.27-default-foo.12.23", KernelVersion{4, 9, 27}, nil}, + // invalid version section + {"-1.2", nil, errInvalidKernelVersion}, + {"3a", nil, errInvalidKernelVersion}, + {"3.a", nil, errInvalidKernelVersion}, + {"3.4.a", nil, errInvalidKernelVersion}, + {"a", nil, errInvalidKernelVersion}, + {"aa", nil, errInvalidKernelVersion}, + {"a.a", nil, errInvalidKernelVersion}, + {"a.a.a", nil, errInvalidKernelVersion}, + {"-3.1", nil, errInvalidKernelVersion}, + {"-3.", nil, errInvalidKernelVersion}, + {"1.-foo", nil, errInvalidKernelVersion}, + {".1", nil, errInvalidKernelVersion}, + {".1.2", nil, errInvalidKernelVersion}, + } { + test := test // copy iterator + t.Run(test.kverStr, func(t *testing.T) { + kver, err := parseKernelVersion(test.kverStr) + if test.expectedErr != nil { + require.Errorf(t, err, "parseKernelVersion(%q)", test.kverStr) + require.ErrorIsf(t, err, test.expectedErr, "parseKernelVersion(%q)", test.kverStr) + assert.Nilf(t, kver, "parseKernelVersion(%q) returned kver", test.kverStr) + } else { + require.NoErrorf(t, err, "parseKernelVersion(%q)", test.kverStr) + assert.Equal(t, test.expected, kver, "parseKernelVersion(%q) return kver", test.kverStr) + } + }) + } +} + +func TestGreaterEqualThan(t *testing.T) { + hostKver, err := getKernelVersion() + require.NoError(t, err) + + for _, test := range []struct { + name string + wantKver KernelVersion + expected bool + }{ + {"HostVersion", hostKver[:], true}, + {"OlderMajor", KernelVersion{hostKver[0] - 1, hostKver[1]}, true}, + {"OlderMinor", KernelVersion{hostKver[0], hostKver[1] - 1}, true}, + {"NewerMajor", KernelVersion{hostKver[0] + 1, hostKver[1]}, false}, + {"NewerMinor", KernelVersion{hostKver[0], hostKver[1] + 1}, false}, + {"ExtraDot", append(hostKver, 1), false}, + {"ExtraZeros", append(hostKver, make(KernelVersion, 10)...), true}, + } { + test := test // copy iterator + t.Run(fmt.Sprintf("%s:%s", test.name, test.wantKver), func(t *testing.T) { + got, err := GreaterEqualThan(test.wantKver) + require.NoErrorf(t, err, "GreaterEqualThan(%s)", test.wantKver) + assert.Equalf(t, test.expected, got, "GreaterEqualThan(%s)", test.wantKver) + }) + } +}