diff --git a/.gitignore b/.gitignore index 81ab3db..eb84e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/v1.3.0.tar.gz +SOURCES/v1.4.0.tar.gz diff --git a/.runc.metadata b/.runc.metadata index 6ee8356..078b9c0 100644 --- a/.runc.metadata +++ b/.runc.metadata @@ -1 +1 @@ -0ea2488912e9ae562782f5980971f7fb0d73df38 SOURCES/v1.3.0.tar.gz +05593fc5246d37362b59d4711dd006f31f0b67ae SOURCES/v1.4.0.tar.gz diff --git a/SOURCES/0001-1.3-openat2-improve-resilience-on-busy-systems.patch b/SOURCES/0001-1.3-openat2-improve-resilience-on-busy-systems.patch deleted file mode 100644 index cea6d79..0000000 --- a/SOURCES/0001-1.3-openat2-improve-resilience-on-busy-systems.patch +++ /dev/null @@ -1,416 +0,0 @@ -From 2df42d4db6bc57ee914fa9cc4455ad3b8daff1d9 Mon Sep 17 00:00:00 2001 -From: Aleksa Sarai -Date: Sat, 1 Nov 2025 17:21:36 +1100 -Subject: [PATCH 1/2] [1.3] openat2: improve resilience on busy systems - -Previously, we would see a ~3% failure rate when starting containers -with mounts that contain ".." (which can trigger -EAGAIN). To counteract -this, filepath-securejoin v0.5.1 includes a bump of the internal retry -limit from 32 to 128, which lowers the failure rate to 0.12%. - -However, there is still a risk of spurious failure on regular systems. -In order to try to provide more resilience (while avoiding DoS attacks), -this patch also includes an additional retry loop that terminates based -on a deadline rather than retry count. The deadline is 2ms, as my -testing found that ~800us for a single pathrs operation was the longest -latency due to -EAGAIN retries, and that was an outlier compared to the -more common ~400us latencies -- so 2ms should be more than enough for -any real system. - -The failure rates above were based on more 50k runs of runc with an -attack script (from libpathrs) running a rename attack on all cores of a -16-core system, which is arguably a worst-case but heavily utilised -servers could likely approach similar results. - -Signed-off-by: Aleksa Sarai -Signed-off-by: Kir Kolyshkin ---- - go.mod | 2 +- - go.sum | 4 +- - internal/pathrs/mkdirall_pathrslite.go | 4 +- - internal/pathrs/procfs_pathrslite.go | 22 ++++--- - internal/pathrs/retry.go | 66 +++++++++++++++++++ - internal/pathrs/root_pathrslite.go | 7 +- - .../cyphar/filepath-securejoin/CHANGELOG.md | 34 +++++++++- - .../cyphar/filepath-securejoin/VERSION | 2 +- - .../internal/{errors.go => errors_linux.go} | 15 ++++- - .../pathrs-lite/internal/fd/openat2_linux.go | 12 ++-- - vendor/modules.txt | 2 +- - 11 files changed, 144 insertions(+), 26 deletions(-) - create mode 100644 internal/pathrs/retry.go - rename vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/{errors.go => errors_linux.go} (70%) - -diff --git a/go.mod b/go.mod -index f2deafc3..a551a4ec 100644 ---- a/go.mod -+++ b/go.mod -@@ -6,7 +6,7 @@ require ( - github.com/checkpoint-restore/go-criu/v6 v6.3.0 - github.com/containerd/console v1.0.5 - github.com/coreos/go-systemd/v22 v22.5.0 -- github.com/cyphar/filepath-securejoin v0.5.0 -+ github.com/cyphar/filepath-securejoin v0.5.1 - github.com/docker/go-units v0.5.0 - github.com/godbus/dbus/v5 v5.1.0 - github.com/moby/sys/capability v0.4.0 -diff --git a/go.sum b/go.sum -index ba395bf0..fb357b43 100644 ---- a/go.sum -+++ b/go.sum -@@ -10,8 +10,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV - github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= - github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= - github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= --github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw= --github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= -+github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48= -+github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= - github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= - github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go -index fb4f7842..a9a0157c 100644 ---- a/internal/pathrs/mkdirall_pathrslite.go -+++ b/internal/pathrs/mkdirall_pathrslite.go -@@ -83,7 +83,9 @@ func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, er - } - defer rootDir.Close() - -- return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) -+ return retryEAGAIN(func() (*os.File, error) { -+ return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) -+ }) - } - - // MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the -diff --git a/internal/pathrs/procfs_pathrslite.go b/internal/pathrs/procfs_pathrslite.go -index a02b0d39..37450a0e 100644 ---- a/internal/pathrs/procfs_pathrslite.go -+++ b/internal/pathrs/procfs_pathrslite.go -@@ -27,13 +27,15 @@ import ( - ) - - func procOpenReopen(openFn func(subpath string) (*os.File, error), subpath string, flags int) (*os.File, error) { -- handle, err := openFn(subpath) -+ handle, err := retryEAGAIN(func() (*os.File, error) { -+ return openFn(subpath) -+ }) - if err != nil { - return nil, err - } - defer handle.Close() - -- f, err := pathrs.Reopen(handle, flags) -+ f, err := Reopen(handle, flags) - if err != nil { - return nil, fmt.Errorf("reopen %s: %w", handle.Name(), err) - } -@@ -44,7 +46,7 @@ func procOpenReopen(openFn func(subpath string) (*os.File, error), subpath strin - // [pathrs.Reopen], to let you one-shot open a procfs file with the given - // flags. - func ProcSelfOpen(subpath string, flags int) (*os.File, error) { -- proc, err := procfs.OpenProcRoot() -+ proc, err := retryEAGAIN(procfs.OpenProcRoot) - if err != nil { - return nil, err - } -@@ -55,7 +57,7 @@ func ProcSelfOpen(subpath string, flags int) (*os.File, error) { - // ProcPidOpen is a wrapper around [procfs.Handle.OpenPid] and [pathrs.Reopen], - // to let you one-shot open a procfs file with the given flags. - func ProcPidOpen(pid int, subpath string, flags int) (*os.File, error) { -- proc, err := procfs.OpenProcRoot() -+ proc, err := retryEAGAIN(procfs.OpenProcRoot) - if err != nil { - return nil, err - } -@@ -70,13 +72,15 @@ func ProcPidOpen(pid int, subpath string, flags int) (*os.File, error) { - // flags. The returned [procfs.ProcThreadSelfCloser] needs the same handling as - // when using pathrs-lite. - func ProcThreadSelfOpen(subpath string, flags int) (_ *os.File, _ procfs.ProcThreadSelfCloser, Err error) { -- proc, err := procfs.OpenProcRoot() -+ proc, err := retryEAGAIN(procfs.OpenProcRoot) - if err != nil { - return nil, nil, err - } - defer proc.Close() - -- handle, closer, err := proc.OpenThreadSelf(subpath) -+ handle, closer, err := retryEAGAIN2(func() (*os.File, procfs.ProcThreadSelfCloser, error) { -+ return proc.OpenThreadSelf(subpath) -+ }) - if err != nil { - return nil, nil, err - } -@@ -89,7 +93,7 @@ func ProcThreadSelfOpen(subpath string, flags int) (_ *os.File, _ procfs.ProcThr - } - defer handle.Close() - -- f, err := pathrs.Reopen(handle, flags) -+ f, err := Reopen(handle, flags) - if err != nil { - return nil, nil, fmt.Errorf("reopen %s: %w", handle.Name(), err) - } -@@ -98,5 +102,7 @@ func ProcThreadSelfOpen(subpath string, flags int) (_ *os.File, _ procfs.ProcThr - - // Reopen is a wrapper around pathrs.Reopen. - func Reopen(file *os.File, flags int) (*os.File, error) { -- return pathrs.Reopen(file, flags) -+ return retryEAGAIN(func() (*os.File, error) { -+ return pathrs.Reopen(file, flags) -+ }) - } -diff --git a/internal/pathrs/retry.go b/internal/pathrs/retry.go -new file mode 100644 -index 00000000..a51d335c ---- /dev/null -+++ b/internal/pathrs/retry.go -@@ -0,0 +1,66 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import ( -+ "errors" -+ "fmt" -+ "time" -+ -+ "golang.org/x/sys/unix" -+) -+ -+// Based on >50k tests running "runc run" on a 16-core system with very heavy -+// rename(2) load, the single longest latency caused by -EAGAIN retries was -+// ~800us (with the vast majority being closer to 400us). So, a 2ms limit -+// should give more than enough headroom for any real system in practice. -+const retryDeadline = 2 * time.Millisecond -+ -+// retryEAGAIN is a top-level retry loop for pathrs to try to returning -+// spurious errors in most normal user cases when using openat2 (libpathrs -+// itself does up to 128 retries already, but this method takes a -+// wallclock-deadline approach to simply retry until a timer elapses). -+func retryEAGAIN[T any](fn func() (T, error)) (T, error) { -+ deadline := time.After(retryDeadline) -+ for { -+ v, err := fn() -+ if !errors.Is(err, unix.EAGAIN) { -+ return v, err -+ } -+ select { -+ case <-deadline: -+ return *new(T), fmt.Errorf("%v retry deadline exceeded: %w", retryDeadline, err) -+ default: -+ // retry -+ } -+ } -+} -+ -+// retryEAGAIN2 is like retryEAGAIN except it returns two values. -+func retryEAGAIN2[T1, T2 any](fn func() (T1, T2, error)) (T1, T2, error) { -+ type ret struct { -+ v1 T1 -+ v2 T2 -+ } -+ v, err := retryEAGAIN(func() (ret, error) { -+ v1, v2, err := fn() -+ return ret{v1: v1, v2: v2}, err -+ }) -+ return v.v1, v.v2, err -+} -diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go -index 0ef81fae..899af270 100644 ---- a/internal/pathrs/root_pathrslite.go -+++ b/internal/pathrs/root_pathrslite.go -@@ -31,12 +31,15 @@ import ( - // is effectively shorthand for [securejoin.OpenInRoot] followed by - // [securejoin.Reopen]. - func OpenInRoot(root, subpath string, flags int) (*os.File, error) { -- handle, err := pathrs.OpenInRoot(root, subpath) -+ handle, err := retryEAGAIN(func() (*os.File, error) { -+ return pathrs.OpenInRoot(root, subpath) -+ }) - if err != nil { - return nil, err - } - defer handle.Close() -- return pathrs.Reopen(handle, flags) -+ -+ return Reopen(handle, flags) - } - - // CreateInRoot creates a new file inside a root (as well as any missing parent -diff --git a/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md b/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -index 6862467c..3faee0bc 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -+++ b/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -@@ -4,7 +4,36 @@ All notable changes to this project will be documented in this file. - The format is based on [Keep a Changelog](http://keepachangelog.com/) - and this project adheres to [Semantic Versioning](http://semver.org/). - --## [Unreleased] ## -+## [Unreleased 0.5.z] ## -+ -+## [0.5.1] - 2025-10-31 ## -+ -+> Spooky scary skeletons send shivers down your spine! -+ -+### Changed ### -+- `openat2` can return `-EAGAIN` if it detects a possible attack in certain -+ scenarios (namely if there was a rename or mount while walking a path with a -+ `..` component). While this is necessary to avoid a denial-of-service in the -+ kernel, it does require retry loops in userspace. -+ -+ In previous versions, `pathrs-lite` would retry `openat2` 32 times before -+ returning an error, but we've received user reports that this limit can be -+ hit on systems with very heavy load. In some synthetic benchmarks (testing -+ the worst-case of an attacker doing renames in a tight loop on every core of -+ a 16-core machine) we managed to get a ~3% failure rate in runc. We have -+ improved this situation in two ways: -+ -+ * We have now increased this limit to 128, which should be good enough for -+ most use-cases without becoming a denial-of-service vector (the number of -+ syscalls called by the `O_PATH` resolver in a typical case is within the -+ same ballpark). The same benchmarks show a failure rate of ~0.12% which -+ (while not zero) is probably sufficient for most users. -+ -+ * In addition, we now return a `unix.EAGAIN` error that is bubbled up and can -+ be detected by callers. This means that callers with stricter requirements -+ to avoid spurious errors can choose to do their own infinite `EAGAIN` retry -+ loop (though we would strongly recommend users use time-based deadlines in -+ such retry loops to avoid potentially unbounded denials-of-service). - - ## [0.5.0] - 2025-09-26 ## - -@@ -354,7 +383,8 @@ This is our first release of `github.com/cyphar/filepath-securejoin`, - containing a full implementation with a coverage of 93.5% (the only missing - cases are the error cases, which are hard to mocktest at the moment). - --[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...HEAD -+[Unreleased 0.5.z]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...release-0.5 -+[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1 - [0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0 - [0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1 - [0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/VERSION b/vendor/github.com/cyphar/filepath-securejoin/VERSION -index 8f0916f7..4b9fcbec 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/VERSION -+++ b/vendor/github.com/cyphar/filepath-securejoin/VERSION -@@ -1 +1 @@ --0.5.0 -+0.5.1 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors_linux.go -similarity index 70% -rename from vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors_linux.go -index c26e440e..d0b200f4 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors_linux.go -@@ -1,5 +1,7 @@ - // SPDX-License-Identifier: MPL-2.0 - -+//go:build linux -+ - // Copyright (C) 2024-2025 Aleksa Sarai - // Copyright (C) 2024-2025 SUSE LLC - // -@@ -12,15 +14,24 @@ package internal - - import ( - "errors" -+ -+ "golang.org/x/sys/unix" - ) - -+type xdevErrorish struct { -+ description string -+} -+ -+func (err xdevErrorish) Error() string { return err.description } -+func (err xdevErrorish) Is(target error) bool { return target == unix.EXDEV } -+ - var ( - // ErrPossibleAttack indicates that some attack was detected. -- ErrPossibleAttack = errors.New("possible attack detected") -+ ErrPossibleAttack error = xdevErrorish{"possible attack detected"} - - // ErrPossibleBreakout indicates that during an operation we ended up in a - // state that could be a breakout but we detected it. -- ErrPossibleBreakout = errors.New("possible breakout detected") -+ ErrPossibleBreakout error = xdevErrorish{"possible breakout detected"} - - // ErrInvalidDirectory indicates an unlinked directory. - ErrInvalidDirectory = errors.New("wandered into deleted directory") -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go -index 23053083..3e937fe3 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go -@@ -17,8 +17,6 @@ import ( - "runtime" - - "golang.org/x/sys/unix" -- -- "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" - ) - - func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { -@@ -34,7 +32,10 @@ func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { - (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV)) - } - --const scopedLookupMaxRetries = 32 -+// This is a fairly arbitrary limit we have just to avoid an attacker being -+// able to make us spin in an infinite retry loop -- callers can choose to -+// retry on EAGAIN if they prefer. -+const scopedLookupMaxRetries = 128 - - // Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry - // logic in case of EAGAIN errors. -@@ -43,10 +44,10 @@ func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) { - // Make sure we always set O_CLOEXEC. - how.Flags |= unix.O_CLOEXEC - var tries int -- for tries < scopedLookupMaxRetries { -+ for { - fd, err := unix.Openat2(dirFd, path, how) - if err != nil { -- if scopedLookupShouldRetry(how, err) { -+ if scopedLookupShouldRetry(how, err) && tries < scopedLookupMaxRetries { - // We retry a couple of times to avoid the spurious errors, and - // if we are being attacked then returning -EAGAIN is the best - // we can do. -@@ -58,5 +59,4 @@ func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) { - runtime.KeepAlive(dir) - return os.NewFile(uintptr(fd), fullPath), nil - } -- return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: internal.ErrPossibleAttack} - } -diff --git a/vendor/modules.txt b/vendor/modules.txt -index f22001c8..18276b61 100644 ---- a/vendor/modules.txt -+++ b/vendor/modules.txt -@@ -27,7 +27,7 @@ github.com/coreos/go-systemd/v22/dbus - # github.com/cpuguy83/go-md2man/v2 v2.0.5 - ## explicit; go 1.11 - github.com/cpuguy83/go-md2man/v2/md2man --# github.com/cyphar/filepath-securejoin v0.5.0 -+# github.com/cyphar/filepath-securejoin v0.5.1 - ## explicit; go 1.18 - github.com/cyphar/filepath-securejoin - github.com/cyphar/filepath-securejoin/internal/consts --- -2.51.1 - diff --git a/SOURCES/0001-1.3.0-CVEs-mega-patch.patch b/SOURCES/0001-1.3.0-CVEs-mega-patch.patch deleted file mode 100644 index f11b462..0000000 --- a/SOURCES/0001-1.3.0-CVEs-mega-patch.patch +++ /dev/null @@ -1,13709 +0,0 @@ -From 7a76bd855ca135d36ab8b4e41e101deae5bee8a1 Mon Sep 17 00:00:00 2001 -From: Aleksa Sarai -Date: Tue, 30 Sep 2025 23:04:02 +1000 -Subject: [PATCH] 1.3.0 CVEs mega patch - -> This is a combination of 28 commits. -> This is the 1st commit message: - -internal: linux: add package doc-comment - -This is necessary for the pre-1.4 backports because internal/linux was -not present and the linters get angry when a new package without a doc -comment gets added. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #2: - -internal/sys: add VerifyInode helper - -This will be used for a few security patches in later patches in this -patchset. The need to verify what kind of inode we are operating on in a -race-free way turns out to be quite a common pattern... - -Signed-off-by: Aleksa Sarai - -> This is the commit message #3: - -internal: move utils.MkdirAllInRoot to internal/pathrs - -We will have more wrappers around filepath-securejoin, and so move them -to their own specific package so that we can eventually use libpathrs -fairly cleanly (by swapping out the implementation). - -Signed-off-by: Aleksa Sarai - -> This is the commit message #4: - -*: switch to safer securejoin.Reopen - -filepath-securejoin v0.3 gave us a much safer re-open primitive, we -should use it to avoid any theoretical attacks. Rather than using it -direcly, add a small pathrs wrapper to make libpathrs migrations in the -future easier... - -Signed-off-by: Aleksa Sarai - -> This is the commit message #5: - -libct: add/use isDevNull, verifyDevNull - -The /dev/null in a container should not be trusted, because when /dev -is a bind mount, /dev/null is not created by runc itself. - -1. Add isDevNull which checks the fd minor/major and device type, - and verifyDevNull which does the stat and the check. - -2. Rewrite maskPath to open and check /dev/null, and use its fd to - perform mounts. Move the loop over the MaskPaths into the function, - and rename it to maskPaths. - -3. reOpenDevNull: use verifyDevNull and isDevNull. - -4. fixStdioPermissions: use isDevNull instead of stat. - -Fixes: GHSA-9493-h29p-rfm2 CVE-2025-31133 -Co-authored-by: Rodrigo Campos -Signed-off-by: Kir Kolyshkin -Signed-off-by: Aleksa Sarai - -> This is the commit message #6: - -libct: maskPaths: only ignore ENOENT on mount dest - -When mounting a path being masked, the /dev/null might disappear from -under us, and mount (even on an opened /dev/null file descriptor) will -return ENOENT, which we deliberately ignore, as there's no need to mask -non-existent paths. - -Let's open the destination path and ignore ENOENT during open, then -mount via the destination file descriptor, not ignoring ENOENT. - -Reported-by: lifubang -Signed-off-by: Kir Kolyshkin - -> This is the commit message #7: - -libct: maskPaths: don't rely on ENOTDIR for mount - -Currently, we rely on mount returning ENOTDIR when the destination is a -directory (and so mount tells us that the source is not), and fall back -to read-only tmpfs bind mount for such cases. - -Theoretically, ENOTDIR can also be returned in some other cases, -resulting in the wrong type of mount being used. - -Let's be more straightforward here -- call fstat on destination file -descriptor, and use the proper mount depending on whether it is a -directory. - -Reported-by: Rodrigo Campos -Signed-off-by: Kir Kolyshkin - -> This is the commit message #8: - -console: use TIOCGPTPEER when allocating peer PTY - -When opening the peer end of a pty, the old kernel API required us to -open /dev/pts/$num inside the container (at least since we fixed console -handling many years ago in commit 244c9fc426ae ("*: console rewrite")). - -The problem is that in a hostile container it is possible for -/dev/pts/$num to be an attacker-controlled symlink that runc can be -tricked into resolving when doing bind-mounts. This allows the attacker -to (among other things) persist /proc/... entries that are later masked -by runc, allowing an attacker to escape through the kernel.core_pattern -sysctl (/proc/sys/kernel/core_pattern). This is the original issue -reported by Lei Wang and Li Fu Bang in CVE-2025-52565. - -However, it should be noted that this is not entirely a newly-discovered -problem. Way back in Linux 4.13 (2017), I added the TIOCGPTPEER ioctl, -which allows us to get a pty peer without touching the /dev/pts inside -the container. The original threat model was around an attacker -replacing /dev/pts/$n or /dev/pts/ptmx with some malicious inode (a DoS -inode, or possibly a PTY they wanted a confused deputy to operate on). -Unfortunately, there was no practical way for runc to cache a safe -O_PATH handle to /dev/pts/ptmx (unlike other runtimes like LXC, which -switched to TIOCGPTPEER way back in 2017). Since it wasn't clear how we -could protect against the main attack TIOCGPTPEER was meant to protect -against, we never switched to it (even though I implemented it -specifically to harden container runtimes). - -Unfortunately, It turns out that mount *sources* are a threat we didn't -fully consider. Since TIOCGPTPEER already solves this problem entirely -for us in a race free way, we should just use that. In a later patch, we -will add some hardening for /dev/pts/$num opening to maintain support -for very old kernels (Linux 4.13 is very old at this point, but RHEL 7 -is still kicking and is stuck on Linux 3.10). - -Fixes: GHSA-qw9x-cqr3-wc7r CVE-2025-52565 -Reported-by: Lei Wang (CVE-2025-52565) -Reported-by: lfbzhm (CVE-2025-52565) -Reported-by: Aleksa Sarai (TIOCGPTPEER) -Signed-off-by: Aleksa Sarai - -> This is the commit message #9: - -console: add fallback for pre-TIOCGPTPEER kernels - -The pty driver has very consistent allocation rules for the major:minor -numbers of /dev/pts/$n inodes, so it is possible to somewhat safely open -/dev/pts/* paths if we validate that the inode is the one we expect. - -It is possible for an attacker to have over-mounted a pts peer from a -different devpts instance, but to fix this would require more tracking -of devpts instances than runc currently can do. - -This means runc should continue to work on very old kernels. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #10: - -console: avoid trivial symlink attacks for /dev/console - -An attacker could make /dev/console a symlink. This presents two -possible issues: - - 1. os.Create will happily truncate targets, which could have resulted - in a worse version of CVE-2024-4531. Luckily, this all happens after - pivot_root(2) so the scope of that particular attack is fairly - limited (you are unlikely to be able to easily access host rootfs - files -- though it might be possible to take advantage of leaks such - as in CVE-2024-21626). However, O_CREAT|O_NOFOLLOW is what we should - be doing for all file creations. - - 2. Because we passed /dev/console as the only mount path (as opposed to - using a /proc/self/fd/$n path), an attacker could swap the symlink - to point to any other path and thus cause us to mount over some - other path. This is not as big of a problem because all the mounts - are in the container namespace after pivot_root(2), and users - usually can create arbitrary mount targets inside the container. - -These issues don't seem particularly exploitable, but they deserve to be -hardened regardless. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #11: - -console: verify /dev/pts/ptmx before use - -This is primarily done out of an abudance of caution against runc exec -being attacked by a container where /dev/pts/ptmx has been replaced with -some other bad inode (a disconnected NFS handle, a symlink that goes -through a leaked runc file descriptor to reference a host ptmx, etc). - -Unfortunately, we cannot trivially verify that /dev/pts/ptmx is actually -the /dev/pts from the container without storing stuff like the fsid in -the runc state.json, which is probably not worth the extra effort. This -should at least avoid the most concerning cases. - -Reported-by: Aleksa Sarai -Signed-off-by: Aleksa Sarai - -> This is the commit message #12: - -go.mod: update to github.com/cyphar/filepath-securejoin@v0.5.0 - -In order to avoid lint errors due to the deprecation of the top-level -securejoin methods ported from libpathrs, we need to adjust -internal/pathrs to use the new pathrs-lite subpackage instead. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #13: - -internal: add wrappers for securejoin.Proc* - -Signed-off-by: Aleksa Sarai - -> This is the commit message #14: - -rootfs: avoid using os.Create for new device inodes - -If an attacker were to make the target of a device inode creation be a -symlink to some host path, os.Create would happily truncate the target -which could lead to all sorts of issues. This exploit is probably not as -exploitable because device inodes are usually only bind-mounted for -rootless containers, which cannot overwrite important host files (though -user files would still be up for grabs). - -The regular inode creation logic could also theoretically be tricked -into changing the access mode and ownership of host files if the -newly-created device inode was swapped with a symlink to a host path. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #15: - -ci: add lint to forbid the usage of os.Create - -os.Create is shorthand for open(O_CREAT|O_TRUNC) *without* O_EXCL, which -is incredibly unsafe for us to do when interacting with a container -rootfs (especially before pivot_root) as an attacker could swap the -target path with a symlink that points to the host filesystem, causing -us to delete the contents of or create host files. - -We did have a similar bug in CVE-2024-45310, but in that case we -(luckily) didn't have O_TRUNC set which avoided the worst possible case. -However, os.Create does set O_TRUNC and we were using it in scenarios -that may have been exploitable. - -Because of how easy it us for us to accidentally introduce this kind of -bug, we should simply not allow the usage of os.Create in our entire -codebase. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #16: - -apparmor: use safe procfs API for labels - -EnsureProcHandle only protects us against a tmpfs mount, but the risk of -a procfs path being used (such as /proc/self/sched) has been known for a -while. Now that filepath-securejoin has a reasonably safe procfs API, -switch to it. - -Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881 -Signed-off-by: Aleksa Sarai - -> This is the commit message #17: - -utils: use safe procfs for /proc/self/fd loop code - -From a safety perspective this might not be strictly required, but it -paves the way for us to remove utils.ProcThreadSelf. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #18: - -utils: remove unneeded EnsureProcHandle - -All of the callers of EnsureProcHandle now use filepath-securejoin's -ProcThreadSelf to get a file handle, which has much stricter -verification to avoid procfs attacks than EnsureProcHandle's very -simplistic filesystem type check. - -Signed-off-by: Aleksa Sarai - -> This is the commit message #19: - -init: write sysctls using safe procfs API - -sysctls could in principle also be used as a write gadget for arbitrary -procfs files. As this requires getting a non-subset=pid /proc handle we -amortise this by only allocating a single procfs handle for all sysctl -writes. - -Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881 -Signed-off-by: Aleksa Sarai - -> This is the commit message #20: - -init: use securejoin for /proc/self/setgroups - -Signed-off-by: Aleksa Sarai - -> This is the commit message #21: - -libct/system: use securejoin for /proc/$pid/stat - -Signed-off-by: Aleksa Sarai - -> This is the commit message #22: - -libct: align param type for mountCgroupV1/V2 functions - -Signed-off-by: lifubang - -> This is the commit message #23: - -criu: improve prepareCriuRestoreMounts - -1. Replace the big "if !" block with the if block and continue, - simplifying the code flow. - -2. Move comments closer to the code, improving readability. - -This commit is best reviewed with --ignore-all-space or similar. - -Signed-off-by: Kir Kolyshkin -(cherry picked from commit 0c93d41c65b6a1055e945d1d3e56943b07b8405b) -Signed-off-by: Kir Kolyshkin -(cherry picked from commit 017d6b693f9a8bfc64f9ba2afa9526b47e03c871) -Signed-off-by: Kir Kolyshkin - -> This is the commit message #24: - -criu: ignore cgroup early in prepareCriuRestoreMounts - -It makes sense to ignore cgroup mounts much early in the code, -saving some time on unnecessary operations. - -Signed-off-by: Kir Kolyshkin -(cherry picked from commit b8aa5481db42b5222b1725e5af939bec829937c5) -Signed-off-by: Kir Kolyshkin -(cherry picked from commit a97c49f96ed7d18ae721da86661d92fc30d522ee) -Signed-off-by: Kir Kolyshkin - -> This is the commit message #25: - -criu: inline makeCriuRestoreMountpoints - -Since its code is now trivial, and it is only called from a single -place, it does not make sense to have it as a separate function. - -Signed-off-by: Kir Kolyshkin -(cherry picked from commit f91fbd34d9e819a833c7da00c6c88f5371a82ac5) -Signed-off-by: Kir Kolyshkin -(cherry picked from commit 69a3439c31aabeb4e86c6c584736132863707b40) -Signed-off-by: Kir Kolyshkin - -> This is the commit message #26: - -criu: simplify isOnTmpfs check in prepareCriuRestoreMounts - -Instead of generating a list of tmpfs mount and have a special function -to check whether the path is in the list, let's go over the list of -mounts directly. This simplifies the code and improves readability. - -Signed-off-by: Kir Kolyshkin -(cherry picked from commit ce3cd4234c9cd90f8109a33ab86f3456c2edf947) -Signed-off-by: Kir Kolyshkin -(cherry picked from commit 02c412828817665cf008a40c5382486d8f0b7ce5) -Signed-off-by: Kir Kolyshkin - -> This is the commit message #27: - -rootfs: switch to fd-based handling of mountpoint targets - -An attacker could race with us during mount configuration in order to -trick us into mounting over an unexpected path. This would bypass -checkProcMount() and would allow for security profiles to be left -unapplied by mounting over /proc/self/attr/... (or even more serious -outcomes such as killing the entire system by tricking runc into writing -strings to /proc/sysrq-trigger). - -This is a larger issue with our current mount infrastructure, and the -ideal solution would be to rewrite it all to be fd-based (which would -also allow us to support the "new" mount API, which also avoids a bunch -of other issues with mount(8)). However, such a rewrite is not really -workable as a security fix, so this patch is a bit of a compromise -approach to fix the issue while also moving us a bit towards that -eventual end-goal. - -The core issue in CVE-2025-52881 is that we currently use the (insecure) -SecureJoin to re-resolve mountpoint target paths multiple times during -mounting. Rather than generating a string from createMountpoint(), we -instead open an *os.File handle to the target mountpoint directly and -then operate on that handle. This will make it easier to remove -utils.WithProcfd() and rework mountViaFds() in the future. - -The only real issue we need to work around is that we need to re-open -the mount target after doing the mount in order to get a handle to the -mountpoint -- pathrs.Reopen() doesn't work in this case (it just -re-opens the inode under the mountpoint) so we need to do a naive -re-open using the full path. Note that if we used move_mount(2) this -wouldn't be a problem because we would have a handle to the mountpoint -itself. - -Note that this is still somewhat of a temporary solution -- ideally -mountViaFds would use *os.File directly to let us avoid some other -issues with using bare /proc/... paths, as well as also letting us more -easily use the new mount API on modern kernels. - -Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881 -Co-developed-by: lifubang -Signed-off-by: Aleksa Sarai - -> This is the commit message #28: - -selinux: use safe procfs API for labels - -Due to the sensitive nature of these fixes, it was not possible to -submit these upstream and vendor the upstream library. Instead, this -patch uses a fork of github.com/opencontainers/selinux, branched at -commit opencontainers/selinux@879a755db558501df06f4ea59461ebc2d0c4a991. - -In order to permit downstreams to build with this patched version, a -snapshot of the forked version has been included in -internal/third_party/selinux. Note that since we use "go mod vendor", -the patched code is usable even without being "go get"-able. Once the -embargo for this issue is lifted we can submit the patches upstream and -switch back to a proper upstream go.mod entry. - -Also, this requires us to temporarily disable the CI job we have that -disallows "replace" directives. - -Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881 -Signed-off-by: Aleksa Sarai ---- - .golangci.yml | 15 + - go.mod | 11 +- - go.sum | 10 +- - internal/linux/doc.go | 3 + - internal/linux/linux.go | 44 + - internal/pathrs/doc.go | 23 + - internal/pathrs/mkdirall_pathrslite.go | 97 ++ - internal/pathrs/path.go | 34 + - internal/pathrs/path_test.go | 53 + - internal/pathrs/procfs_pathrslite.go | 102 ++ - internal/pathrs/root_pathrslite.go | 69 + - internal/sys/doc.go | 5 + - internal/sys/opath_linux.go | 53 + - internal/sys/sysctl_linux.go | 54 + - internal/sys/verify_inode_unix.go | 30 + - internal/third_party/selinux/.codespellrc | 2 + - .../selinux/.github/dependabot.yml | 10 + - .../selinux/.github/workflows/validate.yml | 163 ++ - internal/third_party/selinux/.gitignore | 1 + - internal/third_party/selinux/.golangci.yml | 44 + - internal/third_party/selinux/CODEOWNERS | 1 + - internal/third_party/selinux/CONTRIBUTING.md | 119 ++ - internal/third_party/selinux/LICENSE | 201 +++ - internal/third_party/selinux/MAINTAINERS | 5 + - internal/third_party/selinux/Makefile | 37 + - internal/third_party/selinux/README.md | 23 + - .../third_party/selinux/go-selinux/doc.go | 13 + - .../selinux/go-selinux/label/label.go | 48 + - .../selinux/go-selinux/label/label_linux.go | 136 ++ - .../go-selinux/label/label_linux_test.go | 130 ++ - .../selinux/go-selinux/label/label_stub.go | 44 + - .../go-selinux/label/label_stub_test.go | 76 + - .../selinux/go-selinux/label/label_test.go | 35 + - .../third_party/selinux/go-selinux/selinux.go | 322 ++++ - .../selinux/go-selinux/selinux_linux.go | 1405 +++++++++++++++++ - .../selinux/go-selinux/selinux_linux_test.go | 711 +++++++++ - .../selinux/go-selinux/selinux_stub.go | 159 ++ - .../selinux/go-selinux/selinux_stub_test.go | 127 ++ - .../selinux/go-selinux/xattrs_linux.go | 71 + - internal/third_party/selinux/go.mod | 8 + - internal/third_party/selinux/go.sum | 8 + - .../third_party/selinux/pkg/pwalk/README.md | 52 + - .../third_party/selinux/pkg/pwalk/pwalk.go | 131 ++ - .../selinux/pkg/pwalk/pwalk_test.go | 236 +++ - .../selinux/pkg/pwalkdir/README.md | 56 + - .../selinux/pkg/pwalkdir/pwalkdir.go | 123 ++ - .../selinux/pkg/pwalkdir/pwalkdir_test.go | 239 +++ - libcontainer/apparmor/apparmor_linux.go | 13 +- - libcontainer/console_linux.go | 163 +- - libcontainer/criu_linux.go | 105 +- - libcontainer/exeseal/cloned_binary_linux.go | 3 +- - libcontainer/init_linux.go | 27 +- - libcontainer/integration/exec_test.go | 15 +- - libcontainer/rootfs_linux.go | 419 +++-- - libcontainer/standard_init_linux.go | 28 +- - libcontainer/system/linux.go | 20 + - libcontainer/system/proc.go | 16 +- - libcontainer/utils/utils.go | 4 +- - libcontainer/utils/utils_test.go | 4 +- - libcontainer/utils/utils_unix.go | 127 +- - utils_linux.go | 9 +- - .../containerd/console/console_other.go | 4 +- - .../containerd/console/console_unix.go | 9 + - .../containerd/console/tc_darwin.go | 5 +- - .../containerd/console/tc_freebsd_cgo.go | 5 +- - .../containerd/console/tc_freebsd_nocgo.go | 5 +- - .../github.com/containerd/console/tc_linux.go | 5 +- - .../containerd/console/tc_netbsd.go | 5 +- - .../containerd/console/tc_openbsd_cgo.go | 6 +- - .../containerd/console/tc_openbsd_nocgo.go | 6 +- - .../github.com/containerd/console/tc_zos.go | 5 +- - .../cyphar/filepath-securejoin/.golangci.yml | 56 + - .../cyphar/filepath-securejoin/CHANGELOG.md | 121 +- - .../cyphar/filepath-securejoin/COPYING.md | 447 ++++++ - .../{LICENSE => LICENSE.BSD} | 0 - .../filepath-securejoin/LICENSE.MPL-2.0 | 373 +++++ - .../cyphar/filepath-securejoin/README.md | 21 +- - .../cyphar/filepath-securejoin/VERSION | 2 +- - .../cyphar/filepath-securejoin/codecov.yml | 29 + - .../filepath-securejoin/deprecated_linux.go | 48 + - .../cyphar/filepath-securejoin/doc.go | 34 +- - .../gocompat_generics_go121.go | 32 - - .../gocompat_generics_unsupported.go | 124 -- - .../internal/consts/consts.go | 15 + - .../cyphar/filepath-securejoin/join.go | 23 +- - .../filepath-securejoin/openat2_linux.go | 127 -- - .../filepath-securejoin/openat_linux.go | 59 - - .../filepath-securejoin/pathrs-lite/README.md | 33 + - .../filepath-securejoin/pathrs-lite/doc.go | 14 + - .../pathrs-lite/internal/assert/assert.go | 30 + - .../pathrs-lite/internal/errors.go | 30 + - .../pathrs-lite/internal/fd/at_linux.go | 148 ++ - .../pathrs-lite/internal/fd/fd.go | 55 + - .../pathrs-lite/internal/fd/fd_linux.go | 78 + - .../pathrs-lite/internal/fd/mount_linux.go | 54 + - .../pathrs-lite/internal/fd/openat2_linux.go | 62 + - .../pathrs-lite/internal/gocompat/README.md | 10 + - .../pathrs-lite/internal/gocompat/doc.go | 13 + - .../gocompat}/gocompat_errors_go120.go | 7 +- - .../gocompat}/gocompat_errors_unsupported.go | 8 +- - .../gocompat/gocompat_generics_go121.go | 53 + - .../gocompat/gocompat_generics_unsupported.go | 187 +++ - .../internal/kernelversion/kernel_linux.go | 123 ++ - .../pathrs-lite/internal/linux/doc.go | 12 + - .../pathrs-lite/internal/linux/mount_linux.go | 47 + - .../internal/linux/openat2_linux.go | 31 + - .../internal/procfs/procfs_linux.go | 544 +++++++ - .../internal/procfs/procfs_lookup_linux.go | 222 +++ - .../{ => pathrs-lite}/lookup_linux.go | 61 +- - .../{ => pathrs-lite}/mkdir_linux.go | 46 +- - .../{ => pathrs-lite}/open_linux.go | 59 +- - .../pathrs-lite/openat2_linux.go | 101 ++ - .../pathrs-lite/procfs/procfs_linux.go | 157 ++ - .../filepath-securejoin/procfs_linux.go | 452 ------ - .../cyphar/filepath-securejoin/vfs.go | 2 + - .../selinux/go-selinux/label/label.go | 67 - - .../selinux/go-selinux/label/label_linux.go | 17 +- - .../selinux/go-selinux/label/label_stub.go | 6 - - .../selinux/go-selinux/selinux.go | 26 +- - .../selinux/go-selinux/selinux_linux.go | 311 ++-- - .../selinux/go-selinux/selinux_stub.go | 8 +- - vendor/modules.txt | 17 +- - 122 files changed, 9419 insertions(+), 1530 deletions(-) - create mode 100644 internal/linux/doc.go - create mode 100644 internal/linux/linux.go - create mode 100644 internal/pathrs/doc.go - create mode 100644 internal/pathrs/mkdirall_pathrslite.go - create mode 100644 internal/pathrs/path.go - create mode 100644 internal/pathrs/path_test.go - create mode 100644 internal/pathrs/procfs_pathrslite.go - create mode 100644 internal/pathrs/root_pathrslite.go - create mode 100644 internal/sys/doc.go - create mode 100644 internal/sys/opath_linux.go - create mode 100644 internal/sys/sysctl_linux.go - create mode 100644 internal/sys/verify_inode_unix.go - create mode 100644 internal/third_party/selinux/.codespellrc - create mode 100644 internal/third_party/selinux/.github/dependabot.yml - create mode 100644 internal/third_party/selinux/.github/workflows/validate.yml - create mode 100644 internal/third_party/selinux/.gitignore - create mode 100644 internal/third_party/selinux/.golangci.yml - create mode 100644 internal/third_party/selinux/CODEOWNERS - create mode 100644 internal/third_party/selinux/CONTRIBUTING.md - create mode 100644 internal/third_party/selinux/LICENSE - create mode 100644 internal/third_party/selinux/MAINTAINERS - create mode 100644 internal/third_party/selinux/Makefile - create mode 100644 internal/third_party/selinux/README.md - create mode 100644 internal/third_party/selinux/go-selinux/doc.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label_linux.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label_linux_test.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label_stub.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label_stub_test.go - create mode 100644 internal/third_party/selinux/go-selinux/label/label_test.go - create mode 100644 internal/third_party/selinux/go-selinux/selinux.go - create mode 100644 internal/third_party/selinux/go-selinux/selinux_linux.go - create mode 100644 internal/third_party/selinux/go-selinux/selinux_linux_test.go - create mode 100644 internal/third_party/selinux/go-selinux/selinux_stub.go - create mode 100644 internal/third_party/selinux/go-selinux/selinux_stub_test.go - create mode 100644 internal/third_party/selinux/go-selinux/xattrs_linux.go - create mode 100644 internal/third_party/selinux/go.mod - create mode 100644 internal/third_party/selinux/go.sum - create mode 100644 internal/third_party/selinux/pkg/pwalk/README.md - create mode 100644 internal/third_party/selinux/pkg/pwalk/pwalk.go - create mode 100644 internal/third_party/selinux/pkg/pwalk/pwalk_test.go - create mode 100644 internal/third_party/selinux/pkg/pwalkdir/README.md - create mode 100644 internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go - create mode 100644 internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/.golangci.yml - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/COPYING.md - rename vendor/github.com/cyphar/filepath-securejoin/{LICENSE => LICENSE.BSD} (100%) - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0 - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/codecov.yml - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/deprecated_linux.go - delete mode 100644 vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go - delete mode 100644 vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go - delete mode 100644 vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go - delete mode 100644 vendor/github.com/cyphar/filepath-securejoin/openat_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/doc.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go - rename vendor/github.com/cyphar/filepath-securejoin/{ => pathrs-lite/internal/gocompat}/gocompat_errors_go120.go (69%) - rename vendor/github.com/cyphar/filepath-securejoin/{ => pathrs-lite/internal/gocompat}/gocompat_errors_unsupported.go (80%) - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_go121.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_unsupported.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion/kernel_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_lookup_linux.go - rename vendor/github.com/cyphar/filepath-securejoin/{ => pathrs-lite}/lookup_linux.go (86%) - rename vendor/github.com/cyphar/filepath-securejoin/{ => pathrs-lite}/mkdir_linux.go (86%) - rename vendor/github.com/cyphar/filepath-securejoin/{ => pathrs-lite}/open_linux.go (56%) - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/openat2_linux.go - create mode 100644 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go - delete mode 100644 vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go - -diff --git a/.golangci.yml b/.golangci.yml -index 954a2bc0..1a802316 100644 ---- a/.golangci.yml -+++ b/.golangci.yml -@@ -11,6 +11,7 @@ formatters: - linters: - enable: - - errorlint -+ - forbidigo - - unconvert - - unparam - settings: -@@ -24,6 +25,20 @@ linters: - - -ST1003 # https://staticcheck.dev/docs/checks/#ST1003 Poorly chosen identifier. - - -ST1005 # https://staticcheck.dev/docs/checks/#ST1005 Incorrectly formatted error string. - - -QF1008 # https://staticcheck.dev/docs/checks/#QF1008 Omit embedded fields from selector expression. -+ forbidigo: -+ forbid: -+ # os.Create implies O_TRUNC without O_CREAT|O_EXCL, which can lead to -+ # an even more severe attacks than CVE-2024-45310, where host files -+ # could be wiped. Always use O_EXCL or otherwise ensure we are not -+ # going to be tricked into overwriting host files. -+ - pattern: ^os\.Create$ -+ pkg: ^os$ -+ analyze-types: true - exclusions: -+ rules: -+ # forbidigo lints are only relevant for main code. -+ - path: '(.+)_test\.go' -+ linters: -+ - forbidigo - presets: - - std-error-handling -diff --git a/go.mod b/go.mod -index c0b146b6..f2deafc3 100644 ---- a/go.mod -+++ b/go.mod -@@ -4,9 +4,9 @@ go 1.23.0 - - require ( - github.com/checkpoint-restore/go-criu/v6 v6.3.0 -- github.com/containerd/console v1.0.4 -+ github.com/containerd/console v1.0.5 - github.com/coreos/go-systemd/v22 v22.5.0 -- github.com/cyphar/filepath-securejoin v0.4.1 -+ github.com/cyphar/filepath-securejoin v0.5.0 - github.com/docker/go-units v0.5.0 - github.com/godbus/dbus/v5 v5.1.0 - github.com/moby/sys/capability v0.4.0 -@@ -16,7 +16,7 @@ require ( - github.com/mrunalp/fileutils v0.5.1 - github.com/opencontainers/cgroups v0.0.1 - github.com/opencontainers/runtime-spec v1.2.1 -- github.com/opencontainers/selinux v1.11.1 -+ github.com/opencontainers/selinux v1.12.0 - github.com/seccomp/libseccomp-golang v0.10.0 - github.com/sirupsen/logrus v1.9.3 - github.com/urfave/cli v1.22.16 -@@ -32,3 +32,8 @@ require ( - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/vishvananda/netns v0.0.4 // indirect - ) -+ -+// FIXME: This is only intended as a short-term solution to include a patch for -+// CVE-2025-52881 in go-selinux without pushing the patches upstream. This -+// should be removed as soon as possible after the embargo is lifted. -+replace github.com/opencontainers/selinux => ./internal/third_party/selinux -diff --git a/go.sum b/go.sum -index 1503380c..ba395bf0 100644 ---- a/go.sum -+++ b/go.sum -@@ -3,15 +3,15 @@ github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3 - github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= - github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= - github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= --github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= --github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -+github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= -+github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= - github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= - github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= - github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= - github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= - github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= --github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= --github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= -+github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw= -+github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= - github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= - github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -@@ -53,8 +53,6 @@ github.com/opencontainers/cgroups v0.0.1 h1:MXjMkkFpKv6kpuirUa4USFBas573sSAY082B - github.com/opencontainers/cgroups v0.0.1/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs= - github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= - github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= --github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= --github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= - 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -diff --git a/internal/linux/doc.go b/internal/linux/doc.go -new file mode 100644 -index 00000000..4d1eb900 ---- /dev/null -+++ b/internal/linux/doc.go -@@ -0,0 +1,3 @@ -+// Package linux provides minimal wrappers around Linux system calls, primarily -+// to provide support for automatic EINTR-retries. -+package linux -diff --git a/internal/linux/linux.go b/internal/linux/linux.go -new file mode 100644 -index 00000000..f9e67534 ---- /dev/null -+++ b/internal/linux/linux.go -@@ -0,0 +1,44 @@ -+package linux -+ -+import ( -+ "os" -+ -+ "golang.org/x/sys/unix" -+) -+ -+// Readlinkat wraps [unix.Readlinkat]. -+func Readlinkat(dir *os.File, path string) (string, error) { -+ size := 4096 -+ for { -+ linkBuf := make([]byte, size) -+ n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf) -+ if err != nil { -+ return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err} -+ } -+ if n != size { -+ return string(linkBuf[:n]), nil -+ } -+ // Possible truncation, resize the buffer. -+ size *= 2 -+ } -+} -+ -+// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER). -+func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) { -+ // Make sure O_NOCTTY is always set -- otherwise runc might accidentally -+ // gain it as a controlling terminal. O_CLOEXEC also needs to be set to -+ // make sure we don't leak the handle either. -+ flags |= unix.O_NOCTTY | unix.O_CLOEXEC -+ -+ // There is no nice wrapper for this kind of ioctl in unix. -+ peerFd, _, errno := unix.Syscall( -+ unix.SYS_IOCTL, -+ ptyFd, -+ uintptr(unix.TIOCGPTPEER), -+ uintptr(flags), -+ ) -+ if errno != 0 { -+ return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno) -+ } -+ return os.NewFile(peerFd, unsafePeerPath), nil -+} -diff --git a/internal/pathrs/doc.go b/internal/pathrs/doc.go -new file mode 100644 -index 00000000..496ca595 ---- /dev/null -+++ b/internal/pathrs/doc.go -@@ -0,0 +1,23 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+// Package pathrs provides wrappers around filepath-securejoin to add the -+// minimum set of features needed from libpathrs that are not provided by -+// filepath-securejoin, with the eventual goal being that these can be used to -+// ease the transition by converting them stubs when enabling libpathrs builds. -+package pathrs -diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go -new file mode 100644 -index 00000000..fb4f7842 ---- /dev/null -+++ b/internal/pathrs/mkdirall_pathrslite.go -@@ -0,0 +1,97 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import ( -+ "fmt" -+ "os" -+ "path/filepath" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "github.com/sirupsen/logrus" -+ "golang.org/x/sys/unix" -+) -+ -+// MkdirAllInRootOpen attempts to make -+// -+// path, _ := securejoin.SecureJoin(root, unsafePath) -+// os.MkdirAll(path, mode) -+// os.Open(path) -+// -+// safer against attacks where components in the path are changed between -+// SecureJoin returning and MkdirAll (or Open) being called. In particular, we -+// try to detect any symlink components in the path while we are doing the -+// MkdirAll. -+// -+// NOTE: If unsafePath is a subpath of root, we assume that you have already -+// called SecureJoin and so we use the provided path verbatim without resolving -+// any symlinks (this is done in a way that avoids symlink-exchange races). -+// This means that the path also must not contain ".." elements, otherwise an -+// error will occur. -+// -+// This uses (pathrs-lite).MkdirAllHandle under the hood, but it has special -+// handling if unsafePath has already been scoped within the rootfs (this is -+// needed for a lot of runc callers and fixing this would require reworking a -+// lot of path logic). -+func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) { -+ // If the path is already "within" the root, get the path relative to the -+ // root and use that as the unsafe path. This is necessary because a lot of -+ // MkdirAllInRootOpen callers have already done SecureJoin, and refactoring -+ // all of them to stop using these SecureJoin'd paths would require a fair -+ // amount of work. -+ // TODO(cyphar): Do the refactor to libpathrs once it's ready. -+ if IsLexicallyInRoot(root, unsafePath) { -+ subPath, err := filepath.Rel(root, unsafePath) -+ if err != nil { -+ return nil, err -+ } -+ unsafePath = subPath -+ } -+ -+ // Check for any silly mode bits. -+ if mode&^0o7777 != 0 { -+ return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode) -+ } -+ // Linux (and thus os.MkdirAll) silently ignores the suid and sgid bits if -+ // passed. While it would make sense to return an error in that case (since -+ // the user has asked for a mode that won't be applied), for compatibility -+ // reasons we have to ignore these bits. -+ if ignoredBits := mode &^ 0o1777; ignoredBits != 0 { -+ logrus.Warnf("MkdirAll called with no-op mode bits that are ignored by Linux: 0o%.3o", ignoredBits) -+ mode &= 0o1777 -+ } -+ -+ rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) -+ if err != nil { -+ return nil, fmt.Errorf("open root handle: %w", err) -+ } -+ defer rootDir.Close() -+ -+ return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) -+} -+ -+// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the -+// returned handle, for callers that don't need to use it. -+func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error { -+ f, err := MkdirAllInRootOpen(root, unsafePath, mode) -+ if err == nil { -+ _ = f.Close() -+ } -+ return err -+} -diff --git a/internal/pathrs/path.go b/internal/pathrs/path.go -new file mode 100644 -index 00000000..1ee7c795 ---- /dev/null -+++ b/internal/pathrs/path.go -@@ -0,0 +1,34 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import ( -+ "strings" -+) -+ -+// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"), -+// but properly handling the case where path or root have a "/" suffix. -+// -+// NOTE: The return value only make sense if the path is already mostly cleaned -+// (i.e., doesn't contain "..", ".", nor unneeded "/"s). -+func IsLexicallyInRoot(root, path string) bool { -+ root = strings.TrimRight(root, "/") -+ path = strings.TrimRight(path, "/") -+ return strings.HasPrefix(path+"/", root+"/") -+} -diff --git a/internal/pathrs/path_test.go b/internal/pathrs/path_test.go -new file mode 100644 -index 00000000..19d577fb ---- /dev/null -+++ b/internal/pathrs/path_test.go -@@ -0,0 +1,53 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import "testing" -+ -+func TestIsLexicallyInRoot(t *testing.T) { -+ for _, test := range []struct { -+ name string -+ root, path string -+ expected bool -+ }{ -+ {"Equal1", "/foo", "/foo", true}, -+ {"Equal2", "/bar/baz", "/bar/baz", true}, -+ {"Equal3", "/bar/baz/", "/bar/baz/", true}, -+ {"Root", "/", "/foo/bar", true}, -+ {"Root-Equal", "/", "/", true}, -+ {"InRoot-Basic1", "/foo/bar", "/foo/bar/baz/abcd", true}, -+ {"InRoot-Basic2", "/a/b/c/d", "/a/b/c/d/e/f/g/h", true}, -+ {"InRoot-Long", "/var/lib/docker/container/1234abcde/rootfs", "/var/lib/docker/container/1234abcde/rootfs/a/b/c", true}, -+ {"InRoot-TrailingSlash1", "/foo/bar/", "/foo/bar", true}, -+ {"InRoot-TrailingSlash2", "/foo/", "/foo/bar/baz/boop", true}, -+ {"NotInRoot-Basic1", "/foo", "/bar", false}, -+ {"NotInRoot-Basic2", "/foo", "/bar", false}, -+ {"NotInRoot-Basic3", "/foo/bar/baz", "/foo/boo/baz/abc", false}, -+ {"NotInRoot-Long", "/var/lib/docker/container/1234abcde/rootfs", "/a/b/c", false}, -+ {"NotInRoot-Tricky1", "/foo/bar", "/foo/bara", false}, -+ {"NotInRoot-Tricky2", "/foo/bar", "/foo/ba/r", false}, -+ } { -+ t.Run(test.name, func(t *testing.T) { -+ got := IsLexicallyInRoot(test.root, test.path) -+ if test.expected != got { -+ t.Errorf("IsLexicallyInRoot(%q, %q) = %v (expected %v)", test.root, test.path, got, test.expected) -+ } -+ }) -+ } -+} -diff --git a/internal/pathrs/procfs_pathrslite.go b/internal/pathrs/procfs_pathrslite.go -new file mode 100644 -index 00000000..a02b0d39 ---- /dev/null -+++ b/internal/pathrs/procfs_pathrslite.go -@@ -0,0 +1,102 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2025 Aleksa Sarai -+ * Copyright (C) 2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import ( -+ "fmt" -+ "os" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" -+) -+ -+func procOpenReopen(openFn func(subpath string) (*os.File, error), subpath string, flags int) (*os.File, error) { -+ handle, err := openFn(subpath) -+ if err != nil { -+ return nil, err -+ } -+ defer handle.Close() -+ -+ f, err := pathrs.Reopen(handle, flags) -+ if err != nil { -+ return nil, fmt.Errorf("reopen %s: %w", handle.Name(), err) -+ } -+ return f, nil -+} -+ -+// ProcSelfOpen is a wrapper around [procfs.Handle.OpenSelf] and -+// [pathrs.Reopen], to let you one-shot open a procfs file with the given -+// flags. -+func ProcSelfOpen(subpath string, flags int) (*os.File, error) { -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ return procOpenReopen(proc.OpenSelf, subpath, flags) -+} -+ -+// ProcPidOpen is a wrapper around [procfs.Handle.OpenPid] and [pathrs.Reopen], -+// to let you one-shot open a procfs file with the given flags. -+func ProcPidOpen(pid int, subpath string, flags int) (*os.File, error) { -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ return procOpenReopen(func(subpath string) (*os.File, error) { -+ return proc.OpenPid(pid, subpath) -+ }, subpath, flags) -+} -+ -+// ProcThreadSelfOpen is a wrapper around [procfs.Handle.OpenThreadSelf] and -+// [pathrs.Reopen], to let you one-shot open a procfs file with the given -+// flags. The returned [procfs.ProcThreadSelfCloser] needs the same handling as -+// when using pathrs-lite. -+func ProcThreadSelfOpen(subpath string, flags int) (_ *os.File, _ procfs.ProcThreadSelfCloser, Err error) { -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, nil, err -+ } -+ defer proc.Close() -+ -+ handle, closer, err := proc.OpenThreadSelf(subpath) -+ if err != nil { -+ return nil, nil, err -+ } -+ if closer != nil { -+ defer func() { -+ if Err != nil { -+ closer() -+ } -+ }() -+ } -+ defer handle.Close() -+ -+ f, err := pathrs.Reopen(handle, flags) -+ if err != nil { -+ return nil, nil, fmt.Errorf("reopen %s: %w", handle.Name(), err) -+ } -+ return f, closer, nil -+} -+ -+// Reopen is a wrapper around pathrs.Reopen. -+func Reopen(file *os.File, flags int) (*os.File, error) { -+ return pathrs.Reopen(file, flags) -+} -diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go -new file mode 100644 -index 00000000..0ef81fae ---- /dev/null -+++ b/internal/pathrs/root_pathrslite.go -@@ -0,0 +1,69 @@ -+// SPDX-License-Identifier: Apache-2.0 -+/* -+ * Copyright (C) 2024-2025 Aleksa Sarai -+ * Copyright (C) 2024-2025 SUSE LLC -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+ -+package pathrs -+ -+import ( -+ "fmt" -+ "os" -+ "path/filepath" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "golang.org/x/sys/unix" -+) -+ -+// OpenInRoot opens the given path inside the root with the provided flags. It -+// is effectively shorthand for [securejoin.OpenInRoot] followed by -+// [securejoin.Reopen]. -+func OpenInRoot(root, subpath string, flags int) (*os.File, error) { -+ handle, err := pathrs.OpenInRoot(root, subpath) -+ if err != nil { -+ return nil, err -+ } -+ defer handle.Close() -+ return pathrs.Reopen(handle, flags) -+} -+ -+// CreateInRoot creates a new file inside a root (as well as any missing parent -+// directories) and returns a handle to said file. This effectively has -+// open(O_CREAT|O_NOFOLLOW) semantics. If you want the creation to use O_EXCL, -+// include it in the passed flags. The fileMode argument uses unix.* mode bits, -+// *not* os.FileMode. -+func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) { -+ dir, filename := filepath.Split(subpath) -+ if filepath.Join("/", filename) == "/" { -+ return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename) -+ } -+ -+ dirFd, err := MkdirAllInRootOpen(root, dir, 0o755) -+ if err != nil { -+ return nil, err -+ } -+ defer dirFd.Close() -+ -+ // We know that the filename does not have any "/" components, and that -+ // dirFd is inside the root. O_NOFOLLOW will stop us from following -+ // trailing symlinks, so this is safe to do. libpathrs's Root::create_file -+ // works the same way. -+ flags |= unix.O_CREAT | unix.O_NOFOLLOW -+ fd, err := unix.Openat(int(dirFd.Fd()), filename, flags, fileMode) -+ if err != nil { -+ return nil, err -+ } -+ return os.NewFile(uintptr(fd), root+"/"+subpath), nil -+} -diff --git a/internal/sys/doc.go b/internal/sys/doc.go -new file mode 100644 -index 00000000..075387f7 ---- /dev/null -+++ b/internal/sys/doc.go -@@ -0,0 +1,5 @@ -+// Package sys is an internal package that contains helper methods for dealing -+// with Linux that are more complicated than basic wrappers. Basic wrappers -+// usually belong in internal/linux. If you feel something belongs in -+// libcontainer/utils or libcontainer/system, it probably belongs here instead. -+package sys -diff --git a/internal/sys/opath_linux.go b/internal/sys/opath_linux.go -new file mode 100644 -index 00000000..17a216bc ---- /dev/null -+++ b/internal/sys/opath_linux.go -@@ -0,0 +1,53 @@ -+package sys -+ -+import ( -+ "fmt" -+ "os" -+ "runtime" -+ "strconv" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/opencontainers/runc/internal/pathrs" -+) -+ -+// FchmodFile is a wrapper around fchmodat2(AT_EMPTY_PATH) with fallbacks for -+// older kernels. This is distinct from [File.Chmod] and [unix.Fchmod] in that -+// it works on O_PATH file descriptors. -+func FchmodFile(f *os.File, mode uint32) error { -+ err := unix.Fchmodat(int(f.Fd()), "", mode, unix.AT_EMPTY_PATH) -+ // If fchmodat2(2) is not available at all, golang.org/x/unix (probably -+ // in order to mirror glibc) returns EOPNOTSUPP rather than EINVAL -+ // (what the kernel actually returns for invalid flags, which is being -+ // emulated) or ENOSYS (which is what glibc actually sees). -+ if err != unix.EINVAL && err != unix.EOPNOTSUPP { //nolint:errorlint // unix errors are bare -+ // err == nil is implicitly handled -+ return os.NewSyscallError("fchmodat2 AT_EMPTY_PATH", err) -+ } -+ -+ // AT_EMPTY_PATH support was added to fchmodat2 in Linux 6.6 -+ // (5daeb41a6fc9d0d81cb2291884b7410e062d8fa1). The alternative for -+ // older kernels is to go through /proc. -+ fdDir, closer, err2 := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY) -+ if err2 != nil { -+ return fmt.Errorf("fchmodat2 AT_EMPTY_PATH fallback: %w", err2) -+ } -+ defer closer() -+ defer fdDir.Close() -+ -+ err = unix.Fchmodat(int(fdDir.Fd()), strconv.Itoa(int(f.Fd())), mode, 0) -+ if err != nil { -+ err = fmt.Errorf("fchmodat /proc/self/fd/%d: %w", f.Fd(), err) -+ } -+ runtime.KeepAlive(f) -+ return err -+} -+ -+// FchownFile is a wrapper around fchownat(AT_EMPTY_PATH). This is distinct -+// from [File.Chown] and [unix.Fchown] in that it works on O_PATH file -+// descriptors. -+func FchownFile(f *os.File, uid, gid int) error { -+ err := unix.Fchownat(int(f.Fd()), "", uid, gid, unix.AT_EMPTY_PATH) -+ runtime.KeepAlive(f) -+ return os.NewSyscallError("fchownat AT_EMPTY_PATH", err) -+} -diff --git a/internal/sys/sysctl_linux.go b/internal/sys/sysctl_linux.go -new file mode 100644 -index 00000000..96876a55 ---- /dev/null -+++ b/internal/sys/sysctl_linux.go -@@ -0,0 +1,54 @@ -+package sys -+ -+import ( -+ "fmt" -+ "io" -+ "os" -+ "strings" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" -+) -+ -+func procfsOpenRoot(proc *procfs.Handle, subpath string, flags int) (*os.File, error) { -+ handle, err := proc.OpenRoot(subpath) -+ if err != nil { -+ return nil, err -+ } -+ defer handle.Close() -+ -+ return pathrs.Reopen(handle, flags) -+} -+ -+// WriteSysctls sets the given sysctls to the requested values. -+func WriteSysctls(sysctls map[string]string) error { -+ // We are going to write multiple sysctls, which require writing to an -+ // unmasked procfs which is not going to be cached. To avoid creating a new -+ // procfs instance for each one, just allocate one handle for all of them. -+ proc, err := procfs.OpenUnsafeProcRoot() -+ if err != nil { -+ return err -+ } -+ defer proc.Close() -+ -+ for key, value := range sysctls { -+ keyPath := strings.ReplaceAll(key, ".", "/") -+ -+ sysctlFile, err := procfsOpenRoot(proc, "sys/"+keyPath, unix.O_WRONLY|unix.O_TRUNC|unix.O_CLOEXEC) -+ if err != nil { -+ return fmt.Errorf("open sysctl %s file: %w", key, err) -+ } -+ defer sysctlFile.Close() -+ -+ n, err := io.WriteString(sysctlFile, value) -+ if n != len(value) && err == nil { -+ err = fmt.Errorf("short write to file (%d bytes != %d bytes)", n, len(value)) -+ } -+ if err != nil { -+ return fmt.Errorf("failed to write sysctl %s = %q: %w", key, value, err) -+ } -+ } -+ return nil -+} -diff --git a/internal/sys/verify_inode_unix.go b/internal/sys/verify_inode_unix.go -new file mode 100644 -index 00000000..d5019db5 ---- /dev/null -+++ b/internal/sys/verify_inode_unix.go -@@ -0,0 +1,30 @@ -+package sys -+ -+import ( -+ "fmt" -+ "os" -+ "runtime" -+ -+ "golang.org/x/sys/unix" -+) -+ -+// VerifyInodeFunc is the callback passed to [VerifyInode] to check if the -+// inode is the expected type (and on the correct filesystem type, in the case -+// of filesystem-specific inodes). -+type VerifyInodeFunc func(stat *unix.Stat_t, statfs *unix.Statfs_t) error -+ -+// VerifyInode verifies that the underlying inode for the given file matches an -+// expected inode type (possibly on a particular kind of filesystem). This is -+// mainly a wrapper around [VerifyInodeFunc]. -+func VerifyInode(file *os.File, checkFunc VerifyInodeFunc) error { -+ var stat unix.Stat_t -+ if err := unix.Fstat(int(file.Fd()), &stat); err != nil { -+ return fmt.Errorf("fstat %q: %w", file.Name(), err) -+ } -+ var statfs unix.Statfs_t -+ if err := unix.Fstatfs(int(file.Fd()), &statfs); err != nil { -+ return fmt.Errorf("fstatfs %q: %w", file.Name(), err) -+ } -+ runtime.KeepAlive(file) -+ return checkFunc(&stat, &statfs) -+} -diff --git a/internal/third_party/selinux/.codespellrc b/internal/third_party/selinux/.codespellrc -new file mode 100644 -index 00000000..8f0866a3 ---- /dev/null -+++ b/internal/third_party/selinux/.codespellrc -@@ -0,0 +1,2 @@ -+[codespell] -+skip = ./.git,./go.sum,./go-selinux/testdata -diff --git a/internal/third_party/selinux/.github/dependabot.yml b/internal/third_party/selinux/.github/dependabot.yml -new file mode 100644 -index 00000000..b534a2b9 ---- /dev/null -+++ b/internal/third_party/selinux/.github/dependabot.yml -@@ -0,0 +1,10 @@ -+# Please see the documentation for all configuration options: -+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -+ -+version: 2 -+updates: -+ # Dependencies listed in .github/workflows/*.yml -+ - package-ecosystem: "github-actions" -+ directory: "/" -+ schedule: -+ interval: "daily" -diff --git a/internal/third_party/selinux/.github/workflows/validate.yml b/internal/third_party/selinux/.github/workflows/validate.yml -new file mode 100644 -index 00000000..fab1cb49 ---- /dev/null -+++ b/internal/third_party/selinux/.github/workflows/validate.yml -@@ -0,0 +1,163 @@ -+name: validate -+on: -+ push: -+ tags: -+ - v* -+ branches: -+ - master -+ pull_request: -+ -+jobs: -+ -+ commit: -+ runs-on: ubuntu-24.04 -+ # Only check commits on pull requests. -+ if: github.event_name == 'pull_request' -+ steps: -+ - name: get pr commits -+ id: 'get-pr-commits' -+ uses: tim-actions/get-pr-commits@v1.3.1 -+ with: -+ token: ${{ secrets.GITHUB_TOKEN }} -+ -+ - name: check subject line length -+ uses: tim-actions/commit-message-checker-with-regex@v0.3.2 -+ with: -+ commits: ${{ steps.get-pr-commits.outputs.commits }} -+ pattern: '^.{0,72}(\n.*)*$' -+ error: 'Subject too long (max 72)' -+ -+ lint: -+ runs-on: ubuntu-24.04 -+ steps: -+ - uses: actions/checkout@v5 -+ - uses: actions/setup-go@v6 -+ with: -+ go-version: 1.24.x -+ - uses: golangci/golangci-lint-action@v7 -+ with: -+ version: v2.0 -+ -+ codespell: -+ runs-on: ubuntu-24.04 -+ steps: -+ - uses: actions/checkout@v5 -+ - name: install deps -+ # Version of codespell bundled with Ubuntu is way old, so use pip. -+ run: pip install codespell -+ - name: run codespell -+ run: codespell -+ -+ cross: -+ runs-on: ubuntu-24.04 -+ steps: -+ - uses: actions/checkout@v5 -+ - name: cross -+ run: make build-cross -+ -+ test-stubs: -+ runs-on: macos-latest -+ steps: -+ - uses: actions/checkout@v5 -+ - uses: actions/setup-go@v6 -+ with: -+ go-version: 1.24.x -+ - uses: golangci/golangci-lint-action@v7 -+ with: -+ version: v2.0 -+ - name: test-stubs -+ run: make test -+ -+ test: -+ strategy: -+ fail-fast: false -+ matrix: -+ go-version: [1.19.x, 1.23.x, 1.24.x] -+ race: ["-race", ""] -+ runs-on: ubuntu-24.04 -+ steps: -+ - uses: actions/checkout@v5 -+ -+ - name: install go ${{ matrix.go-version }} -+ uses: actions/setup-go@v6 -+ with: -+ go-version: ${{ matrix.go-version }} -+ -+ - name: build -+ run: make BUILDFLAGS="${{ matrix.race }}" build -+ -+ - name: test -+ run: make TESTFLAGS="${{ matrix.race }}" test -+ -+ vm: -+ name: "VM" -+ strategy: -+ fail-fast: false -+ matrix: -+ template: -+ - template://almalinux-8 -+ - template://centos-stream-9 -+ - template://fedora -+ - template://experimental/opensuse-tumbleweed -+ runs-on: ubuntu-24.04 -+ steps: -+ - uses: actions/checkout@v5 -+ -+ - name: "Install Lima" -+ uses: lima-vm/lima-actions/setup@v1 -+ id: lima-actions-setup -+ -+ - name: "Cache ~/.cache/lima" -+ uses: actions/cache@v4 -+ with: -+ path: ~/.cache/lima -+ key: lima-${{ steps.lima-actions-setup.outputs.version }}-${{ matrix.template }} -+ -+ - name: "Start VM" -+ # --plain is set to disable file sharing, port forwarding, built-in containerd, etc. for faster start up -+ run: limactl start --plain --name=default ${{ matrix.template }} -+ -+ - name: "Initialize VM" -+ run: | -+ set -eux -o pipefail -+ # Sync the current directory to /tmp/selinux in the guest -+ limactl cp -r . default:/tmp/selinux -+ # Install packages -+ if lima command -v dnf >/dev/null; then -+ lima sudo dnf install --setopt=install_weak_deps=false --setopt=tsflags=nodocs -y git-core make golang -+ elif lima command -v zypper >/dev/null; then -+ lima sudo zypper install -y git make go -+ else -+ echo >&2 "Unsupported distribution" -+ exit 1 -+ fi -+ -+ - name: "make test" -+ continue-on-error: true -+ run: lima make -C /tmp/selinux test -+ -+ - name: "32-bit test" -+ continue-on-error: true -+ run: lima make -C /tmp/selinux GOARCH=386 test -+ -+ # https://github.com/opencontainers/selinux/issues/222 -+ # https://github.com/opencontainers/selinux/issues/225 -+ - name: "racy test" -+ continue-on-error: true -+ run: lima bash -c 'cd /tmp/selinux && go test -timeout 10m -count 100000 ./go-selinux' -+ -+ - name: "Show AVC denials" -+ run: lima sudo ausearch -m AVC,USER_AVC || true -+ -+ all-done: -+ needs: -+ - commit -+ - lint -+ - codespell -+ - cross -+ - test-stubs -+ - test -+ - vm -+ runs-on: ubuntu-24.04 -+ steps: -+ - run: echo "All jobs completed" -diff --git a/internal/third_party/selinux/.gitignore b/internal/third_party/selinux/.gitignore -new file mode 100644 -index 00000000..378eac25 ---- /dev/null -+++ b/internal/third_party/selinux/.gitignore -@@ -0,0 +1 @@ -+build -diff --git a/internal/third_party/selinux/.golangci.yml b/internal/third_party/selinux/.golangci.yml -new file mode 100644 -index 00000000..b1b98925 ---- /dev/null -+++ b/internal/third_party/selinux/.golangci.yml -@@ -0,0 +1,44 @@ -+version: "2" -+ -+formatters: -+ enable: -+ - gofumpt -+ -+linters: -+ enable: -+ # - copyloopvar # Detects places where loop variables are copied. TODO enable for Go 1.22+ -+ - dupword # Detects duplicate words. -+ - errorlint # Detects code that may cause problems with Go 1.13 error wrapping. -+ - gocritic # Metalinter; detects bugs, performance, and styling issues. -+ - gosec # Detects security problems. -+ - misspell # Detects commonly misspelled English words in comments. -+ - nilerr # Detects code that returns nil even if it checks that the error is not nil. -+ - nolintlint # Detects ill-formed or insufficient nolint directives. -+ - prealloc # Detects slice declarations that could potentially be pre-allocated. -+ - predeclared # Detects code that shadows one of Go's predeclared identifiers -+ - revive # Metalinter; drop-in replacement for golint. -+ - thelper # Detects test helpers without t.Helper(). -+ - tparallel # Detects inappropriate usage of t.Parallel(). -+ - unconvert # Detects unnecessary type conversions. -+ - usetesting # Reports uses of functions with replacement inside the testing package. -+ settings: -+ govet: -+ enable-all: true -+ settings: -+ shadow: -+ strict: true -+ exclusions: -+ generated: strict -+ presets: -+ - comments -+ - common-false-positives -+ - legacy -+ - std-error-handling -+ rules: -+ - linters: -+ - govet -+ text: '^shadow: declaration of "err" shadows declaration' -+ -+issues: -+ max-issues-per-linter: 0 -+ max-same-issues: 0 -diff --git a/internal/third_party/selinux/CODEOWNERS b/internal/third_party/selinux/CODEOWNERS -new file mode 100644 -index 00000000..14392178 ---- /dev/null -+++ b/internal/third_party/selinux/CODEOWNERS -@@ -0,0 +1 @@ -+* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah -diff --git a/internal/third_party/selinux/CONTRIBUTING.md b/internal/third_party/selinux/CONTRIBUTING.md -new file mode 100644 -index 00000000..dc3ff6a5 ---- /dev/null -+++ b/internal/third_party/selinux/CONTRIBUTING.md -@@ -0,0 +1,119 @@ -+## Contribution Guidelines -+ -+### Security issues -+ -+If you are reporting a security issue, do not create an issue or file a pull -+request on GitHub. Instead, disclose the issue responsibly by sending an email -+to security@opencontainers.org (which is inhabited only by the maintainers of -+the various OCI projects). -+ -+### Pull requests are always welcome -+ -+We are always thrilled to receive pull requests, and do our best to -+process them as fast as possible. Not sure if that typo is worth a pull -+request? Do it! We will appreciate it. -+ -+If your pull request is not accepted on the first try, don't be -+discouraged! If there's a problem with the implementation, hopefully you -+received feedback on what to improve. -+ -+We're trying very hard to keep the project lean and focused. We don't want it -+to do everything for everybody. This means that we might decide against -+incorporating a new feature. -+ -+ -+### Conventions -+ -+Fork the repo and make changes on your fork in a feature branch. -+For larger bugs and enhancements, consider filing a leader issue or mailing-list thread for discussion that is independent of the implementation. -+Small changes or changes that have been discussed on the project mailing list may be submitted without a leader issue. -+ -+If the project has a test suite, submit unit tests for your changes. Take a -+look at existing tests for inspiration. Run the full test suite on your branch -+before submitting a pull request. -+ -+Update the documentation when creating or modifying features. Test -+your documentation changes for clarity, concision, and correctness, as -+well as a clean documentation build. See ``docs/README.md`` for more -+information on building the docs and how docs get released. -+ -+Write clean code. Universally formatted code promotes ease of writing, reading, -+and maintenance. Always run `gofmt -s -w file.go` on each changed file before -+committing your changes. Most editors have plugins that do this automatically. -+ -+Pull requests descriptions should be as clear as possible and include a -+reference to all the issues that they address. -+ -+Commit messages must start with a capitalized and short summary -+written in the imperative, followed by an optional, more detailed -+explanatory text which is separated from the summary by an empty line. -+ -+Code review comments may be added to your pull request. Discuss, then make the -+suggested modifications and push additional commits to your feature branch. Be -+sure to post a comment after pushing. The new commits will show up in the pull -+request automatically, but the reviewers will not be notified unless you -+comment. -+ -+Before the pull request is merged, make sure that you squash your commits into -+logical units of work using `git rebase -i` and `git push -f`. After every -+commit the test suite (if any) should be passing. Include documentation changes -+in the same commit so that a revert would remove all traces of the feature or -+fix. -+ -+Commits that fix or close an issue should include a reference like `Closes #XXX` -+or `Fixes #XXX`, which will automatically close the issue when merged. -+ -+### Sign your work -+ -+The sign-off is a simple line at the end of the explanation for the -+patch, which certifies that you wrote it or otherwise have the right to -+pass it on as an open-source patch. The rules are pretty simple: if you -+can certify the below (from -+[developercertificate.org](http://developercertificate.org/)): -+ -+``` -+Developer Certificate of Origin -+Version 1.1 -+ -+Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -+660 York Street, Suite 102, -+San Francisco, CA 94110 USA -+ -+Everyone is permitted to copy and distribute verbatim copies of this -+license document, but changing it is not allowed. -+ -+ -+Developer's Certificate of Origin 1.1 -+ -+By making a contribution to this project, I certify that: -+ -+(a) The contribution was created in whole or in part by me and I -+ have the right to submit it under the open source license -+ indicated in the file; or -+ -+(b) The contribution is based upon previous work that, to the best -+ of my knowledge, is covered under an appropriate open source -+ license and I have the right under that license to submit that -+ work with modifications, whether created in whole or in part -+ by me, under the same open source license (unless I am -+ permitted to submit under a different license), as indicated -+ in the file; or -+ -+(c) The contribution was provided directly to me by some other -+ person who certified (a), (b) or (c) and I have not modified -+ it. -+ -+(d) I understand and agree that this project and the contribution -+ are public and that a record of the contribution (including all -+ personal information I submit with it, including my sign-off) is -+ maintained indefinitely and may be redistributed consistent with -+ this project or the open source license(s) involved. -+``` -+ -+then you just add a line to every git commit message: -+ -+ Signed-off-by: Joe Smith -+ -+using your real name (sorry, no pseudonyms or anonymous contributions.) -+ -+You can add the sign off when creating the git commit via `git commit -s`. -diff --git a/internal/third_party/selinux/LICENSE b/internal/third_party/selinux/LICENSE -new file mode 100644 -index 00000000..8dada3ed ---- /dev/null -+++ b/internal/third_party/selinux/LICENSE -@@ -0,0 +1,201 @@ -+ Apache License -+ Version 2.0, January 2004 -+ http://www.apache.org/licenses/ -+ -+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -+ -+ 1. Definitions. -+ -+ "License" shall mean the terms and conditions for use, reproduction, -+ and distribution as defined by Sections 1 through 9 of this document. -+ -+ "Licensor" shall mean the copyright owner or entity authorized by -+ the copyright owner that is granting the License. -+ -+ "Legal Entity" shall mean the union of the acting entity and all -+ other entities that control, are controlled by, or are under common -+ control with that entity. For the purposes of this definition, -+ "control" means (i) the power, direct or indirect, to cause the -+ direction or management of such entity, whether by contract or -+ otherwise, or (ii) ownership of fifty percent (50%) or more of the -+ outstanding shares, or (iii) beneficial ownership of such entity. -+ -+ "You" (or "Your") shall mean an individual or Legal Entity -+ exercising permissions granted by this License. -+ -+ "Source" form shall mean the preferred form for making modifications, -+ including but not limited to software source code, documentation -+ source, and configuration files. -+ -+ "Object" form shall mean any form resulting from mechanical -+ transformation or translation of a Source form, including but -+ not limited to compiled object code, generated documentation, -+ and conversions to other media types. -+ -+ "Work" shall mean the work of authorship, whether in Source or -+ Object form, made available under the License, as indicated by a -+ copyright notice that is included in or attached to the work -+ (an example is provided in the Appendix below). -+ -+ "Derivative Works" shall mean any work, whether in Source or Object -+ form, that is based on (or derived from) the Work and for which the -+ editorial revisions, annotations, elaborations, or other modifications -+ represent, as a whole, an original work of authorship. For the purposes -+ of this License, Derivative Works shall not include works that remain -+ separable from, or merely link (or bind by name) to the interfaces of, -+ the Work and Derivative Works thereof. -+ -+ "Contribution" shall mean any work of authorship, including -+ the original version of the Work and any modifications or additions -+ to that Work or Derivative Works thereof, that is intentionally -+ submitted to Licensor for inclusion in the Work by the copyright owner -+ or by an individual or Legal Entity authorized to submit on behalf of -+ the copyright owner. For the purposes of this definition, "submitted" -+ means any form of electronic, verbal, or written communication sent -+ to the Licensor or its representatives, including but not limited to -+ communication on electronic mailing lists, source code control systems, -+ and issue tracking systems that are managed by, or on behalf of, the -+ Licensor for the purpose of discussing and improving the Work, but -+ excluding communication that is conspicuously marked or otherwise -+ designated in writing by the copyright owner as "Not a Contribution." -+ -+ "Contributor" shall mean Licensor and any individual or Legal Entity -+ on behalf of whom a Contribution has been received by Licensor and -+ subsequently incorporated within the Work. -+ -+ 2. Grant of Copyright License. Subject to the terms and conditions of -+ this License, each Contributor hereby grants to You a perpetual, -+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable -+ copyright license to reproduce, prepare Derivative Works of, -+ publicly display, publicly perform, sublicense, and distribute the -+ Work and such Derivative Works in Source or Object form. -+ -+ 3. Grant of Patent License. Subject to the terms and conditions of -+ this License, each Contributor hereby grants to You a perpetual, -+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable -+ (except as stated in this section) patent license to make, have made, -+ use, offer to sell, sell, import, and otherwise transfer the Work, -+ where such license applies only to those patent claims licensable -+ by such Contributor that are necessarily infringed by their -+ Contribution(s) alone or by combination of their Contribution(s) -+ with the Work to which such Contribution(s) was submitted. If You -+ institute patent litigation against any entity (including a -+ cross-claim or counterclaim in a lawsuit) alleging that the Work -+ or a Contribution incorporated within the Work constitutes direct -+ or contributory patent infringement, then any patent licenses -+ granted to You under this License for that Work shall terminate -+ as of the date such litigation is filed. -+ -+ 4. Redistribution. You may reproduce and distribute copies of the -+ Work or Derivative Works thereof in any medium, with or without -+ modifications, and in Source or Object form, provided that You -+ meet the following conditions: -+ -+ (a) You must give any other recipients of the Work or -+ Derivative Works a copy of this License; and -+ -+ (b) You must cause any modified files to carry prominent notices -+ stating that You changed the files; and -+ -+ (c) You must retain, in the Source form of any Derivative Works -+ that You distribute, all copyright, patent, trademark, and -+ attribution notices from the Source form of the Work, -+ excluding those notices that do not pertain to any part of -+ the Derivative Works; and -+ -+ (d) If the Work includes a "NOTICE" text file as part of its -+ distribution, then any Derivative Works that You distribute must -+ include a readable copy of the attribution notices contained -+ within such NOTICE file, excluding those notices that do not -+ pertain to any part of the Derivative Works, in at least one -+ of the following places: within a NOTICE text file distributed -+ as part of the Derivative Works; within the Source form or -+ documentation, if provided along with the Derivative Works; or, -+ within a display generated by the Derivative Works, if and -+ wherever such third-party notices normally appear. The contents -+ of the NOTICE file are for informational purposes only and -+ do not modify the License. You may add Your own attribution -+ notices within Derivative Works that You distribute, alongside -+ or as an addendum to the NOTICE text from the Work, provided -+ that such additional attribution notices cannot be construed -+ as modifying the License. -+ -+ You may add Your own copyright statement to Your modifications and -+ may provide additional or different license terms and conditions -+ for use, reproduction, or distribution of Your modifications, or -+ for any such Derivative Works as a whole, provided Your use, -+ reproduction, and distribution of the Work otherwise complies with -+ the conditions stated in this License. -+ -+ 5. Submission of Contributions. Unless You explicitly state otherwise, -+ any Contribution intentionally submitted for inclusion in the Work -+ by You to the Licensor shall be under the terms and conditions of -+ this License, without any additional terms or conditions. -+ Notwithstanding the above, nothing herein shall supersede or modify -+ the terms of any separate license agreement you may have executed -+ with Licensor regarding such Contributions. -+ -+ 6. Trademarks. This License does not grant permission to use the trade -+ names, trademarks, service marks, or product names of the Licensor, -+ except as required for reasonable and customary use in describing the -+ origin of the Work and reproducing the content of the NOTICE file. -+ -+ 7. Disclaimer of Warranty. Unless required by applicable law or -+ agreed to in writing, Licensor provides the Work (and each -+ Contributor provides its Contributions) on an "AS IS" BASIS, -+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -+ implied, including, without limitation, any warranties or conditions -+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -+ PARTICULAR PURPOSE. You are solely responsible for determining the -+ appropriateness of using or redistributing the Work and assume any -+ risks associated with Your exercise of permissions under this License. -+ -+ 8. Limitation of Liability. In no event and under no legal theory, -+ whether in tort (including negligence), contract, or otherwise, -+ unless required by applicable law (such as deliberate and grossly -+ negligent acts) or agreed to in writing, shall any Contributor be -+ liable to You for damages, including any direct, indirect, special, -+ incidental, or consequential damages of any character arising as a -+ result of this License or out of the use or inability to use the -+ Work (including but not limited to damages for loss of goodwill, -+ work stoppage, computer failure or malfunction, or any and all -+ other commercial damages or losses), even if such Contributor -+ has been advised of the possibility of such damages. -+ -+ 9. Accepting Warranty or Additional Liability. While redistributing -+ the Work or Derivative Works thereof, You may choose to offer, -+ and charge a fee for, acceptance of support, warranty, indemnity, -+ or other liability obligations and/or rights consistent with this -+ License. However, in accepting such obligations, You may act only -+ on Your own behalf and on Your sole responsibility, not on behalf -+ of any other Contributor, and only if You agree to indemnify, -+ defend, and hold each Contributor harmless for any liability -+ incurred by, or claims asserted against, such Contributor by reason -+ of your accepting any such warranty or additional liability. -+ -+ END OF TERMS AND CONDITIONS -+ -+ APPENDIX: How to apply the Apache License to your work. -+ -+ To apply the Apache License to your work, attach the following -+ boilerplate notice, with the fields enclosed by brackets "{}" -+ replaced with your own identifying information. (Don't include -+ the brackets!) The text should be enclosed in the appropriate -+ comment syntax for the file format. We also recommend that a -+ file or class name and description of purpose be included on the -+ same "printed page" as the copyright notice for easier -+ identification within third-party archives. -+ -+ Copyright {yyyy} {name of copyright owner} -+ -+ Licensed under the Apache License, Version 2.0 (the "License"); -+ you may not use this file except in compliance with the License. -+ You may obtain a copy of the License at -+ -+ http://www.apache.org/licenses/LICENSE-2.0 -+ -+ Unless required by applicable law or agreed to in writing, software -+ distributed under the License is distributed on an "AS IS" BASIS, -+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ See the License for the specific language governing permissions and -+ limitations under the License. -diff --git a/internal/third_party/selinux/MAINTAINERS b/internal/third_party/selinux/MAINTAINERS -new file mode 100644 -index 00000000..748c18b4 ---- /dev/null -+++ b/internal/third_party/selinux/MAINTAINERS -@@ -0,0 +1,5 @@ -+Antonio Murdaca (@runcom) -+Daniel J Walsh (@rhatdan) -+Mrunal Patel (@mrunalp) -+Sebastiaan van Stijn (@thaJeztah) -+Kirill Kolyshikin (@kolyshkin) -diff --git a/internal/third_party/selinux/Makefile b/internal/third_party/selinux/Makefile -new file mode 100644 -index 00000000..f7b9c3da ---- /dev/null -+++ b/internal/third_party/selinux/Makefile -@@ -0,0 +1,37 @@ -+GO ?= go -+ -+all: build build-cross -+ -+define go-build -+ GOOS=$(1) GOARCH=$(2) $(GO) build ${BUILDFLAGS} ./... -+endef -+ -+.PHONY: build -+build: -+ $(call go-build,linux,amd64) -+ -+.PHONY: build-cross -+build-cross: -+ $(call go-build,linux,386) -+ $(call go-build,linux,arm) -+ $(call go-build,linux,arm64) -+ $(call go-build,linux,ppc64le) -+ $(call go-build,linux,s390x) -+ $(call go-build,linux,mips64le) -+ $(call go-build,linux,riscv64) -+ $(call go-build,windows,amd64) -+ $(call go-build,windows,386) -+ -+ -+.PHONY: test -+test: -+ $(GO) test -timeout 3m ${TESTFLAGS} -v ./... -+ -+.PHONY: lint -+lint: -+ golangci-lint run -+ -+.PHONY: vendor -+vendor: -+ $(GO) mod tidy -+ $(GO) mod verify -diff --git a/internal/third_party/selinux/README.md b/internal/third_party/selinux/README.md -new file mode 100644 -index 00000000..cd6a60f8 ---- /dev/null -+++ b/internal/third_party/selinux/README.md -@@ -0,0 +1,23 @@ -+# selinux -+ -+[![GoDoc](https://godoc.org/github.com/opencontainers/selinux?status.svg)](https://godoc.org/github.com/opencontainers/selinux) [![Go Report Card](https://goreportcard.com/badge/github.com/opencontainers/selinux)](https://goreportcard.com/report/github.com/opencontainers/selinux) [![Build Status](https://travis-ci.org/opencontainers/selinux.svg?branch=master)](https://travis-ci.org/opencontainers/selinux) -+ -+Common SELinux package used across the container ecosystem. -+ -+## Usage -+ -+Prior to v1.8.0, the `selinux` build tag had to be used to enable selinux functionality for compiling consumers of this project. -+Starting with v1.8.0, the `selinux` build tag is no longer needed. -+ -+For complete documentation, see [godoc](https://godoc.org/github.com/opencontainers/selinux). -+ -+## Code of Conduct -+ -+Participation in the OpenContainers community is governed by [OpenContainer's Code of Conduct][code-of-conduct]. -+ -+## Security -+ -+If you find an issue, please follow the [security][security] protocol to report it. -+ -+[security]: https://github.com/opencontainers/org/blob/master/SECURITY.md -+[code-of-conduct]: https://github.com/opencontainers/org/blob/master/CODE_OF_CONDUCT.md -diff --git a/internal/third_party/selinux/go-selinux/doc.go b/internal/third_party/selinux/go-selinux/doc.go -new file mode 100644 -index 00000000..57a15c9a ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/doc.go -@@ -0,0 +1,13 @@ -+/* -+Package selinux provides a high-level interface for interacting with selinux. -+ -+Usage: -+ -+ import "github.com/opencontainers/selinux/go-selinux" -+ -+ // Ensure that selinux is enforcing mode. -+ if selinux.EnforceMode() != selinux.Enforcing { -+ selinux.SetEnforceMode(selinux.Enforcing) -+ } -+*/ -+package selinux -diff --git a/internal/third_party/selinux/go-selinux/label/label.go b/internal/third_party/selinux/go-selinux/label/label.go -new file mode 100644 -index 00000000..884a8b80 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label.go -@@ -0,0 +1,48 @@ -+package label -+ -+import ( -+ "fmt" -+ -+ "github.com/opencontainers/selinux/go-selinux" -+) -+ -+// Init initialises the labeling system -+func Init() { -+ _ = selinux.GetEnabled() -+} -+ -+// FormatMountLabel returns a string to be used by the mount command. Using -+// the SELinux `context` mount option. Changing labels of files on mount -+// points with this option can never be changed. -+// FormatMountLabel returns a string to be used by the mount command. -+// The format of this string will be used to alter the labeling of the mountpoint. -+// The string returned is suitable to be used as the options field of the mount command. -+// If you need to have additional mount point options, you can pass them in as -+// the first parameter. Second parameter is the label that you wish to apply -+// to all content in the mount point. -+func FormatMountLabel(src, mountLabel string) string { -+ return FormatMountLabelByType(src, mountLabel, "context") -+} -+ -+// FormatMountLabelByType returns a string to be used by the mount command. -+// Allow caller to specify the mount options. For example using the SELinux -+// `fscontext` mount option would allow certain container processes to change -+// labels of files created on the mount points, where as `context` option does -+// not. -+// FormatMountLabelByType returns a string to be used by the mount command. -+// The format of this string will be used to alter the labeling of the mountpoint. -+// The string returned is suitable to be used as the options field of the mount command. -+// If you need to have additional mount point options, you can pass them in as -+// the first parameter. Second parameter is the label that you wish to apply -+// to all content in the mount point. -+func FormatMountLabelByType(src, mountLabel, contextType string) string { -+ if mountLabel != "" { -+ switch src { -+ case "": -+ src = fmt.Sprintf("%s=%q", contextType, mountLabel) -+ default: -+ src = fmt.Sprintf("%s,%s=%q", src, contextType, mountLabel) -+ } -+ } -+ return src -+} -diff --git a/internal/third_party/selinux/go-selinux/label/label_linux.go b/internal/third_party/selinux/go-selinux/label/label_linux.go -new file mode 100644 -index 00000000..95f29e21 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label_linux.go -@@ -0,0 +1,136 @@ -+package label -+ -+import ( -+ "errors" -+ "fmt" -+ "strings" -+ -+ "github.com/opencontainers/selinux/go-selinux" -+) -+ -+// Valid Label Options -+var validOptions = map[string]bool{ -+ "disable": true, -+ "type": true, -+ "filetype": true, -+ "user": true, -+ "role": true, -+ "level": true, -+} -+ -+var ErrIncompatibleLabel = errors.New("bad SELinux option: z and Z can not be used together") -+ -+// InitLabels returns the process label and file labels to be used within -+// the container. A list of options can be passed into this function to alter -+// the labels. The labels returned will include a random MCS String, that is -+// guaranteed to be unique. -+// If the disabled flag is passed in, the process label will not be set, but the mount label will be set -+// to the container_file label with the maximum category. This label is not usable by any confined label. -+func InitLabels(options []string) (plabel string, mlabel string, retErr error) { -+ if !selinux.GetEnabled() { -+ return "", "", nil -+ } -+ processLabel, mountLabel := selinux.ContainerLabels() -+ if processLabel != "" { -+ defer func() { -+ if retErr != nil { -+ selinux.ReleaseLabel(mountLabel) -+ } -+ }() -+ pcon, err := selinux.NewContext(processLabel) -+ if err != nil { -+ return "", "", err -+ } -+ mcsLevel := pcon["level"] -+ mcon, err := selinux.NewContext(mountLabel) -+ if err != nil { -+ return "", "", err -+ } -+ for _, opt := range options { -+ if opt == "disable" { -+ selinux.ReleaseLabel(mountLabel) -+ return "", selinux.PrivContainerMountLabel(), nil -+ } -+ if i := strings.Index(opt, ":"); i == -1 { -+ return "", "", fmt.Errorf("bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt) -+ } -+ con := strings.SplitN(opt, ":", 2) -+ if !validOptions[con[0]] { -+ return "", "", fmt.Errorf("bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0]) -+ } -+ if con[0] == "filetype" { -+ mcon["type"] = con[1] -+ continue -+ } -+ pcon[con[0]] = con[1] -+ if con[0] == "level" || con[0] == "user" { -+ mcon[con[0]] = con[1] -+ } -+ } -+ if pcon.Get() != processLabel { -+ if pcon["level"] != mcsLevel { -+ selinux.ReleaseLabel(processLabel) -+ } -+ processLabel = pcon.Get() -+ selinux.ReserveLabel(processLabel) -+ } -+ mountLabel = mcon.Get() -+ } -+ return processLabel, mountLabel, nil -+} -+ -+// SetFileLabel modifies the "path" label to the specified file label -+func SetFileLabel(path string, fileLabel string) error { -+ if !selinux.GetEnabled() || fileLabel == "" { -+ return nil -+ } -+ return selinux.SetFileLabel(path, fileLabel) -+} -+ -+// SetFileCreateLabel tells the kernel the label for all files to be created -+func SetFileCreateLabel(fileLabel string) error { -+ if !selinux.GetEnabled() { -+ return nil -+ } -+ return selinux.SetFSCreateLabel(fileLabel) -+} -+ -+// Relabel changes the label of path and all the entries beneath the path. -+// It changes the MCS label to s0 if shared is true. -+// This will allow all containers to share the content. -+// -+// The path itself is guaranteed to be relabeled last. -+func Relabel(path string, fileLabel string, shared bool) error { -+ if !selinux.GetEnabled() || fileLabel == "" { -+ return nil -+ } -+ -+ if shared { -+ c, err := selinux.NewContext(fileLabel) -+ if err != nil { -+ return err -+ } -+ -+ c["level"] = "s0" -+ fileLabel = c.Get() -+ } -+ return selinux.Chcon(path, fileLabel, true) -+} -+ -+// Validate checks that the label does not include unexpected options -+func Validate(label string) error { -+ if strings.Contains(label, "z") && strings.Contains(label, "Z") { -+ return ErrIncompatibleLabel -+ } -+ return nil -+} -+ -+// RelabelNeeded checks whether the user requested a relabel -+func RelabelNeeded(label string) bool { -+ return strings.Contains(label, "z") || strings.Contains(label, "Z") -+} -+ -+// IsShared checks that the label includes a "shared" mark -+func IsShared(label string) bool { -+ return strings.Contains(label, "z") -+} -diff --git a/internal/third_party/selinux/go-selinux/label/label_linux_test.go b/internal/third_party/selinux/go-selinux/label/label_linux_test.go -new file mode 100644 -index 00000000..e25ead79 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label_linux_test.go -@@ -0,0 +1,130 @@ -+package label -+ -+import ( -+ "errors" -+ "os" -+ "testing" -+ -+ "github.com/opencontainers/selinux/go-selinux" -+) -+ -+func needSELinux(t *testing.T) { -+ t.Helper() -+ if !selinux.GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+} -+ -+func TestInit(t *testing.T) { -+ needSELinux(t) -+ -+ var testNull []string -+ _, _, err := InitLabels(testNull) -+ if err != nil { -+ t.Fatalf("InitLabels failed: %v:", err) -+ } -+ testDisabled := []string{"disable"} -+ if selinux.ROFileLabel() == "" { -+ t.Fatal("selinux.ROFileLabel: empty") -+ } -+ plabel, mlabel, err := InitLabels(testDisabled) -+ if err != nil { -+ t.Fatalf("InitLabels(disabled) failed: %v", err) -+ } -+ if plabel != "" { -+ t.Fatalf("InitLabels(disabled): %q not empty", plabel) -+ } -+ if mlabel != "system_u:object_r:container_file_t:s0:c1022,c1023" { -+ t.Fatalf("InitLabels Disabled mlabel Failed, %s", mlabel) -+ } -+ -+ testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"} -+ plabel, mlabel, err = InitLabels(testUser) -+ if err != nil { -+ t.Fatalf("InitLabels(user) failed: %v", err) -+ } -+ if plabel != "user_u:user_r:user_t:s0:c1,c15" || (mlabel != "user_u:object_r:container_file_t:s0:c1,c15" && mlabel != "user_u:object_r:svirt_sandbox_file_t:s0:c1,c15") { -+ t.Fatalf("InitLabels(user) failed (plabel=%q, mlabel=%q)", plabel, mlabel) -+ } -+ -+ testBadData := []string{"user", "role:user_r", "type:user_t", "level:s0:c1,c15"} -+ if _, _, err = InitLabels(testBadData); err == nil { -+ t.Fatal("InitLabels(bad): expected error, got nil") -+ } -+} -+ -+func TestRelabel(t *testing.T) { -+ needSELinux(t) -+ -+ testdir := t.TempDir() -+ label := "system_u:object_r:container_file_t:s0:c1,c2" -+ if err := Relabel(testdir, "", true); err != nil { -+ t.Fatalf("Relabel with no label failed: %v", err) -+ } -+ if err := Relabel(testdir, label, true); err != nil { -+ t.Fatalf("Relabel shared failed: %v", err) -+ } -+ if err := Relabel(testdir, label, false); err != nil { -+ t.Fatalf("Relabel unshared failed: %v", err) -+ } -+ if err := Relabel("/etc", label, false); err == nil { -+ t.Fatalf("Relabel /etc succeeded") -+ } -+ if err := Relabel("/", label, false); err == nil { -+ t.Fatalf("Relabel / succeeded") -+ } -+ if err := Relabel("/usr", label, false); err == nil { -+ t.Fatalf("Relabel /usr succeeded") -+ } -+ if err := Relabel("/usr/", label, false); err == nil { -+ t.Fatalf("Relabel /usr/ succeeded") -+ } -+ if err := Relabel("/etc/passwd", label, false); err == nil { -+ t.Fatalf("Relabel /etc/passwd succeeded") -+ } -+ if home := os.Getenv("HOME"); home != "" { -+ if err := Relabel(home, label, false); err == nil { -+ t.Fatalf("Relabel %s succeeded", home) -+ } -+ } -+} -+ -+func TestValidate(t *testing.T) { -+ if err := Validate("zZ"); !errors.Is(err, ErrIncompatibleLabel) { -+ t.Fatalf("Expected incompatible error, got %v", err) -+ } -+ if err := Validate("Z"); err != nil { -+ t.Fatal(err) -+ } -+ if err := Validate("z"); err != nil { -+ t.Fatal(err) -+ } -+ if err := Validate(""); err != nil { -+ t.Fatal(err) -+ } -+} -+ -+func TestIsShared(t *testing.T) { -+ if shared := IsShared("Z"); shared { -+ t.Fatalf("Expected label `Z` to not be shared, got %v", shared) -+ } -+ if shared := IsShared("z"); !shared { -+ t.Fatalf("Expected label `z` to be shared, got %v", shared) -+ } -+ if shared := IsShared("Zz"); !shared { -+ t.Fatalf("Expected label `Zz` to be shared, got %v", shared) -+ } -+} -+ -+func TestFileLabel(t *testing.T) { -+ needSELinux(t) -+ -+ testUser := []string{"filetype:test_file_t", "level:s0:c1,c15"} -+ _, mlabel, err := InitLabels(testUser) -+ if err != nil { -+ t.Fatalf("InitLabels(user) failed: %v", err) -+ } -+ if mlabel != "system_u:object_r:test_file_t:s0:c1,c15" { -+ t.Fatalf("InitLabels(filetype) failed: %v", err) -+ } -+} -diff --git a/internal/third_party/selinux/go-selinux/label/label_stub.go b/internal/third_party/selinux/go-selinux/label/label_stub.go -new file mode 100644 -index 00000000..7a54afc5 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label_stub.go -@@ -0,0 +1,44 @@ -+//go:build !linux -+// +build !linux -+ -+package label -+ -+// InitLabels returns the process label and file labels to be used within -+// the container. A list of options can be passed into this function to alter -+// the labels. -+func InitLabels([]string) (string, string, error) { -+ return "", "", nil -+} -+ -+func SetFileLabel(string, string) error { -+ return nil -+} -+ -+func SetFileCreateLabel(string) error { -+ return nil -+} -+ -+func Relabel(string, string, bool) error { -+ return nil -+} -+ -+// DisableSecOpt returns a security opt that can disable labeling -+// support for future container processes -+func DisableSecOpt() []string { -+ return nil -+} -+ -+// Validate checks that the label does not include unexpected options -+func Validate(string) error { -+ return nil -+} -+ -+// RelabelNeeded checks whether the user requested a relabel -+func RelabelNeeded(string) bool { -+ return false -+} -+ -+// IsShared checks that the label includes a "shared" mark -+func IsShared(string) bool { -+ return false -+} -diff --git a/internal/third_party/selinux/go-selinux/label/label_stub_test.go b/internal/third_party/selinux/go-selinux/label/label_stub_test.go -new file mode 100644 -index 00000000..e92cc8b9 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label_stub_test.go -@@ -0,0 +1,76 @@ -+//go:build !linux -+// +build !linux -+ -+package label -+ -+import ( -+ "testing" -+ -+ "github.com/opencontainers/selinux/go-selinux" -+) -+ -+const testLabel = "system_u:object_r:container_file_t:s0:c1,c2" -+ -+func TestInit(t *testing.T) { -+ var testNull []string -+ _, _, err := InitLabels(testNull) -+ if err != nil { -+ t.Log("InitLabels Failed") -+ t.Fatal(err) -+ } -+ testDisabled := []string{"disable"} -+ if selinux.ROFileLabel() != "" { -+ t.Error("selinux.ROFileLabel Failed") -+ } -+ plabel, mlabel, err := InitLabels(testDisabled) -+ if err != nil { -+ t.Log("InitLabels Disabled Failed") -+ t.Fatal(err) -+ } -+ if plabel != "" { -+ t.Fatal("InitLabels Disabled Failed") -+ } -+ if mlabel != "" { -+ t.Fatal("InitLabels Disabled mlabel Failed") -+ } -+ testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"} -+ _, _, err = InitLabels(testUser) -+ if err != nil { -+ t.Log("InitLabels User Failed") -+ t.Fatal(err) -+ } -+} -+ -+func TestRelabel(t *testing.T) { -+ if err := Relabel("/etc", testLabel, false); err != nil { -+ t.Fatalf("Relabel /etc succeeded") -+ } -+} -+ -+func TestCheckLabelCompile(t *testing.T) { -+ if _, _, err := InitLabels(nil); err != nil { -+ t.Fatal(err) -+ } -+ -+ tmpDir := t.TempDir() -+ -+ if err := SetFileLabel(tmpDir, "foobar"); err != nil { -+ t.Fatal(err) -+ } -+ -+ if err := SetFileCreateLabel("foobar"); err != nil { -+ t.Fatal(err) -+ } -+ -+ DisableSecOpt() -+ -+ if err := Validate("foobar"); err != nil { -+ t.Fatal(err) -+ } -+ if relabel := RelabelNeeded("foobar"); relabel { -+ t.Fatal("Relabel failed") -+ } -+ if shared := IsShared("foobar"); shared { -+ t.Fatal("isshared failed") -+ } -+} -diff --git a/internal/third_party/selinux/go-selinux/label/label_test.go b/internal/third_party/selinux/go-selinux/label/label_test.go -new file mode 100644 -index 00000000..fb172f3f ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/label/label_test.go -@@ -0,0 +1,35 @@ -+package label -+ -+import "testing" -+ -+func TestFormatMountLabel(t *testing.T) { -+ expected := `context="foobar"` -+ if test := FormatMountLabel("", "foobar"); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+ -+ expected = `src,context="foobar"` -+ if test := FormatMountLabel("src", "foobar"); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+ -+ expected = `src` -+ if test := FormatMountLabel("src", ""); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+ -+ expected = `fscontext="foobar"` -+ if test := FormatMountLabelByType("", "foobar", "fscontext"); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+ -+ expected = `src,fscontext="foobar"` -+ if test := FormatMountLabelByType("src", "foobar", "fscontext"); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+ -+ expected = `src` -+ if test := FormatMountLabelByType("src", "", "rootcontext"); test != expected { -+ t.Fatalf("Format failed. Expected %s, got %s", expected, test) -+ } -+} -diff --git a/internal/third_party/selinux/go-selinux/selinux.go b/internal/third_party/selinux/go-selinux/selinux.go -new file mode 100644 -index 00000000..15150d47 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/selinux.go -@@ -0,0 +1,322 @@ -+package selinux -+ -+import ( -+ "errors" -+) -+ -+const ( -+ // Enforcing constant indicate SELinux is in enforcing mode -+ Enforcing = 1 -+ // Permissive constant to indicate SELinux is in permissive mode -+ Permissive = 0 -+ // Disabled constant to indicate SELinux is disabled -+ Disabled = -1 -+ // maxCategory is the maximum number of categories used within containers -+ maxCategory = 1024 -+ // DefaultCategoryRange is the upper bound on the category range -+ DefaultCategoryRange = uint32(maxCategory) -+) -+ -+var ( -+ // ErrMCSAlreadyExists is returned when trying to allocate a duplicate MCS. -+ ErrMCSAlreadyExists = errors.New("MCS label already exists") -+ // ErrEmptyPath is returned when an empty path has been specified. -+ ErrEmptyPath = errors.New("empty path") -+ -+ // ErrInvalidLabel is returned when an invalid label is specified. -+ ErrInvalidLabel = errors.New("invalid Label") -+ -+ // InvalidLabel is returned when an invalid label is specified. -+ // -+ // Deprecated: use [ErrInvalidLabel]. -+ InvalidLabel = ErrInvalidLabel -+ -+ // ErrIncomparable is returned two levels are not comparable -+ ErrIncomparable = errors.New("incomparable levels") -+ // ErrLevelSyntax is returned when a sensitivity or category do not have correct syntax in a level -+ ErrLevelSyntax = errors.New("invalid level syntax") -+ -+ // ErrContextMissing is returned if a requested context is not found in a file. -+ ErrContextMissing = errors.New("context does not have a match") -+ // ErrVerifierNil is returned when a context verifier function is nil. -+ ErrVerifierNil = errors.New("verifier function is nil") -+ -+ // ErrNotTGLeader is returned by [SetKeyLabel] if the calling thread -+ // is not the thread group leader. -+ ErrNotTGLeader = errors.New("calling thread is not the thread group leader") -+ -+ // CategoryRange allows the upper bound on the category range to be adjusted -+ CategoryRange = DefaultCategoryRange -+ -+ privContainerMountLabel string -+) -+ -+// Context is a representation of the SELinux label broken into 4 parts -+type Context map[string]string -+ -+// SetDisabled disables SELinux support for the package -+func SetDisabled() { -+ setDisabled() -+} -+ -+// GetEnabled returns whether SELinux is currently enabled. -+func GetEnabled() bool { -+ return getEnabled() -+} -+ -+// ClassIndex returns the int index for an object class in the loaded policy, -+// or -1 and an error -+func ClassIndex(class string) (int, error) { -+ return classIndex(class) -+} -+ -+// SetFileLabel sets the SELinux label for this path, following symlinks, -+// or returns an error. -+func SetFileLabel(fpath string, label string) error { -+ return setFileLabel(fpath, label) -+} -+ -+// LsetFileLabel sets the SELinux label for this path, not following symlinks, -+// or returns an error. -+func LsetFileLabel(fpath string, label string) error { -+ return lSetFileLabel(fpath, label) -+} -+ -+// FileLabel returns the SELinux label for this path, following symlinks, -+// or returns an error. -+func FileLabel(fpath string) (string, error) { -+ return fileLabel(fpath) -+} -+ -+// LfileLabel returns the SELinux label for this path, not following symlinks, -+// or returns an error. -+func LfileLabel(fpath string) (string, error) { -+ return lFileLabel(fpath) -+} -+ -+// SetFSCreateLabel tells the kernel what label to use for all file system objects -+// created by this task. -+// Set the label to an empty string to return to the default label. Calls to SetFSCreateLabel -+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until file system -+// objects created by this task are finished to guarantee another goroutine does not migrate -+// to the current thread before execution is complete. -+func SetFSCreateLabel(label string) error { -+ return setFSCreateLabel(label) -+} -+ -+// FSCreateLabel returns the default label the kernel which the kernel is using -+// for file system objects created by this task. "" indicates default. -+func FSCreateLabel() (string, error) { -+ return fsCreateLabel() -+} -+ -+// CurrentLabel returns the SELinux label of the current process thread, or an error. -+func CurrentLabel() (string, error) { -+ return currentLabel() -+} -+ -+// PidLabel returns the SELinux label of the given pid, or an error. -+func PidLabel(pid int) (string, error) { -+ return pidLabel(pid) -+} -+ -+// ExecLabel returns the SELinux label that the kernel will use for any programs -+// that are executed by the current process thread, or an error. -+func ExecLabel() (string, error) { -+ return execLabel() -+} -+ -+// CanonicalizeContext takes a context string and writes it to the kernel -+// the function then returns the context that the kernel will use. Use this -+// function to check if two contexts are equivalent -+func CanonicalizeContext(val string) (string, error) { -+ return canonicalizeContext(val) -+} -+ -+// ComputeCreateContext requests the type transition from source to target for -+// class from the kernel. -+func ComputeCreateContext(source string, target string, class string) (string, error) { -+ return computeCreateContext(source, target, class) -+} -+ -+// CalculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound) -+// of a source and target range. -+// The glblub is calculated as the greater of the low sensitivities and -+// the lower of the high sensitivities and the and of each category bitset. -+func CalculateGlbLub(sourceRange, targetRange string) (string, error) { -+ return calculateGlbLub(sourceRange, targetRange) -+} -+ -+// SetExecLabel sets the SELinux label that the kernel will use for any programs -+// that are executed by the current process thread, or an error. Calls to SetExecLabel -+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until execution -+// of the program is finished to guarantee another goroutine does not migrate to the current -+// thread before execution is complete. -+func SetExecLabel(label string) error { -+ return writeConThreadSelf("attr/exec", label) -+} -+ -+// SetTaskLabel sets the SELinux label for the current thread, or an error. -+// This requires the dyntransition permission. Calls to SetTaskLabel should -+// be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee -+// the current thread does not run in a new mislabeled thread. -+func SetTaskLabel(label string) error { -+ return writeConThreadSelf("attr/current", label) -+} -+ -+// SetSocketLabel takes a process label and tells the kernel to assign the -+// label to the next socket that gets created. Calls to SetSocketLabel -+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until -+// the socket is created to guarantee another goroutine does not migrate -+// to the current thread before execution is complete. -+func SetSocketLabel(label string) error { -+ return writeConThreadSelf("attr/sockcreate", label) -+} -+ -+// SocketLabel retrieves the current socket label setting -+func SocketLabel() (string, error) { -+ return readConThreadSelf("attr/sockcreate") -+} -+ -+// PeerLabel retrieves the label of the client on the other side of a socket -+func PeerLabel(fd uintptr) (string, error) { -+ return peerLabel(fd) -+} -+ -+// SetKeyLabel takes a process label and tells the kernel to assign the -+// label to the next kernel keyring that gets created. -+// -+// Calls to SetKeyLabel should be wrapped in -+// runtime.LockOSThread()/runtime.UnlockOSThread() until the kernel keyring is -+// created to guarantee another goroutine does not migrate to the current -+// thread before execution is complete. -+// -+// Only the thread group leader can set key label. -+func SetKeyLabel(label string) error { -+ return setKeyLabel(label) -+} -+ -+// KeyLabel retrieves the current kernel keyring label setting -+func KeyLabel() (string, error) { -+ return keyLabel() -+} -+ -+// Get returns the Context as a string -+func (c Context) Get() string { -+ return c.get() -+} -+ -+// NewContext creates a new Context struct from the specified label -+func NewContext(label string) (Context, error) { -+ return newContext(label) -+} -+ -+// ClearLabels clears all reserved labels -+func ClearLabels() { -+ clearLabels() -+} -+ -+// ReserveLabel reserves the MLS/MCS level component of the specified label -+func ReserveLabel(label string) { -+ reserveLabel(label) -+} -+ -+// MLSEnabled checks if MLS is enabled. -+func MLSEnabled() bool { -+ return isMLSEnabled() -+} -+ -+// EnforceMode returns the current SELinux mode Enforcing, Permissive, Disabled -+func EnforceMode() int { -+ return enforceMode() -+} -+ -+// SetEnforceMode sets the current SELinux mode Enforcing, Permissive. -+// Disabled is not valid, since this needs to be set at boot time. -+func SetEnforceMode(mode int) error { -+ return setEnforceMode(mode) -+} -+ -+// DefaultEnforceMode returns the systems default SELinux mode Enforcing, -+// Permissive or Disabled. Note this is just the default at boot time. -+// EnforceMode tells you the systems current mode. -+func DefaultEnforceMode() int { -+ return defaultEnforceMode() -+} -+ -+// ReleaseLabel un-reserves the MLS/MCS Level field of the specified label, -+// allowing it to be used by another process. -+func ReleaseLabel(label string) { -+ releaseLabel(label) -+} -+ -+// ROFileLabel returns the specified SELinux readonly file label -+func ROFileLabel() string { -+ return roFileLabel() -+} -+ -+// KVMContainerLabels returns the default processLabel and mountLabel to be used -+// for kvm containers by the calling process. -+func KVMContainerLabels() (string, string) { -+ return kvmContainerLabels() -+} -+ -+// InitContainerLabels returns the default processLabel and file labels to be -+// used for containers running an init system like systemd by the calling process. -+func InitContainerLabels() (string, string) { -+ return initContainerLabels() -+} -+ -+// ContainerLabels returns an allocated processLabel and fileLabel to be used for -+// container labeling by the calling process. -+func ContainerLabels() (processLabel string, fileLabel string) { -+ return containerLabels() -+} -+ -+// SecurityCheckContext validates that the SELinux label is understood by the kernel -+func SecurityCheckContext(val string) error { -+ return securityCheckContext(val) -+} -+ -+// CopyLevel returns a label with the MLS/MCS level from src label replaced on -+// the dest label. -+func CopyLevel(src, dest string) (string, error) { -+ return copyLevel(src, dest) -+} -+ -+// Chcon changes the fpath file object to the SELinux label. -+// If fpath is a directory and recurse is true, then Chcon walks the -+// directory tree setting the label. -+// -+// The fpath itself is guaranteed to be relabeled last. -+func Chcon(fpath string, label string, recurse bool) error { -+ return chcon(fpath, label, recurse) -+} -+ -+// DupSecOpt takes an SELinux process label and returns security options that -+// can be used to set the SELinux Type and Level for future container processes. -+func DupSecOpt(src string) ([]string, error) { -+ return dupSecOpt(src) -+} -+ -+// DisableSecOpt returns a security opt that can be used to disable SELinux -+// labeling support for future container processes. -+func DisableSecOpt() []string { -+ return []string{"disable"} -+} -+ -+// GetDefaultContextWithLevel gets a single context for the specified SELinux user -+// identity that is reachable from the specified scon context. The context is based -+// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/ if it exists, -+// and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts -+// file. -+func GetDefaultContextWithLevel(user, level, scon string) (string, error) { -+ return getDefaultContextWithLevel(user, level, scon) -+} -+ -+// PrivContainerMountLabel returns mount label for privileged containers -+func PrivContainerMountLabel() string { -+ // Make sure label is initialized. -+ _ = label("") -+ return privContainerMountLabel -+} -diff --git a/internal/third_party/selinux/go-selinux/selinux_linux.go b/internal/third_party/selinux/go-selinux/selinux_linux.go -new file mode 100644 -index 00000000..70392d98 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/selinux_linux.go -@@ -0,0 +1,1405 @@ -+package selinux -+ -+import ( -+ "bufio" -+ "bytes" -+ "crypto/rand" -+ "encoding/binary" -+ "errors" -+ "fmt" -+ "io" -+ "io/fs" -+ "math/big" -+ "os" -+ "os/user" -+ "path/filepath" -+ "strconv" -+ "strings" -+ "sync" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" -+ "golang.org/x/sys/unix" -+ -+ "github.com/opencontainers/selinux/pkg/pwalkdir" -+) -+ -+const ( -+ minSensLen = 2 -+ contextFile = "/usr/share/containers/selinux/contexts" -+ selinuxDir = "/etc/selinux/" -+ selinuxUsersDir = "contexts/users" -+ defaultContexts = "contexts/default_contexts" -+ selinuxConfig = selinuxDir + "config" -+ selinuxfsMount = "/sys/fs/selinux" -+ selinuxTypeTag = "SELINUXTYPE" -+ selinuxTag = "SELINUX" -+ xattrNameSelinux = "security.selinux" -+) -+ -+type selinuxState struct { -+ mcsList map[string]bool -+ selinuxfs string -+ selinuxfsOnce sync.Once -+ enabledSet bool -+ enabled bool -+ sync.Mutex -+} -+ -+type level struct { -+ cats *big.Int -+ sens int -+} -+ -+type mlsRange struct { -+ low *level -+ high *level -+} -+ -+type defaultSECtx struct { -+ userRdr io.Reader -+ verifier func(string) error -+ defaultRdr io.Reader -+ user, level, scon string -+} -+ -+type levelItem byte -+ -+const ( -+ sensitivity levelItem = 's' -+ category levelItem = 'c' -+) -+ -+var ( -+ readOnlyFileLabel string -+ state = selinuxState{ -+ mcsList: make(map[string]bool), -+ } -+ -+ // for policyRoot() -+ policyRootOnce sync.Once -+ policyRootVal string -+ -+ // for label() -+ loadLabelsOnce sync.Once -+ labels map[string]string -+) -+ -+func policyRoot() string { -+ policyRootOnce.Do(func() { -+ policyRootVal = filepath.Join(selinuxDir, readConfig(selinuxTypeTag)) -+ }) -+ -+ return policyRootVal -+} -+ -+func (s *selinuxState) setEnable(enabled bool) bool { -+ s.Lock() -+ defer s.Unlock() -+ s.enabledSet = true -+ s.enabled = enabled -+ return s.enabled -+} -+ -+func (s *selinuxState) getEnabled() bool { -+ s.Lock() -+ enabled := s.enabled -+ enabledSet := s.enabledSet -+ s.Unlock() -+ if enabledSet { -+ return enabled -+ } -+ -+ enabled = false -+ if fs := getSelinuxMountPoint(); fs != "" { -+ if con, _ := CurrentLabel(); con != "kernel" { -+ enabled = true -+ } -+ } -+ return s.setEnable(enabled) -+} -+ -+// setDisabled disables SELinux support for the package -+func setDisabled() { -+ state.setEnable(false) -+} -+ -+func verifySELinuxfsMount(mnt string) bool { -+ var buf unix.Statfs_t -+ for { -+ err := unix.Statfs(mnt, &buf) -+ if err == nil { -+ break -+ } -+ if err == unix.EAGAIN || err == unix.EINTR { -+ continue -+ } -+ return false -+ } -+ -+ //#nosec G115 -- there is no overflow here. -+ if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) { -+ return false -+ } -+ if (buf.Flags & unix.ST_RDONLY) != 0 { -+ return false -+ } -+ -+ return true -+} -+ -+func findSELinuxfs() string { -+ // fast path: check the default mount first -+ if verifySELinuxfsMount(selinuxfsMount) { -+ return selinuxfsMount -+ } -+ -+ // check if selinuxfs is available before going the slow path -+ fs, err := os.ReadFile("/proc/filesystems") -+ if err != nil { -+ return "" -+ } -+ if !bytes.Contains(fs, []byte("\tselinuxfs\n")) { -+ return "" -+ } -+ -+ // slow path: try to find among the mounts -+ f, err := os.Open("/proc/self/mountinfo") -+ if err != nil { -+ return "" -+ } -+ defer f.Close() -+ -+ scanner := bufio.NewScanner(f) -+ for { -+ mnt := findSELinuxfsMount(scanner) -+ if mnt == "" { // error or not found -+ return "" -+ } -+ if verifySELinuxfsMount(mnt) { -+ return mnt -+ } -+ } -+} -+ -+// findSELinuxfsMount returns a next selinuxfs mount point found, -+// if there is one, or an empty string in case of EOF or error. -+func findSELinuxfsMount(s *bufio.Scanner) string { -+ for s.Scan() { -+ txt := s.Bytes() -+ // The first field after - is fs type. -+ // Safe as spaces in mountpoints are encoded as \040 -+ if !bytes.Contains(txt, []byte(" - selinuxfs ")) { -+ continue -+ } -+ const mPos = 5 // mount point is 5th field -+ fields := bytes.SplitN(txt, []byte(" "), mPos+1) -+ if len(fields) < mPos+1 { -+ continue -+ } -+ return string(fields[mPos-1]) -+ } -+ -+ return "" -+} -+ -+func (s *selinuxState) getSELinuxfs() string { -+ s.selinuxfsOnce.Do(func() { -+ s.selinuxfs = findSELinuxfs() -+ }) -+ -+ return s.selinuxfs -+} -+ -+// getSelinuxMountPoint returns the path to the mountpoint of an selinuxfs -+// filesystem or an empty string if no mountpoint is found. Selinuxfs is -+// a proc-like pseudo-filesystem that exposes the SELinux policy API to -+// processes. The existence of an selinuxfs mount is used to determine -+// whether SELinux is currently enabled or not. -+func getSelinuxMountPoint() string { -+ return state.getSELinuxfs() -+} -+ -+// getEnabled returns whether SELinux is currently enabled. -+func getEnabled() bool { -+ return state.getEnabled() -+} -+ -+func readConfig(target string) string { -+ in, err := os.Open(selinuxConfig) -+ if err != nil { -+ return "" -+ } -+ defer in.Close() -+ -+ scanner := bufio.NewScanner(in) -+ -+ for scanner.Scan() { -+ line := bytes.TrimSpace(scanner.Bytes()) -+ if len(line) == 0 { -+ // Skip blank lines -+ continue -+ } -+ if line[0] == ';' || line[0] == '#' { -+ // Skip comments -+ continue -+ } -+ fields := bytes.SplitN(line, []byte{'='}, 2) -+ if len(fields) != 2 { -+ continue -+ } -+ if bytes.Equal(fields[0], []byte(target)) { -+ return string(bytes.Trim(fields[1], `"`)) -+ } -+ } -+ return "" -+} -+ -+func readConFd(in *os.File) (string, error) { -+ data, err := io.ReadAll(in) -+ if err != nil { -+ return "", err -+ } -+ return string(bytes.TrimSuffix(data, []byte{0})), nil -+} -+ -+func writeConFd(out *os.File, val string) error { -+ var err error -+ if val != "" { -+ _, err = out.Write([]byte(val)) -+ } else { -+ _, err = out.Write(nil) -+ } -+ return err -+} -+ -+// openProcThreadSelf is a small wrapper around [OpenThreadSelf] and -+// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The -+// provided mode must be os.O_* flags to indicate what mode the returned file -+// should be opened with (flags like os.O_CREAT and os.O_EXCL are not -+// supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/thread-self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenThreadSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenThreadSelf -+func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) { -+ if subpath == "" { -+ return nil, nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, nil, err -+ } -+ defer proc.Close() -+ -+ handle, closer, err := proc.OpenThreadSelf(subpath) -+ if err != nil { -+ return nil, nil, fmt.Errorf("open /proc/thread-self/%s handle: %w", subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle -+ -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ closer() -+ return nil, nil, fmt.Errorf("reopen /proc/thread-self/%s handle (%#x): %w", subpath, mode, err) -+ } -+ return file, closer, nil -+} -+ -+// Read the contents of /proc/thread-self/. -+func readConThreadSelf(fpath string) (string, error) { -+ in, closer, err := openProcThreadSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return "", err -+ } -+ defer closer() -+ defer in.Close() -+ -+ return readConFd(in) -+} -+ -+// Write to /proc/thread-self/. -+func writeConThreadSelf(fpath, val string) error { -+ if val == "" { -+ if !getEnabled() { -+ return nil -+ } -+ } -+ -+ out, closer, err := openProcThreadSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return err -+ } -+ defer closer() -+ defer out.Close() -+ -+ return writeConFd(out, val) -+} -+ -+// openProcSelf is a small wrapper around [OpenSelf] and [pathrs.Reopen] to -+// make "one-shot opens" slightly more ergonomic. The provided mode must be -+// os.O_* flags to indicate what mode the returned file should be opened with -+// (flags like os.O_CREAT and os.O_EXCL are not supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenSelf -+func openProcSelf(subpath string, mode int) (*os.File, error) { -+ if subpath == "" { -+ return nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ -+ handle, err := proc.OpenSelf(subpath) -+ if err != nil { -+ return nil, fmt.Errorf("open /proc/self/%s handle: %w", subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle -+ -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ return nil, fmt.Errorf("reopen /proc/self/%s handle (%#x): %w", subpath, mode, err) -+ } -+ return file, nil -+} -+ -+// Read the contents of /proc/self/. -+func readConSelf(fpath string) (string, error) { -+ in, err := openProcSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return "", err -+ } -+ defer in.Close() -+ -+ return readConFd(in) -+} -+ -+// Write to /proc/self/. -+func writeConSelf(fpath, val string) error { -+ if val == "" { -+ if !getEnabled() { -+ return nil -+ } -+ } -+ -+ out, err := openProcSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return err -+ } -+ defer out.Close() -+ -+ return writeConFd(out, val) -+} -+ -+// openProcPid is a small wrapper around [OpenPid] and [pathrs.Reopen] to make -+// "one-shot opens" slightly more ergonomic. The provided mode must be os.O_* -+// flags to indicate what mode the returned file should be opened with (flags -+// like os.O_CREAT and os.O_EXCL are not supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenPid]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenPid -+func openProcPid(pid int, subpath string, mode int) (*os.File, error) { -+ if subpath == "" { -+ return nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ -+ handle, err := proc.OpenPid(pid, subpath) -+ if err != nil { -+ return nil, fmt.Errorf("open /proc/%d/%s handle: %w", pid, subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle -+ -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ return nil, fmt.Errorf("reopen /proc/%d/%s handle (%#x): %w", pid, subpath, mode, err) -+ } -+ return file, nil -+} -+ -+// classIndex returns the int index for an object class in the loaded policy, -+// or -1 and an error -+func classIndex(class string) (int, error) { -+ permpath := fmt.Sprintf("class/%s/index", class) -+ indexpath := filepath.Join(getSelinuxMountPoint(), permpath) -+ -+ indexB, err := os.ReadFile(indexpath) -+ if err != nil { -+ return -1, err -+ } -+ index, err := strconv.Atoi(string(indexB)) -+ if err != nil { -+ return -1, err -+ } -+ -+ return index, nil -+} -+ -+// lSetFileLabel sets the SELinux label for this path, not following symlinks, -+// or returns an error. -+func lSetFileLabel(fpath string, label string) error { -+ if fpath == "" { -+ return ErrEmptyPath -+ } -+ for { -+ err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0) -+ if err == nil { -+ break -+ } -+ if err != unix.EINTR { -+ return &os.PathError{Op: fmt.Sprintf("lsetxattr(label=%s)", label), Path: fpath, Err: err} -+ } -+ } -+ -+ return nil -+} -+ -+// setFileLabel sets the SELinux label for this path, following symlinks, -+// or returns an error. -+func setFileLabel(fpath string, label string) error { -+ if fpath == "" { -+ return ErrEmptyPath -+ } -+ for { -+ err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0) -+ if err == nil { -+ break -+ } -+ if err != unix.EINTR { -+ return &os.PathError{Op: fmt.Sprintf("setxattr(label=%s)", label), Path: fpath, Err: err} -+ } -+ } -+ -+ return nil -+} -+ -+// fileLabel returns the SELinux label for this path, following symlinks, -+// or returns an error. -+func fileLabel(fpath string) (string, error) { -+ if fpath == "" { -+ return "", ErrEmptyPath -+ } -+ -+ label, err := getxattr(fpath, xattrNameSelinux) -+ if err != nil { -+ return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err} -+ } -+ // Trim the NUL byte at the end of the byte buffer, if present. -+ if len(label) > 0 && label[len(label)-1] == '\x00' { -+ label = label[:len(label)-1] -+ } -+ return string(label), nil -+} -+ -+// lFileLabel returns the SELinux label for this path, not following symlinks, -+// or returns an error. -+func lFileLabel(fpath string) (string, error) { -+ if fpath == "" { -+ return "", ErrEmptyPath -+ } -+ -+ label, err := lgetxattr(fpath, xattrNameSelinux) -+ if err != nil { -+ return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err} -+ } -+ // Trim the NUL byte at the end of the byte buffer, if present. -+ if len(label) > 0 && label[len(label)-1] == '\x00' { -+ label = label[:len(label)-1] -+ } -+ return string(label), nil -+} -+ -+func setFSCreateLabel(label string) error { -+ return writeConThreadSelf("attr/fscreate", label) -+} -+ -+// fsCreateLabel returns the default label the kernel which the kernel is using -+// for file system objects created by this task. "" indicates default. -+func fsCreateLabel() (string, error) { -+ return readConThreadSelf("attr/fscreate") -+} -+ -+// currentLabel returns the SELinux label of the current process thread, or an error. -+func currentLabel() (string, error) { -+ return readConThreadSelf("attr/current") -+} -+ -+// pidLabel returns the SELinux label of the given pid, or an error. -+func pidLabel(pid int) (string, error) { -+ it, err := openProcPid(pid, "attr/current", os.O_RDONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return "", nil -+ } -+ defer it.Close() -+ return readConFd(it) -+} -+ -+// ExecLabel returns the SELinux label that the kernel will use for any programs -+// that are executed by the current process thread, or an error. -+func execLabel() (string, error) { -+ return readConThreadSelf("exec") -+} -+ -+// canonicalizeContext takes a context string and writes it to the kernel -+// the function then returns the context that the kernel will use. Use this -+// function to check if two contexts are equivalent -+func canonicalizeContext(val string) (string, error) { -+ return readWriteCon(filepath.Join(getSelinuxMountPoint(), "context"), val) -+} -+ -+// computeCreateContext requests the type transition from source to target for -+// class from the kernel. -+func computeCreateContext(source string, target string, class string) (string, error) { -+ classidx, err := classIndex(class) -+ if err != nil { -+ return "", err -+ } -+ -+ return readWriteCon(filepath.Join(getSelinuxMountPoint(), "create"), fmt.Sprintf("%s %s %d", source, target, classidx)) -+} -+ -+// catsToBitset stores categories in a bitset. -+func catsToBitset(cats string) (*big.Int, error) { -+ bitset := new(big.Int) -+ -+ catlist := strings.Split(cats, ",") -+ for _, r := range catlist { -+ ranges := strings.SplitN(r, ".", 2) -+ if len(ranges) > 1 { -+ catstart, err := parseLevelItem(ranges[0], category) -+ if err != nil { -+ return nil, err -+ } -+ catend, err := parseLevelItem(ranges[1], category) -+ if err != nil { -+ return nil, err -+ } -+ for i := catstart; i <= catend; i++ { -+ bitset.SetBit(bitset, i, 1) -+ } -+ } else { -+ cat, err := parseLevelItem(ranges[0], category) -+ if err != nil { -+ return nil, err -+ } -+ bitset.SetBit(bitset, cat, 1) -+ } -+ } -+ -+ return bitset, nil -+} -+ -+// parseLevelItem parses and verifies that a sensitivity or category are valid -+func parseLevelItem(s string, sep levelItem) (int, error) { -+ if len(s) < minSensLen || levelItem(s[0]) != sep { -+ return 0, ErrLevelSyntax -+ } -+ const bitSize = 31 // Make sure the result fits into signed int32. -+ val, err := strconv.ParseUint(s[1:], 10, bitSize) -+ if err != nil { -+ return 0, err -+ } -+ -+ return int(val), nil -+} -+ -+// parseLevel fills a level from a string that contains -+// a sensitivity and categories -+func (l *level) parseLevel(levelStr string) error { -+ lvl := strings.SplitN(levelStr, ":", 2) -+ sens, err := parseLevelItem(lvl[0], sensitivity) -+ if err != nil { -+ return fmt.Errorf("failed to parse sensitivity: %w", err) -+ } -+ l.sens = sens -+ if len(lvl) > 1 { -+ cats, err := catsToBitset(lvl[1]) -+ if err != nil { -+ return fmt.Errorf("failed to parse categories: %w", err) -+ } -+ l.cats = cats -+ } -+ -+ return nil -+} -+ -+// rangeStrToMLSRange marshals a string representation of a range. -+func rangeStrToMLSRange(rangeStr string) (*mlsRange, error) { -+ r := &mlsRange{} -+ l := strings.SplitN(rangeStr, "-", 2) -+ -+ switch len(l) { -+ // rangeStr that has a low and a high level, e.g. s4:c0.c1023-s6:c0.c1023 -+ case 2: -+ r.high = &level{} -+ if err := r.high.parseLevel(l[1]); err != nil { -+ return nil, fmt.Errorf("failed to parse high level %q: %w", l[1], err) -+ } -+ fallthrough -+ // rangeStr that is single level, e.g. s6:c0,c3,c5,c30.c1023 -+ case 1: -+ r.low = &level{} -+ if err := r.low.parseLevel(l[0]); err != nil { -+ return nil, fmt.Errorf("failed to parse low level %q: %w", l[0], err) -+ } -+ } -+ -+ if r.high == nil { -+ r.high = r.low -+ } -+ -+ return r, nil -+} -+ -+// bitsetToStr takes a category bitset and returns it in the -+// canonical selinux syntax -+func bitsetToStr(c *big.Int) string { -+ var str string -+ -+ length := 0 -+ i0 := int(c.TrailingZeroBits()) //#nosec G115 -- don't expect TralingZeroBits to return values with highest bit set. -+ for i := i0; i < c.BitLen(); i++ { -+ if c.Bit(i) == 0 { -+ continue -+ } -+ if length == 0 { -+ if str != "" { -+ str += "," -+ } -+ str += "c" + strconv.Itoa(i) -+ } -+ if c.Bit(i+1) == 1 { -+ length++ -+ continue -+ } -+ if length == 1 { -+ str += ",c" + strconv.Itoa(i) -+ } else if length > 1 { -+ str += ".c" + strconv.Itoa(i) -+ } -+ length = 0 -+ } -+ -+ return str -+} -+ -+func (l *level) equal(l2 *level) bool { -+ if l2 == nil || l == nil { -+ return l == l2 -+ } -+ if l2.sens != l.sens { -+ return false -+ } -+ if l2.cats == nil || l.cats == nil { -+ return l2.cats == l.cats -+ } -+ return l.cats.Cmp(l2.cats) == 0 -+} -+ -+// String returns an mlsRange as a string. -+func (m mlsRange) String() string { -+ low := "s" + strconv.Itoa(m.low.sens) -+ if m.low.cats != nil && m.low.cats.BitLen() > 0 { -+ low += ":" + bitsetToStr(m.low.cats) -+ } -+ -+ if m.low.equal(m.high) { -+ return low -+ } -+ -+ high := "s" + strconv.Itoa(m.high.sens) -+ if m.high.cats != nil && m.high.cats.BitLen() > 0 { -+ high += ":" + bitsetToStr(m.high.cats) -+ } -+ -+ return low + "-" + high -+} -+ -+// TODO: remove these in favor of built-in min/max -+// once we stop supporting Go < 1.21. -+func maxInt(a, b int) int { -+ if a > b { -+ return a -+ } -+ return b -+} -+ -+func minInt(a, b int) int { -+ if a < b { -+ return a -+ } -+ return b -+} -+ -+// calculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound) -+// of a source and target range. -+// The glblub is calculated as the greater of the low sensitivities and -+// the lower of the high sensitivities and the and of each category bitset. -+func calculateGlbLub(sourceRange, targetRange string) (string, error) { -+ s, err := rangeStrToMLSRange(sourceRange) -+ if err != nil { -+ return "", err -+ } -+ t, err := rangeStrToMLSRange(targetRange) -+ if err != nil { -+ return "", err -+ } -+ -+ if s.high.sens < t.low.sens || t.high.sens < s.low.sens { -+ /* these ranges have no common sensitivities */ -+ return "", ErrIncomparable -+ } -+ -+ outrange := &mlsRange{low: &level{}, high: &level{}} -+ -+ /* take the greatest of the low */ -+ outrange.low.sens = maxInt(s.low.sens, t.low.sens) -+ -+ /* take the least of the high */ -+ outrange.high.sens = minInt(s.high.sens, t.high.sens) -+ -+ /* find the intersecting categories */ -+ if s.low.cats != nil && t.low.cats != nil { -+ outrange.low.cats = new(big.Int) -+ outrange.low.cats.And(s.low.cats, t.low.cats) -+ } -+ if s.high.cats != nil && t.high.cats != nil { -+ outrange.high.cats = new(big.Int) -+ outrange.high.cats.And(s.high.cats, t.high.cats) -+ } -+ -+ return outrange.String(), nil -+} -+ -+func readWriteCon(fpath string, val string) (string, error) { -+ if fpath == "" { -+ return "", ErrEmptyPath -+ } -+ f, err := os.OpenFile(fpath, os.O_RDWR, 0) -+ if err != nil { -+ return "", err -+ } -+ defer f.Close() -+ -+ _, err = f.Write([]byte(val)) -+ if err != nil { -+ return "", err -+ } -+ -+ return readConFd(f) -+} -+ -+// peerLabel retrieves the label of the client on the other side of a socket -+func peerLabel(fd uintptr) (string, error) { -+ l, err := unix.GetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_PEERSEC) -+ if err != nil { -+ return "", &os.PathError{Op: "getsockopt", Path: "fd " + strconv.Itoa(int(fd)), Err: err} -+ } -+ return l, nil -+} -+ -+// setKeyLabel takes a process label and tells the kernel to assign the -+// label to the next kernel keyring that gets created -+func setKeyLabel(label string) error { -+ // Rather than using /proc/thread-self, we want to use /proc/self to -+ // operate on the thread-group leader. -+ err := writeConSelf("attr/keycreate", label) -+ if errors.Is(err, os.ErrNotExist) { -+ return nil -+ } -+ if label == "" && errors.Is(err, os.ErrPermission) { -+ return nil -+ } -+ if errors.Is(err, unix.EACCES) && unix.Getpid() != unix.Gettid() { -+ return ErrNotTGLeader -+ } -+ return err -+} -+ -+// KeyLabel retrieves the current kernel keyring label setting for this -+// thread-group. -+func keyLabel() (string, error) { -+ // Rather than using /proc/thread-self, we want to use /proc/self to -+ // operate on the thread-group leader. -+ return readConSelf("attr/keycreate") -+} -+ -+// get returns the Context as a string -+func (c Context) get() string { -+ if l := c["level"]; l != "" { -+ return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + l -+ } -+ return c["user"] + ":" + c["role"] + ":" + c["type"] -+} -+ -+// newContext creates a new Context struct from the specified label -+func newContext(label string) (Context, error) { -+ c := make(Context) -+ -+ if len(label) != 0 { -+ con := strings.SplitN(label, ":", 4) -+ if len(con) < 3 { -+ return c, ErrInvalidLabel -+ } -+ c["user"] = con[0] -+ c["role"] = con[1] -+ c["type"] = con[2] -+ if len(con) > 3 { -+ c["level"] = con[3] -+ } -+ } -+ return c, nil -+} -+ -+// clearLabels clears all reserved labels -+func clearLabels() { -+ state.Lock() -+ state.mcsList = make(map[string]bool) -+ state.Unlock() -+} -+ -+// reserveLabel reserves the MLS/MCS level component of the specified label -+func reserveLabel(label string) { -+ if len(label) != 0 { -+ con := strings.SplitN(label, ":", 4) -+ if len(con) > 3 { -+ _ = mcsAdd(con[3]) -+ } -+ } -+} -+ -+func selinuxEnforcePath() string { -+ return filepath.Join(getSelinuxMountPoint(), "enforce") -+} -+ -+// isMLSEnabled checks if MLS is enabled. -+func isMLSEnabled() bool { -+ enabledB, err := os.ReadFile(filepath.Join(getSelinuxMountPoint(), "mls")) -+ if err != nil { -+ return false -+ } -+ return bytes.Equal(enabledB, []byte{'1'}) -+} -+ -+// enforceMode returns the current SELinux mode Enforcing, Permissive, Disabled -+func enforceMode() int { -+ var enforce int -+ -+ enforceB, err := os.ReadFile(selinuxEnforcePath()) -+ if err != nil { -+ return -1 -+ } -+ enforce, err = strconv.Atoi(string(enforceB)) -+ if err != nil { -+ return -1 -+ } -+ return enforce -+} -+ -+// setEnforceMode sets the current SELinux mode Enforcing, Permissive. -+// Disabled is not valid, since this needs to be set at boot time. -+func setEnforceMode(mode int) error { -+ return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0) -+} -+ -+// defaultEnforceMode returns the systems default SELinux mode Enforcing, -+// Permissive or Disabled. Note this is just the default at boot time. -+// EnforceMode tells you the systems current mode. -+func defaultEnforceMode() int { -+ switch readConfig(selinuxTag) { -+ case "enforcing": -+ return Enforcing -+ case "permissive": -+ return Permissive -+ } -+ return Disabled -+} -+ -+func mcsAdd(mcs string) error { -+ if mcs == "" { -+ return nil -+ } -+ state.Lock() -+ defer state.Unlock() -+ if state.mcsList[mcs] { -+ return ErrMCSAlreadyExists -+ } -+ state.mcsList[mcs] = true -+ return nil -+} -+ -+func mcsDelete(mcs string) { -+ if mcs == "" { -+ return -+ } -+ state.Lock() -+ defer state.Unlock() -+ state.mcsList[mcs] = false -+} -+ -+func intToMcs(id int, catRange uint32) string { -+ var ( -+ SETSIZE = int(catRange) -+ TIER = SETSIZE -+ ORD = id -+ ) -+ -+ if id < 1 || id > 523776 { -+ return "" -+ } -+ -+ for ORD > TIER { -+ ORD -= TIER -+ TIER-- -+ } -+ TIER = SETSIZE - TIER -+ ORD += TIER -+ return fmt.Sprintf("s0:c%d,c%d", TIER, ORD) -+} -+ -+func uniqMcs(catRange uint32) string { -+ var ( -+ n uint32 -+ c1, c2 uint32 -+ mcs string -+ ) -+ -+ for { -+ _ = binary.Read(rand.Reader, binary.LittleEndian, &n) -+ c1 = n % catRange -+ _ = binary.Read(rand.Reader, binary.LittleEndian, &n) -+ c2 = n % catRange -+ if c1 == c2 { -+ continue -+ } else if c1 > c2 { -+ c1, c2 = c2, c1 -+ } -+ mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2) -+ if err := mcsAdd(mcs); err != nil { -+ continue -+ } -+ break -+ } -+ return mcs -+} -+ -+// releaseLabel un-reserves the MLS/MCS Level field of the specified label, -+// allowing it to be used by another process. -+func releaseLabel(label string) { -+ if len(label) != 0 { -+ con := strings.SplitN(label, ":", 4) -+ if len(con) > 3 { -+ mcsDelete(con[3]) -+ } -+ } -+} -+ -+// roFileLabel returns the specified SELinux readonly file label -+func roFileLabel() string { -+ return readOnlyFileLabel -+} -+ -+func openContextFile() (*os.File, error) { -+ if f, err := os.Open(contextFile); err == nil { -+ return f, nil -+ } -+ return os.Open(filepath.Join(policyRoot(), "contexts", "lxc_contexts")) -+} -+ -+func loadLabels() { -+ labels = make(map[string]string) -+ in, err := openContextFile() -+ if err != nil { -+ return -+ } -+ defer in.Close() -+ -+ scanner := bufio.NewScanner(in) -+ -+ for scanner.Scan() { -+ line := bytes.TrimSpace(scanner.Bytes()) -+ if len(line) == 0 { -+ // Skip blank lines -+ continue -+ } -+ if line[0] == ';' || line[0] == '#' { -+ // Skip comments -+ continue -+ } -+ fields := bytes.SplitN(line, []byte{'='}, 2) -+ if len(fields) != 2 { -+ continue -+ } -+ key, val := bytes.TrimSpace(fields[0]), bytes.TrimSpace(fields[1]) -+ labels[string(key)] = string(bytes.Trim(val, `"`)) -+ } -+ -+ con, _ := NewContext(labels["file"]) -+ con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1) -+ privContainerMountLabel = con.get() -+ reserveLabel(privContainerMountLabel) -+} -+ -+func label(key string) string { -+ loadLabelsOnce.Do(func() { -+ loadLabels() -+ }) -+ return labels[key] -+} -+ -+// kvmContainerLabels returns the default processLabel and mountLabel to be used -+// for kvm containers by the calling process. -+func kvmContainerLabels() (string, string) { -+ processLabel := label("kvm_process") -+ if processLabel == "" { -+ processLabel = label("process") -+ } -+ -+ return addMcs(processLabel, label("file")) -+} -+ -+// initContainerLabels returns the default processLabel and file labels to be -+// used for containers running an init system like systemd by the calling process. -+func initContainerLabels() (string, string) { -+ processLabel := label("init_process") -+ if processLabel == "" { -+ processLabel = label("process") -+ } -+ -+ return addMcs(processLabel, label("file")) -+} -+ -+// containerLabels returns an allocated processLabel and fileLabel to be used for -+// container labeling by the calling process. -+func containerLabels() (processLabel string, fileLabel string) { -+ if !getEnabled() { -+ return "", "" -+ } -+ -+ processLabel = label("process") -+ fileLabel = label("file") -+ readOnlyFileLabel = label("ro_file") -+ -+ if processLabel == "" || fileLabel == "" { -+ return "", fileLabel -+ } -+ -+ if readOnlyFileLabel == "" { -+ readOnlyFileLabel = fileLabel -+ } -+ -+ return addMcs(processLabel, fileLabel) -+} -+ -+func addMcs(processLabel, fileLabel string) (string, string) { -+ scon, _ := NewContext(processLabel) -+ if scon["level"] != "" { -+ mcs := uniqMcs(CategoryRange) -+ scon["level"] = mcs -+ processLabel = scon.Get() -+ scon, _ = NewContext(fileLabel) -+ scon["level"] = mcs -+ fileLabel = scon.Get() -+ } -+ return processLabel, fileLabel -+} -+ -+// securityCheckContext validates that the SELinux label is understood by the kernel -+func securityCheckContext(val string) error { -+ return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0) -+} -+ -+// copyLevel returns a label with the MLS/MCS level from src label replaced on -+// the dest label. -+func copyLevel(src, dest string) (string, error) { -+ if src == "" { -+ return "", nil -+ } -+ if err := SecurityCheckContext(src); err != nil { -+ return "", err -+ } -+ if err := SecurityCheckContext(dest); err != nil { -+ return "", err -+ } -+ scon, err := NewContext(src) -+ if err != nil { -+ return "", err -+ } -+ tcon, err := NewContext(dest) -+ if err != nil { -+ return "", err -+ } -+ mcsDelete(tcon["level"]) -+ _ = mcsAdd(scon["level"]) -+ tcon["level"] = scon["level"] -+ return tcon.Get(), nil -+} -+ -+// chcon changes the fpath file object to the SELinux label. -+// If fpath is a directory and recurse is true, then chcon walks the -+// directory tree setting the label. -+func chcon(fpath string, label string, recurse bool) error { -+ if fpath == "" { -+ return ErrEmptyPath -+ } -+ if label == "" { -+ return nil -+ } -+ -+ excludePaths := map[string]bool{ -+ "/": true, -+ "/bin": true, -+ "/boot": true, -+ "/dev": true, -+ "/etc": true, -+ "/etc/passwd": true, -+ "/etc/pki": true, -+ "/etc/shadow": true, -+ "/home": true, -+ "/lib": true, -+ "/lib64": true, -+ "/media": true, -+ "/opt": true, -+ "/proc": true, -+ "/root": true, -+ "/run": true, -+ "/sbin": true, -+ "/srv": true, -+ "/sys": true, -+ "/tmp": true, -+ "/usr": true, -+ "/var": true, -+ "/var/lib": true, -+ "/var/log": true, -+ } -+ -+ if home := os.Getenv("HOME"); home != "" { -+ excludePaths[home] = true -+ } -+ -+ if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { -+ if usr, err := user.Lookup(sudoUser); err == nil { -+ excludePaths[usr.HomeDir] = true -+ } -+ } -+ -+ if fpath != "/" { -+ fpath = strings.TrimSuffix(fpath, "/") -+ } -+ if excludePaths[fpath] { -+ return fmt.Errorf("SELinux relabeling of %s is not allowed", fpath) -+ } -+ -+ if !recurse { -+ err := lSetFileLabel(fpath, label) -+ if err != nil { -+ // Check if file doesn't exist, must have been removed -+ if errors.Is(err, os.ErrNotExist) { -+ return nil -+ } -+ // Check if current label is correct on disk -+ flabel, nerr := lFileLabel(fpath) -+ if nerr == nil && flabel == label { -+ return nil -+ } -+ // Check if file doesn't exist, must have been removed -+ if errors.Is(nerr, os.ErrNotExist) { -+ return nil -+ } -+ return err -+ } -+ return nil -+ } -+ -+ return rchcon(fpath, label) -+} -+ -+func rchcon(fpath, label string) error { //revive:disable:cognitive-complexity -+ fastMode := false -+ // If the current label matches the new label, assume -+ // other labels are correct. -+ if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label { -+ fastMode = true -+ } -+ return pwalkdir.Walk(fpath, func(p string, _ fs.DirEntry, _ error) error { -+ if fastMode { -+ if cLabel, err := lFileLabel(p); err == nil && cLabel == label { -+ return nil -+ } -+ } -+ err := lSetFileLabel(p, label) -+ // Walk a file tree can race with removal, so ignore ENOENT. -+ if errors.Is(err, os.ErrNotExist) { -+ return nil -+ } -+ return err -+ }) -+} -+ -+// dupSecOpt takes an SELinux process label and returns security options that -+// can be used to set the SELinux Type and Level for future container processes. -+func dupSecOpt(src string) ([]string, error) { -+ if src == "" { -+ return nil, nil -+ } -+ con, err := NewContext(src) -+ if err != nil { -+ return nil, err -+ } -+ if con["user"] == "" || -+ con["role"] == "" || -+ con["type"] == "" { -+ return nil, nil -+ } -+ dup := []string{ -+ "user:" + con["user"], -+ "role:" + con["role"], -+ "type:" + con["type"], -+ } -+ -+ if con["level"] != "" { -+ dup = append(dup, "level:"+con["level"]) -+ } -+ -+ return dup, nil -+} -+ -+// findUserInContext scans the reader for a valid SELinux context -+// match that is verified with the verifier. Invalid contexts are -+// skipped. It returns a matched context or an empty string if no -+// match is found. If a scanner error occurs, it is returned. -+func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) { -+ fromRole := context["role"] -+ fromType := context["type"] -+ scanner := bufio.NewScanner(r) -+ -+ for scanner.Scan() { -+ fromConns := strings.Fields(scanner.Text()) -+ if len(fromConns) == 0 { -+ // Skip blank lines -+ continue -+ } -+ -+ line := fromConns[0] -+ -+ if line[0] == ';' || line[0] == '#' { -+ // Skip comments -+ continue -+ } -+ -+ // user context files contexts are formatted as -+ // role_r:type_t:s0 where the user is missing. -+ lineArr := strings.SplitN(line, ":", 4) -+ // skip context with typo, or role and type do not match -+ if len(lineArr) != 3 || -+ lineArr[0] != fromRole || -+ lineArr[1] != fromType { -+ continue -+ } -+ -+ for _, cc := range fromConns[1:] { -+ toConns := strings.SplitN(cc, ":", 4) -+ if len(toConns) != 3 { -+ continue -+ } -+ -+ context["role"] = toConns[0] -+ context["type"] = toConns[1] -+ -+ outConn := context.get() -+ if err := verifier(outConn); err != nil { -+ continue -+ } -+ -+ return outConn, nil -+ } -+ } -+ if err := scanner.Err(); err != nil { -+ return "", fmt.Errorf("failed to scan for context: %w", err) -+ } -+ -+ return "", nil -+} -+ -+func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { -+ if c.verifier == nil { -+ return "", ErrVerifierNil -+ } -+ -+ context, err := newContext(c.scon) -+ if err != nil { -+ return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err) -+ } -+ -+ // set so the verifier validates the matched context with the provided user and level. -+ context["user"] = c.user -+ context["level"] = c.level -+ -+ conn, err := findUserInContext(context, c.userRdr, c.verifier) -+ if err != nil { -+ return "", err -+ } -+ -+ if conn != "" { -+ return conn, nil -+ } -+ -+ conn, err = findUserInContext(context, c.defaultRdr, c.verifier) -+ if err != nil { -+ return "", err -+ } -+ -+ if conn != "" { -+ return conn, nil -+ } -+ -+ return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing) -+} -+ -+func getDefaultContextWithLevel(user, level, scon string) (string, error) { -+ userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) -+ fu, err := os.Open(userPath) -+ if err != nil { -+ return "", err -+ } -+ defer fu.Close() -+ -+ defaultPath := filepath.Join(policyRoot(), defaultContexts) -+ fd, err := os.Open(defaultPath) -+ if err != nil { -+ return "", err -+ } -+ defer fd.Close() -+ -+ c := defaultSECtx{ -+ user: user, -+ level: level, -+ scon: scon, -+ userRdr: fu, -+ defaultRdr: fd, -+ verifier: securityCheckContext, -+ } -+ -+ return getDefaultContextFromReaders(&c) -+} -diff --git a/internal/third_party/selinux/go-selinux/selinux_linux_test.go b/internal/third_party/selinux/go-selinux/selinux_linux_test.go -new file mode 100644 -index 00000000..71aa0b82 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/selinux_linux_test.go -@@ -0,0 +1,711 @@ -+package selinux -+ -+import ( -+ "bufio" -+ "bytes" -+ "errors" -+ "fmt" -+ "os" -+ "path/filepath" -+ "runtime" -+ "strconv" -+ "strings" -+ "testing" -+ -+ "golang.org/x/sys/unix" -+) -+ -+func TestSetFileLabel(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ const ( -+ tmpFile = "selinux_test" -+ tmpLink = "selinux_test_link" -+ con = "system_u:object_r:bin_t:s0:c1,c2" -+ con2 = "system_u:object_r:bin_t:s0:c3,c4" -+ ) -+ -+ _ = os.Remove(tmpFile) -+ out, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0) -+ if err != nil { -+ t.Fatal(err) -+ } -+ out.Close() -+ defer os.Remove(tmpFile) -+ -+ _ = os.Remove(tmpLink) -+ if err := os.Symlink(tmpFile, tmpLink); err != nil { -+ t.Fatal(err) -+ } -+ defer os.Remove(tmpLink) -+ -+ if err := SetFileLabel(tmpLink, con); err != nil { -+ t.Fatalf("SetFileLabel failed: %s", err) -+ } -+ filelabel, err := FileLabel(tmpLink) -+ if err != nil { -+ t.Fatalf("FileLabel failed: %s", err) -+ } -+ if filelabel != con { -+ t.Fatalf("FileLabel failed, returned %s expected %s", filelabel, con) -+ } -+ -+ // Using LfileLabel to verify that the symlink itself is not labeled. -+ linkLabel, err := LfileLabel(tmpLink) -+ if err != nil { -+ t.Fatalf("LfileLabel failed: %s", err) -+ } -+ if linkLabel == con { -+ t.Fatalf("Label on symlink should not be set, got: %q", linkLabel) -+ } -+ -+ // Use LsetFileLabel to set a label on the symlink itself. -+ if err := LsetFileLabel(tmpLink, con2); err != nil { -+ t.Fatalf("LsetFileLabel failed: %s", err) -+ } -+ filelabel, err = FileLabel(tmpFile) -+ if err != nil { -+ t.Fatalf("FileLabel failed: %s", err) -+ } -+ if filelabel != con { -+ t.Fatalf("FileLabel was updated, returned %s expected %s", filelabel, con) -+ } -+ -+ linkLabel, err = LfileLabel(tmpLink) -+ if err != nil { -+ t.Fatalf("LfileLabel failed: %s", err) -+ } -+ if linkLabel != con2 { -+ t.Fatalf("LfileLabel failed: returned %s expected %s", linkLabel, con2) -+ } -+} -+ -+func TestKVMLabels(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ plabel, flabel := KVMContainerLabels() -+ if plabel == "" { -+ t.Log("Failed to read kvm label") -+ } -+ t.Log(plabel) -+ t.Log(flabel) -+ if _, err := CanonicalizeContext(plabel); err != nil { -+ t.Fatal(err) -+ } -+ if _, err := CanonicalizeContext(flabel); err != nil { -+ t.Fatal(err) -+ } -+ -+ ReleaseLabel(plabel) -+} -+ -+func TestInitLabels(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ plabel, flabel := InitContainerLabels() -+ if plabel == "" { -+ t.Log("Failed to read init label") -+ } -+ t.Log(plabel) -+ t.Log(flabel) -+ if _, err := CanonicalizeContext(plabel); err != nil { -+ t.Fatal(err) -+ } -+ if _, err := CanonicalizeContext(flabel); err != nil { -+ t.Fatal(err) -+ } -+ ReleaseLabel(plabel) -+} -+ -+func TestDuplicateLabel(t *testing.T) { -+ secopt, err := DupSecOpt("system_u:system_r:container_t:s0:c1,c2") -+ if err != nil { -+ t.Fatalf("DupSecOpt: %v", err) -+ } -+ for _, opt := range secopt { -+ con := strings.SplitN(opt, ":", 2) -+ if con[0] == "user" { -+ if con[1] != "system_u" { -+ t.Errorf("DupSecOpt Failed user incorrect") -+ } -+ continue -+ } -+ if con[0] == "role" { -+ if con[1] != "system_r" { -+ t.Errorf("DupSecOpt Failed role incorrect") -+ } -+ continue -+ } -+ if con[0] == "type" { -+ if con[1] != "container_t" { -+ t.Errorf("DupSecOpt Failed type incorrect") -+ } -+ continue -+ } -+ if con[0] == "level" { -+ if con[1] != "s0:c1,c2" { -+ t.Errorf("DupSecOpt Failed level incorrect") -+ } -+ continue -+ } -+ t.Errorf("DupSecOpt failed: invalid field %q", con[0]) -+ } -+ secopt = DisableSecOpt() -+ if secopt[0] != "disable" { -+ t.Errorf(`DisableSecOpt failed: want "disable", got %q`, secopt[0]) -+ } -+} -+ -+func TestSELinuxNoLevel(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ tlabel := "system_u:system_r:container_t" -+ dup, err := DupSecOpt(tlabel) -+ if err != nil { -+ t.Fatal(err) -+ } -+ -+ if len(dup) != 3 { -+ t.Errorf("DupSecOpt failed on non mls label: want 3, got %d", len(dup)) -+ } -+ con, err := NewContext(tlabel) -+ if err != nil { -+ t.Fatal(err) -+ } -+ if con.Get() != tlabel { -+ t.Errorf("NewContext and con.Get() failed on non mls label: want %q, got %q", tlabel, con.Get()) -+ } -+} -+ -+func TestSocketLabel(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ // Ensure the thread stays the same for duration of the test. -+ // Otherwise Go runtime can switch this to a different thread, -+ // which results in EACCES in call to SetSocketLabel. -+ runtime.LockOSThread() -+ defer runtime.UnlockOSThread() -+ -+ label := "system_u:object_r:container_t:s0:c1,c2" -+ if err := SetSocketLabel(label); err != nil { -+ t.Fatal(err) -+ } -+ nlabel, err := SocketLabel() -+ if err != nil { -+ t.Fatal(err) -+ } -+ if label != nlabel { -+ t.Errorf("SocketLabel %s != %s", nlabel, label) -+ } -+} -+ -+func TestKeyLabel(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ // Ensure the thread stays the same for duration of the test. -+ // Otherwise Go runtime can switch this to a different thread, -+ // which results in EACCES in call to SetKeyLabel. -+ runtime.LockOSThread() -+ defer runtime.UnlockOSThread() -+ -+ if unix.Getpid() != unix.Gettid() { -+ t.Skip(ErrNotTGLeader) -+ } -+ -+ label := "system_u:object_r:container_t:s0:c1,c2" -+ if err := SetKeyLabel(label); err != nil { -+ t.Fatal(err) -+ } -+ nlabel, err := KeyLabel() -+ if err != nil { -+ t.Fatal(err) -+ } -+ if label != nlabel { -+ t.Errorf("KeyLabel: want %q, got %q", label, nlabel) -+ } -+} -+ -+func BenchmarkContextGet(b *testing.B) { -+ ctx, err := NewContext("system_u:object_r:container_file_t:s0:c1022,c1023") -+ if err != nil { -+ b.Fatal(err) -+ } -+ str := "" -+ for i := 0; i < b.N; i++ { -+ str = ctx.get() -+ } -+ b.Log(str) -+} -+ -+func TestSELinux(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ // Ensure the thread stays the same for duration of the test. -+ // Otherwise Go runtime can switch this to a different thread, -+ // which results in EACCES in call to SetFSCreateLabel. -+ runtime.LockOSThread() -+ defer runtime.UnlockOSThread() -+ -+ var ( -+ err error -+ plabel, flabel string -+ ) -+ -+ plabel, flabel = ContainerLabels() -+ t.Log(plabel) -+ t.Log(flabel) -+ plabel, flabel = ContainerLabels() -+ t.Log(plabel) -+ t.Log(flabel) -+ ReleaseLabel(plabel) -+ -+ plabel, flabel = ContainerLabels() -+ t.Log(plabel) -+ t.Log(flabel) -+ ClearLabels() -+ t.Log("ClearLabels") -+ plabel, flabel = ContainerLabels() -+ t.Log(plabel) -+ t.Log(flabel) -+ ReleaseLabel(plabel) -+ -+ pid := os.Getpid() -+ t.Logf("PID:%d MCS:%s", pid, intToMcs(pid, 1023)) -+ err = SetFSCreateLabel("unconfined_u:unconfined_r:unconfined_t:s0") -+ if err != nil { -+ t.Fatal("SetFSCreateLabel failed:", err) -+ } -+ t.Log(FSCreateLabel()) -+ err = SetFSCreateLabel("") -+ if err != nil { -+ t.Fatal("SetFSCreateLabel failed:", err) -+ } -+ t.Log(FSCreateLabel()) -+ t.Log(PidLabel(1)) -+} -+ -+func TestSetEnforceMode(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ if os.Geteuid() != 0 { -+ t.Skip("root required, skipping") -+ } -+ -+ t.Log("Enforcing Mode:", EnforceMode()) -+ mode := DefaultEnforceMode() -+ t.Log("Default Enforce Mode:", mode) -+ defer func() { -+ _ = SetEnforceMode(mode) -+ }() -+ -+ if err := SetEnforceMode(Enforcing); err != nil { -+ t.Fatalf("setting selinux mode to enforcing failed: %v", err) -+ } -+ if err := SetEnforceMode(Permissive); err != nil { -+ t.Fatalf("setting selinux mode to permissive failed: %v", err) -+ } -+} -+ -+func TestCanonicalizeContext(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ con := "system_u:object_r:bin_t:s0:c1,c2,c3" -+ checkcon := "system_u:object_r:bin_t:s0:c1.c3" -+ newcon, err := CanonicalizeContext(con) -+ if err != nil { -+ t.Fatal(err) -+ } -+ if newcon != checkcon { -+ t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon) -+ } -+ con = "system_u:object_r:bin_t:s0:c5,c2" -+ checkcon = "system_u:object_r:bin_t:s0:c2,c5" -+ newcon, err = CanonicalizeContext(con) -+ if err != nil { -+ t.Fatal(err) -+ } -+ if newcon != checkcon { -+ t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon) -+ } -+} -+ -+func TestFindSELinuxfsInMountinfo(t *testing.T) { -+ //nolint:dupword // ignore duplicate words (sysfs sysfs) -+ const mountinfo = `18 62 0:17 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw,seclabel -+19 62 0:3 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw -+20 62 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,seclabel,size=3995472k,nr_inodes=998868,mode=755 -+21 18 0:16 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw -+22 20 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw,seclabel -+23 20 0:11 / /dev/pts rw,nosuid,noexec,relatime shared:4 - devpts devpts rw,seclabel,gid=5,mode=620,ptmxmode=000 -+24 62 0:19 / /run rw,nosuid,nodev shared:23 - tmpfs tmpfs rw,seclabel,mode=755 -+25 18 0:20 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:8 - tmpfs tmpfs ro,seclabel,mode=755 -+26 25 0:21 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:9 - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd -+27 18 0:22 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:20 - pstore pstore rw -+28 25 0:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,perf_event -+29 25 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,devices -+30 25 0:25 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:12 - cgroup cgroup rw,cpuacct,cpu -+31 25 0:26 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,freezer -+32 25 0:27 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,net_prio,net_cls -+33 25 0:28 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset -+34 25 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,memory -+35 25 0:30 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids -+36 25 0:31 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,hugetlb -+37 25 0:32 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,blkio -+59 18 0:33 / /sys/kernel/config rw,relatime shared:21 - configfs configfs rw -+62 1 253:1 / / rw,relatime shared:1 - ext4 /dev/vda1 rw,seclabel,data=ordered -+38 18 0:15 / /sys/fs/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw -+39 19 0:35 / /proc/sys/fs/binfmt_misc rw,relatime shared:24 - autofs systemd-1 rw,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=11601 -+40 20 0:36 / /dev/hugepages rw,relatime shared:25 - hugetlbfs hugetlbfs rw,seclabel -+41 20 0:14 / /dev/mqueue rw,relatime shared:26 - mqueue mqueue rw,seclabel -+42 18 0:6 / /sys/kernel/debug rw,relatime shared:27 - debugfs debugfs rw -+112 62 253:1 /var/lib/docker/plugins /var/lib/docker/plugins rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered -+115 62 253:1 /var/lib/docker/overlay2 /var/lib/docker/overlay2 rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered -+118 62 7:0 / /root/mnt rw,relatime shared:66 - ext4 /dev/loop0 rw,seclabel,data=ordered -+121 115 0:38 / /var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/merged rw,relatime - overlay overlay rw,seclabel,lowerdir=/var/lib/docker/overlay2/l/CPD4XI7UD4GGTGSJVPQSHWZKTK:/var/lib/docker/overlay2/l/NQKORR3IS7KNQDER35AZECLH4Z,upperdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/diff,workdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/work -+125 62 0:39 / /var/lib/docker/containers/5e3fce422957c291a5b502c2cf33d512fc1fcac424e4113136c808360e5b7215/shm rw,nosuid,nodev,noexec,relatime shared:68 - tmpfs shm rw,seclabel,size=65536k -+186 24 0:3 / /run/docker/netns/0a08e7496c6d rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw -+130 62 0:15 / /root/chroot/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw -+109 24 0:37 / /run/user/0 rw,nosuid,nodev,relatime shared:62 - tmpfs tmpfs rw,seclabel,size=801032k,mode=700 -+` -+ s := bufio.NewScanner(bytes.NewBuffer([]byte(mountinfo))) -+ for _, expected := range []string{"/sys/fs/selinux", "/root/chroot/selinux", ""} { -+ mnt := findSELinuxfsMount(s) -+ t.Logf("found %q", mnt) -+ if mnt != expected { -+ t.Fatalf("expected %q, got %q", expected, mnt) -+ } -+ } -+} -+ -+func TestSecurityCheckContext(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ // check with valid context -+ context, err := CurrentLabel() -+ if err != nil { -+ t.Fatalf("CurrentLabel() error: %v", err) -+ } -+ if context != "" { -+ t.Logf("SecurityCheckContext(%q)", context) -+ err = SecurityCheckContext(context) -+ if err != nil { -+ t.Errorf("SecurityCheckContext(%q) error: %v", context, err) -+ } -+ } -+ -+ context = "not-syntactically-valid" -+ err = SecurityCheckContext(context) -+ if err == nil { -+ t.Errorf("SecurityCheckContext(%q) succeeded, expected to fail", context) -+ } -+} -+ -+func TestClassIndex(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ idx, err := ClassIndex("process") -+ if err != nil { -+ t.Errorf("Classindex error: %v", err) -+ } -+ // Every known policy has process as index 2, but it isn't guaranteed -+ if idx != 2 { -+ t.Errorf("ClassIndex unexpected answer %d, possibly not reference policy", idx) -+ } -+ -+ _, err = ClassIndex("foobar") -+ if err == nil { -+ t.Errorf("ClassIndex(\"foobar\") succeeded, expected to fail:") -+ } -+} -+ -+func TestComputeCreateContext(t *testing.T) { -+ if !GetEnabled() { -+ t.Skip("SELinux not enabled, skipping.") -+ } -+ -+ // This may or may not be in the loaded policy but any refpolicy based policy should have it -+ init := "system_u:system_r:init_t:s0" -+ tmp := "system_u:object_r:tmp_t:s0" -+ file := "file" -+ t.Logf("ComputeCreateContext(%s, %s, %s)", init, tmp, file) -+ context, err := ComputeCreateContext(init, tmp, file) -+ if err != nil { -+ t.Errorf("ComputeCreateContext error: %v", err) -+ } -+ if context != "system_u:object_r:init_tmp_t:s0" { -+ t.Errorf("ComputeCreateContext unexpected answer %s, possibly not reference policy", context) -+ } -+ -+ badcon := "badcon" -+ process := "process" -+ // Test to ensure that a bad context returns an error -+ t.Logf("ComputeCreateContext(%s, %s, %s)", badcon, tmp, process) -+ _, err = ComputeCreateContext(badcon, tmp, process) -+ if err == nil { -+ t.Errorf("ComputeCreateContext(%s, %s, %s) succeeded, expected failure", badcon, tmp, process) -+ } -+} -+ -+func TestGlbLub(t *testing.T) { -+ tests := []struct { -+ expectedErr error -+ sourceRange string -+ targetRange string -+ expectedRange string -+ }{ -+ { -+ sourceRange: "s0:c0.c100-s10:c0.c150", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedRange: "s5:c50.c100-s10:c0.c149", -+ }, -+ { -+ sourceRange: "s5:c50.c100-s15:c0.c149", -+ targetRange: "s0:c0.c100-s10:c0.c150", -+ expectedRange: "s5:c50.c100-s10:c0.c149", -+ }, -+ { -+ sourceRange: "s0:c0.c100-s10:c0.c150", -+ targetRange: "s0", -+ expectedRange: "s0", -+ }, -+ { -+ sourceRange: "s6:c0.c1023", -+ targetRange: "s6:c0,c2,c11,c201.c429,c431.c511", -+ expectedRange: "s6:c0,c2,c11,c201.c429,c431.c511", -+ }, -+ { -+ sourceRange: "s0-s15:c0.c1023", -+ targetRange: "s6:c0,c2,c11,c201.c429,c431.c511", -+ expectedRange: "s6-s6:c0,c2,c11,c201.c429,c431.c511", -+ }, -+ { -+ sourceRange: "s0:c0.c100,c125,c140,c150-s10", -+ targetRange: "s4:c0.c50,c140", -+ expectedRange: "s4:c0.c50,c140-s4", -+ }, -+ { -+ sourceRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023", -+ targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023", -+ expectedRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023", -+ }, -+ { -+ sourceRange: "s5:c512.c540,c542,c543,c552.c1023-s5:c0.c550,c552.c1023", -+ targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023", -+ expectedRange: "s5:c512.c540,c542,c543,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023", -+ }, -+ { -+ sourceRange: "s5:c50.c100-s15:c0.c149", -+ targetRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023", -+ expectedRange: "s5-s5:c0.c149", -+ }, -+ { -+ sourceRange: "s5-s15", -+ targetRange: "s6-s7", -+ expectedRange: "s6-s7", -+ }, -+ { -+ sourceRange: "s5:c50.c100-s15:c0.c149", -+ targetRange: "s4-s4:c0.c1023", -+ expectedErr: ErrIncomparable, -+ }, -+ { -+ sourceRange: "s4-s4:c0.c1023", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedErr: ErrIncomparable, -+ }, -+ { -+ sourceRange: "s4-s4:c0.c1023.c10000", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedErr: strconv.ErrSyntax, -+ }, -+ { -+ sourceRange: "s4-s4:c0.c1023.c10000-s4", -+ targetRange: "s5:c50.c100-s15:c0.c149-s5", -+ expectedErr: strconv.ErrSyntax, -+ }, -+ { -+ sourceRange: "4-4", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedErr: ErrLevelSyntax, -+ }, -+ { -+ sourceRange: "t4-t4", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedErr: ErrLevelSyntax, -+ }, -+ { -+ sourceRange: "s5:x50.x100-s15:c0.c149", -+ targetRange: "s5:c50.c100-s15:c0.c149", -+ expectedErr: ErrLevelSyntax, -+ }, -+ } -+ -+ for _, tt := range tests { -+ got, err := CalculateGlbLub(tt.sourceRange, tt.targetRange) -+ if !errors.Is(err, tt.expectedErr) { -+ // Go 1.13 strconv errors are not unwrappable, -+ // so do that manually. -+ // TODO remove this once we stop supporting Go 1.13. -+ var numErr *strconv.NumError -+ if errors.As(err, &numErr) && numErr.Err == tt.expectedErr { //nolint:errorlint // see above -+ continue -+ } -+ t.Fatalf("want %q got %q: src: %q tgt: %q", tt.expectedErr, err, tt.sourceRange, tt.targetRange) -+ } -+ -+ if got != tt.expectedRange { -+ t.Errorf("want %q got %q", tt.expectedRange, got) -+ } -+ } -+} -+ -+func TestContextWithLevel(t *testing.T) { -+ want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh" -+ -+ goodDefaultBuff := ` -+foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 -+staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 -+` -+ -+ verifier := func(con string) error { -+ if con != want { -+ return fmt.Errorf("invalid context %s", con) -+ } -+ -+ return nil -+ } -+ -+ tests := []struct { -+ name, userBuff, defaultBuff string -+ }{ -+ { -+ name: "match exists in user context file", -+ userBuff: `# COMMENT -+foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 -+ -+staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 -+`, -+ defaultBuff: goodDefaultBuff, -+ }, -+ { -+ name: "match exists in default context file, but not in user file", -+ userBuff: `# COMMENT -+foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 -+fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 -+`, -+ defaultBuff: goodDefaultBuff, -+ }, -+ } -+ -+ for _, tt := range tests { -+ t.Run(tt.name, func(t *testing.T) { -+ c := defaultSECtx{ -+ user: "bob", -+ level: "SystemLow-SystemHigh", -+ scon: "system_u:staff_r:staff_t:s0", -+ userRdr: bytes.NewBufferString(tt.userBuff), -+ defaultRdr: bytes.NewBufferString(tt.defaultBuff), -+ verifier: verifier, -+ } -+ -+ got, err := getDefaultContextFromReaders(&c) -+ if err != nil { -+ t.Fatalf("err should not exist but is: %v", err) -+ } -+ -+ if got != want { -+ t.Fatalf("got context: %q but expected %q", got, want) -+ } -+ }) -+ } -+ -+ t.Run("no match in user or default context files", func(t *testing.T) { -+ badUserBuff := "" -+ -+ badDefaultBuff := ` -+ foo_r:foo_t:s0 sysadm_r:sysadm_t:s0 -+ dne_r:dne_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0 -+ ` -+ c := defaultSECtx{ -+ user: "bob", -+ level: "SystemLow-SystemHigh", -+ scon: "system_u:staff_r:staff_t:s0", -+ userRdr: bytes.NewBufferString(badUserBuff), -+ defaultRdr: bytes.NewBufferString(badDefaultBuff), -+ verifier: verifier, -+ } -+ -+ _, err := getDefaultContextFromReaders(&c) -+ if err == nil { -+ t.Fatalf("err was expected") -+ } -+ }) -+} -+ -+func BenchmarkChcon(b *testing.B) { -+ file, err := filepath.Abs(os.Args[0]) -+ if err != nil { -+ b.Fatalf("filepath.Abs: %v", err) -+ } -+ dir := filepath.Dir(file) -+ con, err := FileLabel(file) -+ if err != nil { -+ b.Fatalf("FileLabel(%q): %v", file, err) -+ } -+ b.Logf("Chcon(%q, %q)", dir, con) -+ b.ResetTimer() -+ for n := 0; n < b.N; n++ { -+ if err := Chcon(dir, con, true); err != nil { -+ b.Fatal(err) -+ } -+ } -+} -+ -+func BenchmarkCurrentLabel(b *testing.B) { -+ var ( -+ l string -+ err error -+ ) -+ for n := 0; n < b.N; n++ { -+ l, err = CurrentLabel() -+ if err != nil { -+ b.Fatal(err) -+ } -+ } -+ b.Log(l) -+} -+ -+func BenchmarkReadConfig(b *testing.B) { -+ str := "" -+ for n := 0; n < b.N; n++ { -+ str = readConfig(selinuxTypeTag) -+ } -+ b.Log(str) -+} -+ -+func BenchmarkLoadLabels(b *testing.B) { -+ for n := 0; n < b.N; n++ { -+ loadLabels() -+ } -+} -diff --git a/internal/third_party/selinux/go-selinux/selinux_stub.go b/internal/third_party/selinux/go-selinux/selinux_stub.go -new file mode 100644 -index 00000000..26792123 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/selinux_stub.go -@@ -0,0 +1,159 @@ -+//go:build !linux -+// +build !linux -+ -+package selinux -+ -+func attrPath(string) string { -+ return "" -+} -+ -+func readConThreadSelf(string) (string, error) { -+ return "", nil -+} -+ -+func writeConThreadSelf(string, string) error { -+ return nil -+} -+ -+func setDisabled() {} -+ -+func getEnabled() bool { -+ return false -+} -+ -+func classIndex(string) (int, error) { -+ return -1, nil -+} -+ -+func setFileLabel(string, string) error { -+ return nil -+} -+ -+func lSetFileLabel(string, string) error { -+ return nil -+} -+ -+func fileLabel(string) (string, error) { -+ return "", nil -+} -+ -+func lFileLabel(string) (string, error) { -+ return "", nil -+} -+ -+func setFSCreateLabel(string) error { -+ return nil -+} -+ -+func fsCreateLabel() (string, error) { -+ return "", nil -+} -+ -+func currentLabel() (string, error) { -+ return "", nil -+} -+ -+func pidLabel(int) (string, error) { -+ return "", nil -+} -+ -+func execLabel() (string, error) { -+ return "", nil -+} -+ -+func canonicalizeContext(string) (string, error) { -+ return "", nil -+} -+ -+func computeCreateContext(string, string, string) (string, error) { -+ return "", nil -+} -+ -+func calculateGlbLub(string, string) (string, error) { -+ return "", nil -+} -+ -+func peerLabel(uintptr) (string, error) { -+ return "", nil -+} -+ -+func setKeyLabel(string) error { -+ return nil -+} -+ -+func keyLabel() (string, error) { -+ return "", nil -+} -+ -+func (c Context) get() string { -+ return "" -+} -+ -+func newContext(string) (Context, error) { -+ return Context{}, nil -+} -+ -+func clearLabels() { -+} -+ -+func reserveLabel(string) { -+} -+ -+func isMLSEnabled() bool { -+ return false -+} -+ -+func enforceMode() int { -+ return Disabled -+} -+ -+func setEnforceMode(int) error { -+ return nil -+} -+ -+func defaultEnforceMode() int { -+ return Disabled -+} -+ -+func releaseLabel(string) { -+} -+ -+func roFileLabel() string { -+ return "" -+} -+ -+func kvmContainerLabels() (string, string) { -+ return "", "" -+} -+ -+func initContainerLabels() (string, string) { -+ return "", "" -+} -+ -+func containerLabels() (string, string) { -+ return "", "" -+} -+ -+func securityCheckContext(string) error { -+ return nil -+} -+ -+func copyLevel(string, string) (string, error) { -+ return "", nil -+} -+ -+func chcon(string, string, bool) error { -+ return nil -+} -+ -+func dupSecOpt(string) ([]string, error) { -+ return nil, nil -+} -+ -+func getDefaultContextWithLevel(string, string, string) (string, error) { -+ return "", nil -+} -+ -+func label(_ string) string { -+ return "" -+} -diff --git a/internal/third_party/selinux/go-selinux/selinux_stub_test.go b/internal/third_party/selinux/go-selinux/selinux_stub_test.go -new file mode 100644 -index 00000000..19ea636a ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/selinux_stub_test.go -@@ -0,0 +1,127 @@ -+//go:build !linux -+// +build !linux -+ -+package selinux -+ -+import ( -+ "testing" -+) -+ -+const testLabel = "foobar" -+ -+func TestSELinuxStubs(t *testing.T) { -+ if GetEnabled() { -+ t.Error("SELinux enabled on non-linux.") -+ } -+ -+ tmpDir := t.TempDir() -+ if _, err := FileLabel(tmpDir); err != nil { -+ t.Error(err) -+ } -+ -+ if err := SetFileLabel(tmpDir, testLabel); err != nil { -+ t.Error(err) -+ } -+ -+ if _, err := LfileLabel(tmpDir); err != nil { -+ t.Error(err) -+ } -+ if err := LsetFileLabel(tmpDir, testLabel); err != nil { -+ t.Error(err) -+ } -+ -+ if err := SetFSCreateLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ -+ if _, err := FSCreateLabel(); err != nil { -+ t.Error(err) -+ } -+ if _, err := CurrentLabel(); err != nil { -+ t.Error(err) -+ } -+ -+ if _, err := PidLabel(0); err != nil { -+ t.Error(err) -+ } -+ -+ ClearLabels() -+ -+ ReserveLabel(testLabel) -+ ReleaseLabel(testLabel) -+ if _, err := DupSecOpt(testLabel); err != nil { -+ t.Error(err) -+ } -+ if v := DisableSecOpt(); len(v) != 1 || v[0] != "disable" { -+ t.Errorf(`expected "disabled", got %v`, v) -+ } -+ SetDisabled() -+ if enabled := GetEnabled(); enabled { -+ t.Error("Should not be enabled") -+ } -+ if err := SetExecLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ if err := SetTaskLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := ExecLabel(); err != nil { -+ t.Error(err) -+ } -+ if _, err := CanonicalizeContext(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := ComputeCreateContext("foo", "bar", testLabel); err != nil { -+ t.Error(err) -+ } -+ if err := SetSocketLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := ClassIndex(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := SocketLabel(); err != nil { -+ t.Error(err) -+ } -+ if _, err := PeerLabel(0); err != nil { -+ t.Error(err) -+ } -+ if err := SetKeyLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := KeyLabel(); err != nil { -+ t.Error(err) -+ } -+ if err := SetExecLabel(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err := ExecLabel(); err != nil { -+ t.Error(err) -+ } -+ con, err := NewContext(testLabel) -+ if err != nil { -+ t.Error(err) -+ } -+ con.Get() -+ if err = SetEnforceMode(1); err != nil { -+ t.Error(err) -+ } -+ if v := DefaultEnforceMode(); v != Disabled { -+ t.Errorf("expected %d, got %d", Disabled, v) -+ } -+ if v := EnforceMode(); v != Disabled { -+ t.Errorf("expected %d, got %d", Disabled, v) -+ } -+ if v := ROFileLabel(); v != "" { -+ t.Errorf(`expected "", got %q`, v) -+ } -+ if processLbl, fileLbl := ContainerLabels(); processLbl != "" || fileLbl != "" { -+ t.Errorf(`expected fileLbl="", fileLbl="" got processLbl=%q, fileLbl=%q`, processLbl, fileLbl) -+ } -+ if err = SecurityCheckContext(testLabel); err != nil { -+ t.Error(err) -+ } -+ if _, err = CopyLevel("foo", "bar"); err != nil { -+ t.Error(err) -+ } -+} -diff --git a/internal/third_party/selinux/go-selinux/xattrs_linux.go b/internal/third_party/selinux/go-selinux/xattrs_linux.go -new file mode 100644 -index 00000000..559c8510 ---- /dev/null -+++ b/internal/third_party/selinux/go-selinux/xattrs_linux.go -@@ -0,0 +1,71 @@ -+package selinux -+ -+import ( -+ "golang.org/x/sys/unix" -+) -+ -+// lgetxattr returns a []byte slice containing the value of -+// an extended attribute attr set for path. -+func lgetxattr(path, attr string) ([]byte, error) { -+ // Start with a 128 length byte array -+ dest := make([]byte, 128) -+ sz, errno := doLgetxattr(path, attr, dest) -+ for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare -+ // Buffer too small, use zero-sized buffer to get the actual size -+ sz, errno = doLgetxattr(path, attr, []byte{}) -+ if errno != nil { -+ return nil, errno -+ } -+ -+ dest = make([]byte, sz) -+ sz, errno = doLgetxattr(path, attr, dest) -+ } -+ if errno != nil { -+ return nil, errno -+ } -+ -+ return dest[:sz], nil -+} -+ -+// doLgetxattr is a wrapper that retries on EINTR -+func doLgetxattr(path, attr string, dest []byte) (int, error) { -+ for { -+ sz, err := unix.Lgetxattr(path, attr, dest) -+ if err != unix.EINTR { -+ return sz, err -+ } -+ } -+} -+ -+// getxattr returns a []byte slice containing the value of -+// an extended attribute attr set for path. -+func getxattr(path, attr string) ([]byte, error) { -+ // Start with a 128 length byte array -+ dest := make([]byte, 128) -+ sz, errno := dogetxattr(path, attr, dest) -+ for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare -+ // Buffer too small, use zero-sized buffer to get the actual size -+ sz, errno = dogetxattr(path, attr, []byte{}) -+ if errno != nil { -+ return nil, errno -+ } -+ -+ dest = make([]byte, sz) -+ sz, errno = dogetxattr(path, attr, dest) -+ } -+ if errno != nil { -+ return nil, errno -+ } -+ -+ return dest[:sz], nil -+} -+ -+// dogetxattr is a wrapper that retries on EINTR -+func dogetxattr(path, attr string, dest []byte) (int, error) { -+ for { -+ sz, err := unix.Getxattr(path, attr, dest) -+ if err != unix.EINTR { -+ return sz, err -+ } -+ } -+} -diff --git a/internal/third_party/selinux/go.mod b/internal/third_party/selinux/go.mod -new file mode 100644 -index 00000000..24d3261a ---- /dev/null -+++ b/internal/third_party/selinux/go.mod -@@ -0,0 +1,8 @@ -+module github.com/opencontainers/selinux -+ -+go 1.19 -+ -+require ( -+ github.com/cyphar/filepath-securejoin v0.5.0 -+ golang.org/x/sys v0.18.0 -+) -diff --git a/internal/third_party/selinux/go.sum b/internal/third_party/selinux/go.sum -new file mode 100644 -index 00000000..b9ae0987 ---- /dev/null -+++ b/internal/third_party/selinux/go.sum -@@ -0,0 +1,8 @@ -+github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw= -+github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= -+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -+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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -diff --git a/internal/third_party/selinux/pkg/pwalk/README.md b/internal/third_party/selinux/pkg/pwalk/README.md -new file mode 100644 -index 00000000..a060ad36 ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalk/README.md -@@ -0,0 +1,52 @@ -+## pwalk: parallel implementation of filepath.Walk -+ -+This is a wrapper for [filepath.Walk](https://pkg.go.dev/path/filepath?tab=doc#Walk) -+which may speed it up by calling multiple callback functions (WalkFunc) in parallel, -+utilizing goroutines. -+ -+By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks. -+This can be changed by using WalkN function which has the additional -+parameter, specifying the number of goroutines (concurrency). -+ -+### pwalk vs pwalkdir -+ -+This package is deprecated in favor of -+[pwalkdir](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir), -+which is faster, but requires at least Go 1.16. -+ -+### Caveats -+ -+Please note the following limitations of this code: -+ -+* Unlike filepath.Walk, the order of calls is non-deterministic; -+ -+* Only primitive error handling is supported: -+ -+ * filepath.SkipDir is not supported; -+ -+ * ErrNotExist errors from filepath.Walk are silently ignored for any path -+ except the top directory (Walk argument); any other error is returned to -+ the caller of Walk; -+ -+ * no errors are ever passed to WalkFunc; -+ -+ * once any error is returned from any WalkFunc instance, no more new calls -+ to WalkFunc are made, and the error is returned to the caller of Walk; -+ -+ * if more than one walkFunc instance will return an error, only one -+ of such errors will be propagated and returned by Walk, others -+ will be silently discarded. -+ -+### Documentation -+ -+For the official documentation, see -+https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalk?tab=doc -+ -+### Benchmarks -+ -+For a WalkFunc that consists solely of the return statement, this -+implementation is about 10% slower than the standard library's -+filepath.Walk. -+ -+Otherwise (if a WalkFunc is doing something) this is usually faster, -+except when the WalkN(..., 1) is used. -diff --git a/internal/third_party/selinux/pkg/pwalk/pwalk.go b/internal/third_party/selinux/pkg/pwalk/pwalk.go -new file mode 100644 -index 00000000..686c8bac ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalk/pwalk.go -@@ -0,0 +1,131 @@ -+package pwalk -+ -+import ( -+ "errors" -+ "fmt" -+ "os" -+ "path/filepath" -+ "runtime" -+ "sync" -+) -+ -+// WalkFunc is the type of the function called by Walk to visit each -+// file or directory. It is an alias for [filepath.WalkFunc]. -+// -+// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir] and [fs.WalkDirFunc]. -+type WalkFunc = filepath.WalkFunc -+ -+// Walk is a wrapper for filepath.Walk which can call multiple walkFn -+// in parallel, allowing to handle each item concurrently. A maximum of -+// twice the runtime.NumCPU() walkFn will be called at any one time. -+// If you want to change the maximum, use WalkN instead. -+// -+// The order of calls is non-deterministic. -+// -+// Note that this implementation only supports primitive error handling: -+// -+// - no errors are ever passed to walkFn; -+// -+// - once a walkFn returns any error, all further processing stops -+// and the error is returned to the caller of Walk; -+// -+// - filepath.SkipDir is not supported; -+// -+// - if more than one walkFn instance will return an error, only one -+// of such errors will be propagated and returned by Walk, others -+// will be silently discarded. -+// -+// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.Walk] -+func Walk(root string, walkFn WalkFunc) error { -+ return WalkN(root, walkFn, runtime.NumCPU()*2) -+} -+ -+// WalkN is a wrapper for filepath.Walk which can call multiple walkFn -+// in parallel, allowing to handle each item concurrently. A maximum of -+// num walkFn will be called at any one time. -+// -+// Please see Walk documentation for caveats of using this function. -+// -+// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.WalkN] -+func WalkN(root string, walkFn WalkFunc, num int) error { -+ // make sure limit is sensible -+ if num < 1 { -+ return fmt.Errorf("walk(%q): num must be > 0", root) -+ } -+ -+ files := make(chan *walkArgs, 2*num) -+ errCh := make(chan error, 1) // get the first error, ignore others -+ -+ // Start walking a tree asap -+ var ( -+ err error -+ wg sync.WaitGroup -+ -+ rootLen = len(root) -+ rootEntry *walkArgs -+ ) -+ wg.Add(1) -+ go func() { -+ err = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { -+ if err != nil { -+ // Walking a file tree can race with removal, -+ // so ignore ENOENT, except for root. -+ // https://github.com/opencontainers/selinux/issues/199. -+ if errors.Is(err, os.ErrNotExist) && len(p) != rootLen { -+ return nil -+ } -+ -+ close(files) -+ return err -+ } -+ if len(p) == rootLen { -+ // Root entry is processed separately below. -+ rootEntry = &walkArgs{path: p, info: &info} -+ return nil -+ } -+ // add a file to the queue unless a callback sent an error -+ select { -+ case e := <-errCh: -+ close(files) -+ return e -+ default: -+ files <- &walkArgs{path: p, info: &info} -+ return nil -+ } -+ }) -+ if err == nil { -+ close(files) -+ } -+ wg.Done() -+ }() -+ -+ wg.Add(num) -+ for i := 0; i < num; i++ { -+ go func() { -+ for file := range files { -+ if e := walkFn(file.path, *file.info, nil); e != nil { -+ select { -+ case errCh <- e: // sent ok -+ default: // buffer full -+ } -+ } -+ } -+ wg.Done() -+ }() -+ } -+ -+ wg.Wait() -+ -+ if err == nil { -+ err = walkFn(rootEntry.path, *rootEntry.info, nil) -+ } -+ -+ return err -+} -+ -+// walkArgs holds the arguments that were passed to the Walk or WalkN -+// functions. -+type walkArgs struct { -+ info *os.FileInfo -+ path string -+} -diff --git a/internal/third_party/selinux/pkg/pwalk/pwalk_test.go b/internal/third_party/selinux/pkg/pwalk/pwalk_test.go -new file mode 100644 -index 00000000..9cca3b6b ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalk/pwalk_test.go -@@ -0,0 +1,236 @@ -+package pwalk -+ -+import ( -+ "errors" -+ "math/rand" -+ "os" -+ "path/filepath" -+ "runtime" -+ "sync/atomic" -+ "testing" -+ "time" -+) -+ -+func TestWalk(t *testing.T) { -+ var ac atomic.Uint32 -+ concurrency := runtime.NumCPU() * 2 -+ -+ dir, total := prepareTestSet(t, 3, 2, 1) -+ -+ err := WalkN(dir, -+ func(_ string, _ os.FileInfo, _ error) error { -+ ac.Add(1) -+ return nil -+ }, -+ concurrency) -+ if err != nil { -+ t.Errorf("Walk failed: %v", err) -+ } -+ count := ac.Load() -+ if count != total { -+ t.Errorf("File count mismatch: found %d, expected %d", count, total) -+ } -+ -+ t.Logf("concurrency: %d, files found: %d", concurrency, count) -+} -+ -+func TestWalkTopLevelErrNotExistNotIgnored(t *testing.T) { -+ if WalkN("non-existent-directory", cbEmpty, 8) == nil { -+ t.Fatal("expected ErrNotExist, got nil") -+ } -+} -+ -+// https://github.com/opencontainers/selinux/issues/199 -+func TestWalkRaceWithRemoval(t *testing.T) { -+ var ac atomic.Uint32 -+ concurrency := runtime.NumCPU() * 2 -+ // This test is still on a best-effort basis, meaning it can still pass -+ // when there is a bug in the code, but the larger the test set is, the -+ // higher the probability that this test fails (without a fix). -+ // -+ // With this set (4, 5, 6), and the fix commented out, it fails -+ // 100 out of 100 runs on my machine. -+ dir, total := prepareTestSet(t, 4, 5, 6) -+ -+ // Race walk with removal. -+ go os.RemoveAll(dir) -+ err := WalkN(dir, -+ func(_ string, _ os.FileInfo, _ error) error { -+ ac.Add(1) -+ return nil -+ }, -+ concurrency) -+ count := int(ac.Load()) -+ t.Logf("found %d of %d files", count, total) -+ if err != nil { -+ t.Fatalf("expected nil, got %v", err) -+ } -+} -+ -+func TestWalkDirManyErrors(t *testing.T) { -+ var ac atomic.Uint32 -+ -+ dir, total := prepareTestSet(t, 3, 3, 2) -+ -+ maxFiles := total / 2 -+ e42 := errors.New("42") -+ err := Walk(dir, -+ func(_ string, _ os.FileInfo, _ error) error { -+ if ac.Add(1) > maxFiles { -+ return e42 -+ } -+ return nil -+ }) -+ count := ac.Load() -+ t.Logf("found %d of %d files", count, total) -+ -+ if err == nil { -+ t.Errorf("Walk succeeded, but error is expected") -+ if count != total { -+ t.Errorf("File count mismatch: found %d, expected %d", count, total) -+ } -+ } -+} -+ -+func makeManyDirs(prefix string, levels, dirs, files int) (count uint32, err error) { -+ for d := 0; d < dirs; d++ { -+ var dir string -+ dir, err = os.MkdirTemp(prefix, "d-") -+ if err != nil { -+ return count, err -+ } -+ count++ -+ for f := 0; f < files; f++ { -+ var fi *os.File -+ fi, err = os.CreateTemp(dir, "f-") -+ if err != nil { -+ return count, err -+ } -+ _ = fi.Close() -+ count++ -+ } -+ if levels == 0 { -+ continue -+ } -+ var c uint32 -+ if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil { -+ return count, err -+ } -+ count += c -+ } -+ -+ return count, err -+} -+ -+// prepareTestSet() creates a directory tree of shallow files, -+// to be used for testing or benchmarking. -+// -+// Total dirs: dirs^levels + dirs^(levels-1) + ... + dirs^1 -+// Total files: total_dirs * files -+func prepareTestSet(tb testing.TB, levels, dirs, files int) (dir string, total uint32) { -+ tb.Helper() -+ var err error -+ -+ dir = tb.TempDir() -+ total, err = makeManyDirs(dir, levels, dirs, files) -+ if err != nil { -+ tb.Fatal(err) -+ } -+ total++ // this dir -+ -+ return dir, total -+} -+ -+type walkerFunc func(root string, walkFn WalkFunc) error -+ -+func genWalkN(n int) walkerFunc { -+ return func(root string, walkFn WalkFunc) error { -+ return WalkN(root, walkFn, n) -+ } -+} -+ -+func BenchmarkWalk(b *testing.B) { -+ const ( -+ levels = 5 // how deep -+ dirs = 3 // dirs on each levels -+ files = 8 // files on each levels -+ ) -+ -+ benchmarks := []struct { -+ walk filepath.WalkFunc -+ name string -+ }{ -+ {name: "Empty", walk: cbEmpty}, -+ {name: "ReadFile", walk: cbReadFile}, -+ {name: "ChownChmod", walk: cbChownChmod}, -+ {name: "RandomSleep", walk: cbRandomSleep}, -+ } -+ -+ walkers := []struct { -+ walker walkerFunc -+ name string -+ }{ -+ {name: "filepath.Walk", walker: filepath.Walk}, -+ {name: "pwalk.Walk", walker: Walk}, -+ // test WalkN with various values of N -+ {name: "pwalk.Walk1", walker: genWalkN(1)}, -+ {name: "pwalk.Walk2", walker: genWalkN(2)}, -+ {name: "pwalk.Walk4", walker: genWalkN(4)}, -+ {name: "pwalk.Walk8", walker: genWalkN(8)}, -+ {name: "pwalk.Walk16", walker: genWalkN(16)}, -+ {name: "pwalk.Walk32", walker: genWalkN(32)}, -+ {name: "pwalk.Walk64", walker: genWalkN(64)}, -+ {name: "pwalk.Walk128", walker: genWalkN(128)}, -+ {name: "pwalk.Walk256", walker: genWalkN(256)}, -+ } -+ -+ dir, total := prepareTestSet(b, levels, dirs, files) -+ b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total) -+ -+ for _, bm := range benchmarks { -+ for _, w := range walkers { -+ walker := w.walker -+ walkFn := bm.walk -+ // preheat -+ if err := w.walker(dir, bm.walk); err != nil { -+ b.Errorf("walk failed: %v", err) -+ } -+ // benchmark -+ b.Run(bm.name+"/"+w.name, func(b *testing.B) { -+ for i := 0; i < b.N; i++ { -+ if err := walker(dir, walkFn); err != nil { -+ b.Errorf("walk failed: %v", err) -+ } -+ } -+ }) -+ } -+ } -+} -+ -+func cbEmpty(_ string, _ os.FileInfo, _ error) error { -+ return nil -+} -+ -+func cbChownChmod(path string, info os.FileInfo, _ error) error { -+ _ = os.Chown(path, 0, 0) -+ mode := os.FileMode(0o644) -+ if info.Mode().IsDir() { -+ mode = os.FileMode(0o755) -+ } -+ _ = os.Chmod(path, mode) -+ -+ return nil -+} -+ -+func cbReadFile(path string, info os.FileInfo, _ error) error { -+ var err error -+ if info.Mode().IsRegular() { -+ _, err = os.ReadFile(path) -+ } -+ return err -+} -+ -+func cbRandomSleep(_ string, _ os.FileInfo, _ error) error { -+ time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator -+ return nil -+} -diff --git a/internal/third_party/selinux/pkg/pwalkdir/README.md b/internal/third_party/selinux/pkg/pwalkdir/README.md -new file mode 100644 -index 00000000..b827e7dd ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalkdir/README.md -@@ -0,0 +1,56 @@ -+## pwalkdir: parallel implementation of filepath.WalkDir -+ -+This is a wrapper for [filepath.WalkDir](https://pkg.go.dev/path/filepath#WalkDir) -+which may speed it up by calling multiple callback functions (WalkDirFunc) -+in parallel, utilizing goroutines. -+ -+By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks. -+This can be changed by using WalkN function which has the additional -+parameter, specifying the number of goroutines (concurrency). -+ -+### pwalk vs pwalkdir -+ -+This package is very similar to -+[pwalk](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir), -+but utilizes `filepath.WalkDir` (added to Go 1.16), which does not call stat(2) -+on every entry and is therefore faster (up to 3x, depending on usage scenario). -+ -+Users who are OK with requiring Go 1.16+ should switch to this -+implementation. -+ -+### Caveats -+ -+Please note the following limitations of this code: -+ -+* Unlike filepath.WalkDir, the order of calls is non-deterministic; -+ -+* Only primitive error handling is supported: -+ -+ * fs.SkipDir is not supported; -+ -+ * ErrNotExist errors from filepath.WalkDir are silently ignored for any path -+ except the top directory (WalkDir argument); any other error is returned to -+ the caller of WalkDir; -+ -+ * once any error is returned from any walkDirFunc instance, no more calls -+ to WalkDirFunc are made, and the error is returned to the caller of WalkDir; -+ -+ * if more than one WalkDirFunc instance will return an error, only one -+ of such errors will be propagated to and returned by WalkDir, others -+ will be silently discarded. -+ -+### Documentation -+ -+For the official documentation, see -+https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir -+ -+### Benchmarks -+ -+For a WalkDirFunc that consists solely of the return statement, this -+implementation is about 15% slower than the standard library's -+filepath.WalkDir. -+ -+Otherwise (if a WalkDirFunc is actually doing something) this is usually -+faster, except when the WalkDirN(..., 1) is used. Run `go test -bench .` -+to see how different operations can benefit from it, as well as how the -+level of parallelism affects the speed. -diff --git a/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go -new file mode 100644 -index 00000000..5d2d09a2 ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go -@@ -0,0 +1,123 @@ -+//go:build go1.16 -+// +build go1.16 -+ -+package pwalkdir -+ -+import ( -+ "errors" -+ "fmt" -+ "io/fs" -+ "path/filepath" -+ "runtime" -+ "sync" -+) -+ -+// Walk is a wrapper for filepath.WalkDir which can call multiple walkFn -+// in parallel, allowing to handle each item concurrently. A maximum of -+// twice the runtime.NumCPU() walkFn will be called at any one time. -+// If you want to change the maximum, use WalkN instead. -+// -+// The order of calls is non-deterministic. -+// -+// Note that this implementation only supports primitive error handling: -+// -+// - no errors are ever passed to walkFn; -+// -+// - once a walkFn returns any error, all further processing stops -+// and the error is returned to the caller of Walk; -+// -+// - filepath.SkipDir is not supported; -+// -+// - if more than one walkFn instance will return an error, only one -+// of such errors will be propagated and returned by Walk, others -+// will be silently discarded. -+func Walk(root string, walkFn fs.WalkDirFunc) error { -+ return WalkN(root, walkFn, runtime.NumCPU()*2) -+} -+ -+// WalkN is a wrapper for filepath.WalkDir which can call multiple walkFn -+// in parallel, allowing to handle each item concurrently. A maximum of -+// num walkFn will be called at any one time. -+// -+// Please see Walk documentation for caveats of using this function. -+func WalkN(root string, walkFn fs.WalkDirFunc, num int) error { -+ // make sure limit is sensible -+ if num < 1 { -+ return fmt.Errorf("walk(%q): num must be > 0", root) -+ } -+ -+ files := make(chan *walkArgs, 2*num) -+ errCh := make(chan error, 1) // Get the first error, ignore others. -+ -+ // Start walking a tree asap. -+ var ( -+ err error -+ wg sync.WaitGroup -+ -+ rootLen = len(root) -+ rootEntry *walkArgs -+ ) -+ wg.Add(1) -+ go func() { -+ err = filepath.WalkDir(root, func(p string, entry fs.DirEntry, err error) error { -+ if err != nil { -+ // Walking a file tree can race with removal, -+ // so ignore ENOENT, except for root. -+ // https://github.com/opencontainers/selinux/issues/199. -+ if errors.Is(err, fs.ErrNotExist) && len(p) != rootLen { -+ return nil -+ } -+ close(files) -+ return err -+ } -+ if len(p) == rootLen { -+ // Root entry is processed separately below. -+ rootEntry = &walkArgs{path: p, entry: entry} -+ return nil -+ } -+ // Add a file to the queue unless a callback sent an error. -+ select { -+ case e := <-errCh: -+ close(files) -+ return e -+ default: -+ files <- &walkArgs{path: p, entry: entry} -+ return nil -+ } -+ }) -+ if err == nil { -+ close(files) -+ } -+ wg.Done() -+ }() -+ -+ wg.Add(num) -+ for i := 0; i < num; i++ { -+ go func() { -+ for file := range files { -+ if e := walkFn(file.path, file.entry, nil); e != nil { -+ select { -+ case errCh <- e: // sent ok -+ default: // buffer full -+ } -+ } -+ } -+ wg.Done() -+ }() -+ } -+ -+ wg.Wait() -+ -+ if err == nil { -+ err = walkFn(rootEntry.path, rootEntry.entry, nil) -+ } -+ -+ return err -+} -+ -+// walkArgs holds the arguments that were passed to the Walk or WalkN -+// functions. -+type walkArgs struct { -+ entry fs.DirEntry -+ path string -+} -diff --git a/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go -new file mode 100644 -index 00000000..e66a80d1 ---- /dev/null -+++ b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go -@@ -0,0 +1,239 @@ -+//go:build go1.16 -+// +build go1.16 -+ -+package pwalkdir -+ -+import ( -+ "errors" -+ "io/fs" -+ "math/rand" -+ "os" -+ "path/filepath" -+ "runtime" -+ "sync/atomic" -+ "testing" -+ "time" -+) -+ -+func TestWalkDir(t *testing.T) { -+ var ac atomic.Uint32 -+ concurrency := runtime.NumCPU() * 2 -+ dir, total := prepareTestSet(t, 3, 2, 1) -+ -+ err := WalkN(dir, -+ func(_ string, _ fs.DirEntry, _ error) error { -+ ac.Add(1) -+ return nil -+ }, -+ concurrency) -+ if err != nil { -+ t.Errorf("Walk failed: %v", err) -+ } -+ count := ac.Load() -+ if count != total { -+ t.Errorf("File count mismatch: found %d, expected %d", count, total) -+ } -+ -+ t.Logf("concurrency: %d, files found: %d", concurrency, count) -+} -+ -+func TestWalkDirTopLevelErrNotExistNotIgnored(t *testing.T) { -+ err := WalkN("non-existent-directory", cbEmpty, 8) -+ if err == nil { -+ t.Fatal("expected ErrNotExist, got nil") -+ } -+} -+ -+// https://github.com/opencontainers/selinux/issues/199 -+func TestWalkDirRaceWithRemoval(t *testing.T) { -+ var ac atomic.Uint32 -+ concurrency := runtime.NumCPU() * 2 -+ // This test is still on a best-effort basis, meaning it can still pass -+ // when there is a bug in the code, but the larger the test set is, the -+ // higher the probability that this test fails (without a fix). -+ // -+ // With this set (4, 5, 6), and the fix commented out, it fails -+ // about 90 out of 100 runs on my machine. -+ dir, total := prepareTestSet(t, 4, 5, 6) -+ -+ // Make walk race with removal. -+ go os.RemoveAll(dir) -+ err := WalkN(dir, -+ func(_ string, _ fs.DirEntry, _ error) error { -+ ac.Add(1) -+ return nil -+ }, -+ concurrency) -+ count := ac.Load() -+ t.Logf("found %d of %d files", count, total) -+ if err != nil { -+ t.Fatalf("expected nil, got %v", err) -+ } -+} -+ -+func TestWalkDirManyErrors(t *testing.T) { -+ var ac atomic.Uint32 -+ dir, total := prepareTestSet(t, 3, 3, 2) -+ -+ maxFiles := total / 2 -+ e42 := errors.New("42") -+ err := Walk(dir, -+ func(_ string, _ fs.DirEntry, _ error) error { -+ if ac.Add(1) > maxFiles { -+ return e42 -+ } -+ return nil -+ }) -+ count := ac.Load() -+ t.Logf("found %d of %d files", count, total) -+ -+ if err == nil { -+ t.Error("Walk succeeded, but error is expected") -+ if count != total { -+ t.Errorf("File count mismatch: found %d, expected %d", count, total) -+ } -+ } -+} -+ -+func makeManyDirs(prefix string, levels, dirs, files int) (count uint32, err error) { -+ for d := 0; d < dirs; d++ { -+ var dir string -+ dir, err = os.MkdirTemp(prefix, "d-") -+ if err != nil { -+ return count, err -+ } -+ count++ -+ for f := 0; f < files; f++ { -+ var fi *os.File -+ fi, err = os.CreateTemp(dir, "f-") -+ if err != nil { -+ return count, err -+ } -+ fi.Close() -+ count++ -+ } -+ if levels == 0 { -+ continue -+ } -+ var c uint32 -+ if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil { -+ return count, err -+ } -+ count += c -+ } -+ -+ return count, err -+} -+ -+// prepareTestSet() creates a directory tree of shallow files, -+// to be used for testing or benchmarking. -+// -+// Total dirs: dirs^levels + dirs^(levels-1) + ... + dirs^1 -+// Total files: total_dirs * files -+func prepareTestSet(tb testing.TB, levels, dirs, files int) (dir string, total uint32) { -+ tb.Helper() -+ var err error -+ -+ dir = tb.TempDir() -+ total, err = makeManyDirs(dir, levels, dirs, files) -+ if err != nil { -+ tb.Fatal(err) -+ } -+ total++ // this dir -+ -+ return dir, total -+} -+ -+type walkerFunc func(root string, walkFn fs.WalkDirFunc) error -+ -+func genWalkN(n int) walkerFunc { -+ return func(root string, walkFn fs.WalkDirFunc) error { -+ return WalkN(root, walkFn, n) -+ } -+} -+ -+func BenchmarkWalk(b *testing.B) { -+ const ( -+ levels = 5 // how deep -+ dirs = 3 // dirs on each levels -+ files = 8 // files on each levels -+ ) -+ -+ benchmarks := []struct { -+ walk fs.WalkDirFunc -+ name string -+ }{ -+ {name: "Empty", walk: cbEmpty}, -+ {name: "ReadFile", walk: cbReadFile}, -+ {name: "ChownChmod", walk: cbChownChmod}, -+ {name: "RandomSleep", walk: cbRandomSleep}, -+ } -+ -+ walkers := []struct { -+ walker walkerFunc -+ name string -+ }{ -+ {name: "filepath.WalkDir", walker: filepath.WalkDir}, -+ {name: "pwalkdir.Walk", walker: Walk}, -+ // test WalkN with various values of N -+ {name: "pwalkdir.Walk1", walker: genWalkN(1)}, -+ {name: "pwalkdir.Walk2", walker: genWalkN(2)}, -+ {name: "pwalkdir.Walk4", walker: genWalkN(4)}, -+ {name: "pwalkdir.Walk8", walker: genWalkN(8)}, -+ {name: "pwalkdir.Walk16", walker: genWalkN(16)}, -+ {name: "pwalkdir.Walk32", walker: genWalkN(32)}, -+ {name: "pwalkdir.Walk64", walker: genWalkN(64)}, -+ {name: "pwalkdir.Walk128", walker: genWalkN(128)}, -+ {name: "pwalkdir.Walk256", walker: genWalkN(256)}, -+ } -+ -+ dir, total := prepareTestSet(b, levels, dirs, files) -+ b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total) -+ -+ for _, bm := range benchmarks { -+ for _, w := range walkers { -+ walker := w.walker -+ walkFn := bm.walk -+ // preheat -+ if err := w.walker(dir, bm.walk); err != nil { -+ b.Errorf("walk failed: %v", err) -+ } -+ // benchmark -+ b.Run(bm.name+"/"+w.name, func(b *testing.B) { -+ for i := 0; i < b.N; i++ { -+ if err := walker(dir, walkFn); err != nil { -+ b.Errorf("walk failed: %v", err) -+ } -+ } -+ }) -+ } -+ } -+} -+ -+func cbEmpty(_ string, _ fs.DirEntry, _ error) error { -+ return nil -+} -+ -+func cbChownChmod(path string, e fs.DirEntry, _ error) error { -+ _ = os.Chown(path, 0, 0) -+ mode := os.FileMode(0o644) -+ if e.IsDir() { -+ mode = os.FileMode(0o755) -+ } -+ _ = os.Chmod(path, mode) -+ -+ return nil -+} -+ -+func cbReadFile(path string, e fs.DirEntry, _ error) error { -+ var err error -+ if e.Type().IsRegular() { -+ _, err = os.ReadFile(path) -+ } -+ return err -+} -+ -+func cbRandomSleep(_ string, _ fs.DirEntry, _ error) error { -+ time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator -+ return nil -+} -diff --git a/libcontainer/apparmor/apparmor_linux.go b/libcontainer/apparmor/apparmor_linux.go -index 17d36ed1..a3a8e932 100644 ---- a/libcontainer/apparmor/apparmor_linux.go -+++ b/libcontainer/apparmor/apparmor_linux.go -@@ -6,6 +6,9 @@ import ( - "os" - "sync" - -+ "golang.org/x/sys/unix" -+ -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer/utils" - ) - -@@ -36,19 +39,13 @@ func setProcAttr(attr, value string) error { - // Under AppArmor you can only change your own attr, so there's no reason - // to not use /proc/thread-self/ (instead of /proc//, like libapparmor - // does). -- attrPath, closer := utils.ProcThreadSelf(attrSubPath) -- defer closer() -- -- f, err := os.OpenFile(attrPath, os.O_WRONLY, 0) -+ f, closer, err := pathrs.ProcThreadSelfOpen(attrSubPath, unix.O_WRONLY|unix.O_CLOEXEC) - if err != nil { - return err - } -+ defer closer() - defer f.Close() - -- if err := utils.EnsureProcHandle(f); err != nil { -- return err -- } -- - _, err = f.WriteString(value) - return err - } -diff --git a/libcontainer/console_linux.go b/libcontainer/console_linux.go -index e506853e..c93151bc 100644 ---- a/libcontainer/console_linux.go -+++ b/libcontainer/console_linux.go -@@ -1,43 +1,164 @@ - package libcontainer - - import ( -+ "errors" -+ "fmt" - "os" -+ "runtime" - -+ "github.com/containerd/console" - "golang.org/x/sys/unix" -+ -+ "github.com/opencontainers/runc/internal/linux" -+ "github.com/opencontainers/runc/internal/pathrs" -+ "github.com/opencontainers/runc/internal/sys" -+ "github.com/opencontainers/runc/libcontainer/utils" - ) - --// mount initializes the console inside the rootfs mounting with the specified mount label --// and applying the correct ownership of the console. --func mountConsole(slavePath string) error { -- f, err := os.Create("/dev/console") -- if err != nil && !os.IsExist(err) { -- return err -+// checkPtmxHandle checks that the given file handle points to a real -+// /dev/pts/ptmx device inode on a real devpts mount. We cannot (trivially) -+// check that it is *the* /dev/pts for the container itself, but this is good -+// enough. -+func checkPtmxHandle(ptmx *os.File) error { -+ //nolint:revive,staticcheck,nolintlint // ignore "don't use ALL_CAPS" warning // nolintlint is needed to work around the different lint configs -+ const ( -+ PTMX_MAJOR = 5 // from TTYAUX_MAJOR in -+ PTMX_MINOR = 2 // from mknod_ptmx in fs/devpts/inode.c -+ PTMX_INO = 2 // from mknod_ptmx in fs/devpts/inode.c -+ ) -+ return sys.VerifyInode(ptmx, func(stat *unix.Stat_t, statfs *unix.Statfs_t) error { -+ if statfs.Type != unix.DEVPTS_SUPER_MAGIC { -+ return fmt.Errorf("ptmx handle is not on a real devpts mount: super magic is %#x", statfs.Type) -+ } -+ if stat.Ino != PTMX_INO { -+ return fmt.Errorf("ptmx handle has wrong inode number: %v", stat.Ino) -+ } -+ if stat.Mode&unix.S_IFMT != unix.S_IFCHR || stat.Rdev != unix.Mkdev(PTMX_MAJOR, PTMX_MINOR) { -+ return fmt.Errorf("ptmx handle is not a real char ptmx device: ftype %#x %d:%d", -+ stat.Mode&unix.S_IFMT, unix.Major(stat.Rdev), unix.Minor(stat.Rdev)) -+ } -+ return nil -+ }) -+} -+ -+func isPtyNoIoctlError(err error) bool { -+ // The kernel converts -ENOIOCTLCMD to -ENOTTY automatically, but handle -+ // -EINVAL just in case (which some drivers do, include pty). -+ return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOTTY) -+} -+ -+func getPtyPeer(pty console.Console, unsafePeerPath string, flags int) (*os.File, error) { -+ peer, err := linux.GetPtyPeer(pty.Fd(), unsafePeerPath, flags) -+ if err == nil || !isPtyNoIoctlError(err) { -+ return peer, err - } -- if f != nil { -- // Ensure permission bits (can be different because of umask). -- if err := f.Chmod(0o666); err != nil { -- return err -+ -+ // On pre-TIOCGPTPEER kernels (Linux < 4.13), we need to fallback to using -+ // the /dev/pts/$n path generated using TIOCGPTN. We can do some validation -+ // that the inode is correct because the Unix-98 pty has a consistent -+ // numbering scheme for the device number of the peer. -+ -+ peerNum, err := unix.IoctlGetUint32(int(pty.Fd()), unix.TIOCGPTN) -+ if err != nil { -+ return nil, fmt.Errorf("get peer number of pty: %w", err) -+ } -+ //nolint:revive,staticcheck,nolintlint // ignore "don't use ALL_CAPS" warning // nolintlint is needed to work around the different lint configs -+ const ( -+ UNIX98_PTY_SLAVE_MAJOR = 136 // from -+ ) -+ wantPeerDev := unix.Mkdev(UNIX98_PTY_SLAVE_MAJOR, peerNum) -+ -+ // Use O_PATH to avoid opening a bad inode before we validate it. -+ peerHandle, err := os.OpenFile(unsafePeerPath, unix.O_PATH|unix.O_CLOEXEC, 0) -+ if err != nil { -+ return nil, err -+ } -+ defer peerHandle.Close() -+ -+ if err := sys.VerifyInode(peerHandle, func(stat *unix.Stat_t, statfs *unix.Statfs_t) error { -+ if statfs.Type != unix.DEVPTS_SUPER_MAGIC { -+ return fmt.Errorf("pty peer handle is not on a real devpts mount: super magic is %#x", statfs.Type) -+ } -+ if stat.Mode&unix.S_IFMT != unix.S_IFCHR || stat.Rdev != wantPeerDev { -+ return fmt.Errorf("pty peer handle is not the real char device for pty %d: ftype %#x %d:%d", -+ peerNum, stat.Mode&unix.S_IFMT, unix.Major(stat.Rdev), unix.Minor(stat.Rdev)) - } -- f.Close() -+ return nil -+ }); err != nil { -+ return nil, err - } -- return mount(slavePath, "/dev/console", "bind", unix.MS_BIND, "") -+ -+ return pathrs.Reopen(peerHandle, flags) - } - --// dupStdio opens the slavePath for the console and dups the fds to the current --// processes stdio, fd 0,1,2. --func dupStdio(slavePath string) error { -- fd, err := unix.Open(slavePath, unix.O_RDWR, 0) -+// safeAllocPty returns a new (ptmx, peer pty) allocation for use inside a -+// container. -+func safeAllocPty() (pty console.Console, peer *os.File, Err error) { -+ // TODO: Use openat2(RESOLVE_NO_SYMLINKS|RESOLVE_NO_XDEV). -+ ptmxHandle, err := os.OpenFile("/dev/pts/ptmx", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -+ if err != nil { -+ return nil, nil, err -+ } -+ defer ptmxHandle.Close() -+ -+ if err := checkPtmxHandle(ptmxHandle); err != nil { -+ return nil, nil, fmt.Errorf("verify ptmx handle: %w", err) -+ } -+ -+ ptyFile, err := pathrs.Reopen(ptmxHandle, unix.O_RDWR|unix.O_NOCTTY) -+ if err != nil { -+ return nil, nil, fmt.Errorf("reopen ptmx to get new pty pair: %w", err) -+ } -+ // On success, the ownership is transferred to pty. -+ defer func() { -+ if Err != nil { -+ _ = ptyFile.Close() -+ } -+ }() -+ -+ pty, unsafePeerPath, err := console.NewPtyFromFile(ptyFile) - if err != nil { -- return &os.PathError{ -- Op: "open", -- Path: slavePath, -- Err: err, -+ return nil, nil, err -+ } -+ defer func() { -+ if Err != nil { -+ _ = pty.Close() - } -+ }() -+ -+ peer, err = getPtyPeer(pty, unsafePeerPath, unix.O_RDWR|unix.O_NOCTTY) -+ if err != nil { -+ return nil, nil, fmt.Errorf("failed to get peer end of newly-allocated console: %w", err) -+ } -+ return pty, peer, nil -+} -+ -+// mountConsole bind-mounts the provided pty on top of /dev/console so programs -+// that operate on /dev/console operate on the correct container pty. -+func mountConsole(peerPty *os.File) error { -+ console, err := os.OpenFile("/dev/console", unix.O_NOFOLLOW|unix.O_CREAT|unix.O_CLOEXEC, 0o666) -+ if err != nil { -+ return fmt.Errorf("create /dev/console mount target: %w", err) - } -+ defer console.Close() -+ -+ dstFd, closer := utils.ProcThreadSelfFd(console.Fd()) -+ defer closer() -+ -+ mntSrc := &mountSource{ -+ Type: mountSourcePlain, -+ file: peerPty, -+ } -+ return mountViaFds(peerPty.Name(), mntSrc, "/dev/console", dstFd, "bind", unix.MS_BIND, "") -+} -+ -+// dupStdio replaces stdio with the given peerPty. -+func dupStdio(peerPty *os.File) error { - for _, i := range []int{0, 1, 2} { -- if err := unix.Dup3(fd, i, 0); err != nil { -+ if err := unix.Dup3(int(peerPty.Fd()), i, 0); err != nil { - return err - } - } -+ runtime.KeepAlive(peerPty) - return nil - } -diff --git a/libcontainer/criu_linux.go b/libcontainer/criu_linux.go -index 8cd8fa5a..53a0202a 100644 ---- a/libcontainer/criu_linux.go -+++ b/libcontainer/criu_linux.go -@@ -523,34 +523,9 @@ func (c *Container) restoreNetwork(req *criurpc.CriuReq, criuOpts *CriuOpts) { - } - } - --// makeCriuRestoreMountpoints makes the actual mountpoints for the --// restore using CRIU. This function is inspired from the code in --// rootfs_linux.go. --func (c *Container) makeCriuRestoreMountpoints(m *configs.Mount) error { -- if m.Device == "cgroup" { -- // No mount point(s) need to be created: -- // -- // * for v1, mount points are saved by CRIU because -- // /sys/fs/cgroup is a tmpfs mount -- // -- // * for v2, /sys/fs/cgroup is a real mount, but -- // the mountpoint appears as soon as /sys is mounted -- return nil -- } -- // TODO: pass srcFD? Not sure if criu is impacted by issue #2484. -- me := mountEntry{Mount: m} -- // For all other filesystems, just make the target. -- if _, err := createMountpoint(c.config.Rootfs, me); err != nil { -- return fmt.Errorf("create criu restore mountpoint for %s mount: %w", me.Destination, err) -- } -- return nil --} -- --// isPathInPrefixList is a small function for CRIU restore to make sure --// mountpoints, which are on a tmpfs, are not created in the roofs. --func isPathInPrefixList(path string, prefix []string) bool { -- for _, p := range prefix { -- if strings.HasPrefix(path, p+"/") { -+func isOnTmpfs(path string, mounts []*configs.Mount) bool { -+ for _, m := range mounts { -+ if m.Device == "tmpfs" && strings.HasPrefix(path, m.Destination+"/") { - return true - } - } -@@ -564,17 +539,6 @@ func isPathInPrefixList(path string, prefix []string) bool { - // This function also creates missing mountpoints as long as they - // are not on top of a tmpfs, as CRIU will restore tmpfs content anyway. - func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { -- // First get a list of a all tmpfs mounts -- tmpfs := []string{} -- for _, m := range mounts { -- switch m.Device { -- case "tmpfs": -- tmpfs = append(tmpfs, m.Destination) -- } -- } -- // Now go through all mounts and create the mountpoints -- // if the mountpoints are not on a tmpfs, as CRIU will -- // restore the complete tmpfs content from its checkpoint. - umounts := []string{} - defer func() { - for _, u := range umounts { -@@ -590,28 +554,51 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { - }) - } - }() -+ // Now go through all mounts and create the required mountpoints. - for _, m := range mounts { -- if !isPathInPrefixList(m.Destination, tmpfs) { -- if err := c.makeCriuRestoreMountpoints(m); err != nil { -+ // No cgroup mount point(s) need to be created: -+ // * for v1, mount points are saved by CRIU because -+ // /sys/fs/cgroup is a tmpfs mount; -+ // * for v2, /sys/fs/cgroup is a real mount, but -+ // the mountpoint appears as soon as /sys is mounted. -+ if m.Device == "cgroup" { -+ continue -+ } -+ // If the mountpoint is on a tmpfs, skip it as CRIU will -+ // restore the complete tmpfs content from its checkpoint. -+ if isOnTmpfs(m.Destination, mounts) { -+ continue -+ } -+ me := mountEntry{Mount: m} -+ if err := me.createOpenMountpoint(c.config.Rootfs); err != nil { -+ return fmt.Errorf("create criu restore mountpoint for %s mount: %w", me.Destination, err) -+ } -+ if me.dstFile != nil { -+ defer me.dstFile.Close() -+ } -+ // If the mount point is a bind mount, we need to mount -+ // it now so that runc can create the necessary mount -+ // points for mounts in bind mounts. -+ // This also happens during initial container creation. -+ // Without this CRIU restore will fail -+ // See: https://github.com/opencontainers/runc/issues/2748 -+ // It is also not necessary to order the mount points -+ // because during initial container creation mounts are -+ // set up in the order they are configured. -+ if m.Device == "bind" { -+ if err := utils.WithProcfdFile(me.dstFile, func(dstFd string) error { -+ return mountViaFds(m.Source, nil, m.Destination, dstFd, "", unix.MS_BIND|unix.MS_REC, "") -+ }); err != nil { - return err - } -- // If the mount point is a bind mount, we need to mount -- // it now so that runc can create the necessary mount -- // points for mounts in bind mounts. -- // This also happens during initial container creation. -- // Without this CRIU restore will fail -- // See: https://github.com/opencontainers/runc/issues/2748 -- // It is also not necessary to order the mount points -- // because during initial container creation mounts are -- // set up in the order they are configured. -- if m.Device == "bind" { -- if err := utils.WithProcfd(c.config.Rootfs, m.Destination, func(dstFd string) error { -- return mountViaFds(m.Source, nil, m.Destination, dstFd, "", unix.MS_BIND|unix.MS_REC, "") -- }); err != nil { -- return err -- } -- umounts = append(umounts, m.Destination) -- } -+ umounts = append(umounts, m.Destination) -+ } -+ if me.dstFile != nil { -+ // As this is being done in a loop, the defer earlier will be -+ // delayed until all mountpoints are handled -- for a config with -+ // many mountpoints this could result in a lot of open files. So we -+ // opportunistically close the file as well as deferring it. -+ _ = me.dstFile.Close() - } - } - return nil -@@ -1101,7 +1088,7 @@ func (c *Container) criuNotifications(resp *criurpc.CriuResp, process *Process, - logrus.Debugf("notify: %s\n", script) - switch script { - case "post-dump": -- f, err := os.Create(filepath.Join(c.stateDir, "checkpoint")) -+ f, err := os.Create(filepath.Join(c.stateDir, "checkpoint")) //nolint:forbidigo // this is a host-side operation in a runc-controlled directory - if err != nil { - return err - } -diff --git a/libcontainer/exeseal/cloned_binary_linux.go b/libcontainer/exeseal/cloned_binary_linux.go -index 3bafc96a..4d4d0dc0 100644 ---- a/libcontainer/exeseal/cloned_binary_linux.go -+++ b/libcontainer/exeseal/cloned_binary_linux.go -@@ -10,6 +10,7 @@ import ( - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer/system" - ) - -@@ -71,7 +72,7 @@ func sealFile(f **os.File) error { - // When sealing an O_TMPFILE-style descriptor we need to - // re-open the path as O_PATH to clear the existing write - // handle we have. -- opath, err := os.OpenFile(fmt.Sprintf("/proc/self/fd/%d", (*f).Fd()), unix.O_PATH|unix.O_CLOEXEC, 0) -+ opath, err := pathrs.Reopen(*f, unix.O_PATH|unix.O_CLOEXEC) - if err != nil { - return fmt.Errorf("reopen tmpfile: %w", err) - } -diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go -index b6bcddc1..40529200 100644 ---- a/libcontainer/init_linux.go -+++ b/libcontainer/init_linux.go -@@ -5,6 +5,7 @@ import ( - "encoding/json" - "errors" - "fmt" -+ "io" - "net" - "os" - "path/filepath" -@@ -20,6 +21,7 @@ import ( - "golang.org/x/sys/unix" - - "github.com/opencontainers/cgroups" -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer/capabilities" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/system" -@@ -376,12 +378,13 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { - // the UID owner of the console to be the user the process will run as (so - // they can actually control their console). - -- pty, slavePath, err := console.NewPty() -+ pty, peerPty, err := safeAllocPty() - if err != nil { - return err - } - // After we return from here, we don't need the console anymore. - defer pty.Close() -+ defer peerPty.Close() - - if config.ConsoleHeight != 0 && config.ConsoleWidth != 0 { - err = pty.Resize(console.WinSize{ -@@ -395,7 +398,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { - - // Mount the console inside our rootfs. - if mount { -- if err := mountConsole(slavePath); err != nil { -+ if err := mountConsole(peerPty); err != nil { - return err - } - } -@@ -406,7 +409,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { - runtime.KeepAlive(pty) - - // Now, dup over all the things. -- return dupStdio(slavePath) -+ return dupStdio(peerPty) - } - - // syncParentReady sends to the given pipe a JSON payload which indicates that -@@ -468,7 +471,12 @@ func setupUser(config *initConfig) error { - // We don't need to use /proc/thread-self here because setgroups is a - // per-userns file and thus is global to all threads in a thread-group. - // This lets us avoid having to do runtime.LockOSThread. -- setgroups, err := os.ReadFile("/proc/self/setgroups") -+ var setgroups []byte -+ setgroupsFile, err := pathrs.ProcSelfOpen("setgroups", unix.O_RDONLY) -+ if err == nil { -+ setgroups, err = io.ReadAll(setgroupsFile) -+ _ = setgroupsFile.Close() -+ } - if err != nil && !os.IsNotExist(err) { - return err - } -@@ -504,19 +512,16 @@ func setupUser(config *initConfig) error { - // The ownership needs to match because it is created outside of the container and needs to be - // localized. - func fixStdioPermissions(uid int) error { -- var null unix.Stat_t -- if err := unix.Stat("/dev/null", &null); err != nil { -- return &os.PathError{Op: "stat", Path: "/dev/null", Err: err} -- } - for _, file := range []*os.File{os.Stdin, os.Stdout, os.Stderr} { - var s unix.Stat_t - if err := unix.Fstat(int(file.Fd()), &s); err != nil { - return &os.PathError{Op: "fstat", Path: file.Name(), Err: err} - } - -- // Skip chown if uid is already the one we want or any of the STDIO descriptors -- // were redirected to /dev/null. -- if int(s.Uid) == uid || s.Rdev == null.Rdev { -+ // Skip chown if: -+ // - uid is already the one we want, or -+ // - fd is opened to /dev/null. -+ if int(s.Uid) == uid || isDevNull(&s) { - continue - } - -diff --git a/libcontainer/integration/exec_test.go b/libcontainer/integration/exec_test.go -index f18dd50f..22804776 100644 ---- a/libcontainer/integration/exec_test.go -+++ b/libcontainer/integration/exec_test.go -@@ -16,10 +16,11 @@ import ( - - "github.com/opencontainers/cgroups" - "github.com/opencontainers/cgroups/systemd" -+ "github.com/opencontainers/runc/internal/linux" -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/internal/userns" -- "github.com/opencontainers/runc/libcontainer/utils" - "github.com/opencontainers/runtime-spec/specs-go" - - "golang.org/x/sys/unix" -@@ -1693,11 +1694,9 @@ func TestFdLeaksSystemd(t *testing.T) { - } - - func fdList(t *testing.T) []string { -- procSelfFd, closer := utils.ProcThreadSelf("fd") -- defer closer() -- -- fdDir, err := os.Open(procSelfFd) -+ fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC) - ok(t, err) -+ defer closer() - defer fdDir.Close() - - fds, err := fdDir.Readdirnames(-1) -@@ -1736,8 +1735,10 @@ func testFdLeaks(t *testing.T, systemd bool) { - - count := 0 - -- procSelfFd, closer := utils.ProcThreadSelf("fd/") -+ procSelfFd, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC) -+ ok(t, err) - defer closer() -+ defer procSelfFd.Close() - - next_fd: - for _, fd1 := range fds1 { -@@ -1746,7 +1747,7 @@ next_fd: - continue next_fd - } - } -- dst, _ := os.Readlink(filepath.Join(procSelfFd, fd1)) -+ dst, _ := linux.Readlinkat(procSelfFd, fd1) - for _, ex := range excludedPaths { - if ex == dst { - continue next_fd -diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go -index 4ecb3d45..d85e7321 100644 ---- a/libcontainer/rootfs_linux.go -+++ b/libcontainer/rootfs_linux.go -@@ -5,14 +5,15 @@ import ( - "errors" - "fmt" - "os" -- "path" - "path/filepath" -+ "runtime" - "strconv" - "strings" - "syscall" - "time" - - securejoin "github.com/cyphar/filepath-securejoin" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" - "github.com/moby/sys/mountinfo" - "github.com/moby/sys/userns" - "github.com/mrunalp/fileutils" -@@ -24,6 +25,8 @@ import ( - "github.com/opencontainers/cgroups" - devices "github.com/opencontainers/cgroups/devices/config" - "github.com/opencontainers/cgroups/fs2" -+ "github.com/opencontainers/runc/internal/pathrs" -+ "github.com/opencontainers/runc/internal/sys" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/utils" - ) -@@ -43,6 +46,7 @@ type mountConfig struct { - type mountEntry struct { - *configs.Mount - srcFile *mountSource -+ dstFile *os.File - } - - // srcName is only meant for error messages, it returns a "friendly" name. -@@ -282,8 +286,8 @@ func cleanupTmp(tmpdir string) { - _ = os.RemoveAll(tmpdir) - } - --func mountCgroupV1(m *configs.Mount, c *mountConfig) error { -- binds, err := getCgroupMounts(m) -+func mountCgroupV1(m mountEntry, c *mountConfig) error { -+ binds, err := getCgroupMounts(m.Mount) - if err != nil { - return err - } -@@ -314,7 +318,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { - // inside the tmpfs, so we don't want to resolve symlinks). - subsystemPath := filepath.Join(c.root, b.Destination) - subsystemName := filepath.Base(b.Destination) -- if err := utils.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil { -+ if err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil { - return err - } - if err := utils.WithProcfd(c.root, b.Destination, func(dstFd string) error { -@@ -353,8 +357,8 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { - return nil - } - --func mountCgroupV2(m *configs.Mount, c *mountConfig) error { -- err := utils.WithProcfd(c.root, m.Destination, func(dstFd string) error { -+func mountCgroupV2(m mountEntry, c *mountConfig) error { -+ err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error { - return mountViaFds(m.Source, nil, m.Destination, dstFd, "cgroup2", uintptr(m.Flags), m.Data) - }) - if err == nil || (!errors.Is(err, unix.EPERM) && !errors.Is(err, unix.EBUSY)) { -@@ -383,14 +387,14 @@ func mountCgroupV2(m *configs.Mount, c *mountConfig) error { - // - // Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted - // with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`). -- err = utils.WithProcfd(c.root, m.Destination, func(procfd string) error { -- return maskPath(procfd, c.label) -+ err = utils.WithProcfdFile(m.dstFile, func(procfd string) error { -+ return maskPaths([]string{procfd}, c.label) - }) - } - return err - } - --func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) { -+func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) { - // Set up a scratch dir for the tmpfs on the host. - tmpdir, err := prepareTmp("/tmp") - if err != nil { -@@ -403,13 +407,19 @@ func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) { - } - defer os.RemoveAll(tmpDir) - -- // Configure the *host* tmpdir as if it's the container mount. We change -- // m.Destination since we are going to mount *on the host*. -- oldDest := m.Destination -- m.Destination = tmpDir -- err = mountPropagate(m, "/", mountLabel) -- m.Destination = oldDest -+ tmpDirFile, err := os.OpenFile(tmpDir, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { -+ return fmt.Errorf("tmpcopyup: %w", err) -+ } -+ defer tmpDirFile.Close() -+ -+ // Configure the *host* tmpdir as if it's the container mount. We change -+ // m.dstFile since we are going to mount *on the host*. -+ hostMount := mountEntry{ -+ Mount: m.Mount, -+ dstFile: tmpDirFile, -+ } -+ if err := hostMount.mountPropagate("/", mountLabel); err != nil { - return err - } - defer func() { -@@ -420,7 +430,7 @@ func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) { - } - }() - -- return utils.WithProcfd(rootfs, m.Destination, func(dstFd string) (Err error) { -+ return utils.WithProcfdFile(m.dstFile, func(dstFd string) (Err error) { - // Copy the container data to the host tmpdir. We append "/" to force - // CopyDirectory to resolve the symlink rather than trying to copy the - // symlink itself. -@@ -482,72 +492,76 @@ func statfsToMountFlags(st unix.Statfs_t) int { - - var errRootfsToFile = errors.New("config tries to change rootfs to file") - --func createMountpoint(rootfs string, m mountEntry) (string, error) { -- dest, err := securejoin.SecureJoin(rootfs, m.Destination) -+func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { -+ unsafePath := utils.StripRoot(rootfs, m.Destination) -+ dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH) -+ defer func() { -+ if dstFile != nil && Err != nil { -+ _ = dstFile.Close() -+ } -+ }() - if err != nil { -- return "", err -- } -- if err := checkProcMount(rootfs, dest, m); err != nil { -- return "", fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err) -- } -+ if !errors.Is(err, unix.ENOENT) { -+ return fmt.Errorf("lookup mountpoint target: %w", err) -+ } - -- switch m.Device { -- case "bind": -- fi, _, err := m.srcStat() -- if err != nil { -- // Error out if the source of a bind mount does not exist as we -- // will be unable to bind anything to it. -- return "", err -- } -- // If the original source is not a directory, make the target a file. -- if !fi.IsDir() { -- // Make sure we aren't tricked into trying to make the root a file. -- if rootfs == dest { -- return "", fmt.Errorf("%w: file bind mount over rootfs", errRootfsToFile) -- } -- // Make the parent directory. -- destDir, destBase := filepath.Split(dest) -- destDirFd, err := utils.MkdirAllInRootOpen(rootfs, destDir, 0o755) -+ // If the mountpoint doesn't already exist, we want to create a mountpoint -+ // that makes sense for the source. For file bind-mounts this is an empty -+ // file, for everything else it's a directory. -+ dstIsFile := false -+ if m.Device == "bind" { -+ fi, _, err := m.srcStat() - if err != nil { -- return "", fmt.Errorf("make parent dir of file bind-mount: %w", err) -- } -- defer destDirFd.Close() -- // Make the target file. We want to avoid opening any file that is -- // already there because it could be a "bad" file like an invalid -- // device or hung tty that might cause a DoS, so we use mknodat. -- // destBase does not contain any "/" components, and mknodat does -- // not follow trailing symlinks, so we can safely just call mknodat -- // here. -- if err := unix.Mknodat(int(destDirFd.Fd()), destBase, unix.S_IFREG|0o644, 0); err != nil { -- // If we get EEXIST, there was already an inode there and -- // we can consider that a success. -- if !errors.Is(err, unix.EEXIST) { -- err = &os.PathError{Op: "mknod regular file", Path: dest, Err: err} -- return "", fmt.Errorf("create target of file bind-mount: %w", err) -- } -+ // Error out if the source of a bind mount does not exist as we -+ // will be unable to bind anything to it. -+ return err - } -- // Nothing left to do. -- return dest, nil -+ dstIsFile = !fi.IsDir() - } - -- case "tmpfs": -- // If the original target exists, copy the mode for the tmpfs mount. -- if stat, err := os.Stat(dest); err == nil { -- dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode())) -- if m.Data != "" { -- dt = dt + "," + m.Data -- } -- m.Data = dt -+ if dstIsFile { -+ dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) -+ } else { -+ dstFile, err = pathrs.MkdirAllInRootOpen(rootfs, unsafePath, 0o755) -+ } -+ if err != nil { -+ return fmt.Errorf("make mountpoint %q: %w", m.Destination, err) -+ } -+ } - -- // Nothing left to do. -- return dest, nil -+ if m.Device == "tmpfs" { -+ // If the original target exists, copy the mode for the tmpfs mount. -+ stat, err := dstFile.Stat() -+ if err != nil { -+ return fmt.Errorf("check tmpfs source mode: %w", err) - } -+ dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode())) -+ if m.Data != "" { -+ dt = dt + "," + m.Data -+ } -+ m.Data = dt - } - -- if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil { -- return "", err -+ dstFullPath, err := procfs.ProcSelfFdReadlink(dstFile) -+ if err != nil { -+ return fmt.Errorf("get mount destination real path: %w", err) -+ } -+ if !pathrs.IsLexicallyInRoot(rootfs, dstFullPath) { -+ return fmt.Errorf("mountpoint %q is outside of rootfs %q", dstFullPath, rootfs) -+ } -+ if relPath, err := filepath.Rel(rootfs, dstFullPath); err != nil { -+ return fmt.Errorf("get relative path of %q: %w", dstFullPath, err) -+ } else if relPath == "." { -+ return fmt.Errorf("mountpoint %q is on the top of rootfs %q", dstFullPath, rootfs) - } -- return dest, nil -+ // TODO: Make checkProcMount use dstFile directly to avoid the need to -+ // operate on paths here. -+ if err := checkProcMount(rootfs, dstFullPath, *m); err != nil { -+ return fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err) -+ } -+ // Update mountEntry. -+ m.dstFile = dstFile -+ return nil - } - - func mountToRootfs(c *mountConfig, m mountEntry) error { -@@ -563,7 +577,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { - // TODO: This won't be necessary once we switch to libpathrs and we can - // stop all of these symlink-exchange attacks. - dest := filepath.Clean(m.Destination) -- if !utils.IsLexicallyInRoot(rootfs, dest) { -+ if !pathrs.IsLexicallyInRoot(rootfs, dest) { - // Do not use securejoin as it resolves symlinks. - dest = filepath.Join(rootfs, dest) - } -@@ -577,36 +591,47 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { - } else if !fi.IsDir() { - return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device) - } -- if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil { -+ dstFile, err := pathrs.MkdirAllInRootOpen(rootfs, dest, 0o755) -+ if err != nil { - return err - } -- // Selinux kernels do not support labeling of /proc or /sys. -- return mountPropagate(m, rootfs, "") -+ defer dstFile.Close() -+ // "proc" and "sys" mounts need special handling (without resolving the -+ // destination) to avoid attacks. -+ m.dstFile = dstFile -+ return m.mountPropagate(rootfs, "") - } - -- dest, err := createMountpoint(rootfs, m) -- if err != nil { -+ mountLabel := c.label -+ if err := m.createOpenMountpoint(rootfs); err != nil { - return fmt.Errorf("create mountpoint for %s mount: %w", m.Destination, err) - } -- mountLabel := c.label -+ defer func() { -+ if m.dstFile != nil { -+ _ = m.dstFile.Close() -+ m.dstFile = nil -+ } -+ }() - - switch m.Device { - case "mqueue": -- if err := mountPropagate(m, rootfs, ""); err != nil { -+ if err := m.mountPropagate(rootfs, ""); err != nil { - return err - } -- return label.SetFileLabel(dest, mountLabel) -+ return utils.WithProcfdFile(m.dstFile, func(dstFd string) error { -+ return label.SetFileLabel(dstFd, mountLabel) -+ }) - case "tmpfs": -+ var err error - if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP { -- err = doTmpfsCopyUp(m, rootfs, mountLabel) -+ err = doTmpfsCopyUp(m, mountLabel) - } else { -- err = mountPropagate(m, rootfs, mountLabel) -+ err = m.mountPropagate(rootfs, mountLabel) - } -- - return err - case "bind": - // open_tree()-related shenanigans are all handled in mountViaFds. -- if err := mountPropagate(m, rootfs, mountLabel); err != nil { -+ if err := m.mountPropagate(rootfs, mountLabel); err != nil { - return err - } - -@@ -620,7 +645,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { - // contrast to mount(8)'s current behaviour, but is what users probably - // expect. See . - if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 { -- if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error { -+ if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error { - flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT - // The runtime-spec says we SHOULD map to the relevant mount(8) - // behaviour. However, it's not clear whether we want the -@@ -721,14 +746,14 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { - return err - } - } -- return setRecAttr(m.Mount, rootfs) -+ return setRecAttr(m) - case "cgroup": - if cgroups.IsCgroup2UnifiedMode() { -- return mountCgroupV2(m.Mount, c) -+ return mountCgroupV2(m, c) - } -- return mountCgroupV1(m.Mount, c) -+ return mountCgroupV1(m, c) - default: -- return mountPropagate(m, rootfs, mountLabel) -+ return m.mountPropagate(rootfs, mountLabel) - } - } - -@@ -876,20 +901,20 @@ func setupDevSymlinks(rootfs string) error { - // needs to be called after we chroot/pivot into the container's rootfs so that any - // symlinks are resolved locally. - func reOpenDevNull() error { -- var stat, devNullStat unix.Stat_t - file, err := os.OpenFile("/dev/null", os.O_RDWR, 0) - if err != nil { - return err - } -- defer file.Close() //nolint: errcheck -- if err := unix.Fstat(int(file.Fd()), &devNullStat); err != nil { -- return &os.PathError{Op: "fstat", Path: file.Name(), Err: err} -+ defer file.Close() -+ if err := verifyDevNull(file); err != nil { -+ return fmt.Errorf("can't reopen /dev/null: %w", err) - } - for fd := 0; fd < 3; fd++ { -+ var stat unix.Stat_t - if err := unix.Fstat(fd, &stat); err != nil { - return &os.PathError{Op: "fstat", Path: "fd " + strconv.Itoa(fd), Err: err} - } -- if stat.Rdev == devNullStat.Rdev { -+ if isDevNull(&stat) { - // Close and re-open the fd. - if err := unix.Dup3(int(file.Fd()), fd, 0); err != nil { - return &os.PathError{ -@@ -922,16 +947,15 @@ func createDevices(config *configs.Config) error { - return nil - } - --func bindMountDeviceNode(rootfs, dest string, node *devices.Device) error { -- f, err := os.Create(dest) -- if err != nil && !os.IsExist(err) { -- return err -- } -- if f != nil { -- _ = f.Close() -+func bindMountDeviceNode(destDir *os.File, destName string, node *devices.Device) error { -+ dstFile, err := utils.Openat(destDir, destName, unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o000) -+ if err != nil { -+ return fmt.Errorf("create device inode %s: %w", node.Path, err) - } -- return utils.WithProcfd(rootfs, dest, func(dstFd string) error { -- return mountViaFds(node.Path, nil, dest, dstFd, "bind", unix.MS_BIND, "") -+ defer dstFile.Close() -+ -+ return utils.WithProcfdFile(dstFile, func(dstFd string) error { -+ return mountViaFds(node.Path, nil, dstFile.Name(), dstFd, "bind", unix.MS_BIND, "") - }) - } - -@@ -941,31 +965,33 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { - // The node only exists for cgroup reasons, ignore it here. - return nil - } -- dest, err := securejoin.SecureJoin(rootfs, node.Path) -+ destPath, err := securejoin.SecureJoin(rootfs, node.Path) - if err != nil { - return err - } -- if dest == rootfs { -+ if destPath == rootfs { - return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile) - } -- if err := utils.MkdirAllInRoot(rootfs, filepath.Dir(dest), 0o755); err != nil { -- return err -+ destDirPath, destName := filepath.Split(destPath) -+ destDir, err := pathrs.MkdirAllInRootOpen(rootfs, destDirPath, 0o755) -+ if err != nil { -+ return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err) - } - if bind { -- return bindMountDeviceNode(rootfs, dest, node) -+ return bindMountDeviceNode(destDir, destName, node) - } -- if err := mknodDevice(dest, node); err != nil { -+ if err := mknodDevice(destDir, destName, node); err != nil { - if errors.Is(err, os.ErrExist) { - return nil - } else if errors.Is(err, os.ErrPermission) { -- return bindMountDeviceNode(rootfs, dest, node) -+ return bindMountDeviceNode(destDir, destName, node) - } - return err - } - return nil - } - --func mknodDevice(dest string, node *devices.Device) error { -+func mknodDevice(destDir *os.File, destName string, node *devices.Device) error { - fileMode := node.FileMode - switch node.Type { - case devices.BlockDevice: -@@ -981,14 +1007,44 @@ func mknodDevice(dest string, node *devices.Device) error { - if err != nil { - return err - } -- if err := unix.Mknod(dest, uint32(fileMode), int(dev)); err != nil { -- return &os.PathError{Op: "mknod", Path: dest, Err: err} -+ if err := unix.Mknodat(int(destDir.Fd()), destName, uint32(fileMode), int(dev)); err != nil { -+ return &os.PathError{Op: "mknodat", Path: filepath.Join(destDir.Name(), destName), Err: err} - } -- // Ensure permission bits (can be different because of umask). -- if err := os.Chmod(dest, fileMode); err != nil { -+ -+ // Get a handle and verify that it matches the expected inode type and -+ // major:minor before we operate on it. -+ devFile, err := utils.Openat(destDir, destName, unix.O_NOFOLLOW|unix.O_PATH, 0) -+ if err != nil { -+ return fmt.Errorf("open new %c device inode %s: %w", node.Type, node.Path, err) -+ } -+ defer devFile.Close() -+ -+ if err := sys.VerifyInode(devFile, func(stat *unix.Stat_t, _ *unix.Statfs_t) error { -+ if stat.Mode&unix.S_IFMT != uint32(fileMode)&unix.S_IFMT { -+ return fmt.Errorf("new %c device inode %s has incorrect ftype: %#x doesn't match expected %#v", -+ node.Type, node.Path, -+ stat.Mode&unix.S_IFMT, fileMode&unix.S_IFMT) -+ } -+ if stat.Rdev != dev { -+ return fmt.Errorf("new %c device inode %s has incorrect major:minor: %d:%d doesn't match expected %d:%d", -+ node.Type, node.Path, -+ unix.Major(stat.Rdev), unix.Minor(stat.Rdev), -+ unix.Major(dev), unix.Minor(dev)) -+ } -+ return nil -+ }); err != nil { - return err - } -- return os.Chown(dest, int(node.Uid), int(node.Gid)) -+ -+ // Ensure permission bits (can be different because of umask). -+ if err := sys.FchmodFile(devFile, uint32(fileMode)); err != nil { -+ return fmt.Errorf("update new %c device inode %s file mode: %w", node.Type, node.Path, err) -+ } -+ if err := sys.FchownFile(devFile, int(node.Uid), int(node.Gid)); err != nil { -+ return fmt.Errorf("update new %c device inode %s owner: %w", node.Type, node.Path, err) -+ } -+ runtime.KeepAlive(devFile) -+ return nil - } - - // rootfsParentMountPrivate ensures rootfs parent mount is private. -@@ -1242,31 +1298,111 @@ func remountReadonly(m *configs.Mount) error { - return fmt.Errorf("unable to mount %s as readonly max retries reached", dest) - } - --// maskPath masks the top of the specified path inside a container to avoid -+func isDevNull(st *unix.Stat_t) bool { -+ return st.Mode&unix.S_IFMT == unix.S_IFCHR && st.Rdev == unix.Mkdev(1, 3) -+} -+ -+func verifyDevNull(f *os.File) error { -+ return sys.VerifyInode(f, func(st *unix.Stat_t, _ *unix.Statfs_t) error { -+ if !isDevNull(st) { -+ return errors.New("container's /dev/null is invalid") -+ } -+ return nil -+ }) -+} -+ -+// maskPaths masks the top of the specified paths inside a container to avoid - // security issues from processes reading information from non-namespace aware - // mounts ( proc/kcore ). - // For files, maskPath bind mounts /dev/null over the top of the specified path. - // For directories, maskPath mounts read-only tmpfs over the top of the specified path. --func maskPath(path string, mountLabel string) error { -- if err := mount("/dev/null", path, "", unix.MS_BIND, ""); err != nil && !errors.Is(err, os.ErrNotExist) { -- if errors.Is(err, unix.ENOTDIR) { -- return mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel)) -+func maskPaths(paths []string, mountLabel string) error { -+ devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0) -+ if err != nil { -+ return fmt.Errorf("can't mask paths: %w", err) -+ } -+ defer devNull.Close() -+ if err := verifyDevNull(devNull); err != nil { -+ return fmt.Errorf("can't mask paths: %w", err) -+ } -+ devNullSrc := &mountSource{Type: mountSourcePlain, file: devNull} -+ procSelfFd, closer := utils.ProcThreadSelf("fd/") -+ defer closer() -+ -+ for _, path := range paths { -+ // Open the target path; skip if it doesn't exist. -+ dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0) -+ if err != nil { -+ if errors.Is(err, os.ErrNotExist) { -+ continue -+ } -+ return fmt.Errorf("can't mask path %q: %w", path, err) -+ } -+ st, err := dstFh.Stat() -+ if err != nil { -+ dstFh.Close() -+ return fmt.Errorf("can't mask path %q: %w", path, err) -+ } -+ var dstType string -+ if st.IsDir() { -+ // Destination is a directory: bind mount a ro tmpfs over it. -+ dstType = "dir" -+ err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel)) -+ } else { -+ // Destination is a file: mount it to /dev/null. -+ dstType = "path" -+ dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd()))) -+ err = mountViaFds("", devNullSrc, path, dstFd, "", unix.MS_BIND, "") -+ } -+ dstFh.Close() -+ if err != nil { -+ return fmt.Errorf("can't mask %s %q: %w", dstType, path, err) - } -- return err - } -+ - return nil - } - --// writeSystemProperty writes the value to a path under /proc/sys as determined from the key. --// For e.g. net.ipv4.ip_forward translated to /proc/sys/net/ipv4/ip_forward. --func writeSystemProperty(key, value string) error { -- keyPath := strings.ReplaceAll(key, ".", "/") -- return os.WriteFile(path.Join("/proc/sys", keyPath), []byte(value), 0o644) -+func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err error) { -+ fullPath, err := procfs.ProcSelfFdReadlink(f) -+ if err != nil { -+ return nil, fmt.Errorf("get full path: %w", err) -+ } -+ if !pathrs.IsLexicallyInRoot(rootfs, fullPath) { -+ return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs) -+ } -+ unsafePath := utils.StripRoot(rootfs, fullPath) -+ reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags) -+ if err != nil { -+ return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err) -+ } -+ defer func() { -+ if Err != nil { -+ _ = reopened.Close() -+ } -+ }() -+ -+ // NOTE: The best we can do here is confirm that the new mountpoint handle -+ // matches the original target handle, but an attacker could've swapped a -+ // different path to replace it. In the worst case this could result in us -+ // applying later vfsmount flags onto the wrong mount. -+ // -+ // This is far from ideal, but the only way of doing this in a race-free -+ // way is to switch the new mount API (move_mount(2) does not require this -+ // re-opening step, and thus no such races are possible). -+ reopenedFullPath, err := procfs.ProcSelfFdReadlink(reopened) -+ if err != nil { -+ return nil, fmt.Errorf("check full path of re-opened mountpoint: %w", err) -+ } -+ if reopenedFullPath != fullPath { -+ return nil, fmt.Errorf("mountpoint %q was moved while re-opening", unsafePath) -+ } -+ return reopened, nil - } - - // Do the mount operation followed by additional mounts required to take care - // of propagation flags. This will always be scoped inside the container rootfs. --func mountPropagate(m mountEntry, rootfs string, mountLabel string) error { -+func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error { - var ( - data = label.FormatMountLabel(m.Data, mountLabel) - flags = m.Flags -@@ -1279,19 +1415,30 @@ func mountPropagate(m mountEntry, rootfs string, mountLabel string) error { - flags &= ^unix.MS_RDONLY - } - -- // Because the destination is inside a container path which might be -- // mutating underneath us, we verify that we are actually going to mount -- // inside the container with WithProcfd() -- mounting through a procfd -- // mounts on the target. -- if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error { -+ if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error { - return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data) - }); err != nil { - return err - } -+ -+ // We need to re-open the mountpoint after doing the mount, in order for us -+ // to operate on the new mount we just created. However, we cannot use -+ // pathrs.Reopen because we need to re-resolve from the parent directory to -+ // get a new handle to the top mount. -+ // -+ // TODO: Use move_mount(2) on newer kernels so that this is no longer -+ // necessary on modern systems. -+ newDstFile, err := reopenAfterMount(rootfs, m.dstFile, unix.O_PATH) -+ if err != nil { -+ return fmt.Errorf("reopen mountpoint after mount: %w", err) -+ } -+ _ = m.dstFile.Close() -+ m.dstFile = newDstFile -+ - // We have to apply mount propagation flags in a separate WithProcfd() call - // because the previous call invalidates the passed procfd -- the mount - // target needs to be re-opened. -- if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error { -+ if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error { - for _, pflag := range m.PropagationFlags { - if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil { - return err -@@ -1304,11 +1451,11 @@ func mountPropagate(m mountEntry, rootfs string, mountLabel string) error { - return nil - } - --func setRecAttr(m *configs.Mount, rootfs string) error { -+func setRecAttr(m mountEntry) error { - if m.RecAttr == nil { - return nil - } -- return utils.WithProcfd(rootfs, m.Destination, func(procfd string) error { -+ return utils.WithProcfdFile(m.dstFile, func(procfd string) error { - return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr) - }) - } -diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go -index 384750bf..21516bd3 100644 ---- a/libcontainer/standard_init_linux.go -+++ b/libcontainer/standard_init_linux.go -@@ -11,6 +11,8 @@ import ( - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - -+ "github.com/opencontainers/runc/internal/pathrs" -+ "github.com/opencontainers/runc/internal/sys" - "github.com/opencontainers/runc/libcontainer/apparmor" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/keys" -@@ -130,20 +132,17 @@ func (l *linuxStandardInit) Init() error { - return fmt.Errorf("unable to apply apparmor profile: %w", err) - } - -- for key, value := range l.config.Config.Sysctl { -- if err := writeSystemProperty(key, value); err != nil { -- return err -- } -+ if err := sys.WriteSysctls(l.config.Config.Sysctl); err != nil { -+ return err - } - for _, path := range l.config.Config.ReadonlyPaths { - if err := readonlyPath(path); err != nil { - return fmt.Errorf("can't make %q read-only: %w", path, err) - } - } -- for _, path := range l.config.Config.MaskPaths { -- if err := maskPath(path, l.config.Config.MountLabel); err != nil { -- return fmt.Errorf("can't mask path %s: %w", path, err) -- } -+ -+ if err := maskPaths(l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil { -+ return err - } - pdeath, err := system.GetParentDeathSignal() - if err != nil { -@@ -252,19 +251,17 @@ func (l *linuxStandardInit) Init() error { - return fmt.Errorf("close log pipe: %w", err) - } - -- fifoPath, closer := utils.ProcThreadSelfFd(l.fifoFile.Fd()) -- defer closer() -- - // Wait for the FIFO to be opened on the other side before exec-ing the - // user process. We open it through /proc/self/fd/$fd, because the fd that - // was given to us was an O_PATH fd to the fifo itself. Linux allows us to - // re-open an O_PATH fd through /proc. -- fd, err := unix.Open(fifoPath, unix.O_WRONLY|unix.O_CLOEXEC, 0) -+ fifoFile, err := pathrs.Reopen(l.fifoFile, unix.O_WRONLY|unix.O_CLOEXEC) - if err != nil { -- return &os.PathError{Op: "open exec fifo", Path: fifoPath, Err: err} -+ return fmt.Errorf("reopen exec fifo: %w", err) - } -- if _, err := unix.Write(fd, []byte("0")); err != nil { -- return &os.PathError{Op: "write exec fifo", Path: fifoPath, Err: err} -+ defer fifoFile.Close() -+ if _, err := fifoFile.Write([]byte("0")); err != nil { -+ return &os.PathError{Op: "write exec fifo", Path: fifoFile.Name(), Err: err} - } - - // Close the O_PATH fifofd fd before exec because the kernel resets -@@ -273,6 +270,7 @@ func (l *linuxStandardInit) Init() error { - // N.B. the core issue itself (passing dirfds to the host filesystem) has - // since been resolved. - // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318 -+ _ = fifoFile.Close() - _ = l.fifoFile.Close() - - if s := l.config.SpecState; s != nil { -diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go -index e8ce0eca..5e558c4f 100644 ---- a/libcontainer/system/linux.go -+++ b/libcontainer/system/linux.go -@@ -169,3 +169,23 @@ func SetLinuxPersonality(personality int) error { - } - return nil - } -+ -+// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER). -+func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) { -+ // Make sure O_NOCTTY is always set -- otherwise runc might accidentally -+ // gain it as a controlling terminal. O_CLOEXEC also needs to be set to -+ // make sure we don't leak the handle either. -+ flags |= unix.O_NOCTTY | unix.O_CLOEXEC -+ -+ // There is no nice wrapper for this kind of ioctl in unix. -+ peerFd, _, errno := unix.Syscall( -+ unix.SYS_IOCTL, -+ ptyFd, -+ uintptr(unix.TIOCGPTPEER), -+ uintptr(flags), -+ ) -+ if errno != 0 { -+ return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno) -+ } -+ return os.NewFile(peerFd, unsafePeerPath), nil -+} -diff --git a/libcontainer/system/proc.go b/libcontainer/system/proc.go -index 774443ec..34850dd8 100644 ---- a/libcontainer/system/proc.go -+++ b/libcontainer/system/proc.go -@@ -2,10 +2,12 @@ package system - - import ( - "fmt" -+ "io" - "os" -- "path/filepath" - "strconv" - "strings" -+ -+ "github.com/opencontainers/runc/internal/pathrs" - ) - - // State is the status of a process. -@@ -66,8 +68,16 @@ type Stat_t struct { - } - - // Stat returns a Stat_t instance for the specified process. --func Stat(pid int) (stat Stat_t, err error) { -- bytes, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat")) -+func Stat(pid int) (Stat_t, error) { -+ var stat Stat_t -+ -+ statFile, err := pathrs.ProcPidOpen(pid, "stat", os.O_RDONLY) -+ if err != nil { -+ return stat, err -+ } -+ defer statFile.Close() -+ -+ bytes, err := io.ReadAll(statFile) - if err != nil { - return stat, err - } -diff --git a/libcontainer/utils/utils.go b/libcontainer/utils/utils.go -index 23003e17..46da94f4 100644 ---- a/libcontainer/utils/utils.go -+++ b/libcontainer/utils/utils.go -@@ -65,11 +65,11 @@ func CleanPath(path string) string { - return path - } - --// stripRoot returns the passed path, stripping the root path if it was -+// StripRoot returns the passed path, stripping the root path if it was - // (lexicially) inside it. Note that both passed paths will always be treated - // as absolute, and the returned path will also always be absolute. In - // addition, the paths are cleaned before stripping the root. --func stripRoot(root, path string) string { -+func StripRoot(root, path string) string { - // Make the paths clean and absolute. - root, path = CleanPath("/"+root), CleanPath("/"+path) - switch { -diff --git a/libcontainer/utils/utils_test.go b/libcontainer/utils/utils_test.go -index 06c042f5..4b5fd833 100644 ---- a/libcontainer/utils/utils_test.go -+++ b/libcontainer/utils/utils_test.go -@@ -131,9 +131,9 @@ func TestStripRoot(t *testing.T) { - {"/foo/bar", "foo/bar/baz/beef", "/baz/beef"}, - {"foo/bar", "foo/bar/baz/beets", "/baz/beets"}, - } { -- got := stripRoot(test.root, test.path) -+ got := StripRoot(test.root, test.path) - if got != test.out { -- t.Errorf("stripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out) -+ t.Errorf("StripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out) - } - } - } -diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go -index f6b3fefb..7dbec54d 100644 ---- a/libcontainer/utils/utils_unix.go -+++ b/libcontainer/utils/utils_unix.go -@@ -9,27 +9,15 @@ import ( - "path/filepath" - "runtime" - "strconv" -- "strings" - "sync" - _ "unsafe" // for go:linkname - - securejoin "github.com/cyphar/filepath-securejoin" -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - ) - --// EnsureProcHandle returns whether or not the given file handle is on procfs. --func EnsureProcHandle(fh *os.File) error { -- var buf unix.Statfs_t -- if err := unix.Fstatfs(int(fh.Fd()), &buf); err != nil { -- return fmt.Errorf("ensure %s is on procfs: %w", fh.Name(), err) -- } -- if buf.Type != unix.PROC_SUPER_MAGIC { -- return fmt.Errorf("%s is not on procfs", fh.Name()) -- } -- return nil --} -- - var ( - haveCloseRangeCloexecBool bool - haveCloseRangeCloexecOnce sync.Once -@@ -59,19 +47,13 @@ type fdFunc func(fd int) - // fdRangeFrom calls the passed fdFunc for each file descriptor that is open in - // the current process. - func fdRangeFrom(minFd int, fn fdFunc) error { -- procSelfFd, closer := ProcThreadSelf("fd") -- defer closer() -- -- fdDir, err := os.Open(procSelfFd) -+ fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC) - if err != nil { -- return err -+ return fmt.Errorf("get handle to /proc/thread-self/fd: %w", err) - } -+ defer closer() - defer fdDir.Close() - -- if err := EnsureProcHandle(fdDir); err != nil { -- return err -- } -- - fdList, err := fdDir.Readdirnames(-1) - if err != nil { - return err -@@ -170,8 +152,8 @@ func NewSockPair(name string) (parent, child *os.File, err error) { - // the passed closure (the file handle will be freed once the closure returns). - func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { - // Remove the root then forcefully resolve inside the root. -- unsafePath = stripRoot(root, unsafePath) -- path, err := securejoin.SecureJoin(root, unsafePath) -+ unsafePath = StripRoot(root, unsafePath) -+ fullPath, err := securejoin.SecureJoin(root, unsafePath) - if err != nil { - return fmt.Errorf("resolving path inside rootfs failed: %w", err) - } -@@ -180,7 +162,7 @@ func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { - defer closer() - - // Open the target path. -- fh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0) -+ fh, err := os.OpenFile(fullPath, unix.O_PATH|unix.O_CLOEXEC, 0) - if err != nil { - return fmt.Errorf("open o_path procfd: %w", err) - } -@@ -190,13 +172,24 @@ func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { - // Double-check the path is the one we expected. - if realpath, err := os.Readlink(procfd); err != nil { - return fmt.Errorf("procfd verification failed: %w", err) -- } else if realpath != path { -+ } else if realpath != fullPath { - return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath) - } - - return fn(procfd) - } - -+// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd], intended -+// to make migrating from [WithProcfd] and [WithProcfdPath] usage easier. The -+// caller is responsible for making sure that the provided file handle is -+// actually safe to operate on. -+func WithProcfdFile(file *os.File, fn func(procfd string) error) error { -+ fdpath, closer := ProcThreadSelfFd(file.Fd()) -+ defer closer() -+ -+ return fn(fdpath) -+} -+ - type ProcThreadSelfCloser func() - - var ( -@@ -268,88 +261,6 @@ func ProcThreadSelfFd(fd uintptr) (string, ProcThreadSelfCloser) { - return ProcThreadSelf("fd/" + strconv.FormatUint(uint64(fd), 10)) - } - --// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"), --// but properly handling the case where path or root are "/". --// --// NOTE: The return value only make sense if the path doesn't contain "..". --func IsLexicallyInRoot(root, path string) bool { -- if root != "/" { -- root += "/" -- } -- if path != "/" { -- path += "/" -- } -- return strings.HasPrefix(path, root) --} -- --// MkdirAllInRootOpen attempts to make --// --// path, _ := securejoin.SecureJoin(root, unsafePath) --// os.MkdirAll(path, mode) --// os.Open(path) --// --// safer against attacks where components in the path are changed between --// SecureJoin returning and MkdirAll (or Open) being called. In particular, we --// try to detect any symlink components in the path while we are doing the --// MkdirAll. --// --// NOTE: If unsafePath is a subpath of root, we assume that you have already --// called SecureJoin and so we use the provided path verbatim without resolving --// any symlinks (this is done in a way that avoids symlink-exchange races). --// This means that the path also must not contain ".." elements, otherwise an --// error will occur. --// --// This uses securejoin.MkdirAllHandle under the hood, but it has special --// handling if unsafePath has already been scoped within the rootfs (this is --// needed for a lot of runc callers and fixing this would require reworking a --// lot of path logic). --func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (_ *os.File, Err error) { -- // If the path is already "within" the root, get the path relative to the -- // root and use that as the unsafe path. This is necessary because a lot of -- // MkdirAllInRootOpen callers have already done SecureJoin, and refactoring -- // all of them to stop using these SecureJoin'd paths would require a fair -- // amount of work. -- // TODO(cyphar): Do the refactor to libpathrs once it's ready. -- if IsLexicallyInRoot(root, unsafePath) { -- subPath, err := filepath.Rel(root, unsafePath) -- if err != nil { -- return nil, err -- } -- unsafePath = subPath -- } -- -- // Check for any silly mode bits. -- if mode&^0o7777 != 0 { -- return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode) -- } -- // Linux (and thus os.MkdirAll) silently ignores the suid and sgid bits if -- // passed. While it would make sense to return an error in that case (since -- // the user has asked for a mode that won't be applied), for compatibility -- // reasons we have to ignore these bits. -- if ignoredBits := mode &^ 0o1777; ignoredBits != 0 { -- logrus.Warnf("MkdirAll called with no-op mode bits that are ignored by Linux: 0o%.3o", ignoredBits) -- mode &= 0o1777 -- } -- -- rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) -- if err != nil { -- return nil, fmt.Errorf("open root handle: %w", err) -- } -- defer rootDir.Close() -- -- return securejoin.MkdirAllHandle(rootDir, unsafePath, mode) --} -- --// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the --// returned handle, for callers that don't need to use it. --func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error { -- f, err := MkdirAllInRootOpen(root, unsafePath, mode) -- if err == nil { -- _ = f.Close() -- } -- return err --} -- - // Openat is a Go-friendly openat(2) wrapper. - func Openat(dir *os.File, path string, flags int, mode uint32) (*os.File, error) { - dirFd := unix.AT_FDCWD -diff --git a/utils_linux.go b/utils_linux.go -index 9c9e1e83..a4cc5bdf 100644 ---- a/utils_linux.go -+++ b/utils_linux.go -@@ -16,6 +16,7 @@ import ( - "github.com/urfave/cli" - "golang.org/x/sys/unix" - -+ "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/specconv" -@@ -240,10 +241,14 @@ func (r *runner) run(config *specs.Process) (int, error) { - process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...) - } - baseFd := 3 + len(process.ExtraFiles) -- procSelfFd, closer := utils.ProcThreadSelf("fd/") -+ procSelfFd, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC) -+ if err != nil { -+ return -1, err -+ } - defer closer() -+ defer procSelfFd.Close() - for i := baseFd; i < baseFd+r.preserveFDs; i++ { -- _, err = os.Stat(filepath.Join(procSelfFd, strconv.Itoa(i))) -+ err := unix.Faccessat(int(procSelfFd.Fd()), strconv.Itoa(i), unix.F_OK, 0) - if err != nil { - return -1, fmt.Errorf("unable to stat preserved-fd %d (of %d): %w", i-baseFd, r.preserveFDs, err) - } -diff --git a/vendor/github.com/containerd/console/console_other.go b/vendor/github.com/containerd/console/console_other.go -index 933dfadd..968c5771 100644 ---- a/vendor/github.com/containerd/console/console_other.go -+++ b/vendor/github.com/containerd/console/console_other.go -@@ -1,5 +1,5 @@ --//go:build !darwin && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos --// +build !darwin,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos -+//go:build !darwin && !freebsd && !linux && !netbsd && !openbsd && !windows && !zos -+// +build !darwin,!freebsd,!linux,!netbsd,!openbsd,!windows,!zos - - /* - Copyright The containerd Authors. -diff --git a/vendor/github.com/containerd/console/console_unix.go b/vendor/github.com/containerd/console/console_unix.go -index 161f5d12..aa4c6962 100644 ---- a/vendor/github.com/containerd/console/console_unix.go -+++ b/vendor/github.com/containerd/console/console_unix.go -@@ -31,6 +31,15 @@ func NewPty() (Console, string, error) { - if err != nil { - return nil, "", err - } -+ return NewPtyFromFile(f) -+} -+ -+// NewPtyFromFile creates a new pty pair, just like [NewPty] except that the -+// provided [os.File] is used as the master rather than automatically creating -+// a new master from /dev/ptmx. The ownership of [os.File] is passed to the -+// returned [Console], so the caller must be careful to not call Close on the -+// underlying file. -+func NewPtyFromFile(f File) (Console, string, error) { - slave, err := ptsname(f) - if err != nil { - return nil, "", err -diff --git a/vendor/github.com/containerd/console/tc_darwin.go b/vendor/github.com/containerd/console/tc_darwin.go -index 78715458..77c695a4 100644 ---- a/vendor/github.com/containerd/console/tc_darwin.go -+++ b/vendor/github.com/containerd/console/tc_darwin.go -@@ -18,7 +18,6 @@ package console - - import ( - "fmt" -- "os" - - "golang.org/x/sys/unix" - ) -@@ -30,12 +29,12 @@ const ( - - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - return unix.IoctlSetPointerInt(int(f.Fd()), unix.TIOCPTYUNLK, 0) - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCPTYGNAME) - if err != nil { - return "", err -diff --git a/vendor/github.com/containerd/console/tc_freebsd_cgo.go b/vendor/github.com/containerd/console/tc_freebsd_cgo.go -index 33282579..627f7d55 100644 ---- a/vendor/github.com/containerd/console/tc_freebsd_cgo.go -+++ b/vendor/github.com/containerd/console/tc_freebsd_cgo.go -@@ -21,7 +21,6 @@ package console - - import ( - "fmt" -- "os" - - "golang.org/x/sys/unix" - ) -@@ -39,7 +38,7 @@ const ( - - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - fd := C.int(f.Fd()) - if _, err := C.unlockpt(fd); err != nil { - C.close(fd) -@@ -49,7 +48,7 @@ func unlockpt(f *os.File) error { - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCGPTN) - if err != nil { - return "", err -diff --git a/vendor/github.com/containerd/console/tc_freebsd_nocgo.go b/vendor/github.com/containerd/console/tc_freebsd_nocgo.go -index 18a9b9cb..434ba46e 100644 ---- a/vendor/github.com/containerd/console/tc_freebsd_nocgo.go -+++ b/vendor/github.com/containerd/console/tc_freebsd_nocgo.go -@@ -21,7 +21,6 @@ package console - - import ( - "fmt" -- "os" - - "golang.org/x/sys/unix" - ) -@@ -42,12 +41,12 @@ const ( - - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - panic("unlockpt() support requires cgo.") - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCGPTN) - if err != nil { - return "", err -diff --git a/vendor/github.com/containerd/console/tc_linux.go b/vendor/github.com/containerd/console/tc_linux.go -index 7d552ea4..e98dc022 100644 ---- a/vendor/github.com/containerd/console/tc_linux.go -+++ b/vendor/github.com/containerd/console/tc_linux.go -@@ -18,7 +18,6 @@ package console - - import ( - "fmt" -- "os" - "unsafe" - - "golang.org/x/sys/unix" -@@ -31,7 +30,7 @@ const ( - - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - var u int32 - // XXX do not use unix.IoctlSetPointerInt here, see commit dbd69c59b81. - if _, _, err := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))); err != 0 { -@@ -41,7 +40,7 @@ func unlockpt(f *os.File) error { - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - var u uint32 - // XXX do not use unix.IoctlGetInt here, see commit dbd69c59b81. - if _, _, err := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&u))); err != 0 { -diff --git a/vendor/github.com/containerd/console/tc_netbsd.go b/vendor/github.com/containerd/console/tc_netbsd.go -index 71227aef..73cf4397 100644 ---- a/vendor/github.com/containerd/console/tc_netbsd.go -+++ b/vendor/github.com/containerd/console/tc_netbsd.go -@@ -18,7 +18,6 @@ package console - - import ( - "bytes" -- "os" - - "golang.org/x/sys/unix" - ) -@@ -31,12 +30,12 @@ const ( - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. - // This does not exist on NetBSD, it does not allocate controlling terminals on open --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - return nil - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - ptm, err := unix.IoctlGetPtmget(int(f.Fd()), unix.TIOCPTSNAME) - if err != nil { - return "", err -diff --git a/vendor/github.com/containerd/console/tc_openbsd_cgo.go b/vendor/github.com/containerd/console/tc_openbsd_cgo.go -index 0e76f6cc..46f4250c 100644 ---- a/vendor/github.com/containerd/console/tc_openbsd_cgo.go -+++ b/vendor/github.com/containerd/console/tc_openbsd_cgo.go -@@ -20,8 +20,6 @@ - package console - - import ( -- "os" -- - "golang.org/x/sys/unix" - ) - -@@ -34,7 +32,7 @@ const ( - ) - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - ptspath, err := C.ptsname(C.int(f.Fd())) - if err != nil { - return "", err -@@ -44,7 +42,7 @@ func ptsname(f *os.File) (string, error) { - - // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. - // unlockpt should be called before opening the slave side of a pty. --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - if _, err := C.grantpt(C.int(f.Fd())); err != nil { - return err - } -diff --git a/vendor/github.com/containerd/console/tc_openbsd_nocgo.go b/vendor/github.com/containerd/console/tc_openbsd_nocgo.go -index dca92418..a8f9f6c2 100644 ---- a/vendor/github.com/containerd/console/tc_openbsd_nocgo.go -+++ b/vendor/github.com/containerd/console/tc_openbsd_nocgo.go -@@ -29,8 +29,6 @@ - package console - - import ( -- "os" -- - "golang.org/x/sys/unix" - ) - -@@ -39,10 +37,10 @@ const ( - cmdTcSet = unix.TIOCSETA - ) - --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - panic("ptsname() support requires cgo.") - } - --func unlockpt(f *os.File) error { -+func unlockpt(f File) error { - panic("unlockpt() support requires cgo.") - } -diff --git a/vendor/github.com/containerd/console/tc_zos.go b/vendor/github.com/containerd/console/tc_zos.go -index fc90ba5f..23b0bd28 100644 ---- a/vendor/github.com/containerd/console/tc_zos.go -+++ b/vendor/github.com/containerd/console/tc_zos.go -@@ -17,7 +17,6 @@ - package console - - import ( -- "os" - "strings" - - "golang.org/x/sys/unix" -@@ -29,11 +28,11 @@ const ( - ) - - // unlockpt is a no-op on zos. --func unlockpt(_ *os.File) error { -+func unlockpt(File) error { - return nil - } - - // ptsname retrieves the name of the first available pts for the given master. --func ptsname(f *os.File) (string, error) { -+func ptsname(f File) (string, error) { - return "/dev/ttyp" + strings.TrimPrefix(f.Name(), "/dev/ptyp"), nil - } -diff --git a/vendor/github.com/cyphar/filepath-securejoin/.golangci.yml b/vendor/github.com/cyphar/filepath-securejoin/.golangci.yml -new file mode 100644 -index 00000000..e965034e ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/.golangci.yml -@@ -0,0 +1,56 @@ -+# SPDX-License-Identifier: MPL-2.0 -+ -+# Copyright (C) 2025 Aleksa Sarai -+# Copyright (C) 2025 SUSE LLC -+# -+# This Source Code Form is subject to the terms of the Mozilla Public -+# License, v. 2.0. If a copy of the MPL was not distributed with this -+# file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+version: "2" -+ -+linters: -+ enable: -+ - asasalint -+ - asciicheck -+ - containedctx -+ - contextcheck -+ - errcheck -+ - errorlint -+ - exhaustive -+ - forcetypeassert -+ - godot -+ - goprintffuncname -+ - govet -+ - importas -+ - ineffassign -+ - makezero -+ - misspell -+ - musttag -+ - nilerr -+ - nilnesserr -+ - nilnil -+ - noctx -+ - prealloc -+ - revive -+ - staticcheck -+ - testifylint -+ - unconvert -+ - unparam -+ - unused -+ - usetesting -+ settings: -+ govet: -+ enable: -+ - nilness -+ testifylint: -+ enable-all: true -+ -+formatters: -+ enable: -+ - gofumpt -+ - goimports -+ settings: -+ goimports: -+ local-prefixes: -+ - github.com/cyphar/filepath-securejoin -diff --git a/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md b/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -index ca0e3c62..6862467c 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -+++ b/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md -@@ -6,6 +6,122 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - - ## [Unreleased] ## - -+## [0.5.0] - 2025-09-26 ## -+ -+> Let the past die. Kill it if you have to. -+ -+> **NOTE**: With this release, some parts of -+> `github.com/cyphar/filepath-securejoin` are now licensed under the Mozilla -+> Public License (version 2). Please see [COPYING.md][] as well as the the -+> license header in each file for more details. -+ -+[COPYING.md]: ./COPYING.md -+ -+### Breaking ### -+- The new API introduced in the [0.3.0][] release has been moved to a new -+ subpackage called `pathrs-lite`. This was primarily done to better indicate -+ the split between the new and old APIs, as well as indicate to users the -+ purpose of this subpackage (it is a less complete version of [libpathrs][]). -+ -+ We have added some wrappers to the top-level package to ease the transition, -+ but those are deprecated and will be removed in the next minor release of -+ filepath-securejoin. Users should update their import paths. -+ -+ This new subpackage has also been relicensed under the Mozilla Public License -+ (version 2), please see [COPYING.md][] for more details. -+ -+### Added ### -+- Most of the key bits the safe `procfs` API have now been exported and are -+ available in `github.com/cyphar/filepath-securejoin/pathrs-lite/procfs`. At -+ the moment this primarily consists of a new `procfs.Handle` API: -+ -+ * `OpenProcRoot` returns a new handle to `/proc`, endeavouring to make it -+ safe if possible (`subset=pid` to protect against mistaken write attacks -+ and leaks, as well as using `fsopen(2)` to avoid racing mount attacks). -+ -+ `OpenUnsafeProcRoot` returns a handle without attempting to create one -+ with `subset=pid`, which makes it more dangerous to leak. Most users -+ should use `OpenProcRoot` (even if you need to use `ProcRoot` as the base -+ of an operation, as filepath-securejoin will internally open a handle when -+ necessary). -+ -+ * The `(*procfs.Handle).Open*` family of methods lets you get a safe -+ `O_PATH` handle to subpaths within `/proc` for certain subpaths. -+ -+ For `OpenThreadSelf`, the returned `ProcThreadSelfCloser` needs to be -+ called after you completely finish using the handle (this is necessary -+ because Go is multi-threaded and `ProcThreadSelf` references -+ `/proc/thread-self` which may disappear if we do not -+ `runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently equivalent -+ to `runtime.UnlockOSThread`). -+ -+ Note that you cannot open any `procfs` symlinks (most notably magic-links) -+ using this API. At the moment, filepath-securejoin does not support this -+ feature (but [libpathrs][] does). -+ -+ * `ProcSelfFdReadlink` lets you get the in-kernel path representation of a -+ file descriptor (think `readlink("/proc/self/fd/...")`), except that we -+ verify that there aren't any tricky overmounts that could fool the -+ process. -+ -+ Please be aware that the returned string is simply a snapshot at that -+ particular moment, and an attacker could move the file being pointed to. -+ In addition, complex namespace configurations could result in non-sensical -+ or confusing paths to be returned. The value received from this function -+ should only be used as secondary verification of some security property, -+ not as proof that a particular handle has a particular path. -+ -+ The procfs handle used internally by the API is the same as the rest of -+ `filepath-securejoin` (for privileged programs this is usually a private -+ in-process `procfs` instance created with `fsopen(2)`). -+ -+ As before, this is intended as a stop-gap before users migrate to -+ [libpathrs][], which provides a far more extensive safe `procfs` API and is -+ generally more robust. -+ -+- Previously, the hardened procfs implementation (used internally within -+ `Reopen` and `Open(at)InRoot`) only protected against overmount attacks on -+ systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or -+ `open_tree(2)` (Linux 5.2) and programs with privileges to use them (with -+ some caveats about locked mounts that probably affect very few users). For -+ other users, an attacker with the ability to create malicious mounts (on most -+ systems, a sysadmin) could trick you into operating on files you didn't -+ expect. This attack only really makes sense in the context of container -+ runtime implementations. -+ -+ This was considered a reasonable trade-off, as the long-term intention was to -+ get all users to just switch to [libpathrs][] if they wanted to use the safe -+ `procfs` API (which had more extensive protections, and is what these new -+ protections in `filepath-securejoin` are based on). However, as the API -+ is now being exported it seems unwise to advertise the API as "safe" if we do -+ not protect against known attacks. -+ -+ The procfs API is now more protected against attackers on systems lacking the -+ aforementioned protections. However, the most comprehensive of these -+ protections effectively rely on [`statx(STATX_MNT_ID)`][statx.2] (Linux 5.8). -+ On older kernel versions, there is no effective protection (there is some -+ minimal protection against non-`procfs` filesystem components but a -+ sufficiently clever attacker can work around those). In addition, -+ `STATX_MNT_ID` is vulnerable to mount ID reuse attacks by sufficiently -+ motivated and privileged attackers -- this problem is mitigated with -+ `STATX_MNT_ID_UNIQUE` (Linux 6.8) but that raises the minimum kernel version -+ for more protection. -+ -+ The fact that these protections are quite limited despite needing a fair bit -+ of extra code to handle was one of the primary reasons we did not initially -+ implement this in `filepath-securejoin` ([libpathrs][] supports all of this, -+ of course). -+ -+### Fixed ### -+- RHEL 8 kernels have backports of `fsopen(2)` but in some testing we've found -+ that it has very bad (and very difficult to debug) performance issues, and so -+ we will explicitly refuse to use `fsopen(2)` if the running kernel version is -+ pre-5.2 and will instead fallback to `open("/proc")`. -+ -+[CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv -+[libpathrs]: https://github.com/cyphar/libpathrs -+[statx.2]: https://www.man7.org/linux/man-pages/man2/statx.2.html -+ - ## [0.4.1] - 2025-01-28 ## - - ### Fixed ### -@@ -173,7 +289,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - safe to start migrating to as we have extensive tests ensuring they behave - correctly and are safe against various races and other attacks. - --[libpathrs]: https://github.com/openSUSE/libpathrs -+[libpathrs]: https://github.com/cyphar/libpathrs - [open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html - - ## [0.2.5] - 2024-05-03 ## -@@ -238,7 +354,8 @@ This is our first release of `github.com/cyphar/filepath-securejoin`, - containing a full implementation with a coverage of 93.5% (the only missing - cases are the error cases, which are hard to mocktest at the moment). - --[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...HEAD -+[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...HEAD -+[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0 - [0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1 - [0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0 - [0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/COPYING.md b/vendor/github.com/cyphar/filepath-securejoin/COPYING.md -new file mode 100644 -index 00000000..520e822b ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/COPYING.md -@@ -0,0 +1,447 @@ -+## COPYING ## -+ -+`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0` -+ -+This project is made up of code licensed under different licenses. Which code -+you use will have an impact on whether only one or both licenses apply to your -+usage of this library. -+ -+Note that **each file** in this project individually has a code comment at the -+start describing the license of that particular file -- this is the most -+accurate license information of this project; in case there is any conflict -+between this document and the comment at the start of a file, the comment shall -+take precedence. The only purpose of this document is to work around [a known -+technical limitation of pkg.go.dev's license checking tool when dealing with -+non-trivial project licenses][go75067]. -+ -+[go75067]: https://go.dev/issue/75067 -+ -+### `BSD-3-Clause` ### -+ -+At time of writing, the following files and directories are licensed under the -+BSD-3-Clause license: -+ -+ * `doc.go` -+ * `join*.go` -+ * `vfs.go` -+ * `internal/consts/*.go` -+ * `pathrs-lite/internal/gocompat/*.go` -+ * `pathrs-lite/internal/kernelversion/*.go` -+ -+The text of the BSD-3-Clause license used by this project is the following (the -+text is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file): -+ -+``` -+Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. -+Copyright (C) 2017-2024 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. -+``` -+ -+### `MPL-2.0` ### -+ -+All other files (unless otherwise marked) are licensed under the Mozilla Public -+License (version 2.0). -+ -+The text of the Mozilla Public License (version 2.0) is the following (the text -+is also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file): -+ -+``` -+Mozilla Public License Version 2.0 -+================================== -+ -+1. Definitions -+-------------- -+ -+1.1. "Contributor" -+ means each individual or legal entity that creates, contributes to -+ the creation of, or owns Covered Software. -+ -+1.2. "Contributor Version" -+ means the combination of the Contributions of others (if any) used -+ by a Contributor and that particular Contributor's Contribution. -+ -+1.3. "Contribution" -+ means Covered Software of a particular Contributor. -+ -+1.4. "Covered Software" -+ means Source Code Form to which the initial Contributor has attached -+ the notice in Exhibit A, the Executable Form of such Source Code -+ Form, and Modifications of such Source Code Form, in each case -+ including portions thereof. -+ -+1.5. "Incompatible With Secondary Licenses" -+ means -+ -+ (a) that the initial Contributor has attached the notice described -+ in Exhibit B to the Covered Software; or -+ -+ (b) that the Covered Software was made available under the terms of -+ version 1.1 or earlier of the License, but not also under the -+ terms of a Secondary License. -+ -+1.6. "Executable Form" -+ means any form of the work other than Source Code Form. -+ -+1.7. "Larger Work" -+ means a work that combines Covered Software with other material, in -+ a separate file or files, that is not Covered Software. -+ -+1.8. "License" -+ means this document. -+ -+1.9. "Licensable" -+ means having the right to grant, to the maximum extent possible, -+ whether at the time of the initial grant or subsequently, any and -+ all of the rights conveyed by this License. -+ -+1.10. "Modifications" -+ means any of the following: -+ -+ (a) any file in Source Code Form that results from an addition to, -+ deletion from, or modification of the contents of Covered -+ Software; or -+ -+ (b) any new file in Source Code Form that contains any Covered -+ Software. -+ -+1.11. "Patent Claims" of a Contributor -+ means any patent claim(s), including without limitation, method, -+ process, and apparatus claims, in any patent Licensable by such -+ Contributor that would be infringed, but for the grant of the -+ License, by the making, using, selling, offering for sale, having -+ made, import, or transfer of either its Contributions or its -+ Contributor Version. -+ -+1.12. "Secondary License" -+ means either the GNU General Public License, Version 2.0, the GNU -+ Lesser General Public License, Version 2.1, the GNU Affero General -+ Public License, Version 3.0, or any later versions of those -+ licenses. -+ -+1.13. "Source Code Form" -+ means the form of the work preferred for making modifications. -+ -+1.14. "You" (or "Your") -+ means an individual or a legal entity exercising rights under this -+ License. For legal entities, "You" includes any entity that -+ controls, is controlled by, or is under common control with You. For -+ purposes of this definition, "control" means (a) the power, direct -+ or indirect, to cause the direction or management of such entity, -+ whether by contract or otherwise, or (b) ownership of more than -+ fifty percent (50%) of the outstanding shares or beneficial -+ ownership of such entity. -+ -+2. License Grants and Conditions -+-------------------------------- -+ -+2.1. Grants -+ -+Each Contributor hereby grants You a world-wide, royalty-free, -+non-exclusive license: -+ -+(a) under intellectual property rights (other than patent or trademark) -+ Licensable by such Contributor to use, reproduce, make available, -+ modify, display, perform, distribute, and otherwise exploit its -+ Contributions, either on an unmodified basis, with Modifications, or -+ as part of a Larger Work; and -+ -+(b) under Patent Claims of such Contributor to make, use, sell, offer -+ for sale, have made, import, and otherwise transfer either its -+ Contributions or its Contributor Version. -+ -+2.2. Effective Date -+ -+The licenses granted in Section 2.1 with respect to any Contribution -+become effective for each Contribution on the date the Contributor first -+distributes such Contribution. -+ -+2.3. Limitations on Grant Scope -+ -+The licenses granted in this Section 2 are the only rights granted under -+this License. No additional rights or licenses will be implied from the -+distribution or licensing of Covered Software under this License. -+Notwithstanding Section 2.1(b) above, no patent license is granted by a -+Contributor: -+ -+(a) for any code that a Contributor has removed from Covered Software; -+ or -+ -+(b) for infringements caused by: (i) Your and any other third party's -+ modifications of Covered Software, or (ii) the combination of its -+ Contributions with other software (except as part of its Contributor -+ Version); or -+ -+(c) under Patent Claims infringed by Covered Software in the absence of -+ its Contributions. -+ -+This License does not grant any rights in the trademarks, service marks, -+or logos of any Contributor (except as may be necessary to comply with -+the notice requirements in Section 3.4). -+ -+2.4. Subsequent Licenses -+ -+No Contributor makes additional grants as a result of Your choice to -+distribute the Covered Software under a subsequent version of this -+License (see Section 10.2) or under the terms of a Secondary License (if -+permitted under the terms of Section 3.3). -+ -+2.5. Representation -+ -+Each Contributor represents that the Contributor believes its -+Contributions are its original creation(s) or it has sufficient rights -+to grant the rights to its Contributions conveyed by this License. -+ -+2.6. Fair Use -+ -+This License is not intended to limit any rights You have under -+applicable copyright doctrines of fair use, fair dealing, or other -+equivalents. -+ -+2.7. Conditions -+ -+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -+in Section 2.1. -+ -+3. Responsibilities -+------------------- -+ -+3.1. Distribution of Source Form -+ -+All distribution of Covered Software in Source Code Form, including any -+Modifications that You create or to which You contribute, must be under -+the terms of this License. You must inform recipients that the Source -+Code Form of the Covered Software is governed by the terms of this -+License, and how they can obtain a copy of this License. You may not -+attempt to alter or restrict the recipients' rights in the Source Code -+Form. -+ -+3.2. Distribution of Executable Form -+ -+If You distribute Covered Software in Executable Form then: -+ -+(a) such Covered Software must also be made available in Source Code -+ Form, as described in Section 3.1, and You must inform recipients of -+ the Executable Form how they can obtain a copy of such Source Code -+ Form by reasonable means in a timely manner, at a charge no more -+ than the cost of distribution to the recipient; and -+ -+(b) You may distribute such Executable Form under the terms of this -+ License, or sublicense it under different terms, provided that the -+ license for the Executable Form does not attempt to limit or alter -+ the recipients' rights in the Source Code Form under this License. -+ -+3.3. Distribution of a Larger Work -+ -+You may create and distribute a Larger Work under terms of Your choice, -+provided that You also comply with the requirements of this License for -+the Covered Software. If the Larger Work is a combination of Covered -+Software with a work governed by one or more Secondary Licenses, and the -+Covered Software is not Incompatible With Secondary Licenses, this -+License permits You to additionally distribute such Covered Software -+under the terms of such Secondary License(s), so that the recipient of -+the Larger Work may, at their option, further distribute the Covered -+Software under the terms of either this License or such Secondary -+License(s). -+ -+3.4. Notices -+ -+You may not remove or alter the substance of any license notices -+(including copyright notices, patent notices, disclaimers of warranty, -+or limitations of liability) contained within the Source Code Form of -+the Covered Software, except that You may alter any license notices to -+the extent required to remedy known factual inaccuracies. -+ -+3.5. Application of Additional Terms -+ -+You may choose to offer, and to charge a fee for, warranty, support, -+indemnity or liability obligations to one or more recipients of Covered -+Software. However, You may do so only on Your own behalf, and not on -+behalf of any Contributor. You must make it absolutely clear that any -+such warranty, support, indemnity, or liability obligation is offered by -+You alone, and You hereby agree to indemnify every Contributor for any -+liability incurred by such Contributor as a result of warranty, support, -+indemnity or liability terms You offer. You may include additional -+disclaimers of warranty and limitations of liability specific to any -+jurisdiction. -+ -+4. Inability to Comply Due to Statute or Regulation -+--------------------------------------------------- -+ -+If it is impossible for You to comply with any of the terms of this -+License with respect to some or all of the Covered Software due to -+statute, judicial order, or regulation then You must: (a) comply with -+the terms of this License to the maximum extent possible; and (b) -+describe the limitations and the code they affect. Such description must -+be placed in a text file included with all distributions of the Covered -+Software under this License. Except to the extent prohibited by statute -+or regulation, such description must be sufficiently detailed for a -+recipient of ordinary skill to be able to understand it. -+ -+5. Termination -+-------------- -+ -+5.1. The rights granted under this License will terminate automatically -+if You fail to comply with any of its terms. However, if You become -+compliant, then the rights granted under this License from a particular -+Contributor are reinstated (a) provisionally, unless and until such -+Contributor explicitly and finally terminates Your grants, and (b) on an -+ongoing basis, if such Contributor fails to notify You of the -+non-compliance by some reasonable means prior to 60 days after You have -+come back into compliance. Moreover, Your grants from a particular -+Contributor are reinstated on an ongoing basis if such Contributor -+notifies You of the non-compliance by some reasonable means, this is the -+first time You have received notice of non-compliance with this License -+from such Contributor, and You become compliant prior to 30 days after -+Your receipt of the notice. -+ -+5.2. If You initiate litigation against any entity by asserting a patent -+infringement claim (excluding declaratory judgment actions, -+counter-claims, and cross-claims) alleging that a Contributor Version -+directly or indirectly infringes any patent, then the rights granted to -+You by any and all Contributors for the Covered Software under Section -+2.1 of this License shall terminate. -+ -+5.3. In the event of termination under Sections 5.1 or 5.2 above, all -+end user license agreements (excluding distributors and resellers) which -+have been validly granted by You or Your distributors under this License -+prior to termination shall survive termination. -+ -+************************************************************************ -+* * -+* 6. Disclaimer of Warranty * -+* ------------------------- * -+* * -+* Covered Software is provided under this License on an "as is" * -+* basis, without warranty of any kind, either expressed, implied, or * -+* statutory, including, without limitation, warranties that the * -+* Covered Software is free of defects, merchantable, fit for a * -+* particular purpose or non-infringing. The entire risk as to the * -+* quality and performance of the Covered Software is with You. * -+* Should any Covered Software prove defective in any respect, You * -+* (not any Contributor) assume the cost of any necessary servicing, * -+* repair, or correction. This disclaimer of warranty constitutes an * -+* essential part of this License. No use of any Covered Software is * -+* authorized under this License except under this disclaimer. * -+* * -+************************************************************************ -+ -+************************************************************************ -+* * -+* 7. Limitation of Liability * -+* -------------------------- * -+* * -+* Under no circumstances and under no legal theory, whether tort * -+* (including negligence), contract, or otherwise, shall any * -+* Contributor, or anyone who distributes Covered Software as * -+* permitted above, be liable to You for any direct, indirect, * -+* special, incidental, or consequential damages of any character * -+* including, without limitation, damages for lost profits, loss of * -+* goodwill, work stoppage, computer failure or malfunction, or any * -+* and all other commercial damages or losses, even if such party * -+* shall have been informed of the possibility of such damages. This * -+* limitation of liability shall not apply to liability for death or * -+* personal injury resulting from such party's negligence to the * -+* extent applicable law prohibits such limitation. Some * -+* jurisdictions do not allow the exclusion or limitation of * -+* incidental or consequential damages, so this exclusion and * -+* limitation may not apply to You. * -+* * -+************************************************************************ -+ -+8. Litigation -+------------- -+ -+Any litigation relating to this License may be brought only in the -+courts of a jurisdiction where the defendant maintains its principal -+place of business and such litigation shall be governed by laws of that -+jurisdiction, without reference to its conflict-of-law provisions. -+Nothing in this Section shall prevent a party's ability to bring -+cross-claims or counter-claims. -+ -+9. Miscellaneous -+---------------- -+ -+This License represents the complete agreement concerning the subject -+matter hereof. If any provision of this License is held to be -+unenforceable, such provision shall be reformed only to the extent -+necessary to make it enforceable. Any law or regulation which provides -+that the language of a contract shall be construed against the drafter -+shall not be used to construe this License against a Contributor. -+ -+10. Versions of the License -+--------------------------- -+ -+10.1. New Versions -+ -+Mozilla Foundation is the license steward. Except as provided in Section -+10.3, no one other than the license steward has the right to modify or -+publish new versions of this License. Each version will be given a -+distinguishing version number. -+ -+10.2. Effect of New Versions -+ -+You may distribute the Covered Software under the terms of the version -+of the License under which You originally received the Covered Software, -+or under the terms of any subsequent version published by the license -+steward. -+ -+10.3. Modified Versions -+ -+If you create software not governed by this License, and you want to -+create a new license for such software, you may create and use a -+modified version of this License if you rename the license and remove -+any references to the name of the license steward (except to note that -+such modified license differs from this License). -+ -+10.4. Distributing Source Code Form that is Incompatible With Secondary -+Licenses -+ -+If You choose to distribute Source Code Form that is Incompatible With -+Secondary Licenses under the terms of this version of the License, the -+notice described in Exhibit B of this License must be attached. -+ -+Exhibit A - Source Code Form License Notice -+------------------------------------------- -+ -+ This Source Code Form is subject to the terms of the Mozilla Public -+ License, v. 2.0. If a copy of the MPL was not distributed with this -+ file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+If it is not possible or desirable to put the notice in a particular -+file, then You may include the notice in a location (such as a LICENSE -+file in a relevant directory) where a recipient would be likely to look -+for such a notice. -+ -+You may add additional accurate notices of copyright ownership. -+ -+Exhibit B - "Incompatible With Secondary Licenses" Notice -+--------------------------------------------------------- -+ -+ This Source Code Form is "Incompatible With Secondary Licenses", as -+ defined by the Mozilla Public License, v. 2.0. -+``` -diff --git a/vendor/github.com/cyphar/filepath-securejoin/LICENSE b/vendor/github.com/cyphar/filepath-securejoin/LICENSE.BSD -similarity index 100% -rename from vendor/github.com/cyphar/filepath-securejoin/LICENSE -rename to vendor/github.com/cyphar/filepath-securejoin/LICENSE.BSD -diff --git a/vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0 b/vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0 -new file mode 100644 -index 00000000..d0a1fa14 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0 -@@ -0,0 +1,373 @@ -+Mozilla Public License Version 2.0 -+================================== -+ -+1. Definitions -+-------------- -+ -+1.1. "Contributor" -+ means each individual or legal entity that creates, contributes to -+ the creation of, or owns Covered Software. -+ -+1.2. "Contributor Version" -+ means the combination of the Contributions of others (if any) used -+ by a Contributor and that particular Contributor's Contribution. -+ -+1.3. "Contribution" -+ means Covered Software of a particular Contributor. -+ -+1.4. "Covered Software" -+ means Source Code Form to which the initial Contributor has attached -+ the notice in Exhibit A, the Executable Form of such Source Code -+ Form, and Modifications of such Source Code Form, in each case -+ including portions thereof. -+ -+1.5. "Incompatible With Secondary Licenses" -+ means -+ -+ (a) that the initial Contributor has attached the notice described -+ in Exhibit B to the Covered Software; or -+ -+ (b) that the Covered Software was made available under the terms of -+ version 1.1 or earlier of the License, but not also under the -+ terms of a Secondary License. -+ -+1.6. "Executable Form" -+ means any form of the work other than Source Code Form. -+ -+1.7. "Larger Work" -+ means a work that combines Covered Software with other material, in -+ a separate file or files, that is not Covered Software. -+ -+1.8. "License" -+ means this document. -+ -+1.9. "Licensable" -+ means having the right to grant, to the maximum extent possible, -+ whether at the time of the initial grant or subsequently, any and -+ all of the rights conveyed by this License. -+ -+1.10. "Modifications" -+ means any of the following: -+ -+ (a) any file in Source Code Form that results from an addition to, -+ deletion from, or modification of the contents of Covered -+ Software; or -+ -+ (b) any new file in Source Code Form that contains any Covered -+ Software. -+ -+1.11. "Patent Claims" of a Contributor -+ means any patent claim(s), including without limitation, method, -+ process, and apparatus claims, in any patent Licensable by such -+ Contributor that would be infringed, but for the grant of the -+ License, by the making, using, selling, offering for sale, having -+ made, import, or transfer of either its Contributions or its -+ Contributor Version. -+ -+1.12. "Secondary License" -+ means either the GNU General Public License, Version 2.0, the GNU -+ Lesser General Public License, Version 2.1, the GNU Affero General -+ Public License, Version 3.0, or any later versions of those -+ licenses. -+ -+1.13. "Source Code Form" -+ means the form of the work preferred for making modifications. -+ -+1.14. "You" (or "Your") -+ means an individual or a legal entity exercising rights under this -+ License. For legal entities, "You" includes any entity that -+ controls, is controlled by, or is under common control with You. For -+ purposes of this definition, "control" means (a) the power, direct -+ or indirect, to cause the direction or management of such entity, -+ whether by contract or otherwise, or (b) ownership of more than -+ fifty percent (50%) of the outstanding shares or beneficial -+ ownership of such entity. -+ -+2. License Grants and Conditions -+-------------------------------- -+ -+2.1. Grants -+ -+Each Contributor hereby grants You a world-wide, royalty-free, -+non-exclusive license: -+ -+(a) under intellectual property rights (other than patent or trademark) -+ Licensable by such Contributor to use, reproduce, make available, -+ modify, display, perform, distribute, and otherwise exploit its -+ Contributions, either on an unmodified basis, with Modifications, or -+ as part of a Larger Work; and -+ -+(b) under Patent Claims of such Contributor to make, use, sell, offer -+ for sale, have made, import, and otherwise transfer either its -+ Contributions or its Contributor Version. -+ -+2.2. Effective Date -+ -+The licenses granted in Section 2.1 with respect to any Contribution -+become effective for each Contribution on the date the Contributor first -+distributes such Contribution. -+ -+2.3. Limitations on Grant Scope -+ -+The licenses granted in this Section 2 are the only rights granted under -+this License. No additional rights or licenses will be implied from the -+distribution or licensing of Covered Software under this License. -+Notwithstanding Section 2.1(b) above, no patent license is granted by a -+Contributor: -+ -+(a) for any code that a Contributor has removed from Covered Software; -+ or -+ -+(b) for infringements caused by: (i) Your and any other third party's -+ modifications of Covered Software, or (ii) the combination of its -+ Contributions with other software (except as part of its Contributor -+ Version); or -+ -+(c) under Patent Claims infringed by Covered Software in the absence of -+ its Contributions. -+ -+This License does not grant any rights in the trademarks, service marks, -+or logos of any Contributor (except as may be necessary to comply with -+the notice requirements in Section 3.4). -+ -+2.4. Subsequent Licenses -+ -+No Contributor makes additional grants as a result of Your choice to -+distribute the Covered Software under a subsequent version of this -+License (see Section 10.2) or under the terms of a Secondary License (if -+permitted under the terms of Section 3.3). -+ -+2.5. Representation -+ -+Each Contributor represents that the Contributor believes its -+Contributions are its original creation(s) or it has sufficient rights -+to grant the rights to its Contributions conveyed by this License. -+ -+2.6. Fair Use -+ -+This License is not intended to limit any rights You have under -+applicable copyright doctrines of fair use, fair dealing, or other -+equivalents. -+ -+2.7. Conditions -+ -+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -+in Section 2.1. -+ -+3. Responsibilities -+------------------- -+ -+3.1. Distribution of Source Form -+ -+All distribution of Covered Software in Source Code Form, including any -+Modifications that You create or to which You contribute, must be under -+the terms of this License. You must inform recipients that the Source -+Code Form of the Covered Software is governed by the terms of this -+License, and how they can obtain a copy of this License. You may not -+attempt to alter or restrict the recipients' rights in the Source Code -+Form. -+ -+3.2. Distribution of Executable Form -+ -+If You distribute Covered Software in Executable Form then: -+ -+(a) such Covered Software must also be made available in Source Code -+ Form, as described in Section 3.1, and You must inform recipients of -+ the Executable Form how they can obtain a copy of such Source Code -+ Form by reasonable means in a timely manner, at a charge no more -+ than the cost of distribution to the recipient; and -+ -+(b) You may distribute such Executable Form under the terms of this -+ License, or sublicense it under different terms, provided that the -+ license for the Executable Form does not attempt to limit or alter -+ the recipients' rights in the Source Code Form under this License. -+ -+3.3. Distribution of a Larger Work -+ -+You may create and distribute a Larger Work under terms of Your choice, -+provided that You also comply with the requirements of this License for -+the Covered Software. If the Larger Work is a combination of Covered -+Software with a work governed by one or more Secondary Licenses, and the -+Covered Software is not Incompatible With Secondary Licenses, this -+License permits You to additionally distribute such Covered Software -+under the terms of such Secondary License(s), so that the recipient of -+the Larger Work may, at their option, further distribute the Covered -+Software under the terms of either this License or such Secondary -+License(s). -+ -+3.4. Notices -+ -+You may not remove or alter the substance of any license notices -+(including copyright notices, patent notices, disclaimers of warranty, -+or limitations of liability) contained within the Source Code Form of -+the Covered Software, except that You may alter any license notices to -+the extent required to remedy known factual inaccuracies. -+ -+3.5. Application of Additional Terms -+ -+You may choose to offer, and to charge a fee for, warranty, support, -+indemnity or liability obligations to one or more recipients of Covered -+Software. However, You may do so only on Your own behalf, and not on -+behalf of any Contributor. You must make it absolutely clear that any -+such warranty, support, indemnity, or liability obligation is offered by -+You alone, and You hereby agree to indemnify every Contributor for any -+liability incurred by such Contributor as a result of warranty, support, -+indemnity or liability terms You offer. You may include additional -+disclaimers of warranty and limitations of liability specific to any -+jurisdiction. -+ -+4. Inability to Comply Due to Statute or Regulation -+--------------------------------------------------- -+ -+If it is impossible for You to comply with any of the terms of this -+License with respect to some or all of the Covered Software due to -+statute, judicial order, or regulation then You must: (a) comply with -+the terms of this License to the maximum extent possible; and (b) -+describe the limitations and the code they affect. Such description must -+be placed in a text file included with all distributions of the Covered -+Software under this License. Except to the extent prohibited by statute -+or regulation, such description must be sufficiently detailed for a -+recipient of ordinary skill to be able to understand it. -+ -+5. Termination -+-------------- -+ -+5.1. The rights granted under this License will terminate automatically -+if You fail to comply with any of its terms. However, if You become -+compliant, then the rights granted under this License from a particular -+Contributor are reinstated (a) provisionally, unless and until such -+Contributor explicitly and finally terminates Your grants, and (b) on an -+ongoing basis, if such Contributor fails to notify You of the -+non-compliance by some reasonable means prior to 60 days after You have -+come back into compliance. Moreover, Your grants from a particular -+Contributor are reinstated on an ongoing basis if such Contributor -+notifies You of the non-compliance by some reasonable means, this is the -+first time You have received notice of non-compliance with this License -+from such Contributor, and You become compliant prior to 30 days after -+Your receipt of the notice. -+ -+5.2. If You initiate litigation against any entity by asserting a patent -+infringement claim (excluding declaratory judgment actions, -+counter-claims, and cross-claims) alleging that a Contributor Version -+directly or indirectly infringes any patent, then the rights granted to -+You by any and all Contributors for the Covered Software under Section -+2.1 of this License shall terminate. -+ -+5.3. In the event of termination under Sections 5.1 or 5.2 above, all -+end user license agreements (excluding distributors and resellers) which -+have been validly granted by You or Your distributors under this License -+prior to termination shall survive termination. -+ -+************************************************************************ -+* * -+* 6. Disclaimer of Warranty * -+* ------------------------- * -+* * -+* Covered Software is provided under this License on an "as is" * -+* basis, without warranty of any kind, either expressed, implied, or * -+* statutory, including, without limitation, warranties that the * -+* Covered Software is free of defects, merchantable, fit for a * -+* particular purpose or non-infringing. The entire risk as to the * -+* quality and performance of the Covered Software is with You. * -+* Should any Covered Software prove defective in any respect, You * -+* (not any Contributor) assume the cost of any necessary servicing, * -+* repair, or correction. This disclaimer of warranty constitutes an * -+* essential part of this License. No use of any Covered Software is * -+* authorized under this License except under this disclaimer. * -+* * -+************************************************************************ -+ -+************************************************************************ -+* * -+* 7. Limitation of Liability * -+* -------------------------- * -+* * -+* Under no circumstances and under no legal theory, whether tort * -+* (including negligence), contract, or otherwise, shall any * -+* Contributor, or anyone who distributes Covered Software as * -+* permitted above, be liable to You for any direct, indirect, * -+* special, incidental, or consequential damages of any character * -+* including, without limitation, damages for lost profits, loss of * -+* goodwill, work stoppage, computer failure or malfunction, or any * -+* and all other commercial damages or losses, even if such party * -+* shall have been informed of the possibility of such damages. This * -+* limitation of liability shall not apply to liability for death or * -+* personal injury resulting from such party's negligence to the * -+* extent applicable law prohibits such limitation. Some * -+* jurisdictions do not allow the exclusion or limitation of * -+* incidental or consequential damages, so this exclusion and * -+* limitation may not apply to You. * -+* * -+************************************************************************ -+ -+8. Litigation -+------------- -+ -+Any litigation relating to this License may be brought only in the -+courts of a jurisdiction where the defendant maintains its principal -+place of business and such litigation shall be governed by laws of that -+jurisdiction, without reference to its conflict-of-law provisions. -+Nothing in this Section shall prevent a party's ability to bring -+cross-claims or counter-claims. -+ -+9. Miscellaneous -+---------------- -+ -+This License represents the complete agreement concerning the subject -+matter hereof. If any provision of this License is held to be -+unenforceable, such provision shall be reformed only to the extent -+necessary to make it enforceable. Any law or regulation which provides -+that the language of a contract shall be construed against the drafter -+shall not be used to construe this License against a Contributor. -+ -+10. Versions of the License -+--------------------------- -+ -+10.1. New Versions -+ -+Mozilla Foundation is the license steward. Except as provided in Section -+10.3, no one other than the license steward has the right to modify or -+publish new versions of this License. Each version will be given a -+distinguishing version number. -+ -+10.2. Effect of New Versions -+ -+You may distribute the Covered Software under the terms of the version -+of the License under which You originally received the Covered Software, -+or under the terms of any subsequent version published by the license -+steward. -+ -+10.3. Modified Versions -+ -+If you create software not governed by this License, and you want to -+create a new license for such software, you may create and use a -+modified version of this License if you rename the license and remove -+any references to the name of the license steward (except to note that -+such modified license differs from this License). -+ -+10.4. Distributing Source Code Form that is Incompatible With Secondary -+Licenses -+ -+If You choose to distribute Source Code Form that is Incompatible With -+Secondary Licenses under the terms of this version of the License, the -+notice described in Exhibit B of this License must be attached. -+ -+Exhibit A - Source Code Form License Notice -+------------------------------------------- -+ -+ This Source Code Form is subject to the terms of the Mozilla Public -+ License, v. 2.0. If a copy of the MPL was not distributed with this -+ file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+If it is not possible or desirable to put the notice in a particular -+file, then You may include the notice in a location (such as a LICENSE -+file in a relevant directory) where a recipient would be likely to look -+for such a notice. -+ -+You may add additional accurate notices of copyright ownership. -+ -+Exhibit B - "Incompatible With Secondary Licenses" Notice -+--------------------------------------------------------- -+ -+ This Source Code Form is "Incompatible With Secondary Licenses", as -+ defined by the Mozilla Public License, v. 2.0. -diff --git a/vendor/github.com/cyphar/filepath-securejoin/README.md b/vendor/github.com/cyphar/filepath-securejoin/README.md -index eaeb53fc..6673abfc 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/README.md -+++ b/vendor/github.com/cyphar/filepath-securejoin/README.md -@@ -67,7 +67,8 @@ func SecureJoin(root, unsafePath string) (string, error) { - [libpathrs]: https://github.com/openSUSE/libpathrs - [go#20126]: https://github.com/golang/go/issues/20126 - --### New API ### -+### New API ### -+[#new-api]: #new-api - - While we recommend users switch to [libpathrs][libpathrs] as soon as it has a - stable release, some methods implemented by libpathrs have been ported to this -@@ -165,5 +166,19 @@ after `MkdirAll`). - - ### License ### - --The license of this project is the same as Go, which is a BSD 3-clause license --available in the `LICENSE` file. -+`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0` -+ -+Some of the code in this project is derived from Go, and is licensed under a -+BSD 3-clause license (available in `LICENSE.BSD`). Other files (many of which -+are derived from [libpathrs][libpathrs]) are licensed under the Mozilla Public -+License version 2.0 (available in `LICENSE.MPL-2.0`). If you are using the -+["New API" described above][#new-api], you are probably using code from files -+released under this license. -+ -+Every source file in this project has a copyright header describing its -+license. Please check the license headers of each file to see what license -+applies to it. -+ -+See [COPYING.md](./COPYING.md) for some more details. -+ -+[umoci]: https://github.com/opencontainers/umoci -diff --git a/vendor/github.com/cyphar/filepath-securejoin/VERSION b/vendor/github.com/cyphar/filepath-securejoin/VERSION -index 267577d4..8f0916f7 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/VERSION -+++ b/vendor/github.com/cyphar/filepath-securejoin/VERSION -@@ -1 +1 @@ --0.4.1 -+0.5.0 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/codecov.yml b/vendor/github.com/cyphar/filepath-securejoin/codecov.yml -new file mode 100644 -index 00000000..ff284dbf ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/codecov.yml -@@ -0,0 +1,29 @@ -+# SPDX-License-Identifier: MPL-2.0 -+ -+# Copyright (C) 2025 Aleksa Sarai -+# Copyright (C) 2025 SUSE LLC -+# -+# This Source Code Form is subject to the terms of the Mozilla Public -+# License, v. 2.0. If a copy of the MPL was not distributed with this -+# file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+comment: -+ layout: "condensed_header, reach, diff, components, condensed_files, condensed_footer" -+ require_changes: true -+ branches: -+ - main -+ -+coverage: -+ range: 60..100 -+ status: -+ project: -+ default: -+ target: 85% -+ threshold: 0% -+ patch: -+ default: -+ target: auto -+ informational: true -+ -+github_checks: -+ annotations: false -diff --git a/vendor/github.com/cyphar/filepath-securejoin/deprecated_linux.go b/vendor/github.com/cyphar/filepath-securejoin/deprecated_linux.go -new file mode 100644 -index 00000000..3e427b16 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/deprecated_linux.go -@@ -0,0 +1,48 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package securejoin -+ -+import ( -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+) -+ -+var ( -+ // MkdirAll is a wrapper around [pathrs.MkdirAll]. -+ // -+ // Deprecated: You should use [pathrs.MkdirAll] directly instead. This -+ // wrapper will be removed in filepath-securejoin v0.6. -+ MkdirAll = pathrs.MkdirAll -+ -+ // MkdirAllHandle is a wrapper around [pathrs.MkdirAllHandle]. -+ // -+ // Deprecated: You should use [pathrs.MkdirAllHandle] directly instead. -+ // This wrapper will be removed in filepath-securejoin v0.6. -+ MkdirAllHandle = pathrs.MkdirAllHandle -+ -+ // OpenInRoot is a wrapper around [pathrs.OpenInRoot]. -+ // -+ // Deprecated: You should use [pathrs.OpenInRoot] directly instead. This -+ // wrapper will be removed in filepath-securejoin v0.6. -+ OpenInRoot = pathrs.OpenInRoot -+ -+ // OpenatInRoot is a wrapper around [pathrs.OpenatInRoot]. -+ // -+ // Deprecated: You should use [pathrs.OpenatInRoot] directly instead. This -+ // wrapper will be removed in filepath-securejoin v0.6. -+ OpenatInRoot = pathrs.OpenatInRoot -+ -+ // Reopen is a wrapper around [pathrs.Reopen]. -+ // -+ // Deprecated: You should use [pathrs.Reopen] directly instead. This -+ // wrapper will be removed in filepath-securejoin v0.6. -+ Reopen = pathrs.Reopen -+) -diff --git a/vendor/github.com/cyphar/filepath-securejoin/doc.go b/vendor/github.com/cyphar/filepath-securejoin/doc.go -index 1ec7d065..1438fc9c 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/doc.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/doc.go -@@ -1,3 +1,5 @@ -+// SPDX-License-Identifier: BSD-3-Clause -+ - // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. - // Copyright (C) 2017-2024 SUSE LLC. All rights reserved. - // Use of this source code is governed by a BSD-style -@@ -14,14 +16,13 @@ - // **not** safe against race conditions where an attacker changes the - // filesystem after (or during) the [SecureJoin] operation. - // --// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived --// functions). These are safe against racing attackers and have several other --// protections that are not provided by the legacy API. There are many more --// operations that most programs expect to be able to do safely, but we do not --// provide explicit support for them because we want to encourage users to --// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a --// cross-language next-generation library that is entirely designed around --// operating on paths safely. -+// The new API is available in the [pathrs-lite] subpackage, and provide -+// protections against racing attackers as well as several other key -+// protections against attacks often seen by container runtimes. As the name -+// suggests, [pathrs-lite] is a stripped down (pure Go) reimplementation of -+// [libpathrs]. The main APIs provided are [OpenInRoot], [MkdirAll], and -+// [procfs.Handle] -- other APIs are not planned to be ported. The long-term -+// goal is for users to migrate to [libpathrs] which is more fully-featured. - // - // securejoin has been used by several container runtimes (Docker, runc, - // Kubernetes, etc) for quite a few years as a de-facto standard for operating -@@ -31,9 +32,16 @@ - // API as soon as possible (or even better, switch to libpathrs). - // - // This project was initially intended to be included in the Go standard --// library, but [it was rejected](https://go.dev/issue/20126). There is now a --// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API --// that shares some of the goals of filepath-securejoin. However, that design --// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the --// usecase of container runtimes and most system tools. -+// library, but it was rejected (see https://go.dev/issue/20126). Much later, -+// [os.Root] was added to the Go stdlib that shares some of the goals of -+// filepath-securejoin. However, its design is intended to work like -+// openat2(RESOLVE_BENEATH) which does not fit the usecase of container -+// runtimes and most system tools. -+// -+// [pathrs-lite]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite -+// [libpathrs]: https://github.com/openSUSE/libpathrs -+// [OpenInRoot]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#OpenInRoot -+// [MkdirAll]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#MkdirAll -+// [procfs.Handle]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle -+// [os.Root]: https:///pkg.go.dev/os#Root - package securejoin -diff --git a/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go b/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go -deleted file mode 100644 -index ddd6fa9a..00000000 ---- a/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go -+++ /dev/null -@@ -1,32 +0,0 @@ --//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 { -- return slices.DeleteFunc(slice, delFn) --} -- --func slices_Contains[S ~[]E, E comparable](slice S, val E) bool { -- return slices.Contains(slice, val) --} -- --func slices_Clone[S ~[]E, E any](slice S) S { -- return slices.Clone(slice) --} -- --func sync_OnceValue[T any](f func() T) func() T { -- return sync.OnceValue(f) --} -- --func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { -- return sync.OnceValues(f) --} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go b/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go -deleted file mode 100644 -index f1e6fe7e..00000000 ---- a/vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go -+++ /dev/null -@@ -1,124 +0,0 @@ --//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 ( -- "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 { -- 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 { -- 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 { -- 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 { -- // 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 { -- 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 -- } --} -- --// Copied from the Go 1.24 stdlib implementation. --func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { -- var ( -- once sync.Once -- valid bool -- p any -- r1 T1 -- r2 T2 -- ) -- g := func() { -- defer func() { -- p = recover() -- if !valid { -- panic(p) -- } -- }() -- r1, r2 = f() -- f = nil -- valid = true -- } -- return func() (T1, T2) { -- once.Do(g) -- if !valid { -- panic(p) -- } -- return r1, r2 -- } --} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go b/vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go -new file mode 100644 -index 00000000..c69c4da9 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go -@@ -0,0 +1,15 @@ -+// SPDX-License-Identifier: BSD-3-Clause -+ -+// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. -+// Copyright (C) 2017-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 consts contains the definitions of internal constants used -+// throughout filepath-securejoin. -+package consts -+ -+// MaxSymlinkLimit is the maximum number of symlinks that can be encountered -+// during a single lookup before returning -ELOOP. At time of writing, Linux -+// has an internal limit of 40. -+const MaxSymlinkLimit = 255 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/join.go b/vendor/github.com/cyphar/filepath-securejoin/join.go -index e6634d47..199c1d83 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/join.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/join.go -@@ -1,3 +1,5 @@ -+// SPDX-License-Identifier: BSD-3-Clause -+ - // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. - // Copyright (C) 2017-2025 SUSE LLC. All rights reserved. - // Use of this source code is governed by a BSD-style -@@ -11,9 +13,9 @@ import ( - "path/filepath" - "strings" - "syscall" --) - --const maxSymlinkLimit = 255 -+ "github.com/cyphar/filepath-securejoin/internal/consts" -+) - - // IsNotExist tells you if err is an error that implies that either the path - // accessed does not exist (or path components don't exist). This is -@@ -49,12 +51,13 @@ func hasDotDot(path string) bool { - return strings.Contains("/"+path+"/", "/../") - } - --// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except --// that the returned path is guaranteed to be scoped inside the provided root --// path (when evaluated). Any symbolic links in the path are evaluated with the --// given root treated as the root of the filesystem, similar to a chroot. The --// filesystem state is evaluated through the given [VFS] interface (if nil, the --// standard [os].* family of functions are used). -+// SecureJoinVFS joins the two given path components (similar to -+// [filepath.Join]) except that the returned path is guaranteed to be scoped -+// inside the provided root path (when evaluated). Any symbolic links in the -+// path are evaluated with the given root treated as the root of the -+// filesystem, similar to a chroot. The filesystem state is evaluated through -+// the given [VFS] interface (if nil, the standard [os].* family of functions -+// are used). - // - // Note that the guarantees provided by this function only apply if the path - // components in the returned string are not modified (in other words are not -@@ -78,7 +81,7 @@ func hasDotDot(path string) bool { - // fully resolved using [filepath.EvalSymlinks] or otherwise constructed to - // avoid containing symlink components. Of course, the root also *must not* be - // attacker-controlled. --func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { -+func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:revive // name is part of public API - // The root path must not contain ".." components, otherwise when we join - // the subpath we will end up with a weird path. We could work around this - // in other ways but users shouldn't be giving us non-lexical root paths in -@@ -138,7 +141,7 @@ func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { - // It's a symlink, so get its contents and expand it by prepending it - // to the yet-unparsed path. - linksWalked++ -- if linksWalked > maxSymlinkLimit { -+ if linksWalked > consts.MaxSymlinkLimit { - return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP} - } - -diff --git a/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go b/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go -deleted file mode 100644 -index f7a13e69..00000000 ---- a/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go -+++ /dev/null -@@ -1,127 +0,0 @@ --//go:build linux -- --// 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 ( -- "errors" -- "fmt" -- "os" -- "path/filepath" -- "strings" -- -- "golang.org/x/sys/unix" --) -- --var hasOpenat2 = sync_OnceValue(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, -- }) -- if err != nil { -- return false -- } -- _ = unix.Close(fd) -- return true --}) -- --func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { -- // RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve -- // ".." while a mount or rename occurs anywhere on the system. This could -- // happen spuriously, or as the result of an attacker trying to mess with -- // us during lookup. -- // -- // In addition, scoped lookups have a "safety check" at the end of -- // complete_walk which will return -EXDEV if the final path is not in the -- // root. -- return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 && -- (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV)) --} -- --const scopedLookupMaxRetries = 10 -- --func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) { -- fullPath := dir.Name() + "/" + path -- // Make sure we always set O_CLOEXEC. -- how.Flags |= unix.O_CLOEXEC -- var tries int -- for tries < scopedLookupMaxRetries { -- fd, err := unix.Openat2(int(dir.Fd()), path, how) -- if err != nil { -- if scopedLookupShouldRetry(how, err) { -- // We retry a couple of times to avoid the spurious errors, and -- // if we are being attacked then returning -EAGAIN is the best -- // we can do. -- tries++ -- continue -- } -- return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err} -- } -- // If we are using RESOLVE_IN_ROOT, the name we generated may be wrong. -- // NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise -- // you'll get infinite recursion here. -- if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT { -- if actualPath, err := rawProcSelfFdReadlink(fd); err == nil { -- fullPath = actualPath -- } -- } -- return os.NewFile(uintptr(fd), fullPath), nil -- } -- return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack} --} -- --func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) { -- if !partial { -- file, err := openat2File(root, unsafePath, &unix.OpenHow{ -- Flags: unix.O_PATH | unix.O_CLOEXEC, -- Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, -- }) -- return file, "", err -- } -- return partialLookupOpenat2(root, unsafePath) --} -- --// partialLookupOpenat2 is an alternative implementation of --// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a --// handle to the deepest existing child of the requested path within the root. --func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) { -- // TODO: Implement this as a git-bisect-like binary search. -- -- unsafePath = filepath.ToSlash(unsafePath) // noop -- endIdx := len(unsafePath) -- var lastError error -- for endIdx > 0 { -- subpath := unsafePath[:endIdx] -- -- handle, err := openat2File(root, subpath, &unix.OpenHow{ -- Flags: unix.O_PATH | unix.O_CLOEXEC, -- Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, -- }) -- if err == nil { -- // Jump over the slash if we have a non-"" remainingPath. -- if endIdx < len(unsafePath) { -- endIdx += 1 -- } -- // We found a subpath! -- return handle, unsafePath[endIdx:], lastError -- } -- if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) { -- // That path doesn't exist, let's try the next directory up. -- endIdx = strings.LastIndexByte(subpath, '/') -- lastError = err -- continue -- } -- return nil, "", fmt.Errorf("open subpath: %w", err) -- } -- // If we couldn't open anything, the whole subpath is missing. Return a -- // copy of the root fd so that the caller doesn't close this one by -- // accident. -- rootClone, err := dupFile(root) -- if err != nil { -- return nil, "", err -- } -- return rootClone, unsafePath, lastError --} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go b/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go -deleted file mode 100644 -index 949fb5f2..00000000 ---- a/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go -+++ /dev/null -@@ -1,59 +0,0 @@ --//go:build linux -- --// 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 ( -- "os" -- "path/filepath" -- -- "golang.org/x/sys/unix" --) -- --func dupFile(f *os.File) (*os.File, error) { -- fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0) -- if err != nil { -- return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err) -- } -- return os.NewFile(uintptr(fd), f.Name()), nil --} -- --func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) { -- // Make sure we always set O_CLOEXEC. -- flags |= unix.O_CLOEXEC -- fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode)) -- if err != nil { -- return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err} -- } -- // All of the paths we use with openatFile(2) are guaranteed to be -- // lexically safe, so we can use path.Join here. -- fullPath := filepath.Join(dir.Name(), path) -- return os.NewFile(uintptr(fd), fullPath), nil --} -- --func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) { -- var stat unix.Stat_t -- if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil { -- return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err} -- } -- return stat, nil --} -- --func readlinkatFile(dir *os.File, path string) (string, error) { -- size := 4096 -- for { -- linkBuf := make([]byte, size) -- n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf) -- if err != nil { -- return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err} -- } -- if n != size { -- return string(linkBuf[:n]), nil -- } -- // Possible truncation, resize the buffer. -- size *= 2 -- } --} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md -new file mode 100644 -index 00000000..1be727e7 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md -@@ -0,0 +1,33 @@ -+## `pathrs-lite` ## -+ -+`github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure -+Go** implementation of the core bits of [libpathrs][]. This is not intended to -+be a complete replacement for libpathrs, instead it is mainly intended to be -+useful as a transition tool for existing Go projects. -+ -+The long-term plan for `pathrs-lite` is to provide a build tag that will cause -+all `pathrs-lite` operations to call into libpathrs directly, thus removing -+code duplication for projects that wish to make use of libpathrs (and providing -+the ability for software packagers to opt-in to libpathrs support without -+needing to patch upstream). -+ -+[libpathrs]: https://github.com/cyphar/libpathrs -+ -+### License ### -+ -+Most of this subpackage is licensed under the Mozilla Public License (version -+2.0). For more information, see the top-level [COPYING.md][] and -+[LICENSE.MPL-2.0][] files, as well as the individual license headers for each -+file. -+ -+``` -+Copyright (C) 2024-2025 Aleksa Sarai -+Copyright (C) 2024-2025 SUSE LLC -+ -+This Source Code Form is subject to the terms of the Mozilla Public -+License, v. 2.0. If a copy of the MPL was not distributed with this -+file, You can obtain one at https://mozilla.org/MPL/2.0/. -+``` -+ -+[COPYING.md]: ../COPYING.md -+[LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0 -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/doc.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/doc.go -new file mode 100644 -index 00000000..d3d74517 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/doc.go -@@ -0,0 +1,14 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package pathrs (pathrs-lite) is a less complete pure Go implementation of -+// some of the APIs provided by [libpathrs]. -+package pathrs -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go -new file mode 100644 -index 00000000..595dfbf1 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go -@@ -0,0 +1,30 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+// Copyright (C) 2025 Aleksa Sarai -+// Copyright (C) 2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package assert provides some basic assertion helpers for Go. -+package assert -+ -+import ( -+ "fmt" -+) -+ -+// Assert panics if the predicate is false with the provided argument. -+func Assert(predicate bool, msg any) { -+ if !predicate { -+ panic(msg) -+ } -+} -+ -+// Assertf panics if the predicate is false and formats the message using the -+// same formatting as [fmt.Printf]. -+// -+// [fmt.Printf]: https://pkg.go.dev/fmt#Printf -+func Assertf(predicate bool, fmtMsg string, args ...any) { -+ Assert(predicate, fmt.Sprintf(fmtMsg, args...)) -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go -new file mode 100644 -index 00000000..c26e440e ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors.go -@@ -0,0 +1,30 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package internal contains unexported common code for filepath-securejoin. -+package internal -+ -+import ( -+ "errors" -+) -+ -+var ( -+ // ErrPossibleAttack indicates that some attack was detected. -+ ErrPossibleAttack = errors.New("possible attack detected") -+ -+ // ErrPossibleBreakout indicates that during an operation we ended up in a -+ // state that could be a breakout but we detected it. -+ ErrPossibleBreakout = errors.New("possible breakout detected") -+ -+ // ErrInvalidDirectory indicates an unlinked directory. -+ ErrInvalidDirectory = errors.New("wandered into deleted directory") -+ -+ // ErrDeletedInode indicates an unlinked file (non-directory). -+ ErrDeletedInode = errors.New("cannot verify path of deleted inode") -+) -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go -new file mode 100644 -index 00000000..09105491 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go -@@ -0,0 +1,148 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package fd -+ -+import ( -+ "fmt" -+ "os" -+ "path/filepath" -+ "runtime" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+) -+ -+// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using -+// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally -+// don't want to allow relative-to-cwd paths. The returned path is an -+// *informational* string that describes a reasonable pathname for the given -+// *at(2) arguments. You must not use the full path for any actual filesystem -+// operations. -+func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) { -+ dirFd, dirPath := -int(unix.EBADF), "." -+ if dir != nil { -+ dirFd, dirPath = int(dir.Fd()), dir.Name() -+ } -+ if !filepath.IsAbs(path) { -+ // only prepend the dirfd path for relative paths -+ path = dirPath + "/" + path -+ } -+ // NOTE: If path is "." or "", the returned path won't be filepath.Clean, -+ // but that's okay since this path is either used for errors (in which case -+ // a trailing "/" or "/." is important information) or will be -+ // filepath.Clean'd later (in the case of fd.Openat). -+ return dirFd, path -+} -+ -+// Openat is an [Fd]-based wrapper around unix.Openat. -+func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func -+ dirFd, fullPath := prepareAt(dir, path) -+ // Make sure we always set O_CLOEXEC. -+ flags |= unix.O_CLOEXEC -+ fd, err := unix.Openat(dirFd, path, flags, uint32(mode)) -+ if err != nil { -+ return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ // openat is only used with lexically-safe paths so we can use -+ // filepath.Clean here, and also the path itself is not going to be used -+ // for actual path operations. -+ fullPath = filepath.Clean(fullPath) -+ return os.NewFile(uintptr(fd), fullPath), nil -+} -+ -+// Fstatat is an [Fd]-based wrapper around unix.Fstatat. -+func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) { -+ dirFd, fullPath := prepareAt(dir, path) -+ var stat unix.Stat_t -+ if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil { -+ return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ return stat, nil -+} -+ -+// Faccessat is an [Fd]-based wrapper around unix.Faccessat. -+func Faccessat(dir Fd, path string, mode uint32, flags int) error { -+ dirFd, fullPath := prepareAt(dir, path) -+ err := unix.Faccessat(dirFd, path, mode, flags) -+ if err != nil { -+ err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ return err -+} -+ -+// Readlinkat is an [Fd]-based wrapper around unix.Readlinkat. -+func Readlinkat(dir Fd, path string) (string, error) { -+ dirFd, fullPath := prepareAt(dir, path) -+ size := 4096 -+ for { -+ linkBuf := make([]byte, size) -+ n, err := unix.Readlinkat(dirFd, path, linkBuf) -+ if err != nil { -+ return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ if n != size { -+ return string(linkBuf[:n]), nil -+ } -+ // Possible truncation, resize the buffer. -+ size *= 2 -+ } -+} -+ -+const ( -+ // STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to -+ // avoid bumping the requirement for a single constant we can just define it -+ // ourselves. -+ _STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name -+ -+ // We don't care which mount ID we get. The kernel will give us the unique -+ // one if it is supported. If the kernel doesn't support -+ // STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask -+ // will only contain STATX_MNT_ID (if supported). -+ wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID -+) -+ -+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 -+}) -+ -+// GetMountID gets the mount identifier associated with the fd and path -+// combination. It is effectively a wrapper around fetching -+// STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the -+// kernel doesn't support the feature. -+func GetMountID(dir Fd, path string) (uint64, error) { -+ // If we don't have statx(STATX_MNT_ID*) support, we can't do anything. -+ if !hasStatxMountID() { -+ return 0, nil -+ } -+ -+ dirFd, fullPath := prepareAt(dir, path) -+ -+ var stx unix.Statx_t -+ err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx) -+ if stx.Mask&wantStatxMntMask == 0 { -+ // It's not a kernel limitation, for some reason we couldn't get a -+ // mount ID. Assume it's some kind of attack. -+ err = fmt.Errorf("could not get mount id: %w", err) -+ } -+ if err != nil { -+ return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ return stx.Mnt_id, nil -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd.go -new file mode 100644 -index 00000000..d2206a38 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd.go -@@ -0,0 +1,55 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+// Copyright (C) 2025 Aleksa Sarai -+// Copyright (C) 2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package fd provides a drop-in interface-based replacement of [*os.File] that -+// allows for things like noop-Close wrappers to be used. -+// -+// [*os.File]: https://pkg.go.dev/os#File -+package fd -+ -+import ( -+ "io" -+ "os" -+) -+ -+// Fd is an interface that mirrors most of the API of [*os.File], allowing you -+// to create wrappers that can be used in place of [*os.File]. -+// -+// [*os.File]: https://pkg.go.dev/os#File -+type Fd interface { -+ io.Closer -+ Name() string -+ Fd() uintptr -+} -+ -+// Compile-time interface checks. -+var ( -+ _ Fd = (*os.File)(nil) -+ _ Fd = noClose{} -+) -+ -+type noClose struct{ inner Fd } -+ -+func (f noClose) Name() string { return f.inner.Name() } -+func (f noClose) Fd() uintptr { return f.inner.Fd() } -+ -+func (f noClose) Close() error { return nil } -+ -+// NopCloser returns an [*os.File]-like object where the [Close] method is now -+// a no-op. -+// -+// Note that for [*os.File] and similar objects, the Go garbage collector will -+// still call [Close] on the underlying file unless you use -+// [runtime.SetFinalizer] to disable this behaviour. This is up to the caller -+// to do (if necessary). -+// -+// [*os.File]: https://pkg.go.dev/os#File -+// [Close]: https://pkg.go.dev/io#Closer -+// [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer -+func NopCloser(f Fd) Fd { return noClose{inner: f} } -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go -new file mode 100644 -index 00000000..e1ec3c0b ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go -@@ -0,0 +1,78 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package fd -+ -+import ( -+ "fmt" -+ "os" -+ "runtime" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" -+) -+ -+// DupWithName creates a new file descriptor referencing the same underlying -+// file, but with the provided name instead of fd.Name(). -+func DupWithName(fd Fd, name string) (*os.File, error) { -+ fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0) -+ if err != nil { -+ return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err) -+ } -+ runtime.KeepAlive(fd) -+ return os.NewFile(uintptr(fd2), name), nil -+} -+ -+// Dup creates a new file description referencing the same underlying file. -+func Dup(fd Fd) (*os.File, error) { -+ return DupWithName(fd, fd.Name()) -+} -+ -+// Fstat is an [Fd]-based wrapper around unix.Fstat. -+func Fstat(fd Fd) (unix.Stat_t, error) { -+ var stat unix.Stat_t -+ if err := unix.Fstat(int(fd.Fd()), &stat); err != nil { -+ return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err} -+ } -+ runtime.KeepAlive(fd) -+ return stat, nil -+} -+ -+// Fstatfs is an [Fd]-based wrapper around unix.Fstatfs. -+func Fstatfs(fd Fd) (unix.Statfs_t, error) { -+ var statfs unix.Statfs_t -+ if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil { -+ return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err} -+ } -+ runtime.KeepAlive(fd) -+ return statfs, nil -+} -+ -+// IsDeadInode detects whether the file has been unlinked from a filesystem and -+// is thus a "dead inode" from the kernel's perspective. -+func IsDeadInode(file Fd) error { -+ // If the nlink of a file drops to 0, there is an attacker deleting -+ // directories during our walk, which could result in weird /proc values. -+ // It's better to error out in this case. -+ stat, err := Fstat(file) -+ if err != nil { -+ return fmt.Errorf("check for dead inode: %w", err) -+ } -+ if stat.Nlink == 0 { -+ err := internal.ErrDeletedInode -+ if stat.Mode&unix.S_IFMT == unix.S_IFDIR { -+ err = internal.ErrInvalidDirectory -+ } -+ return fmt.Errorf("%w %q", err, file.Name()) -+ } -+ return nil -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go -new file mode 100644 -index 00000000..77549c7a ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go -@@ -0,0 +1,54 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package fd -+ -+import ( -+ "os" -+ "runtime" -+ -+ "golang.org/x/sys/unix" -+) -+ -+// Fsopen is an [Fd]-based wrapper around unix.Fsopen. -+func Fsopen(fsName string, flags int) (*os.File, error) { -+ // Make sure we always set O_CLOEXEC. -+ flags |= unix.FSOPEN_CLOEXEC -+ fd, err := unix.Fsopen(fsName, flags) -+ if err != nil { -+ return nil, os.NewSyscallError("fsopen "+fsName, err) -+ } -+ return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil -+} -+ -+// Fsmount is an [Fd]-based wrapper around unix.Fsmount. -+func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) { -+ // Make sure we always set O_CLOEXEC. -+ flags |= unix.FSMOUNT_CLOEXEC -+ fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs) -+ if err != nil { -+ return nil, os.NewSyscallError("fsmount "+ctx.Name(), err) -+ } -+ return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil -+} -+ -+// OpenTree is an [Fd]-based wrapper around unix.OpenTree. -+func OpenTree(dir Fd, path string, flags uint) (*os.File, error) { -+ dirFd, fullPath := prepareAt(dir, path) -+ // Make sure we always set O_CLOEXEC. -+ flags |= unix.OPEN_TREE_CLOEXEC -+ fd, err := unix.OpenTree(dirFd, path, flags) -+ if err != nil { -+ return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ return os.NewFile(uintptr(fd), fullPath), nil -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go -new file mode 100644 -index 00000000..23053083 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go -@@ -0,0 +1,62 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package fd -+ -+import ( -+ "errors" -+ "os" -+ "runtime" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" -+) -+ -+func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { -+ // RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve -+ // ".." while a mount or rename occurs anywhere on the system. This could -+ // happen spuriously, or as the result of an attacker trying to mess with -+ // us during lookup. -+ // -+ // In addition, scoped lookups have a "safety check" at the end of -+ // complete_walk which will return -EXDEV if the final path is not in the -+ // root. -+ return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 && -+ (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV)) -+} -+ -+const scopedLookupMaxRetries = 32 -+ -+// Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry -+// logic in case of EAGAIN errors. -+func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) { -+ dirFd, fullPath := prepareAt(dir, path) -+ // Make sure we always set O_CLOEXEC. -+ how.Flags |= unix.O_CLOEXEC -+ var tries int -+ for tries < scopedLookupMaxRetries { -+ fd, err := unix.Openat2(dirFd, path, how) -+ if err != nil { -+ if scopedLookupShouldRetry(how, err) { -+ // We retry a couple of times to avoid the spurious errors, and -+ // if we are being attacked then returning -EAGAIN is the best -+ // we can do. -+ tries++ -+ continue -+ } -+ return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err} -+ } -+ runtime.KeepAlive(dir) -+ return os.NewFile(uintptr(fd), fullPath), nil -+ } -+ return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: internal.ErrPossibleAttack} -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md -new file mode 100644 -index 00000000..5dcb6ae0 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/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/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go -new file mode 100644 -index 00000000..4b1803f5 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/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/vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_go120.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_go120.go -similarity index 69% -rename from vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_go120.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_go120.go -index 42452bbf..4a114bd3 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_go120.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_go120.go -@@ -1,18 +1,19 @@ -+// SPDX-License-Identifier: BSD-3-Clause - //go:build linux && go1.20 - - // 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 -+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/vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_unsupported.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_unsupported.go -similarity index 80% -rename from vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_unsupported.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_unsupported.go -index e7adca3f..3061016a 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/gocompat_errors_unsupported.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_unsupported.go -@@ -1,10 +1,12 @@ -+// SPDX-License-Identifier: BSD-3-Clause -+ - //go:build linux && !go1.20 - - // 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 -+package gocompat - - import ( - "fmt" -@@ -27,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/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_go121.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_go121.go -new file mode 100644 -index 00000000..d4a93818 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/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/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_unsupported.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_unsupported.go -new file mode 100644 -index 00000000..0ea6218a ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/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/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion/kernel_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion/kernel_linux.go -new file mode 100644 -index 00000000..cb6de418 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/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/pathrs-lite/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/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go -new file mode 100644 -index 00000000..4635714f ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go -@@ -0,0 +1,12 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package linux returns information about what features are supported on the -+// running kernel. -+package linux -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go -new file mode 100644 -index 00000000..b29905bf ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go -@@ -0,0 +1,47 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package linux -+ -+import ( -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion" -+) -+ -+// HasNewMountAPI returns whether the new fsopen(2) mount API is supported on -+// the running kernel. -+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.2[1,2], so we can -+ // just check for one of the syscalls and the others should also be -+ // available. -+ // -+ // Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE. -+ // This is equivalent to openat(2), but tells us if open_tree is -+ // available (and thus all of the other basic new mount API syscalls). -+ // open_tree(2) is most light-weight syscall to test here. -+ // -+ // [1]: merge commit 400913252d09 -+ // [2]: -+ fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC) -+ if err != nil { -+ return false -+ } -+ _ = unix.Close(fd) -+ -+ // 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 -+}) -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go -new file mode 100644 -index 00000000..399609dc ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go -@@ -0,0 +1,31 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package linux -+ -+import ( -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+) -+ -+// HasOpenat2 returns whether openat2(2) is supported on the running kernel. -+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, -+ }) -+ if err != nil { -+ return false -+ } -+ _ = unix.Close(fd) -+ return true -+}) -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go -new file mode 100644 -index 00000000..21e0a62e ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go -@@ -0,0 +1,544 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package procfs provides a safe API for operating on /proc on Linux. Note -+// that this is the *internal* procfs API, mainy needed due to Go's -+// restrictions on cyclic dependencies and its incredibly minimal visibility -+// system without making a separate internal/ package. -+package procfs -+ -+import ( -+ "errors" -+ "fmt" -+ "io" -+ "os" -+ "runtime" -+ "strconv" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" -+) -+ -+// The kernel guarantees that the root inode of a procfs mount has an -+// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO. -+const ( -+ procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC -+ procRootIno = 1 // PROC_ROOT_INO -+) -+ -+// verifyProcHandle checks that the handle is from a procfs filesystem. -+// Contrast this to [verifyProcRoot], which also verifies that the handle is -+// the root of a procfs mount. -+func verifyProcHandle(procHandle fd.Fd) error { -+ if statfs, err := fd.Fstatfs(procHandle); err != nil { -+ return err -+ } else if statfs.Type != procSuperMagic { -+ return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type) -+ } -+ return nil -+} -+ -+// verifyProcRoot verifies that the handle is the root of a procfs filesystem. -+// Contrast this to [verifyProcHandle], which only verifies if the handle is -+// some file on procfs (regardless of what file it is). -+func verifyProcRoot(procRoot fd.Fd) error { -+ if err := verifyProcHandle(procRoot); err != nil { -+ return err -+ } -+ if stat, err := fd.Fstat(procRoot); err != nil { -+ return err -+ } else if stat.Ino != procRootIno { -+ return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino) -+ } -+ return nil -+} -+ -+type procfsFeatures struct { -+ // hasSubsetPid was added in Linux 5.8, along with hidepid=ptraceable (and -+ // string-based hidepid= values). Before this patchset, it was not really -+ // safe to try to modify procfs superblock flags because the superblock was -+ // shared -- so if this feature is not available, **you should not set any -+ // superblock flags**. -+ // -+ // 6814ef2d992a ("proc: add option to mount only a pids subset") -+ // fa10fed30f25 ("proc: allow to mount many instances of proc in one pid namespace") -+ // 24a71ce5c47f ("proc: instantiate only pids that we can ptrace on 'hidepid=4' mount option") -+ // 1c6c4d112e81 ("proc: use human-readable values for hidepid") -+ // 9ff7258575d5 ("Merge branch 'proc-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/ebiederm/user-namespace") -+ hasSubsetPid bool -+} -+ -+var getProcfsFeatures = gocompat.SyncOnceValue(func() procfsFeatures { -+ if !linux.HasNewMountAPI() { -+ return procfsFeatures{} -+ } -+ procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC) -+ if err != nil { -+ return procfsFeatures{} -+ } -+ defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here -+ -+ return procfsFeatures{ -+ hasSubsetPid: unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") == nil, -+ } -+}) -+ -+func newPrivateProcMount(subset bool) (_ *Handle, Err error) { -+ procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC) -+ if err != nil { -+ return nil, err -+ } -+ defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here -+ -+ if subset && getProcfsFeatures().hasSubsetPid { -+ // Try to configure hidepid=ptraceable,subset=pid if possible, but -+ // ignore errors. -+ _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable") -+ _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") -+ } -+ -+ // Get an actual handle. -+ if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil { -+ return nil, os.NewSyscallError("fsconfig create procfs", err) -+ } -+ // TODO: Output any information from the fscontext log to debug logs. -+ procRoot, err := fd.Fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID) -+ if err != nil { -+ return nil, err -+ } -+ defer func() { -+ if Err != nil { -+ _ = procRoot.Close() -+ } -+ }() -+ return newHandle(procRoot) -+} -+ -+func clonePrivateProcMount() (_ *Handle, Err error) { -+ // Try to make a clone without using AT_RECURSIVE if we can. If this works, -+ // we can be sure there are no over-mounts and so if the root is valid then -+ // we're golden. Otherwise, we have to deal with over-mounts. -+ procRoot, err := fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE) -+ if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procRoot) { -+ procRoot, err = fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE) -+ } -+ if err != nil { -+ return nil, fmt.Errorf("creating a detached procfs clone: %w", err) -+ } -+ defer func() { -+ if Err != nil { -+ _ = procRoot.Close() -+ } -+ }() -+ return newHandle(procRoot) -+} -+ -+func privateProcRoot(subset bool) (*Handle, error) { -+ if !linux.HasNewMountAPI() || hookForceGetProcRootUnsafe() { -+ return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) -+ } -+ // Try to create a new procfs mount from scratch if we can. This ensures we -+ // can get a procfs mount even if /proc is fake (for whatever reason). -+ procRoot, err := newPrivateProcMount(subset) -+ if err != nil || hookForcePrivateProcRootOpenTree(procRoot) { -+ // Try to clone /proc then... -+ procRoot, err = clonePrivateProcMount() -+ } -+ return procRoot, err -+} -+ -+func unsafeHostProcRoot() (_ *Handle, Err error) { -+ procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) -+ if err != nil { -+ return nil, err -+ } -+ defer func() { -+ if Err != nil { -+ _ = procRoot.Close() -+ } -+ }() -+ return newHandle(procRoot) -+} -+ -+// Handle is a wrapper around an *os.File handle to "/proc", which can be used -+// to do further procfs-related operations in a safe way. -+type Handle struct { -+ Inner fd.Fd -+ // Does this handle have subset=pid set? -+ isSubset bool -+} -+ -+func newHandle(procRoot fd.Fd) (*Handle, error) { -+ if err := verifyProcRoot(procRoot); err != nil { -+ // This is only used in methods that -+ _ = procRoot.Close() -+ return nil, err -+ } -+ proc := &Handle{Inner: procRoot} -+ // With subset=pid we can be sure that /proc/uptime will not exist. -+ if err := fd.Faccessat(proc.Inner, "uptime", unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil { -+ proc.isSubset = errors.Is(err, os.ErrNotExist) -+ } -+ return proc, nil -+} -+ -+// Close closes the underlying file for the Handle. -+func (proc *Handle) Close() error { return proc.Inner.Close() } -+ -+var getCachedProcRoot = gocompat.SyncOnceValue(func() *Handle { -+ procRoot, err := getProcRoot(true) -+ if err != nil { -+ return nil // just don't cache if we see an error -+ } -+ if !procRoot.isSubset { -+ return nil // we only cache verified subset=pid handles -+ } -+ -+ // Disarm (*Handle).Close() to stop someone from accidentally closing -+ // the global handle. -+ procRoot.Inner = fd.NopCloser(procRoot.Inner) -+ return procRoot -+}) -+ -+// OpenProcRoot tries to open a "safer" handle to "/proc". -+func OpenProcRoot() (*Handle, error) { -+ if proc := getCachedProcRoot(); proc != nil { -+ return proc, nil -+ } -+ return getProcRoot(true) -+} -+ -+// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or -+// masked paths (but also without "subset=pid"). -+func OpenUnsafeProcRoot() (*Handle, error) { return getProcRoot(false) } -+ -+func getProcRoot(subset bool) (*Handle, error) { -+ proc, err := privateProcRoot(subset) -+ if err != nil { -+ // Fall back to using a /proc handle if making a private mount failed. -+ // If we have openat2, at least we can avoid some kinds of over-mount -+ // attacks, but without openat2 there's not much we can do. -+ proc, err = unsafeHostProcRoot() -+ } -+ return proc, err -+} -+ -+var hasProcThreadSelf = gocompat.SyncOnceValue(func() bool { -+ return unix.Access("/proc/thread-self/", unix.F_OK) == nil -+}) -+ -+var errUnsafeProcfs = errors.New("unsafe procfs detected") -+ -+// lookup is a very minimal wrapper around [procfsLookupInRoot] which is -+// intended to be called from the external API. -+func (proc *Handle) lookup(subpath string) (*os.File, error) { -+ handle, err := procfsLookupInRoot(proc.Inner, subpath) -+ if err != nil { -+ return nil, err -+ } -+ return handle, nil -+} -+ -+// procfsBase is an enum indicating the prefix of a subpath in operations -+// involving [Handle]s. -+type procfsBase string -+ -+const ( -+ // ProcRoot refers to the root of the procfs (i.e., "/proc/"). -+ ProcRoot procfsBase = "/proc" -+ // ProcSelf refers to the current process' subdirectory (i.e., -+ // "/proc/self/"). -+ ProcSelf procfsBase = "/proc/self" -+ // ProcThreadSelf refers to the current thread's subdirectory (i.e., -+ // "/proc/thread-self/"). In multi-threaded programs (i.e., all Go -+ // programs) where one thread has a different CLONE_FS, it is possible for -+ // "/proc/self" to point the wrong thread and so "/proc/thread-self" may be -+ // necessary. Note that on pre-3.17 kernels, "/proc/thread-self" doesn't -+ // exist and so a fallback will be used in that case. -+ ProcThreadSelf procfsBase = "/proc/thread-self" -+ // TODO: Switch to an interface setup so we can have a more type-safe -+ // version of ProcPid and remove the need to worry about invalid string -+ // values. -+) -+ -+// prefix returns a prefix that can be used with the given [Handle]. -+func (base procfsBase) prefix(proc *Handle) (string, error) { -+ switch base { -+ case ProcRoot: -+ return ".", nil -+ case ProcSelf: -+ return "self", nil -+ case ProcThreadSelf: -+ threadSelf := "thread-self" -+ if !hasProcThreadSelf() || hookForceProcSelfTask() { -+ // Pre-3.17 kernels don't have /proc/thread-self, so do it -+ // manually. -+ threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) -+ if err := fd.Faccessat(proc.Inner, threadSelf, unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() { -+ // In this case, we running in a pid namespace that doesn't -+ // match the /proc mount we have. This can happen inside runc. -+ // -+ // Unfortunately, there is no nice way to get the correct TID -+ // to use here because of the age of the kernel, so we have to -+ // just use /proc/self and hope that it works. -+ threadSelf = "self" -+ } -+ } -+ return threadSelf, nil -+ } -+ return "", fmt.Errorf("invalid procfs base %q", base) -+} -+ -+// ProcThreadSelfCloser is a callback that needs to be called when you are done -+// operating on an [os.File] fetched using [ProcThreadSelf]. -+// -+// [os.File]: https://pkg.go.dev/os#File -+type ProcThreadSelfCloser func() -+ -+// open is the core lookup operation for [Handle]. It returns a handle to -+// "/proc//". If the returned [ProcThreadSelfCloser] is non-nil, -+// you should call it after you are done interacting with the returned handle. -+// -+// In general you should use prefer to use the other helpers, as they remove -+// the need to interact with [procfsBase] and do not return a nil -+// [ProcThreadSelfCloser] for [procfsBase] values other than [ProcThreadSelf] -+// where it is necessary. -+func (proc *Handle) open(base procfsBase, subpath string) (_ *os.File, closer ProcThreadSelfCloser, Err error) { -+ prefix, err := base.prefix(proc) -+ if err != nil { -+ return nil, nil, err -+ } -+ subpath = prefix + "/" + subpath -+ -+ switch base { -+ case ProcRoot: -+ file, err := proc.lookup(subpath) -+ if errors.Is(err, os.ErrNotExist) { -+ // The Handle handle in use might be a subset=pid one, which will -+ // result in spurious errors. In this case, just open a temporary -+ // unmasked procfs handle for this operation. -+ proc, err2 := OpenUnsafeProcRoot() // !subset=pid -+ if err2 != nil { -+ return nil, nil, err -+ } -+ defer proc.Close() //nolint:errcheck // close failures aren't critical here -+ -+ file, err = proc.lookup(subpath) -+ } -+ return file, nil, err -+ -+ case ProcSelf: -+ file, err := proc.lookup(subpath) -+ return file, nil, err -+ -+ case ProcThreadSelf: -+ // We need to lock our thread until the caller is done with the handle -+ // because between getting the handle and using it we could get -+ // interrupted by the Go runtime and hit the case where the underlying -+ // thread is swapped out and the original thread is killed, resulting -+ // in pull-your-hair-out-hard-to-debug issues in the caller. -+ runtime.LockOSThread() -+ defer func() { -+ if Err != nil { -+ runtime.UnlockOSThread() -+ closer = nil -+ } -+ }() -+ -+ file, err := proc.lookup(subpath) -+ return file, runtime.UnlockOSThread, err -+ } -+ // should never be reached -+ return nil, nil, fmt.Errorf("[internal error] invalid procfs base %q", base) -+} -+ -+// OpenThreadSelf returns a handle to "/proc/thread-self/" (or an -+// equivalent handle on older kernels where "/proc/thread-self" doesn't exist). -+// Once finished with the handle, you must call the returned closer function -+// (runtime.UnlockOSThread). You must not pass the returned *os.File to other -+// Go threads or use the handle after calling the closer. -+func (proc *Handle) OpenThreadSelf(subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) { -+ return proc.open(ProcThreadSelf, subpath) -+} -+ -+// OpenSelf returns a handle to /proc/self/. -+func (proc *Handle) OpenSelf(subpath string) (*os.File, error) { -+ file, closer, err := proc.open(ProcSelf, subpath) -+ assert.Assert(closer == nil, "closer for ProcSelf must be nil") -+ return file, err -+} -+ -+// OpenRoot returns a handle to /proc/. -+func (proc *Handle) OpenRoot(subpath string) (*os.File, error) { -+ file, closer, err := proc.open(ProcRoot, subpath) -+ assert.Assert(closer == nil, "closer for ProcRoot must be nil") -+ return file, err -+} -+ -+// OpenPid returns a handle to /proc/$pid/ (pid can be a pid or tid). -+// This is mainly intended for usage when operating on other processes. -+func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) { -+ return proc.OpenRoot(strconv.Itoa(pid) + "/" + subpath) -+} -+ -+// checkSubpathOvermount checks if the dirfd and path combination is on the -+// same mount as the given root. -+func checkSubpathOvermount(root, dir fd.Fd, path string) error { -+ // Get the mntID of our procfs handle. -+ expectedMountID, err := fd.GetMountID(root, "") -+ if err != nil { -+ return fmt.Errorf("get root mount id: %w", err) -+ } -+ // Get the mntID of the target magic-link. -+ gotMountID, err := fd.GetMountID(dir, path) -+ if err != nil { -+ return fmt.Errorf("get subpath mount id: %w", err) -+ } -+ // As long as the directory mount is alive, even with wrapping mount IDs, -+ // we would expect to see a different mount ID here. (Of course, if we're -+ // using unsafeHostProcRoot() then an attaker could change this after we -+ // did this check.) -+ if expectedMountID != gotMountID { -+ return fmt.Errorf("%w: subpath %s/%s has an overmount obscuring the real path (mount ids do not match %d != %d)", -+ errUnsafeProcfs, dir.Name(), path, expectedMountID, gotMountID) -+ } -+ return nil -+} -+ -+// Readlink performs a readlink operation on "/proc//" in a way -+// that should be free from race attacks. This is most commonly used to get the -+// real path of a file by looking at "/proc/self/fd/$n", with the same safety -+// protections as [Open] (as well as some additional checks against -+// overmounts). -+func (proc *Handle) Readlink(base procfsBase, subpath string) (string, error) { -+ link, closer, err := proc.open(base, subpath) -+ if closer != nil { -+ defer closer() -+ } -+ if err != nil { -+ return "", fmt.Errorf("get safe %s/%s handle: %w", base, subpath, err) -+ } -+ defer link.Close() //nolint:errcheck // close failures aren't critical here -+ -+ // Try to detect if there is a mount on top of the magic-link. This should -+ // be safe in general (a mount on top of the path afterwards would not -+ // affect the handle itself) and will definitely be safe if we are using -+ // privateProcRoot() (at least since Linux 5.12[1], when anonymous mount -+ // namespaces were completely isolated from external mounts including mount -+ // propagation events). -+ // -+ // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts -+ // onto targets that reside on shared mounts"). -+ if err := checkSubpathOvermount(proc.Inner, link, ""); err != nil { -+ return "", fmt.Errorf("check safety of %s/%s magiclink: %w", base, subpath, err) -+ } -+ -+ // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit -+ // 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty -+ // relative pathnames"). -+ return fd.Readlinkat(link, "") -+} -+ -+// ProcSelfFdReadlink gets the real path of the given file by looking at -+// readlink(/proc/thread-self/fd/$n). -+// -+// This is just a wrapper around [Handle.Readlink]. -+func ProcSelfFdReadlink(fd fd.Fd) (string, error) { -+ procRoot, err := OpenProcRoot() // subset=pid -+ if err != nil { -+ return "", err -+ } -+ defer procRoot.Close() //nolint:errcheck // close failures aren't critical here -+ -+ fdPath := "fd/" + strconv.Itoa(int(fd.Fd())) -+ return procRoot.Readlink(ProcThreadSelf, fdPath) -+} -+ -+// CheckProcSelfFdPath returns whether the given file handle matches the -+// expected path. (This is inherently racy.) -+func CheckProcSelfFdPath(path string, file fd.Fd) error { -+ if err := fd.IsDeadInode(file); err != nil { -+ return err -+ } -+ actualPath, err := ProcSelfFdReadlink(file) -+ if err != nil { -+ return fmt.Errorf("get path of handle: %w", err) -+ } -+ if actualPath != path { -+ return fmt.Errorf("%w: handle path %q doesn't match expected path %q", internal.ErrPossibleBreakout, actualPath, path) -+ } -+ return nil -+} -+ -+// ReopenFd takes an existing file descriptor and "re-opens" it through -+// /proc/thread-self/fd/. This allows for O_PATH file descriptors to be -+// upgraded to regular file descriptors, as well as changing the open mode of a -+// regular file descriptor. Some filesystems have unique handling of open(2) -+// which make this incredibly useful (such as /dev/ptmx). -+func ReopenFd(handle fd.Fd, flags int) (*os.File, error) { -+ procRoot, err := OpenProcRoot() // subset=pid -+ if err != nil { -+ return nil, err -+ } -+ defer procRoot.Close() //nolint:errcheck // close failures aren't critical here -+ -+ // We can't operate on /proc/thread-self/fd/$n directly when doing a -+ // re-open, so we need to open /proc/thread-self/fd and then open a single -+ // final component. -+ procFdDir, closer, err := procRoot.OpenThreadSelf("fd/") -+ if err != nil { -+ return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err) -+ } -+ defer procFdDir.Close() //nolint:errcheck // close failures aren't critical here -+ defer closer() -+ -+ // Try to detect if there is a mount on top of the magic-link we are about -+ // to open. If we are using unsafeHostProcRoot(), this could change after -+ // we check it (and there's nothing we can do about that) but for -+ // privateProcRoot() this should be guaranteed to be safe (at least since -+ // Linux 5.12[1], when anonymous mount namespaces were completely isolated -+ // from external mounts including mount propagation events). -+ // -+ // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts -+ // onto targets that reside on shared mounts"). -+ fdStr := strconv.Itoa(int(handle.Fd())) -+ if err := checkSubpathOvermount(procRoot.Inner, procFdDir, fdStr); err != nil { -+ return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err) -+ } -+ -+ flags |= unix.O_CLOEXEC -+ // Rather than just wrapping fd.Openat, open-code it so we can copy -+ // handle.Name(). -+ reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0) -+ if err != nil { -+ return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err) -+ } -+ return os.NewFile(uintptr(reopenFd), handle.Name()), nil -+} -+ -+// Test hooks used in the procfs tests to verify that the fallback logic works. -+// See testing_mocks_linux_test.go and procfs_linux_test.go for more details. -+var ( -+ hookForcePrivateProcRootOpenTree = hookDummyFile -+ hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile -+ hookForceGetProcRootUnsafe = hookDummy -+ -+ hookForceProcSelfTask = hookDummy -+ hookForceProcSelf = hookDummy -+) -+ -+func hookDummy() bool { return false } -+func hookDummyFile(_ io.Closer) bool { return false } -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_lookup_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_lookup_linux.go -new file mode 100644 -index 00000000..1ad1f18e ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_lookup_linux.go -@@ -0,0 +1,222 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// This code is adapted to be a minimal version of the libpathrs proc resolver -+// . -+// As we only need O_PATH|O_NOFOLLOW support, this is not too much to port. -+ -+package procfs -+ -+import ( -+ "fmt" -+ "os" -+ "path" -+ "path/filepath" -+ "strings" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/internal/consts" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" -+) -+ -+// procfsLookupInRoot is a stripped down version of completeLookupInRoot, -+// entirely designed to support the very small set of features necessary to -+// make procfs handling work. Unlike completeLookupInRoot, we always have -+// O_PATH|O_NOFOLLOW behaviour for trailing symlinks. -+// -+// The main restrictions are: -+// -+// - ".." is not supported (as it requires either os.Root-style replays, -+// which is more bug-prone; or procfs verification, which is not possible -+// due to re-entrancy issues). -+// - Absolute symlinks for the same reason (and all absolute symlinks in -+// procfs are magic-links, which we want to skip anyway). -+// - If statx is supported (checkSymlinkOvermount), any mount-point crossings -+// (which is the main attack of concern against /proc). -+// - Partial lookups are not supported, so the symlink stack is not needed. -+// - Trailing slash special handling is not necessary in most cases (if we -+// operating on procfs, it's usually with programmer-controlled strings -+// that will then be re-opened), so we skip it since whatever re-opens it -+// can deal with it. It's a creature comfort anyway. -+// -+// If the system supports openat2(), this is implemented using equivalent flags -+// (RESOLVE_BENEATH | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS). -+func procfsLookupInRoot(procRoot fd.Fd, unsafePath string) (Handle *os.File, _ error) { -+ unsafePath = filepath.ToSlash(unsafePath) // noop -+ -+ // Make sure that an empty unsafe path still returns something sane, even -+ // with openat2 (which doesn't have AT_EMPTY_PATH semantics yet). -+ if unsafePath == "" { -+ unsafePath = "." -+ } -+ -+ // This is already checked by getProcRoot, but make sure here since the -+ // core security of this lookup is based on this assumption. -+ if err := verifyProcRoot(procRoot); err != nil { -+ return nil, err -+ } -+ -+ if linux.HasOpenat2() { -+ // We prefer being able to use RESOLVE_NO_XDEV if we can, to be -+ // absolutely sure we are operating on a clean /proc handle that -+ // doesn't have any cheeky overmounts that could trick us (including -+ // symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't -+ // strictly needed, but just use it since we have it. -+ // -+ // NOTE: /proc/self is technically a magic-link (the contents of the -+ // symlink are generated dynamically), but it doesn't use -+ // nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it. -+ // -+ // TODO: It would be nice to have RESOLVE_NO_DOTDOT, purely for -+ // self-consistency with the backup O_PATH resolver. -+ handle, err := fd.Openat2(procRoot, unsafePath, &unix.OpenHow{ -+ Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC, -+ Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS, -+ }) -+ if err != nil { -+ // TODO: Once we bump the minimum Go version to 1.20, we can use -+ // 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, gocompat.WrapBaseError(err, errUnsafeProcfs) -+ } -+ return handle, nil -+ } -+ -+ // To mirror openat2(RESOLVE_BENEATH), we need to return an error if the -+ // path is absolute. -+ if path.IsAbs(unsafePath) { -+ return nil, fmt.Errorf("%w: cannot resolve absolute paths in procfs resolver", internal.ErrPossibleBreakout) -+ } -+ -+ currentDir, err := fd.Dup(procRoot) -+ if err != nil { -+ return nil, fmt.Errorf("clone root fd: %w", err) -+ } -+ defer func() { -+ // If a handle is not returned, close the internal handle. -+ if Handle == nil { -+ _ = currentDir.Close() -+ } -+ }() -+ -+ var ( -+ linksWalked int -+ currentPath string -+ remainingPath = unsafePath -+ ) -+ for remainingPath != "" { -+ // Get the next path component. -+ var part string -+ if i := strings.IndexByte(remainingPath, '/'); i == -1 { -+ part, remainingPath = remainingPath, "" -+ } else { -+ part, remainingPath = remainingPath[:i], remainingPath[i+1:] -+ } -+ if part == "" { -+ // no-op component, but treat it the same as "." -+ part = "." -+ } -+ if part == ".." { -+ // not permitted -+ return nil, fmt.Errorf("%w: cannot walk into '..' in procfs resolver", internal.ErrPossibleBreakout) -+ } -+ -+ // Apply the component lexically to the path we are building. -+ // currentPath does not contain any symlinks, and we are lexically -+ // dealing with a single component, so it's okay to do a filepath.Clean -+ // here. (Not to mention that ".." isn't allowed.) -+ nextPath := path.Join("/", currentPath, part) -+ // If we logically hit the root, just clone the root rather than -+ // opening the part and doing all of the other checks. -+ if nextPath == "/" { -+ // Jump to root. -+ rootClone, err := fd.Dup(procRoot) -+ if err != nil { -+ return nil, fmt.Errorf("clone root fd: %w", err) -+ } -+ _ = currentDir.Close() -+ currentDir = rootClone -+ currentPath = nextPath -+ continue -+ } -+ -+ // Try to open the next component. -+ nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -+ if err != nil { -+ return nil, err -+ } -+ -+ // Make sure we are still on procfs and haven't crossed mounts. -+ if err := verifyProcHandle(nextDir); err != nil { -+ _ = nextDir.Close() -+ return nil, fmt.Errorf("check %q component is on procfs: %w", part, err) -+ } -+ if err := checkSubpathOvermount(procRoot, nextDir, ""); err != nil { -+ _ = nextDir.Close() -+ return nil, fmt.Errorf("check %q component is not overmounted: %w", part, err) -+ } -+ -+ // We are emulating O_PATH|O_NOFOLLOW, so we only need to traverse into -+ // trailing symlinks if we are not the final component. Otherwise we -+ // can just return the currentDir. -+ if remainingPath != "" { -+ st, err := nextDir.Stat() -+ if err != nil { -+ _ = nextDir.Close() -+ return nil, fmt.Errorf("stat component %q: %w", part, err) -+ } -+ -+ if st.Mode()&os.ModeType == os.ModeSymlink { -+ // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See -+ // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and -+ // fstatat() with empty relative pathnames"). -+ linkDest, err := fd.Readlinkat(nextDir, "") -+ // We don't need the handle anymore. -+ _ = nextDir.Close() -+ if err != nil { -+ return nil, err -+ } -+ -+ linksWalked++ -+ if linksWalked > consts.MaxSymlinkLimit { -+ return nil, &os.PathError{Op: "securejoin.procfsLookupInRoot", Path: "/proc/" + unsafePath, Err: unix.ELOOP} -+ } -+ -+ // Update our logical remaining path. -+ remainingPath = linkDest + "/" + remainingPath -+ // Absolute symlinks are probably magiclinks, we reject them. -+ if path.IsAbs(linkDest) { -+ return nil, fmt.Errorf("%w: cannot jump to / in procfs resolver -- possible magiclink", internal.ErrPossibleBreakout) -+ } -+ continue -+ } -+ } -+ -+ // Walk into the next component. -+ _ = currentDir.Close() -+ currentDir = nextDir -+ currentPath = nextPath -+ } -+ -+ // One final sanity-check. -+ if err := verifyProcHandle(currentDir); err != nil { -+ return nil, fmt.Errorf("check final handle is on procfs: %w", err) -+ } -+ if err := checkSubpathOvermount(procRoot, currentDir, ""); err != nil { -+ return nil, fmt.Errorf("check final handle is not overmounted: %w", err) -+ } -+ return currentDir, nil -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/lookup_linux.go -similarity index 86% -rename from vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/lookup_linux.go -index be81e498..f47504e6 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/lookup_linux.go -@@ -1,10 +1,15 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ - //go:build linux - --// 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. -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. - --package securejoin -+package pathrs - - import ( - "errors" -@@ -15,6 +20,12 @@ import ( - "strings" - - "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/internal/consts" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" - ) - - type symlinkStackEntry struct { -@@ -112,12 +123,12 @@ 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 == "." }) - - // Copy the directory so the caller doesn't close our copy. -- dirCopy, err := dupFile(dir) -+ dirCopy, err := fd.Dup(dir) - if err != nil { - return err - } -@@ -159,11 +170,11 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { - // within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing - // component of the requested path, returning a file handle to the final - // existing component and a string containing the remaining path components. --func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) { -+func partialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) { - return lookupInRoot(root, unsafePath, true) - } - --func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) { -+func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) { - handle, remainingPath, err := lookupInRoot(root, unsafePath, false) - if remainingPath != "" && err == nil { - // should never happen -@@ -174,7 +185,7 @@ func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) { - return handle, err - } - --func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) { -+func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) { - unsafePath = filepath.ToSlash(unsafePath) // noop - - // This is very similar to SecureJoin, except that we operate on the -@@ -182,20 +193,20 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - // managed open, along with the remaining path components not opened. - - // Try to use openat2 if possible. -- if hasOpenat2() { -+ if linux.HasOpenat2() { - return lookupOpenat2(root, unsafePath, partial) - } - - // Get the "actual" root path from /proc/self/fd. This is necessary if the - // root is some magic-link like /proc/$pid/root, in which case we want to -- // make sure when we do checkProcSelfFdPath that we are using the correct -- // root path. -- logicalRootPath, err := procSelfFdReadlink(root) -+ // make sure when we do procfs.CheckProcSelfFdPath that we are using the -+ // correct root path. -+ logicalRootPath, err := procfs.ProcSelfFdReadlink(root) - if err != nil { - return nil, "", fmt.Errorf("get real root path: %w", err) - } - -- currentDir, err := dupFile(root) -+ currentDir, err := fd.Dup(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } -@@ -260,7 +271,7 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err) - } - // Jump to root. -- rootClone, err := dupFile(root) -+ rootClone, err := fd.Dup(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } -@@ -271,21 +282,21 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - } - - // Try to open the next component. -- nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -- switch { -- case err == nil: -+ nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -+ switch err { -+ case nil: - st, err := nextDir.Stat() - if err != nil { - _ = nextDir.Close() - return nil, "", fmt.Errorf("stat component %q: %w", part, err) - } - -- switch st.Mode() & os.ModeType { -+ switch st.Mode() & os.ModeType { //nolint:exhaustive // just a glorified if statement - case os.ModeSymlink: - // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See - // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and - // fstatat() with empty relative pathnames"). -- linkDest, err := readlinkatFile(nextDir, "") -+ linkDest, err := fd.Readlinkat(nextDir, "") - // We don't need the handle anymore. - _ = nextDir.Close() - if err != nil { -@@ -293,7 +304,7 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - } - - linksWalked++ -- if linksWalked > maxSymlinkLimit { -+ if linksWalked > consts.MaxSymlinkLimit { - return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP} - } - -@@ -307,7 +318,7 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - // Absolute symlinks reset any work we've already done. - if path.IsAbs(linkDest) { - // Jump to root. -- rootClone, err := dupFile(root) -+ rootClone, err := fd.Dup(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } -@@ -335,12 +346,12 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - // rename or mount on the system. - if part == ".." { - // Make sure the root hasn't moved. -- if err := checkProcSelfFdPath(logicalRootPath, root); err != nil { -+ if err := procfs.CheckProcSelfFdPath(logicalRootPath, root); err != nil { - return nil, "", fmt.Errorf("root path moved during lookup: %w", err) - } - // Make sure the path is what we expect. - fullPath := logicalRootPath + nextPath -- if err := checkProcSelfFdPath(fullPath, currentDir); err != nil { -+ if err := procfs.CheckProcSelfFdPath(fullPath, currentDir); err != nil { - return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err) - } - } -@@ -371,7 +382,7 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi - // context of openat2, a trailing slash and a trailing "/." are completely - // equivalent. - if strings.HasSuffix(unsafePath, "/") { -- nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -+ nextDir, err := fd.Openat(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - if !partial { - _ = currentDir.Close() -diff --git a/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir_linux.go -similarity index 86% -rename from vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir_linux.go -index a17ae3b0..f3c62b0d 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir_linux.go -@@ -1,10 +1,15 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ - //go:build linux - --// 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. -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. - --package securejoin -+package pathrs - - import ( - "errors" -@@ -14,13 +19,14 @@ import ( - "strings" - - "golang.org/x/sys/unix" --) - --var ( -- errInvalidMode = errors.New("invalid permission mode") -- errPossibleAttack = errors.New("possible attack detected") -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" - ) - -+var errInvalidMode = errors.New("invalid permission mode") -+ - // modePermExt is like os.ModePerm except that it also includes the set[ug]id - // and sticky bits. - const modePermExt = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky -@@ -66,6 +72,8 @@ func toUnixMode(mode os.FileMode) (uint32, error) { - // a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after - // doing [MkdirAll]. If you intend to open the directory after creating it, you - // should use MkdirAllHandle. -+// -+// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin - func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) { - unixMode, err := toUnixMode(mode) - if err != nil { -@@ -102,7 +110,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F - // - // This is mostly a quality-of-life check, because mkdir will simply fail - // later if the attacker deletes the tree after this check. -- if err := isDeadInode(currentDir); err != nil { -+ if err := fd.IsDeadInode(currentDir); err != nil { - return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err) - } - -@@ -113,13 +121,13 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F - return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR) - } else if err != nil { - return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err) -- } else { -+ } else { //nolint:revive // indent-error-flow lint doesn't make sense here - _ = currentDir.Close() - currentDir = reopenDir - } - - 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 -@@ -150,12 +158,12 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F - if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) { - err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err} - // Make the error a bit nicer if the directory is dead. -- if deadErr := isDeadInode(currentDir); deadErr != nil { -+ if deadErr := fd.IsDeadInode(currentDir); deadErr != nil { - // TODO: Once we bump the minimum Go version to 1.20, we can use - // 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 = fmt.Errorf("%w (%w)", err, deadErr) -+ err = gocompat.WrapBaseError(err, deadErr) - } - return nil, err - } -@@ -163,13 +171,13 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F - // Get a handle to the next component. O_DIRECTORY means we don't need - // to use O_PATH. - var nextDir *os.File -- if hasOpenat2() { -- nextDir, err = openat2File(currentDir, part, &unix.OpenHow{ -+ if linux.HasOpenat2() { -+ nextDir, err = openat2(currentDir, part, &unix.OpenHow{ - Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV, - }) - } else { -- nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) -+ nextDir, err = fd.Openat(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - } - if err != nil { - return nil, err -@@ -220,12 +228,14 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F - // If you plan to open the directory after you have created it or want to use - // an open directory handle as the root, you should use [MkdirAllHandle] instead. - // This function is a wrapper around [MkdirAllHandle]. -+// -+// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin - func MkdirAll(root, unsafePath string, mode os.FileMode) error { - rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return err - } -- defer rootDir.Close() -+ defer rootDir.Close() //nolint:errcheck // close failures aren't critical here - - f, err := MkdirAllHandle(rootDir, unsafePath, mode) - if err != nil { -diff --git a/vendor/github.com/cyphar/filepath-securejoin/open_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open_linux.go -similarity index 56% -rename from vendor/github.com/cyphar/filepath-securejoin/open_linux.go -rename to vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open_linux.go -index 230be73f..7492d8cf 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/open_linux.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open_linux.go -@@ -1,17 +1,22 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ - //go:build linux - --// 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. -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. - --package securejoin -+package pathrs - - import ( -- "fmt" - "os" -- "strconv" - - "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" - ) - - // OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided -@@ -40,12 +45,14 @@ func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { - // disconnected TTY that could cause a DoS, or some other issue). In order to - // use the returned handle, you can "upgrade" it to a proper handle using - // [Reopen]. -+// -+// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin - func OpenInRoot(root, unsafePath string) (*os.File, error) { - rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return nil, err - } -- defer rootDir.Close() -+ defer rootDir.Close() //nolint:errcheck // close failures aren't critical here - return OpenatInRoot(rootDir, unsafePath) - } - -@@ -63,41 +70,5 @@ func OpenInRoot(root, unsafePath string) (*os.File, error) { - // - // [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw - func Reopen(handle *os.File, flags int) (*os.File, error) { -- procRoot, err := getProcRoot() -- if err != nil { -- return nil, err -- } -- -- // We can't operate on /proc/thread-self/fd/$n directly when doing a -- // re-open, so we need to open /proc/thread-self/fd and then open a single -- // final component. -- procFdDir, closer, err := procThreadSelf(procRoot, "fd/") -- if err != nil { -- return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err) -- } -- defer procFdDir.Close() -- defer closer() -- -- // Try to detect if there is a mount on top of the magic-link we are about -- // to open. If we are using unsafeHostProcRoot(), this could change after -- // we check it (and there's nothing we can do about that) but for -- // privateProcRoot() this should be guaranteed to be safe (at least since -- // Linux 5.12[1], when anonymous mount namespaces were completely isolated -- // from external mounts including mount propagation events). -- // -- // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts -- // onto targets that reside on shared mounts"). -- fdStr := strconv.Itoa(int(handle.Fd())) -- if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil { -- return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err) -- } -- -- flags |= unix.O_CLOEXEC -- // Rather than just wrapping openatFile, open-code it so we can copy -- // handle.Name(). -- reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0) -- if err != nil { -- return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err) -- } -- return os.NewFile(uintptr(reopenFd), handle.Name()), nil -+ return procfs.ReopenFd(handle, flags) - } -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/openat2_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/openat2_linux.go -new file mode 100644 -index 00000000..937bc435 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/openat2_linux.go -@@ -0,0 +1,101 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+package pathrs -+ -+import ( -+ "errors" -+ "fmt" -+ "os" -+ "path/filepath" -+ "strings" -+ -+ "golang.org/x/sys/unix" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" -+) -+ -+func openat2(dir fd.Fd, path string, how *unix.OpenHow) (*os.File, error) { -+ file, err := fd.Openat2(dir, path, how) -+ if err != nil { -+ return nil, err -+ } -+ // If we are using RESOLVE_IN_ROOT, the name we generated may be wrong. -+ if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT { -+ if actualPath, err := procfs.ProcSelfFdReadlink(file); err == nil { -+ // TODO: Ideally we would not need to dup the fd, but you cannot -+ // easily just swap an *os.File with one from the same fd -+ // (the GC will close the old one, and you cannot clear the -+ // finaliser easily because it is associated with an internal -+ // field of *os.File not *os.File itself). -+ newFile, err := fd.DupWithName(file, actualPath) -+ if err != nil { -+ return nil, err -+ } -+ file = newFile -+ } -+ } -+ return file, nil -+} -+ -+func lookupOpenat2(root fd.Fd, unsafePath string, partial bool) (*os.File, string, error) { -+ if !partial { -+ file, err := openat2(root, unsafePath, &unix.OpenHow{ -+ Flags: unix.O_PATH | unix.O_CLOEXEC, -+ Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, -+ }) -+ return file, "", err -+ } -+ return partialLookupOpenat2(root, unsafePath) -+} -+ -+// partialLookupOpenat2 is an alternative implementation of -+// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a -+// handle to the deepest existing child of the requested path within the root. -+func partialLookupOpenat2(root fd.Fd, unsafePath string) (*os.File, string, error) { -+ // TODO: Implement this as a git-bisect-like binary search. -+ -+ unsafePath = filepath.ToSlash(unsafePath) // noop -+ endIdx := len(unsafePath) -+ var lastError error -+ for endIdx > 0 { -+ subpath := unsafePath[:endIdx] -+ -+ handle, err := openat2(root, subpath, &unix.OpenHow{ -+ Flags: unix.O_PATH | unix.O_CLOEXEC, -+ Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, -+ }) -+ if err == nil { -+ // Jump over the slash if we have a non-"" remainingPath. -+ if endIdx < len(unsafePath) { -+ endIdx++ -+ } -+ // We found a subpath! -+ return handle, unsafePath[endIdx:], lastError -+ } -+ if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) { -+ // That path doesn't exist, let's try the next directory up. -+ endIdx = strings.LastIndexByte(subpath, '/') -+ lastError = err -+ continue -+ } -+ return nil, "", fmt.Errorf("open subpath: %w", err) -+ } -+ // If we couldn't open anything, the whole subpath is missing. Return a -+ // copy of the root fd so that the caller doesn't close this one by -+ // accident. -+ rootClone, err := fd.Dup(root) -+ if err != nil { -+ return nil, "", err -+ } -+ return rootClone, unsafePath, lastError -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go -new file mode 100644 -index 00000000..ec187a41 ---- /dev/null -+++ b/vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go -@@ -0,0 +1,157 @@ -+// SPDX-License-Identifier: MPL-2.0 -+ -+//go:build linux -+ -+// Copyright (C) 2024-2025 Aleksa Sarai -+// Copyright (C) 2024-2025 SUSE LLC -+// -+// This Source Code Form is subject to the terms of the Mozilla Public -+// License, v. 2.0. If a copy of the MPL was not distributed with this -+// file, You can obtain one at https://mozilla.org/MPL/2.0/. -+ -+// Package procfs provides a safe API for operating on /proc on Linux. -+package procfs -+ -+import ( -+ "os" -+ -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" -+) -+ -+// This package mostly just wraps internal/procfs APIs. This is necessary -+// because we are forced to export some things from internal/procfs in order to -+// avoid some dependency cycle issues, but we don't want users to see or use -+// them. -+ -+// ProcThreadSelfCloser is a callback that needs to be called when you are done -+// operating on an [os.File] fetched using [Handle.OpenThreadSelf]. -+// -+// [os.File]: https://pkg.go.dev/os#File -+type ProcThreadSelfCloser = procfs.ProcThreadSelfCloser -+ -+// Handle is a wrapper around an *os.File handle to "/proc", which can be used -+// to do further procfs-related operations in a safe way. -+type Handle struct { -+ inner *procfs.Handle -+} -+ -+// Close close the resources associated with this [Handle]. Note that if this -+// [Handle] was created with [OpenProcRoot], on some kernels the underlying -+// procfs handle is cached and so this Close operation may be a no-op. However, -+// you should always call Close on [Handle]s once you are done with them. -+func (proc *Handle) Close() error { return proc.inner.Close() } -+ -+// OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the -+// "subset=pid" mount option applied, available from Linux 5.8). Unless you -+// plan to do many [Handle.OpenRoot] operations, users should prefer to use -+// this over [OpenUnsafeProcRoot] which is far more dangerous to keep open. -+// -+// If a safe handle cannot be opened, OpenProcRoot will fall back to opening a -+// regular "/proc" handle. -+// -+// Note that using [Handle.OpenRoot] will still work with handles returned by -+// this function. If a subpath cannot be operated on with a safe "/proc" -+// handle, then [OpenUnsafeProcRoot] will be called internally and a temporary -+// unsafe handle will be used. -+func OpenProcRoot() (*Handle, error) { -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ return &Handle{inner: proc}, nil -+} -+ -+// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or -+// masked paths. You must be extremely careful to make sure this handle is -+// never leaked to a container and that you program cannot be tricked into -+// writing to arbitrary paths within it. -+// -+// This is not necessary if you just wish to use [Handle.OpenRoot], as handles -+// returned by [OpenProcRoot] will fall back to using a *temporary* unsafe -+// handle in that case. You should only really use this if you need to do many -+// operations with [Handle.OpenRoot] and the performance overhead of making -+// many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you -+// should make sure to close the handle as soon as possible to avoid -+// known-fd-number attacks. -+func OpenUnsafeProcRoot() (*Handle, error) { -+ proc, err := procfs.OpenUnsafeProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ return &Handle{inner: proc}, nil -+} -+ -+// OpenThreadSelf returns a handle to "/proc/thread-self/" (or an -+// equivalent handle on older kernels where "/proc/thread-self" doesn't exist). -+// Once finished with the handle, you must call the returned closer function -+// ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other -+// Go threads or use the handle after calling the closer. -+// -+// [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread -+func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) { -+ return proc.inner.OpenThreadSelf(subpath) -+} -+ -+// OpenSelf returns a handle to /proc/self/. -+// -+// Note that in Go programs with non-homogenous threads, this may result in -+// spurious errors. If you are monkeying around with APIs that are -+// thread-specific, you probably want to use [Handle.OpenThreadSelf] instead -+// which will guarantee that the handle refers to the same thread as the caller -+// is executing on. -+func (proc *Handle) OpenSelf(subpath string) (*os.File, error) { -+ return proc.inner.OpenSelf(subpath) -+} -+ -+// OpenRoot returns a handle to /proc/. -+// -+// You should only use this when you need to operate on global procfs files -+// (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf], -+// [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally -+// for this operation will never use "subset=pid", which makes it a more juicy -+// target for [CVE-2024-21626]-style attacks (and doing something like opening -+// a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as -+// the file descriptor is open). -+// -+// [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv -+func (proc *Handle) OpenRoot(subpath string) (*os.File, error) { -+ return proc.inner.OpenRoot(subpath) -+} -+ -+// OpenPid returns a handle to /proc/$pid/ (pid can be a pid or tid). -+// This is mainly intended for usage when operating on other processes. -+// -+// You should not use this for the current thread, as special handling is -+// needed for /proc/thread-self (or /proc/self/task/) when dealing with -+// goroutine scheduling -- use [Handle.OpenThreadSelf] instead. -+// -+// To refer to the current thread-group, you should use prefer -+// [Handle.OpenSelf] to passing os.Getpid as the pid argument. -+func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) { -+ return proc.inner.OpenPid(pid, subpath) -+} -+ -+// ProcSelfFdReadlink gets the real path of the given file by looking at -+// /proc/self/fd/ with [readlink]. It is effectively just shorthand for -+// something along the lines of: -+// -+// proc, err := procfs.OpenProcRoot() -+// if err != nil { -+// return err -+// } -+// link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd())) -+// if err != nil { -+// return err -+// } -+// defer link.Close() -+// var buf [4096]byte -+// n, err := unix.Readlinkat(int(link.Fd()), "", buf[:]) -+// if err != nil { -+// return err -+// } -+// pathname := buf[:n] -+// -+// [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat -+func ProcSelfFdReadlink(f *os.File) (string, error) { -+ return procfs.ProcSelfFdReadlink(f) -+} -diff --git a/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go b/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go -deleted file mode 100644 -index 809a579c..00000000 ---- a/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go -+++ /dev/null -@@ -1,452 +0,0 @@ --//go:build linux -- --// 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 ( -- "errors" -- "fmt" -- "os" -- "runtime" -- "strconv" -- -- "golang.org/x/sys/unix" --) -- --func fstat(f *os.File) (unix.Stat_t, error) { -- var stat unix.Stat_t -- if err := unix.Fstat(int(f.Fd()), &stat); err != nil { -- return stat, &os.PathError{Op: "fstat", Path: f.Name(), Err: err} -- } -- return stat, nil --} -- --func fstatfs(f *os.File) (unix.Statfs_t, error) { -- var statfs unix.Statfs_t -- if err := unix.Fstatfs(int(f.Fd()), &statfs); err != nil { -- return statfs, &os.PathError{Op: "fstatfs", Path: f.Name(), Err: err} -- } -- return statfs, nil --} -- --// The kernel guarantees that the root inode of a procfs mount has an --// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO. --const ( -- procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC -- procRootIno = 1 // PROC_ROOT_INO --) -- --func verifyProcRoot(procRoot *os.File) error { -- if statfs, err := fstatfs(procRoot); err != nil { -- return err -- } else if statfs.Type != procSuperMagic { -- return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type) -- } -- if stat, err := fstat(procRoot); err != nil { -- return err -- } else if stat.Ino != procRootIno { -- return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino) -- } -- return nil --} -- --var hasNewMountApi = sync_OnceValue(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 -- // just check for one of the syscalls and the others should also be -- // available. -- // -- // Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE. -- // This is equivalent to openat(2), but tells us if open_tree is -- // available (and thus all of the other basic new mount API syscalls). -- // open_tree(2) is most light-weight syscall to test here. -- // -- // [1]: merge commit 400913252d09 -- // [2]: -- fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC) -- if err != nil { -- return false -- } -- _ = unix.Close(fd) -- return true --}) -- --func fsopen(fsName string, flags int) (*os.File, error) { -- // Make sure we always set O_CLOEXEC. -- flags |= unix.FSOPEN_CLOEXEC -- fd, err := unix.Fsopen(fsName, flags) -- if err != nil { -- return nil, os.NewSyscallError("fsopen "+fsName, err) -- } -- return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil --} -- --func fsmount(ctx *os.File, flags, mountAttrs int) (*os.File, error) { -- // Make sure we always set O_CLOEXEC. -- flags |= unix.FSMOUNT_CLOEXEC -- fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs) -- if err != nil { -- return nil, os.NewSyscallError("fsmount "+ctx.Name(), err) -- } -- return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil --} -- --func newPrivateProcMount() (*os.File, error) { -- procfsCtx, err := fsopen("proc", unix.FSOPEN_CLOEXEC) -- if err != nil { -- return nil, err -- } -- defer procfsCtx.Close() -- -- // Try to configure hidepid=ptraceable,subset=pid if possible, but ignore errors. -- _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable") -- _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") -- -- // Get an actual handle. -- if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil { -- return nil, os.NewSyscallError("fsconfig create procfs", err) -- } -- return fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID) --} -- --func openTree(dir *os.File, path string, flags uint) (*os.File, error) { -- dirFd := -int(unix.EBADF) -- dirName := "." -- if dir != nil { -- dirFd = int(dir.Fd()) -- dirName = dir.Name() -- } -- // Make sure we always set O_CLOEXEC. -- flags |= unix.OPEN_TREE_CLOEXEC -- fd, err := unix.OpenTree(dirFd, path, flags) -- if err != nil { -- return nil, &os.PathError{Op: "open_tree", Path: path, Err: err} -- } -- return os.NewFile(uintptr(fd), dirName+"/"+path), nil --} -- --func clonePrivateProcMount() (_ *os.File, Err error) { -- // Try to make a clone without using AT_RECURSIVE if we can. If this works, -- // we can be sure there are no over-mounts and so if the root is valid then -- // we're golden. Otherwise, we have to deal with over-mounts. -- procfsHandle, err := openTree(nil, "/proc", unix.OPEN_TREE_CLONE) -- if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procfsHandle) { -- procfsHandle, err = openTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE) -- } -- if err != nil { -- return nil, fmt.Errorf("creating a detached procfs clone: %w", err) -- } -- defer func() { -- if Err != nil { -- _ = procfsHandle.Close() -- } -- }() -- if err := verifyProcRoot(procfsHandle); err != nil { -- return nil, err -- } -- return procfsHandle, nil --} -- --func privateProcRoot() (*os.File, error) { -- if !hasNewMountApi() || hookForceGetProcRootUnsafe() { -- return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) -- } -- // Try to create a new procfs mount from scratch if we can. This ensures we -- // can get a procfs mount even if /proc is fake (for whatever reason). -- procRoot, err := newPrivateProcMount() -- if err != nil || hookForcePrivateProcRootOpenTree(procRoot) { -- // Try to clone /proc then... -- procRoot, err = clonePrivateProcMount() -- } -- return procRoot, err --} -- --func unsafeHostProcRoot() (_ *os.File, Err error) { -- procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) -- if err != nil { -- return nil, err -- } -- defer func() { -- if Err != nil { -- _ = procRoot.Close() -- } -- }() -- if err := verifyProcRoot(procRoot); err != nil { -- return nil, err -- } -- return procRoot, nil --} -- --func doGetProcRoot() (*os.File, error) { -- procRoot, err := privateProcRoot() -- if err != nil { -- // Fall back to using a /proc handle if making a private mount failed. -- // If we have openat2, at least we can avoid some kinds of over-mount -- // attacks, but without openat2 there's not much we can do. -- procRoot, err = unsafeHostProcRoot() -- } -- return procRoot, err --} -- --var getProcRoot = sync_OnceValues(func() (*os.File, error) { -- return doGetProcRoot() --}) -- --var hasProcThreadSelf = sync_OnceValue(func() bool { -- return unix.Access("/proc/thread-self/", unix.F_OK) == nil --}) -- --var errUnsafeProcfs = errors.New("unsafe procfs detected") -- --type procThreadSelfCloser func() -- --// procThreadSelf returns a handle to /proc/thread-self/ (or an --// equivalent handle on older kernels where /proc/thread-self doesn't exist). --// Once finished with the handle, you must call the returned closer function --// (runtime.UnlockOSThread). You must not pass the returned *os.File to other --// Go threads or use the handle after calling the closer. --// --// This is similar to ProcThreadSelf from runc, but with extra hardening --// applied and using *os.File. --func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) { -- // We need to lock our thread until the caller is done with the handle -- // because between getting the handle and using it we could get interrupted -- // by the Go runtime and hit the case where the underlying thread is -- // swapped out and the original thread is killed, resulting in -- // pull-your-hair-out-hard-to-debug issues in the caller. -- runtime.LockOSThread() -- defer func() { -- if Err != nil { -- runtime.UnlockOSThread() -- } -- }() -- -- // Figure out what prefix we want to use. -- threadSelf := "thread-self/" -- if !hasProcThreadSelf() || hookForceProcSelfTask() { -- /// Pre-3.17 kernels don't have /proc/thread-self, so do it manually. -- threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) + "/" -- if _, err := fstatatFile(procRoot, threadSelf, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() { -- // In this case, we running in a pid namespace that doesn't match -- // the /proc mount we have. This can happen inside runc. -- // -- // Unfortunately, there is no nice way to get the correct TID to -- // use here because of the age of the kernel, so we have to just -- // use /proc/self and hope that it works. -- threadSelf = "self/" -- } -- } -- -- // Grab the handle. -- var ( -- handle *os.File -- err error -- ) -- if hasOpenat2() { -- // We prefer being able to use RESOLVE_NO_XDEV if we can, to be -- // absolutely sure we are operating on a clean /proc handle that -- // doesn't have any cheeky overmounts that could trick us (including -- // symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't -- // strictly needed, but just use it since we have it. -- // -- // NOTE: /proc/self is technically a magic-link (the contents of the -- // symlink are generated dynamically), but it doesn't use -- // nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it. -- // -- // NOTE: We MUST NOT use RESOLVE_IN_ROOT here, as openat2File uses -- // procSelfFdReadlink to clean up the returned f.Name() if we use -- // RESOLVE_IN_ROOT (which would lead to an infinite recursion). -- handle, err = openat2File(procRoot, threadSelf+subpath, &unix.OpenHow{ -- Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC, -- Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS, -- }) -- if err != nil { -- // TODO: Once we bump the minimum Go version to 1.20, we can use -- // 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, nil, wrapBaseError(err, errUnsafeProcfs) -- } -- } else { -- handle, err = openatFile(procRoot, threadSelf+subpath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) -- if err != nil { -- // TODO: Once we bump the minimum Go version to 1.20, we can use -- // 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, nil, wrapBaseError(err, errUnsafeProcfs) -- } -- defer func() { -- if Err != nil { -- _ = handle.Close() -- } -- }() -- // We can't detect bind-mounts of different parts of procfs on top of -- // /proc (a-la RESOLVE_NO_XDEV), but we can at least be sure that we -- // aren't on the wrong filesystem here. -- if statfs, err := fstatfs(handle); err != nil { -- return nil, nil, err -- } else if statfs.Type != procSuperMagic { -- return nil, nil, fmt.Errorf("%w: incorrect /proc/self/fd filesystem type 0x%x", errUnsafeProcfs, statfs.Type) -- } -- } -- return handle, runtime.UnlockOSThread, nil --} -- --// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to --// avoid bumping the requirement for a single constant we can just define it --// ourselves. --const STATX_MNT_ID_UNIQUE = 0x4000 -- --var hasStatxMountId = sync_OnceValue(func() bool { -- var ( -- stx unix.Statx_t -- // We don't care which mount ID we get. The kernel will give us the -- // unique one if it is supported. -- wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID -- ) -- err := unix.Statx(-int(unix.EBADF), "/", 0, int(wantStxMask), &stx) -- return err == nil && stx.Mask&wantStxMask != 0 --}) -- --func getMountId(dir *os.File, path string) (uint64, error) { -- // If we don't have statx(STATX_MNT_ID*) support, we can't do anything. -- if !hasStatxMountId() { -- return 0, nil -- } -- -- var ( -- stx unix.Statx_t -- // We don't care which mount ID we get. The kernel will give us the -- // unique one if it is supported. -- wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID -- ) -- -- err := unix.Statx(int(dir.Fd()), path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx) -- if stx.Mask&wantStxMask == 0 { -- // It's not a kernel limitation, for some reason we couldn't get a -- // mount ID. Assume it's some kind of attack. -- err = fmt.Errorf("%w: could not get mount id", errUnsafeProcfs) -- } -- if err != nil { -- return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: dir.Name() + "/" + path, Err: err} -- } -- return stx.Mnt_id, nil --} -- --func checkSymlinkOvermount(procRoot *os.File, dir *os.File, path string) error { -- // Get the mntId of our procfs handle. -- expectedMountId, err := getMountId(procRoot, "") -- if err != nil { -- return err -- } -- // Get the mntId of the target magic-link. -- gotMountId, err := getMountId(dir, path) -- if err != nil { -- return err -- } -- // As long as the directory mount is alive, even with wrapping mount IDs, -- // we would expect to see a different mount ID here. (Of course, if we're -- // using unsafeHostProcRoot() then an attaker could change this after we -- // did this check.) -- if expectedMountId != gotMountId { -- return fmt.Errorf("%w: symlink %s/%s has an overmount obscuring the real link (mount ids do not match %d != %d)", errUnsafeProcfs, dir.Name(), path, expectedMountId, gotMountId) -- } -- return nil --} -- --func doRawProcSelfFdReadlink(procRoot *os.File, fd int) (string, error) { -- fdPath := fmt.Sprintf("fd/%d", fd) -- procFdLink, closer, err := procThreadSelf(procRoot, fdPath) -- if err != nil { -- return "", fmt.Errorf("get safe /proc/thread-self/%s handle: %w", fdPath, err) -- } -- defer procFdLink.Close() -- defer closer() -- -- // Try to detect if there is a mount on top of the magic-link. Since we use the handle directly -- // provide to the closure. If the closure uses the handle directly, this -- // should be safe in general (a mount on top of the path afterwards would -- // not affect the handle itself) and will definitely be safe if we are -- // using privateProcRoot() (at least since Linux 5.12[1], when anonymous -- // mount namespaces were completely isolated from external mounts including -- // mount propagation events). -- // -- // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts -- // onto targets that reside on shared mounts"). -- if err := checkSymlinkOvermount(procRoot, procFdLink, ""); err != nil { -- return "", fmt.Errorf("check safety of /proc/thread-self/fd/%d magiclink: %w", fd, err) -- } -- -- // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit -- // 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty -- // relative pathnames"). -- return readlinkatFile(procFdLink, "") --} -- --func rawProcSelfFdReadlink(fd int) (string, error) { -- procRoot, err := getProcRoot() -- if err != nil { -- return "", err -- } -- return doRawProcSelfFdReadlink(procRoot, fd) --} -- --func procSelfFdReadlink(f *os.File) (string, error) { -- return rawProcSelfFdReadlink(int(f.Fd())) --} -- --var ( -- errPossibleBreakout = errors.New("possible breakout detected") -- errInvalidDirectory = errors.New("wandered into deleted directory") -- errDeletedInode = errors.New("cannot verify path of deleted inode") --) -- --func isDeadInode(file *os.File) error { -- // If the nlink of a file drops to 0, there is an attacker deleting -- // directories during our walk, which could result in weird /proc values. -- // It's better to error out in this case. -- stat, err := fstat(file) -- if err != nil { -- return fmt.Errorf("check for dead inode: %w", err) -- } -- if stat.Nlink == 0 { -- err := errDeletedInode -- if stat.Mode&unix.S_IFMT == unix.S_IFDIR { -- err = errInvalidDirectory -- } -- return fmt.Errorf("%w %q", err, file.Name()) -- } -- return nil --} -- --func checkProcSelfFdPath(path string, file *os.File) error { -- if err := isDeadInode(file); err != nil { -- return err -- } -- actualPath, err := procSelfFdReadlink(file) -- if err != nil { -- return fmt.Errorf("get path of handle: %w", err) -- } -- if actualPath != path { -- return fmt.Errorf("%w: handle path %q doesn't match expected path %q", errPossibleBreakout, actualPath, path) -- } -- return nil --} -- --// Test hooks used in the procfs tests to verify that the fallback logic works. --// See testing_mocks_linux_test.go and procfs_linux_test.go for more details. --var ( -- hookForcePrivateProcRootOpenTree = hookDummyFile -- hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile -- hookForceGetProcRootUnsafe = hookDummy -- -- hookForceProcSelfTask = hookDummy -- hookForceProcSelf = hookDummy --) -- --func hookDummy() bool { return false } --func hookDummyFile(_ *os.File) bool { return false } -diff --git a/vendor/github.com/cyphar/filepath-securejoin/vfs.go b/vendor/github.com/cyphar/filepath-securejoin/vfs.go -index 36373f8c..4d89a481 100644 ---- a/vendor/github.com/cyphar/filepath-securejoin/vfs.go -+++ b/vendor/github.com/cyphar/filepath-securejoin/vfs.go -@@ -1,3 +1,5 @@ -+// SPDX-License-Identifier: BSD-3-Clause -+ - // Copyright (C) 2017-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. -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go -index 07e0f77d..884a8b80 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go -@@ -6,78 +6,11 @@ import ( - "github.com/opencontainers/selinux/go-selinux" - ) - --// Deprecated: use selinux.ROFileLabel --var ROMountLabel = selinux.ROFileLabel -- --// SetProcessLabel takes a process label and tells the kernel to assign the --// label to the next program executed by the current process. --// Deprecated: use selinux.SetExecLabel --var SetProcessLabel = selinux.SetExecLabel -- --// ProcessLabel returns the process label that the kernel will assign --// to the next program executed by the current process. If "" is returned --// this indicates that the default labeling will happen for the process. --// Deprecated: use selinux.ExecLabel --var ProcessLabel = selinux.ExecLabel -- --// SetSocketLabel takes a process label and tells the kernel to assign the --// label to the next socket that gets created --// Deprecated: use selinux.SetSocketLabel --var SetSocketLabel = selinux.SetSocketLabel -- --// SocketLabel retrieves the current default socket label setting --// Deprecated: use selinux.SocketLabel --var SocketLabel = selinux.SocketLabel -- --// SetKeyLabel takes a process label and tells the kernel to assign the --// label to the next kernel keyring that gets created --// Deprecated: use selinux.SetKeyLabel --var SetKeyLabel = selinux.SetKeyLabel -- --// KeyLabel retrieves the current default kernel keyring label setting --// Deprecated: use selinux.KeyLabel --var KeyLabel = selinux.KeyLabel -- --// FileLabel returns the label for specified path --// Deprecated: use selinux.FileLabel --var FileLabel = selinux.FileLabel -- --// PidLabel will return the label of the process running with the specified pid --// Deprecated: use selinux.PidLabel --var PidLabel = selinux.PidLabel -- - // Init initialises the labeling system - func Init() { - _ = selinux.GetEnabled() - } - --// ClearLabels will clear all reserved labels --// Deprecated: use selinux.ClearLabels --var ClearLabels = selinux.ClearLabels -- --// ReserveLabel will record the fact that the MCS label has already been used. --// This will prevent InitLabels from using the MCS label in a newly created --// container --// Deprecated: use selinux.ReserveLabel --func ReserveLabel(label string) error { -- selinux.ReserveLabel(label) -- return nil --} -- --// ReleaseLabel will remove the reservation of the MCS label. --// This will allow InitLabels to use the MCS label in a newly created --// containers --// Deprecated: use selinux.ReleaseLabel --func ReleaseLabel(label string) error { -- selinux.ReleaseLabel(label) -- return nil --} -- --// DupSecOpt takes a process label and returns security options that --// can be used to set duplicate labels on future container processes --// Deprecated: use selinux.DupSecOpt --var DupSecOpt = selinux.DupSecOpt -- - // FormatMountLabel returns a string to be used by the mount command. Using - // the SELinux `context` mount option. Changing labels of files on mount - // points with this option can never be changed. -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go -index e49e6d53..95f29e21 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go -@@ -18,7 +18,7 @@ var validOptions = map[string]bool{ - "level": true, - } - --var ErrIncompatibleLabel = errors.New("Bad SELinux option z and Z can not be used together") -+var ErrIncompatibleLabel = errors.New("bad SELinux option: z and Z can not be used together") - - // InitLabels returns the process label and file labels to be used within - // the container. A list of options can be passed into this function to alter -@@ -52,11 +52,11 @@ func InitLabels(options []string) (plabel string, mlabel string, retErr error) { - return "", selinux.PrivContainerMountLabel(), nil - } - if i := strings.Index(opt, ":"); i == -1 { -- return "", "", fmt.Errorf("Bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt) -+ return "", "", fmt.Errorf("bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt) - } - con := strings.SplitN(opt, ":", 2) - if !validOptions[con[0]] { -- return "", "", fmt.Errorf("Bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0]) -+ return "", "", fmt.Errorf("bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0]) - } - if con[0] == "filetype" { - mcon["type"] = con[1] -@@ -79,12 +79,6 @@ func InitLabels(options []string) (plabel string, mlabel string, retErr error) { - return processLabel, mountLabel, nil - } - --// Deprecated: The GenLabels function is only to be used during the transition --// to the official API. Use InitLabels(strings.Fields(options)) instead. --func GenLabels(options string) (string, string, error) { -- return InitLabels(strings.Fields(options)) --} -- - // SetFileLabel modifies the "path" label to the specified file label - func SetFileLabel(path string, fileLabel string) error { - if !selinux.GetEnabled() || fileLabel == "" { -@@ -123,11 +117,6 @@ func Relabel(path string, fileLabel string, shared bool) error { - return selinux.Chcon(path, fileLabel, true) - } - --// DisableSecOpt returns a security opt that can disable labeling --// support for future container processes --// Deprecated: use selinux.DisableSecOpt --var DisableSecOpt = selinux.DisableSecOpt -- - // Validate checks that the label does not include unexpected options - func Validate(label string) error { - if strings.Contains(label, "z") && strings.Contains(label, "Z") { -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_stub.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_stub.go -index 1c260cb2..7a54afc5 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_stub.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_stub.go -@@ -10,12 +10,6 @@ func InitLabels([]string) (string, string, error) { - return "", "", nil - } - --// Deprecated: The GenLabels function is only to be used during the transition --// to the official API. Use InitLabels(strings.Fields(options)) instead. --func GenLabels(string) (string, string, error) { -- return "", "", nil --} -- - func SetFileLabel(string, string) error { - return nil - } -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go -index af058b84..15150d47 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go -@@ -41,6 +41,10 @@ var ( - // ErrVerifierNil is returned when a context verifier function is nil. - ErrVerifierNil = errors.New("verifier function is nil") - -+ // ErrNotTGLeader is returned by [SetKeyLabel] if the calling thread -+ // is not the thread group leader. -+ ErrNotTGLeader = errors.New("calling thread is not the thread group leader") -+ - // CategoryRange allows the upper bound on the category range to be adjusted - CategoryRange = DefaultCategoryRange - -@@ -149,7 +153,7 @@ func CalculateGlbLub(sourceRange, targetRange string) (string, error) { - // of the program is finished to guarantee another goroutine does not migrate to the current - // thread before execution is complete. - func SetExecLabel(label string) error { -- return writeCon(attrPath("exec"), label) -+ return writeConThreadSelf("attr/exec", label) - } - - // SetTaskLabel sets the SELinux label for the current thread, or an error. -@@ -157,7 +161,7 @@ func SetExecLabel(label string) error { - // be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee - // the current thread does not run in a new mislabeled thread. - func SetTaskLabel(label string) error { -- return writeCon(attrPath("current"), label) -+ return writeConThreadSelf("attr/current", label) - } - - // SetSocketLabel takes a process label and tells the kernel to assign the -@@ -166,12 +170,12 @@ func SetTaskLabel(label string) error { - // the socket is created to guarantee another goroutine does not migrate - // to the current thread before execution is complete. - func SetSocketLabel(label string) error { -- return writeCon(attrPath("sockcreate"), label) -+ return writeConThreadSelf("attr/sockcreate", label) - } - - // SocketLabel retrieves the current socket label setting - func SocketLabel() (string, error) { -- return readCon(attrPath("sockcreate")) -+ return readConThreadSelf("attr/sockcreate") - } - - // PeerLabel retrieves the label of the client on the other side of a socket -@@ -180,17 +184,21 @@ func PeerLabel(fd uintptr) (string, error) { - } - - // SetKeyLabel takes a process label and tells the kernel to assign the --// label to the next kernel keyring that gets created. Calls to SetKeyLabel --// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until --// the kernel keyring is created to guarantee another goroutine does not migrate --// to the current thread before execution is complete. -+// label to the next kernel keyring that gets created. -+// -+// Calls to SetKeyLabel should be wrapped in -+// runtime.LockOSThread()/runtime.UnlockOSThread() until the kernel keyring is -+// created to guarantee another goroutine does not migrate to the current -+// thread before execution is complete. -+// -+// Only the thread group leader can set key label. - func SetKeyLabel(label string) error { - return setKeyLabel(label) - } - - // KeyLabel retrieves the current kernel keyring label setting - func KeyLabel() (string, error) { -- return readCon("/proc/self/attr/keycreate") -+ return keyLabel() - } - - // Get returns the Context as a string -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go -index c80c1097..70392d98 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go -@@ -17,8 +17,11 @@ import ( - "strings" - "sync" - -- "github.com/opencontainers/selinux/pkg/pwalkdir" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite" -+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" - "golang.org/x/sys/unix" -+ -+ "github.com/opencontainers/selinux/pkg/pwalkdir" - ) - - const ( -@@ -45,7 +48,7 @@ type selinuxState struct { - - type level struct { - cats *big.Int -- sens uint -+ sens int - } - - type mlsRange struct { -@@ -73,10 +76,6 @@ var ( - mcsList: make(map[string]bool), - } - -- // for attrPath() -- attrPathOnce sync.Once -- haveThreadSelf bool -- - // for policyRoot() - policyRootOnce sync.Once - policyRootVal string -@@ -138,6 +137,7 @@ func verifySELinuxfsMount(mnt string) bool { - return false - } - -+ //#nosec G115 -- there is no overflow here. - if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) { - return false - } -@@ -255,48 +255,187 @@ func readConfig(target string) string { - return "" - } - --func isProcHandle(fh *os.File) error { -- var buf unix.Statfs_t -+func readConFd(in *os.File) (string, error) { -+ data, err := io.ReadAll(in) -+ if err != nil { -+ return "", err -+ } -+ return string(bytes.TrimSuffix(data, []byte{0})), nil -+} - -- for { -- err := unix.Fstatfs(int(fh.Fd()), &buf) -- if err == nil { -- break -- } -- if err != unix.EINTR { -- return &os.PathError{Op: "fstatfs", Path: fh.Name(), Err: err} -- } -+func writeConFd(out *os.File, val string) error { -+ var err error -+ if val != "" { -+ _, err = out.Write([]byte(val)) -+ } else { -+ _, err = out.Write(nil) - } -- if buf.Type != unix.PROC_SUPER_MAGIC { -- return fmt.Errorf("file %q is not on procfs", fh.Name()) -+ return err -+} -+ -+// openProcThreadSelf is a small wrapper around [OpenThreadSelf] and -+// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The -+// provided mode must be os.O_* flags to indicate what mode the returned file -+// should be opened with (flags like os.O_CREAT and os.O_EXCL are not -+// supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/thread-self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenThreadSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenThreadSelf -+func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) { -+ if subpath == "" { -+ return nil, nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, nil, err - } -+ defer proc.Close() - -- return nil --} -+ handle, closer, err := proc.OpenThreadSelf(subpath) -+ if err != nil { -+ return nil, nil, fmt.Errorf("open /proc/thread-self/%s handle: %w", subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle - --func readCon(fpath string) (string, error) { -- if fpath == "" { -- return "", ErrEmptyPath -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ closer() -+ return nil, nil, fmt.Errorf("reopen /proc/thread-self/%s handle (%#x): %w", subpath, mode, err) - } -+ return file, closer, nil -+} - -- in, err := os.Open(fpath) -+// Read the contents of /proc/thread-self/. -+func readConThreadSelf(fpath string) (string, error) { -+ in, closer, err := openProcThreadSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC) - if err != nil { - return "", err - } -+ defer closer() - defer in.Close() - -- if err := isProcHandle(in); err != nil { -+ return readConFd(in) -+} -+ -+// Write to /proc/thread-self/. -+func writeConThreadSelf(fpath, val string) error { -+ if val == "" { -+ if !getEnabled() { -+ return nil -+ } -+ } -+ -+ out, closer, err := openProcThreadSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return err -+ } -+ defer closer() -+ defer out.Close() -+ -+ return writeConFd(out, val) -+} -+ -+// openProcSelf is a small wrapper around [OpenSelf] and [pathrs.Reopen] to -+// make "one-shot opens" slightly more ergonomic. The provided mode must be -+// os.O_* flags to indicate what mode the returned file should be opened with -+// (flags like os.O_CREAT and os.O_EXCL are not supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenSelf -+func openProcSelf(subpath string, mode int) (*os.File, error) { -+ if subpath == "" { -+ return nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ -+ handle, err := proc.OpenSelf(subpath) -+ if err != nil { -+ return nil, fmt.Errorf("open /proc/self/%s handle: %w", subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle -+ -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ return nil, fmt.Errorf("reopen /proc/self/%s handle (%#x): %w", subpath, mode, err) -+ } -+ return file, nil -+} -+ -+// Read the contents of /proc/self/. -+func readConSelf(fpath string) (string, error) { -+ in, err := openProcSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC) -+ if err != nil { - return "", err - } -+ defer in.Close() -+ - return readConFd(in) - } - --func readConFd(in *os.File) (string, error) { -- data, err := io.ReadAll(in) -+// Write to /proc/self/. -+func writeConSelf(fpath, val string) error { -+ if val == "" { -+ if !getEnabled() { -+ return nil -+ } -+ } -+ -+ out, err := openProcSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC) - if err != nil { -- return "", err -+ return err - } -- return string(bytes.TrimSuffix(data, []byte{0})), nil -+ defer out.Close() -+ -+ return writeConFd(out, val) -+} -+ -+// openProcPid is a small wrapper around [OpenPid] and [pathrs.Reopen] to make -+// "one-shot opens" slightly more ergonomic. The provided mode must be os.O_* -+// flags to indicate what mode the returned file should be opened with (flags -+// like os.O_CREAT and os.O_EXCL are not supported). -+// -+// If no error occurred, the returned handle is guaranteed to be exactly -+// /proc/self/ with no tricky mounts or symlinks causing you to -+// operate on an unexpected path (with some caveats on pre-openat2 or -+// pre-fsopen kernels). -+// -+// [OpenPid]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenPid -+func openProcPid(pid int, subpath string, mode int) (*os.File, error) { -+ if subpath == "" { -+ return nil, ErrEmptyPath -+ } -+ -+ proc, err := procfs.OpenProcRoot() -+ if err != nil { -+ return nil, err -+ } -+ defer proc.Close() -+ -+ handle, err := proc.OpenPid(pid, subpath) -+ if err != nil { -+ return nil, fmt.Errorf("open /proc/%d/%s handle: %w", pid, subpath, err) -+ } -+ defer handle.Close() // we will return a re-opened handle -+ -+ file, err := pathrs.Reopen(handle, mode) -+ if err != nil { -+ return nil, fmt.Errorf("reopen /proc/%d/%s handle (%#x): %w", pid, subpath, mode, err) -+ } -+ return file, nil - } - - // classIndex returns the int index for an object class in the loaded policy, -@@ -392,78 +531,34 @@ func lFileLabel(fpath string) (string, error) { - } - - func setFSCreateLabel(label string) error { -- return writeCon(attrPath("fscreate"), label) -+ return writeConThreadSelf("attr/fscreate", label) - } - - // fsCreateLabel returns the default label the kernel which the kernel is using - // for file system objects created by this task. "" indicates default. - func fsCreateLabel() (string, error) { -- return readCon(attrPath("fscreate")) -+ return readConThreadSelf("attr/fscreate") - } - - // currentLabel returns the SELinux label of the current process thread, or an error. - func currentLabel() (string, error) { -- return readCon(attrPath("current")) -+ return readConThreadSelf("attr/current") - } - - // pidLabel returns the SELinux label of the given pid, or an error. - func pidLabel(pid int) (string, error) { -- return readCon(fmt.Sprintf("/proc/%d/attr/current", pid)) -+ it, err := openProcPid(pid, "attr/current", os.O_RDONLY|unix.O_CLOEXEC) -+ if err != nil { -+ return "", nil -+ } -+ defer it.Close() -+ return readConFd(it) - } - - // ExecLabel returns the SELinux label that the kernel will use for any programs - // that are executed by the current process thread, or an error. - func execLabel() (string, error) { -- return readCon(attrPath("exec")) --} -- --func writeCon(fpath, val string) error { -- if fpath == "" { -- return ErrEmptyPath -- } -- if val == "" { -- if !getEnabled() { -- return nil -- } -- } -- -- out, err := os.OpenFile(fpath, os.O_WRONLY, 0) -- if err != nil { -- return err -- } -- defer out.Close() -- -- if err := isProcHandle(out); err != nil { -- return err -- } -- -- if val != "" { -- _, err = out.Write([]byte(val)) -- } else { -- _, err = out.Write(nil) -- } -- if err != nil { -- return err -- } -- return nil --} -- --func attrPath(attr string) string { -- // Linux >= 3.17 provides this -- const threadSelfPrefix = "/proc/thread-self/attr" -- -- attrPathOnce.Do(func() { -- st, err := os.Stat(threadSelfPrefix) -- if err == nil && st.Mode().IsDir() { -- haveThreadSelf = true -- } -- }) -- -- if haveThreadSelf { -- return filepath.Join(threadSelfPrefix, attr) -- } -- -- return filepath.Join("/proc/self/task", strconv.Itoa(unix.Gettid()), "attr", attr) -+ return readConThreadSelf("exec") - } - - // canonicalizeContext takes a context string and writes it to the kernel -@@ -501,14 +596,14 @@ func catsToBitset(cats string) (*big.Int, error) { - return nil, err - } - for i := catstart; i <= catend; i++ { -- bitset.SetBit(bitset, int(i), 1) -+ bitset.SetBit(bitset, i, 1) - } - } else { - cat, err := parseLevelItem(ranges[0], category) - if err != nil { - return nil, err - } -- bitset.SetBit(bitset, int(cat), 1) -+ bitset.SetBit(bitset, cat, 1) - } - } - -@@ -516,16 +611,17 @@ func catsToBitset(cats string) (*big.Int, error) { - } - - // parseLevelItem parses and verifies that a sensitivity or category are valid --func parseLevelItem(s string, sep levelItem) (uint, error) { -+func parseLevelItem(s string, sep levelItem) (int, error) { - if len(s) < minSensLen || levelItem(s[0]) != sep { - return 0, ErrLevelSyntax - } -- val, err := strconv.ParseUint(s[1:], 10, 32) -+ const bitSize = 31 // Make sure the result fits into signed int32. -+ val, err := strconv.ParseUint(s[1:], 10, bitSize) - if err != nil { - return 0, err - } - -- return uint(val), nil -+ return int(val), nil - } - - // parseLevel fills a level from a string that contains -@@ -582,7 +678,8 @@ func bitsetToStr(c *big.Int) string { - var str string - - length := 0 -- for i := int(c.TrailingZeroBits()); i < c.BitLen(); i++ { -+ i0 := int(c.TrailingZeroBits()) //#nosec G115 -- don't expect TralingZeroBits to return values with highest bit set. -+ for i := i0; i < c.BitLen(); i++ { - if c.Bit(i) == 0 { - continue - } -@@ -622,7 +719,7 @@ func (l *level) equal(l2 *level) bool { - - // String returns an mlsRange as a string. - func (m mlsRange) String() string { -- low := "s" + strconv.Itoa(int(m.low.sens)) -+ low := "s" + strconv.Itoa(m.low.sens) - if m.low.cats != nil && m.low.cats.BitLen() > 0 { - low += ":" + bitsetToStr(m.low.cats) - } -@@ -631,7 +728,7 @@ func (m mlsRange) String() string { - return low - } - -- high := "s" + strconv.Itoa(int(m.high.sens)) -+ high := "s" + strconv.Itoa(m.high.sens) - if m.high.cats != nil && m.high.cats.BitLen() > 0 { - high += ":" + bitsetToStr(m.high.cats) - } -@@ -639,15 +736,16 @@ func (m mlsRange) String() string { - return low + "-" + high - } - --// TODO: remove min and max once Go < 1.21 is not supported. --func max(a, b uint) uint { -+// TODO: remove these in favor of built-in min/max -+// once we stop supporting Go < 1.21. -+func maxInt(a, b int) int { - if a > b { - return a - } - return b - } - --func min(a, b uint) uint { -+func minInt(a, b int) int { - if a < b { - return a - } -@@ -676,10 +774,10 @@ func calculateGlbLub(sourceRange, targetRange string) (string, error) { - outrange := &mlsRange{low: &level{}, high: &level{}} - - /* take the greatest of the low */ -- outrange.low.sens = max(s.low.sens, t.low.sens) -+ outrange.low.sens = maxInt(s.low.sens, t.low.sens) - - /* take the least of the high */ -- outrange.high.sens = min(s.high.sens, t.high.sens) -+ outrange.high.sens = minInt(s.high.sens, t.high.sens) - - /* find the intersecting categories */ - if s.low.cats != nil && t.low.cats != nil { -@@ -724,16 +822,29 @@ func peerLabel(fd uintptr) (string, error) { - // setKeyLabel takes a process label and tells the kernel to assign the - // label to the next kernel keyring that gets created - func setKeyLabel(label string) error { -- err := writeCon("/proc/self/attr/keycreate", label) -+ // Rather than using /proc/thread-self, we want to use /proc/self to -+ // operate on the thread-group leader. -+ err := writeConSelf("attr/keycreate", label) - if errors.Is(err, os.ErrNotExist) { - return nil - } - if label == "" && errors.Is(err, os.ErrPermission) { - return nil - } -+ if errors.Is(err, unix.EACCES) && unix.Getpid() != unix.Gettid() { -+ return ErrNotTGLeader -+ } - return err - } - -+// KeyLabel retrieves the current kernel keyring label setting for this -+// thread-group. -+func keyLabel() (string, error) { -+ // Rather than using /proc/thread-self, we want to use /proc/self to -+ // operate on the thread-group leader. -+ return readConSelf("attr/keycreate") -+} -+ - // get returns the Context as a string - func (c Context) get() string { - if l := c["level"]; l != "" { -@@ -809,8 +920,7 @@ func enforceMode() int { - // setEnforceMode sets the current SELinux mode Enforcing, Permissive. - // Disabled is not valid, since this needs to be set at boot time. - func setEnforceMode(mode int) error { -- //nolint:gosec // ignore G306: permissions to be 0600 or less. -- return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0o644) -+ return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0) - } - - // defaultEnforceMode returns the systems default SELinux mode Enforcing, -@@ -1017,8 +1127,7 @@ func addMcs(processLabel, fileLabel string) (string, string) { - - // securityCheckContext validates that the SELinux label is understood by the kernel - func securityCheckContext(val string) error { -- //nolint:gosec // ignore G306: permissions to be 0600 or less. -- return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0o644) -+ return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0) - } - - // copyLevel returns a label with the MLS/MCS level from src label replaced on -diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go -index 0889fbe0..26792123 100644 ---- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go -+++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go -@@ -7,11 +7,11 @@ func attrPath(string) string { - return "" - } - --func readCon(string) (string, error) { -+func readConThreadSelf(string) (string, error) { - return "", nil - } - --func writeCon(string, string) error { -+func writeConThreadSelf(string, string) error { - return nil - } - -@@ -81,6 +81,10 @@ func setKeyLabel(string) error { - return nil - } - -+func keyLabel() (string, error) { -+ return "", nil -+} -+ - func (c Context) get() string { - return "" - } -diff --git a/vendor/modules.txt b/vendor/modules.txt -index 0ead139c..f22001c8 100644 ---- a/vendor/modules.txt -+++ b/vendor/modules.txt -@@ -17,7 +17,7 @@ github.com/cilium/ebpf/internal/testutils/fdtrace - github.com/cilium/ebpf/internal/tracefs - github.com/cilium/ebpf/internal/unix - github.com/cilium/ebpf/link --# github.com/containerd/console v1.0.4 -+# github.com/containerd/console v1.0.5 - ## explicit; go 1.13 - github.com/containerd/console - # github.com/coreos/go-systemd/v22 v22.5.0 -@@ -27,9 +27,19 @@ github.com/coreos/go-systemd/v22/dbus - # github.com/cpuguy83/go-md2man/v2 v2.0.5 - ## explicit; go 1.11 - github.com/cpuguy83/go-md2man/v2/md2man --# github.com/cyphar/filepath-securejoin v0.4.1 -+# github.com/cyphar/filepath-securejoin v0.5.0 - ## explicit; go 1.18 - github.com/cyphar/filepath-securejoin -+github.com/cyphar/filepath-securejoin/internal/consts -+github.com/cyphar/filepath-securejoin/pathrs-lite -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux -+github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs -+github.com/cyphar/filepath-securejoin/pathrs-lite/procfs - # github.com/docker/go-units v0.5.0 - ## explicit - github.com/docker/go-units -@@ -66,7 +76,7 @@ github.com/opencontainers/cgroups/systemd - ## explicit - github.com/opencontainers/runtime-spec/specs-go - github.com/opencontainers/runtime-spec/specs-go/features --# github.com/opencontainers/selinux v1.11.1 -+# github.com/opencontainers/selinux v1.12.0 => ./internal/third_party/selinux - ## explicit; go 1.19 - github.com/opencontainers/selinux/go-selinux - github.com/opencontainers/selinux/go-selinux/label -@@ -127,3 +137,4 @@ google.golang.org/protobuf/reflect/protoreflect - google.golang.org/protobuf/reflect/protoregistry - google.golang.org/protobuf/runtime/protoiface - google.golang.org/protobuf/runtime/protoimpl -+# github.com/opencontainers/selinux => ./internal/third_party/selinux --- -2.51.1 - diff --git a/SOURCES/0002-1.3-rootfs-re-allow-dangling-symlinks-in-mount-targe.patch b/SOURCES/0002-1.3-rootfs-re-allow-dangling-symlinks-in-mount-targe.patch deleted file mode 100644 index feda435..0000000 --- a/SOURCES/0002-1.3-rootfs-re-allow-dangling-symlinks-in-mount-targe.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 2a9b44aabfa52bb071ff2e3564427da0bb82312e Mon Sep 17 00:00:00 2001 -From: Aleksa Sarai -Date: Wed, 5 Nov 2025 02:04:02 +1100 -Subject: [PATCH 2/2] [1.3] rootfs: re-allow dangling symlinks in mount targets - -It seems there are a fair few images where dangling symlinks are used as -path components for mount targets, which pathrs-lite does not support -(and it would be difficult to fully support this in a race-free way). - -This was actually meant to be blocked by commit 63c2908164f3 ("rootfs: -try to scope MkdirAll to stay inside the rootfs"), followed by commit -dd827f7b715a ("utils: switch to securejoin.MkdirAllHandle"). However, we -still used SecureJoin to construct mountpoint targets, which means that -dangling symlinks were "resolved" before reaching pathrs-lite. - -This patch basically re-adds this hack in order to reduce the breakages -we've seen so far. - -Signed-off-by: Aleksa Sarai -Signed-off-by: Kir Kolyshkin ---- - libcontainer/rootfs_linux.go | 11 +++++++++++ - 1 file changed, 11 insertions(+) - -diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go -index d85e7321..2fda3c9d 100644 ---- a/libcontainer/rootfs_linux.go -+++ b/libcontainer/rootfs_linux.go -@@ -519,6 +519,17 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { - dstIsFile = !fi.IsDir() - } - -+ // In previous runc versions, we would tolerate nonsense paths with -+ // dangling symlinks as path components. pathrs-lite does not support -+ // this, so instead we have to emulate this behaviour by doing -+ // SecureJoin *purely to get a semi-reasonable path to use* and then we -+ // use pathrs-lite to operate on the path safely. -+ newUnsafePath, err := securejoin.SecureJoin(rootfs, unsafePath) -+ if err != nil { -+ return err -+ } -+ unsafePath = utils.StripRoot(rootfs, newUnsafePath) -+ - if dstIsFile { - dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) - } else { --- -2.51.1 - diff --git a/SPECS/runc.spec b/SPECS/runc.spec index 10290f8..75e0829 100644 --- a/SPECS/runc.spec +++ b/SPECS/runc.spec @@ -19,8 +19,8 @@ go build -buildmode pie -compiler gc -tags="rpm_crashtraceback libtrust_openssl Epoch: 4 Name: %{repo} -Version: 1.3.0 -Release: 4%{?dist} +Version: 1.4.0 +Release: 1%{?dist} Summary: CLI for running Open Containers # https://fedoraproject.org/wiki/PackagingDrafts/Go#Go_Language_Architectures #ExclusiveArch: %%{go_arches} @@ -30,9 +30,6 @@ ExcludeArch: %{ix86} License: ASL 2.0 URL: %{git0} Source0: %{git0}/archive/v%{version}.tar.gz -Patch0: 0001-1.3.0-CVEs-mega-patch.patch -Patch1: 0001-1.3-openat2-improve-resilience-on-busy-systems.patch -Patch2: 0002-1.3-rootfs-re-allow-dangling-symlinks-in-mount-targe.patch Provides: oci-runtime BuildRequires: golang >= 1.22.4 BuildRequires: git @@ -87,6 +84,14 @@ make install install-man install-bash DESTDIR=$RPM_BUILD_ROOT PREFIX=%{_prefix} %{_datadir}/bash-completion/completions/%{name} %changelog +* Thu Dec 04 2025 Jindrich Novy - 4:1.4.0-1 +- update to https://github.com/opencontainers/runc/releases/tag/v1.4.0 +- Resolves: RHEL-132800 + +* Tue Nov 11 2025 Jindrich Novy - 4:1.3.0-5 +- fix premission regression +- Related: RHEL-122400 + * Thu Nov 06 2025 Jindrich Novy - 4:1.3.0-4 - rename errors.go to errors_linux.go - Related: RHEL-122400