diff --git a/gocompat_generics_go121.go b/gocompat_generics_go121.go deleted file mode 100644 index b0e5d36..0000000 --- a/gocompat_generics_go121.go +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -//go:build linux && go1.21 - -// Copyright (C) 2024 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 securejoin - -import ( - "slices" - "sync" -) - -func slices_DeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S { //nolint:revive // name is meant to mirror stdlib - return slices.DeleteFunc(slice, delFn) -} - -func slices_Contains[S ~[]E, E comparable](slice S, val E) bool { //nolint:revive // name is meant to mirror stdlib - return slices.Contains(slice, val) -} - -func slices_Clone[S ~[]E, E any](slice S) S { //nolint:revive // name is meant to mirror stdlib - return slices.Clone(slice) -} - -func sync_OnceValue[T any](f func() T) func() T { //nolint:revive // name is meant to mirror stdlib - return sync.OnceValue(f) -} diff --git a/gocompat_generics_unsupported.go b/gocompat_generics_unsupported.go deleted file mode 100644 index dd02cd1..0000000 --- a/gocompat_generics_unsupported.go +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -//go:build linux && !go1.21 - -// Copyright (C) 2021, 2022 The Go Authors. All rights reserved. -// Copyright (C) 2024 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 securejoin - -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. - -// clearSlice is equivalent to the builtin clear from Go 1.21. -// Copied from the Go 1.24 stdlib implementation. -func clearSlice[S ~[]E, E any](slice S) { - var zero E - for i := range slice { - slice[i] = zero - } -} - -// Copied from the Go 1.24 stdlib implementation. -func slices_IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { //nolint:revive // name is meant to mirror stdlib - for i := range s { - if f(s[i]) { - return i - } - } - return -1 -} - -// Copied from the Go 1.24 stdlib implementation. -func slices_DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { //nolint:revive // name is meant to mirror stdlib - i := slices_IndexFunc(s, del) - if i == -1 { - return s - } - // Don't start copying elements until we find one to delete. - for j := i + 1; j < len(s); j++ { - if v := s[j]; !del(v) { - s[i] = v - i++ - } - } - clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC - return s[:i] -} - -// Similar to the stdlib slices.Contains, except that we don't have -// slices.Index so we need to use slices.IndexFunc for this non-Func helper. -func slices_Contains[S ~[]E, E comparable](s S, v E) bool { //nolint:revive // name is meant to mirror stdlib - return slices_IndexFunc(s, func(e E) bool { return e == v }) >= 0 -} - -// Copied from the Go 1.24 stdlib implementation. -func slices_Clone[S ~[]E, E any](s S) S { //nolint:revive // name is meant to mirror stdlib - // Preserve nil in case it matters. - if s == nil { - return nil - } - return append(S([]E{}), s...) -} - -// Copied from the Go 1.24 stdlib implementation. -func sync_OnceValue[T any](f func() T) func() T { //nolint:revive // name is meant to mirror stdlib - var ( - once sync.Once - valid bool - p any - result T - ) - g := func() { - defer func() { - p = recover() - if !valid { - panic(p) - } - }() - result = f() - f = nil - valid = true - } - return func() T { - once.Do(g) - if !valid { - panic(p) - } - return result - } -} diff --git a/internal/gocompat/README.md b/internal/gocompat/README.md new file mode 100644 index 0000000..5dcb6ae --- /dev/null +++ b/internal/gocompat/README.md @@ -0,0 +1,10 @@ +## gocompat ## + +This directory contains backports of stdlib functions from later Go versions so +the filepath-securejoin can continue to be used by projects that are stuck with +Go 1.18 support. Note that 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. + +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/internal/gocompat/doc.go b/internal/gocompat/doc.go new file mode 100644 index 0000000..4b1803f --- /dev/null +++ b/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/gocompat_errors_go120.go b/internal/gocompat/gocompat_errors_go120.go similarity index 76% rename from gocompat_errors_go120.go rename to internal/gocompat/gocompat_errors_go120.go index c6b1829..4a114bd 100644 --- a/gocompat_errors_go120.go +++ b/internal/gocompat/gocompat_errors_go120.go @@ -5,15 +5,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package securejoin +package gocompat import ( "fmt" ) -// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except +// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except // that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap) // is only guaranteed to give you baseErr. -func wrapBaseError(baseErr, extraErr error) error { +func WrapBaseError(baseErr, extraErr error) error { return fmt.Errorf("%w: %w", extraErr, baseErr) } diff --git a/gocompat_errors_test.go b/internal/gocompat/gocompat_errors_test.go similarity index 92% rename from gocompat_errors_test.go rename to internal/gocompat/gocompat_errors_test.go index ee4114e..a5b1ec3 100644 --- a/gocompat_errors_test.go +++ b/internal/gocompat/gocompat_errors_test.go @@ -6,7 +6,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package securejoin +package gocompat import ( "errors" @@ -20,7 +20,7 @@ func TestGoCompatErrorWrap(t *testing.T) { baseErr := errors.New("base error") extraErr := errors.New("extra error") - err := wrapBaseError(baseErr, extraErr) + err := WrapBaseError(baseErr, extraErr) require.Error(t, err) assert.ErrorIs(t, err, baseErr, "wrapped error should contain base error") //nolint:testifylint // we are testing error behaviour directly diff --git a/gocompat_errors_unsupported.go b/internal/gocompat/gocompat_errors_unsupported.go similarity index 84% rename from gocompat_errors_unsupported.go rename to internal/gocompat/gocompat_errors_unsupported.go index c4c8eb3..3061016 100644 --- a/gocompat_errors_unsupported.go +++ b/internal/gocompat/gocompat_errors_unsupported.go @@ -6,7 +6,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package securejoin +package gocompat import ( "fmt" @@ -29,10 +29,10 @@ func (err wrappedError) Error() string { return fmt.Sprintf("%v: %v", err.isError, err.inner) } -// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except +// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except // that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap) // is only guaranteed to give you baseErr. -func wrapBaseError(baseErr, extraErr error) error { +func WrapBaseError(baseErr, extraErr error) error { return wrappedError{ inner: baseErr, isError: extraErr, diff --git a/internal/gocompat/gocompat_generics_go121.go b/internal/gocompat/gocompat_generics_go121.go new file mode 100644 index 0000000..d4a9381 --- /dev/null +++ b/internal/gocompat/gocompat_generics_go121.go @@ -0,0 +1,53 @@ +// 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" + "slices" + "sync" +) + +// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc. +func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S { + return slices.DeleteFunc(slice, delFn) +} + +// SlicesContains is equivalent to Go 1.21's slices.Contains. +func SlicesContains[S ~[]E, E comparable](slice S, val E) bool { + return slices.Contains(slice, val) +} + +// SlicesClone is equivalent to Go 1.21's slices.Clone. +func SlicesClone[S ~[]E, E any](slice S) S { + return slices.Clone(slice) +} + +// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue. +func SyncOnceValue[T any](f func() T) func() T { + return sync.OnceValue(f) +} + +// 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/internal/gocompat/gocompat_generics_unsupported.go b/internal/gocompat/gocompat_generics_unsupported.go new file mode 100644 index 0000000..0ea6218 --- /dev/null +++ b/internal/gocompat/gocompat_generics_unsupported.go @@ -0,0 +1,187 @@ +// 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. + +// clearSlice is equivalent to Go 1.21's builtin clear. +// Copied from the Go 1.24 stdlib implementation. +func clearSlice[S ~[]E, E any](slice S) { + var zero E + for i := range slice { + slice[i] = zero + } +} + +// slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc. +// Copied from the Go 1.24 stdlib implementation. +func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc. +// Copied from the Go 1.24 stdlib implementation. +func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { + i := slicesIndexFunc(s, del) + if i == -1 { + return s + } + // Don't start copying elements until we find one to delete. + for j := i + 1; j < len(s); j++ { + if v := s[j]; !del(v) { + s[i] = v + i++ + } + } + clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC + return s[:i] +} + +// SlicesContains is equivalent to Go 1.21's slices.Contains. +// Similar to the stdlib slices.Contains, except that we don't have +// slices.Index so we need to use slices.IndexFunc for this non-Func helper. +func SlicesContains[S ~[]E, E comparable](s S, v E) bool { + return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0 +} + +// SlicesClone is equivalent to Go 1.21's slices.Clone. +// Copied from the Go 1.24 stdlib implementation. +func SlicesClone[S ~[]E, E any](s S) S { + // Preserve nil in case it matters. + if s == nil { + return nil + } + return append(S([]E{}), s...) +} + +// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue. +// Copied from the Go 1.25 stdlib implementation. +func SyncOnceValue[T any](f func() T) func() T { + // Use a struct so that there's a single heap allocation. + d := struct { + f func() T + once sync.Once + valid bool + p any + result T + }{ + f: f, + } + return func() T { + d.once.Do(func() { + defer func() { + d.f = nil + d.p = recover() + if !d.valid { + panic(d.p) + } + }() + d.result = d.f() + d.valid = true + }) + if !d.valid { + panic(d.p) + } + return d.result + } +} + +// 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/internal/kernelversion/kernel_linux.go b/internal/kernelversion/kernel_linux.go new file mode 100644 index 0000000..6756bf1 --- /dev/null +++ b/internal/kernelversion/kernel_linux.go @@ -0,0 +1,123 @@ +// 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. +// +// TODO(cyphar): This API has been copied around to a lot of different projects +// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should +// put it in a separate project? + +// 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/cyphar/filepath-securejoin/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/internal/kernelversion/kernel_linux_test.go b/internal/kernelversion/kernel_linux_test.go new file mode 100644 index 0000000..694e2e7 --- /dev/null +++ b/internal/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) + }) + } +} diff --git a/lookup_linux.go b/lookup_linux.go index 31419b4..c53b0e2 100644 --- a/lookup_linux.go +++ b/lookup_linux.go @@ -20,6 +20,8 @@ import ( "strings" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" ) type symlinkStackEntry struct { @@ -117,7 +119,7 @@ func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) erro return nil } // Split the link target and clean up any "" parts. - linkTargetParts := slices_DeleteFunc( + linkTargetParts := gocompat.SlicesDeleteFunc( strings.Split(linkTarget, "/"), func(part string) bool { return part == "" || part == "." }) diff --git a/lookup_linux_test.go b/lookup_linux_test.go index 0afc342..ff7b609 100644 --- a/lookup_linux_test.go +++ b/lookup_linux_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" ) type partialLookupFunc func(root *os.File, unsafePath string) (*os.File, string, error) @@ -475,36 +477,36 @@ func TestPartialLookup_RacingRename(t *testing.T) { allowedResults []lookupResult }{ // Swap a symlink in and out. - "swap-dir-link1-basic": {"a/b", "b-link", "a/b/c/d/e", nil, slices_Clone(defaultExpected)}, - "swap-dir-link2-basic": {"a/b/c", "c-link", "a/b/c/d/e", nil, slices_Clone(defaultExpected)}, - "swap-dir-link1-dotdot1": {"a/b", "b-link", "a/b/../b/../b/../b/../b/../b/../b/c/d/../d/../d/../d/../d/../d/e", nil, slices_Clone(defaultExpected)}, - "swap-dir-link1-dotdot2": {"a/b", "b-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", nil, slices_Clone(defaultExpected)}, - "swap-dir-link2-dotdot": {"a/b/c", "c-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", nil, slices_Clone(defaultExpected)}, + "swap-dir-link1-basic": {"a/b", "b-link", "a/b/c/d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-dir-link2-basic": {"a/b/c", "c-link", "a/b/c/d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-dir-link1-dotdot1": {"a/b", "b-link", "a/b/../b/../b/../b/../b/../b/../b/c/d/../d/../d/../d/../d/../d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-dir-link1-dotdot2": {"a/b", "b-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-dir-link2-dotdot": {"a/b/c", "c-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", nil, gocompat.SlicesClone(defaultExpected)}, // TODO: Swap a directory. // Swap a non-directory. "swap-dir-file-basic": {"a/b", "file", "a/b/c/d/e", []error{unix.ENOTDIR, unix.ENOENT}, append( // We could hit one of the final paths. - slices_Clone(defaultExpected), + gocompat.SlicesClone(defaultExpected), // We could hit the file and stop resolving. lookupResult{handlePath: "/file", remainingPath: "c/d/e", fileType: unix.S_IFREG}, )}, "swap-dir-file-dotdot": {"a/b", "file", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", []error{unix.ENOTDIR, unix.ENOENT}, append( // We could hit one of the final paths. - slices_Clone(defaultExpected), + gocompat.SlicesClone(defaultExpected), // We could hit the file and stop resolving. lookupResult{handlePath: "/file", remainingPath: "c/d/e", fileType: unix.S_IFREG}, )}, // Swap a dangling symlink. - "swap-dir-danglinglink-basic": {"a/b", "bad-link", "a/b/c/d/e", []error{unix.ENOENT}, slices_Clone(defaultExpected)}, - "swap-dir-danglinglink-dotdot": {"a/b", "bad-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", []error{unix.ENOENT}, slices_Clone(defaultExpected)}, + "swap-dir-danglinglink-basic": {"a/b", "bad-link", "a/b/c/d/e", []error{unix.ENOENT}, gocompat.SlicesClone(defaultExpected)}, + "swap-dir-danglinglink-dotdot": {"a/b", "bad-link", "a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e", []error{unix.ENOENT}, gocompat.SlicesClone(defaultExpected)}, // Swap the root. - "swap-root-basic": {".", "../outsideroot", "a/b/c/d/e", nil, slices_Clone(defaultExpected)}, - "swap-root-dotdot": {".", "../outsideroot", "a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e", nil, slices_Clone(defaultExpected)}, - "swap-root-dotdot-extra": {".", "../outsideroot", "a/" + strings.Repeat("b/c/d/../../../", 10) + "b/c/d/e", nil, slices_Clone(defaultExpected)}, + "swap-root-basic": {".", "../outsideroot", "a/b/c/d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-root-dotdot": {".", "../outsideroot", "a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e", nil, gocompat.SlicesClone(defaultExpected)}, + "swap-root-dotdot-extra": {".", "../outsideroot", "a/" + strings.Repeat("b/c/d/../../../", 10) + "b/c/d/e", nil, gocompat.SlicesClone(defaultExpected)}, // Swap one of our walking paths outside the root. "swap-dir-outsideroot-basic": {"a/b", "../outsideroot", "a/b/c/d/e", nil, append( // We could hit the expected path. - slices_Clone(defaultExpected), + gocompat.SlicesClone(defaultExpected), // We could also land in the "outsideroot" path. This is okay // because there was a moment when this directory was inside // the root, and the attacker moved it outside the root. If we @@ -516,7 +518,7 @@ func TestPartialLookup_RacingRename(t *testing.T) { )}, "swap-dir-outsideroot-dotdot": {"a/b", "../outsideroot", "a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e", nil, append( // We could hit the expected path. - slices_Clone(defaultExpected), + gocompat.SlicesClone(defaultExpected), // We could also land in the "outsideroot" path. This is okay // because there was a moment when this directory was inside // the root, and the attacker moved it outside the root. diff --git a/mkdir_linux.go b/mkdir_linux.go index 0ca1311..5b9b1c6 100644 --- a/mkdir_linux.go +++ b/mkdir_linux.go @@ -19,6 +19,8 @@ import ( "strings" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" ) var ( @@ -124,7 +126,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F } remainingParts := strings.Split(remainingPath, string(filepath.Separator)) - if slices_Contains(remainingParts, "..") { + if gocompat.SlicesContains(remainingParts, "..") { // The path contained ".." components after the end of the "real" // components. We could try to safely resolve ".." here but that would // add a bunch of extra logic for something that it's not clear even @@ -160,7 +162,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w (%w)", err, deadErr) - err = wrapBaseError(err, deadErr) + err = gocompat.WrapBaseError(err, deadErr) } return nil, err } diff --git a/openat2_linux.go b/openat2_linux.go index 4b668a1..998d02e 100644 --- a/openat2_linux.go +++ b/openat2_linux.go @@ -20,9 +20,11 @@ import ( "strings" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" ) -var hasOpenat2 = sync_OnceValue(func() bool { +var hasOpenat2 = gocompat.SyncOnceValue(func() bool { fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{ Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT, diff --git a/procfs_linux.go b/procfs_linux.go index 45e65e1..326f640 100644 --- a/procfs_linux.go +++ b/procfs_linux.go @@ -19,6 +19,9 @@ import ( "strconv" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" + "github.com/cyphar/filepath-securejoin/internal/kernelversion" ) func fstat(f *os.File) (unix.Stat_t, error) { @@ -71,9 +74,9 @@ func verifyProcRoot(procRoot *os.File) error { return nil } -var hasNewMountAPI = sync_OnceValue(func() bool { +var hasNewMountAPI = gocompat.SyncOnceValue(func() bool { // All of the pieces of the new mount API we use (fsopen, fsconfig, - // fsmount, open_tree) were added together in Linux 5.1[1,2], so we can + // fsmount, open_tree) were added together in Linux 5.2[1,2], so we can // just check for one of the syscalls and the others should also be // available. // @@ -89,7 +92,12 @@ var hasNewMountAPI = sync_OnceValue(func() bool { return false } _ = unix.Close(fd) - return true + + // RHEL 8 has a backport of fsopen(2) that appears to have some very + // difficult to debug performance pathology. As such, it seems prudent to + // simply reject pre-5.2 kernels. + isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{5, 2}) + return isNotBackport }) func fsopen(fsName string, flags int) (*os.File, error) { @@ -127,7 +135,7 @@ type procfsFeatures struct { hasSubsetPid bool } -var getProcfsFeatures = sync_OnceValue(func() procfsFeatures { +var getProcfsFeatures = gocompat.SyncOnceValue(func() procfsFeatures { if !hasNewMountAPI() { return procfsFeatures{} } @@ -242,7 +250,7 @@ func getProcRoot(subset bool) (*os.File, error) { return procRoot, err } -var hasProcThreadSelf = sync_OnceValue(func() bool { +var hasProcThreadSelf = gocompat.SyncOnceValue(func() bool { return unix.Access("/proc/thread-self/", unix.F_OK) == nil }) @@ -269,7 +277,7 @@ func procOpen(procRoot *os.File, subpath string) (*os.File, error) { // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w: %w", errUnsafeProcfs, err) - return nil, wrapBaseError(err, errUnsafeProcfs) + return nil, gocompat.WrapBaseError(err, errUnsafeProcfs) } return handle, nil } @@ -396,7 +404,7 @@ const ( wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID ) -var hasStatxMountID = sync_OnceValue(func() bool { +var hasStatxMountID = gocompat.SyncOnceValue(func() bool { var stx unix.Statx_t err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx) return err == nil && stx.Mask&wantStatxMntMask != 0 @@ -420,7 +428,7 @@ func getMountID(dir *os.File, path string) (uint64, error) { // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w: could not get mount id: %w", errUnsafeProcfs, err) - err = wrapBaseError(fmt.Errorf("could not get mount id: %w", err), errUnsafeProcfs) + err = gocompat.WrapBaseError(fmt.Errorf("could not get mount id: %w", err), errUnsafeProcfs) } if err != nil { return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err} diff --git a/procfs_lookup_linux.go b/procfs_lookup_linux.go index 4011171..a26b9aa 100644 --- a/procfs_lookup_linux.go +++ b/procfs_lookup_linux.go @@ -23,6 +23,8 @@ import ( "strings" "golang.org/x/sys/unix" + + "github.com/cyphar/filepath-securejoin/internal/gocompat" ) // procfsLookupInRoot is a stripped down version of completeLookupInRoot, @@ -88,7 +90,7 @@ func procfsLookupInRoot(procRoot *os.File, unsafePath string) (Handle *os.File, // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w: %w", errUnsafeProcfs, err) - return nil, wrapBaseError(err, errUnsafeProcfs) + return nil, gocompat.WrapBaseError(err, errUnsafeProcfs) } return handle, nil }