Skip to content

Commit 422c59f

Browse files
committed
internal: add kernelversion package from runc
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
1 parent 7618c01 commit 422c59f

File tree

4 files changed

+287
-1
lines changed

4 files changed

+287
-1
lines changed

internal/gocompat/gocompat_generics_go121.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package gocompat
1010

1111
import (
12+
"cmp"
1213
"slices"
1314
"sync"
1415
)
@@ -37,3 +38,16 @@ func SyncOnceValue[T any](f func() T) func() T {
3738
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
3839
return sync.OnceValues(f)
3940
}
41+
42+
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
43+
type CmpOrdered = cmp.Ordered
44+
45+
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
46+
func CmpCompare[T CmpOrdered](x, y T) int {
47+
return cmp.Compare(x, y)
48+
}
49+
50+
// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters).
51+
func Max2[T CmpOrdered](x, y T) T {
52+
return max(x, y)
53+
}

internal/gocompat/gocompat_generics_unsupported.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Copyright (C) 2021, 2022 The Go Authors. All rights reserved.
66
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
77
// Use of this source code is governed by a BSD-style
8-
// license that can be found in the LICENSE file.
8+
// license that can be found in the LICENSE.BSD file.
99

1010
package gocompat
1111

@@ -137,3 +137,51 @@ func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
137137
return d.r1, d.r2
138138
}
139139
}
140+
141+
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
142+
// Copied from the Go 1.25 stdlib implementation.
143+
type CmpOrdered interface {
144+
~int | ~int8 | ~int16 | ~int32 | ~int64 |
145+
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
146+
~float32 | ~float64 |
147+
~string
148+
}
149+
150+
// isNaN reports whether x is a NaN without requiring the math package.
151+
// This will always return false if T is not floating-point.
152+
// Copied from the Go 1.25 stdlib implementation.
153+
func isNaN[T CmpOrdered](x T) bool {
154+
return x != x
155+
}
156+
157+
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
158+
// Copied from the Go 1.25 stdlib implementation.
159+
func CmpCompare[T CmpOrdered](x, y T) int {
160+
xNaN := isNaN(x)
161+
yNaN := isNaN(y)
162+
if xNaN {
163+
if yNaN {
164+
return 0
165+
}
166+
return -1
167+
}
168+
if yNaN {
169+
return +1
170+
}
171+
if x < y {
172+
return -1
173+
}
174+
if x > y {
175+
return +1
176+
}
177+
return 0
178+
}
179+
180+
// Max2 is equivalent to Go 1.21's max builtin for two parameters.
181+
func Max2[T CmpOrdered](x, y T) T {
182+
m := x
183+
if y > m {
184+
m = y
185+
}
186+
return m
187+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
3+
// Copyright (C) 2022 The Go Authors. All rights reserved.
4+
// Copyright (C) 2025 SUSE LLC. All rights reserved.
5+
// Use of this source code is governed by a BSD-style
6+
// license that can be found in the LICENSE.BSD file.
7+
8+
// The parsing logic is very loosely based on the Go stdlib's
9+
// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks
10+
// a bit like runc's libcontainer/system/kernelversion.
11+
//
12+
// TODO(cyphar): This API has been copied around to a lot of different projects
13+
// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should
14+
// put it in a separate project?
15+
16+
// Package kernelversion provides a simple mechanism for checking whether the
17+
// running kernel is at least as new as some baseline kernel version. This is
18+
// often useful when checking for features that would be too complicated to
19+
// test support for (or in cases where we know that some kernel features in
20+
// backport-heavy kernels are broken and need to be avoided).
21+
package kernelversion
22+
23+
import (
24+
"bytes"
25+
"errors"
26+
"fmt"
27+
"strconv"
28+
"strings"
29+
30+
"golang.org/x/sys/unix"
31+
32+
"github.com/cyphar/filepath-securejoin/internal/gocompat"
33+
)
34+
35+
// KernelVersion is a numeric representation of the key numerical elements of a
36+
// kernel version (for instance, "4.1.2-default-1" would be represented as
37+
// KernelVersion{4, 1, 2}).
38+
type KernelVersion []uint64
39+
40+
func (kver KernelVersion) String() string {
41+
var str strings.Builder
42+
for idx, elem := range kver {
43+
if idx != 0 {
44+
_, _ = str.WriteRune('.')
45+
}
46+
_, _ = str.WriteString(strconv.FormatUint(elem, 10))
47+
}
48+
return str.String()
49+
}
50+
51+
var errInvalidKernelVersion = errors.New("invalid kernel version")
52+
53+
// parseKernelVersion parses a string and creates a KernelVersion based on it.
54+
func parseKernelVersion(kverStr string) (KernelVersion, error) {
55+
kver := make(KernelVersion, 1, 3)
56+
for idx, ch := range kverStr {
57+
if '0' <= ch && ch <= '9' {
58+
v := &kver[len(kver)-1]
59+
*v = (*v * 10) + uint64(ch-'0')
60+
} else {
61+
if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] {
62+
// "." must be preceded by a digit while in version section
63+
return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr)
64+
}
65+
if ch != '.' {
66+
break
67+
}
68+
kver = append(kver, 0)
69+
}
70+
}
71+
if len(kver) < 2 {
72+
return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr)
73+
}
74+
return kver, nil
75+
}
76+
77+
// getKernelVersion gets the current kernel version.
78+
var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) {
79+
var uts unix.Utsname
80+
if err := unix.Uname(&uts); err != nil {
81+
return nil, err
82+
}
83+
// Remove the \x00 from the release.
84+
release := uts.Release[:]
85+
return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)]))
86+
})
87+
88+
// GreaterEqualThan returns true if the the host kernel version is greater than
89+
// or equal to the provided [KernelVersion]. When doing this comparison, any
90+
// non-numerical suffixes of the host kernel version are ignored.
91+
//
92+
// If the number of components provided is not equal to the number of numerical
93+
// components of the host kernel version, any missing components are treated as
94+
// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the
95+
// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the
96+
// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will
97+
// return false (because the host version will be treated as "4.0").
98+
func GreaterEqualThan(wantKver KernelVersion) (bool, error) {
99+
hostKver, err := getKernelVersion()
100+
if err != nil {
101+
return false, err
102+
}
103+
104+
// Pad out the kernel version lengths to match one another.
105+
cmpLen := gocompat.Max2(len(hostKver), len(wantKver))
106+
hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...)
107+
wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...)
108+
109+
for i := 0; i < cmpLen; i++ {
110+
switch gocompat.CmpCompare(hostKver[i], wantKver[i]) {
111+
case -1:
112+
// host < want
113+
return false, nil
114+
case +1:
115+
// host > want
116+
return true, nil
117+
case 0:
118+
continue
119+
}
120+
}
121+
// equal version values
122+
return true, nil
123+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
3+
// Copyright (C) 2025 SUSE LLC. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE.BSD file.
6+
7+
package kernelversion
8+
9+
import (
10+
"fmt"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestGetKernelVersion(t *testing.T) {
18+
version, err := getKernelVersion()
19+
require.NoError(t, err)
20+
assert.GreaterOrEqualf(t, len(version), 2, "KernelVersion %#v must have at least 2 elements", version)
21+
}
22+
23+
func TestParseKernelVersion(t *testing.T) {
24+
for _, test := range []struct {
25+
kverStr string
26+
expected KernelVersion
27+
expectedErr error
28+
}{
29+
// <2 components
30+
{"", nil, errInvalidKernelVersion},
31+
{"dummy", nil, errInvalidKernelVersion},
32+
{"1", nil, errInvalidKernelVersion},
33+
{"420", nil, errInvalidKernelVersion},
34+
// >=2 components
35+
{"3.7", KernelVersion{3, 7}, nil},
36+
{"3.8", KernelVersion{3, 8}, nil},
37+
{"3.8.0", KernelVersion{3, 8, 0}, nil},
38+
{"3.8.12", KernelVersion{3, 8, 12}, nil},
39+
{"3.8.12.10.0.2", KernelVersion{3, 8, 12, 10, 0, 2}, nil},
40+
{"42.12.1000", KernelVersion{42, 12, 1000}, nil},
41+
// suffix
42+
{"2.6.16foobar", KernelVersion{2, 6, 16}, nil},
43+
{"2.6.16f00b4r", KernelVersion{2, 6, 16}, nil},
44+
{"3.8.16-generic", KernelVersion{3, 8, 16}, nil},
45+
{"6.12.0-1-default", KernelVersion{6, 12, 0}, nil},
46+
{"4.9.27-default-foo.12.23", KernelVersion{4, 9, 27}, nil},
47+
// invalid version section
48+
{"-1.2", nil, errInvalidKernelVersion},
49+
{"3a", nil, errInvalidKernelVersion},
50+
{"3.a", nil, errInvalidKernelVersion},
51+
{"3.4.a", nil, errInvalidKernelVersion},
52+
{"a", nil, errInvalidKernelVersion},
53+
{"aa", nil, errInvalidKernelVersion},
54+
{"a.a", nil, errInvalidKernelVersion},
55+
{"a.a.a", nil, errInvalidKernelVersion},
56+
{"-3.1", nil, errInvalidKernelVersion},
57+
{"-3.", nil, errInvalidKernelVersion},
58+
{"1.-foo", nil, errInvalidKernelVersion},
59+
{".1", nil, errInvalidKernelVersion},
60+
{".1.2", nil, errInvalidKernelVersion},
61+
} {
62+
test := test // copy iterator
63+
t.Run(test.kverStr, func(t *testing.T) {
64+
kver, err := parseKernelVersion(test.kverStr)
65+
if test.expectedErr != nil {
66+
require.Errorf(t, err, "parseKernelVersion(%q)", test.kverStr)
67+
require.ErrorIsf(t, err, test.expectedErr, "parseKernelVersion(%q)", test.kverStr)
68+
assert.Nilf(t, kver, "parseKernelVersion(%q) returned kver", test.kverStr)
69+
} else {
70+
require.NoErrorf(t, err, "parseKernelVersion(%q)", test.kverStr)
71+
assert.Equal(t, test.expected, kver, "parseKernelVersion(%q) return kver", test.kverStr)
72+
}
73+
})
74+
}
75+
}
76+
77+
func TestGreaterEqualThan(t *testing.T) {
78+
hostKver, err := getKernelVersion()
79+
require.NoError(t, err)
80+
81+
for _, test := range []struct {
82+
name string
83+
wantKver KernelVersion
84+
expected bool
85+
}{
86+
{"HostVersion", hostKver[:], true},
87+
{"OlderMajor", KernelVersion{hostKver[0] - 1, hostKver[1]}, true},
88+
{"OlderMinor", KernelVersion{hostKver[0], hostKver[1] - 1}, true},
89+
{"NewerMajor", KernelVersion{hostKver[0] + 1, hostKver[1]}, false},
90+
{"NewerMinor", KernelVersion{hostKver[0], hostKver[1] + 1}, false},
91+
{"ExtraDot", append(hostKver, 1), false},
92+
{"ExtraZeros", append(hostKver, make(KernelVersion, 10)...), true},
93+
} {
94+
test := test // copy iterator
95+
t.Run(fmt.Sprintf("%s:%s", test.name, test.wantKver), func(t *testing.T) {
96+
got, err := GreaterEqualThan(test.wantKver)
97+
require.NoErrorf(t, err, "GreaterEqualThan(%s)", test.wantKver)
98+
assert.Equalf(t, test.expected, got, "GreaterEqualThan(%s)", test.wantKver)
99+
})
100+
}
101+
}

0 commit comments

Comments
 (0)