From 3bdc25e8f7c47de6947e8b3d0bb8f20385543ddf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:39:06 +0000 Subject: [PATCH] Bump github.com/opencontainers/runc from 1.3.2 to 1.3.3 Bumps [github.com/opencontainers/runc](https://github.com/opencontainers/runc) from 1.3.2 to 1.3.3. - [Release notes](https://github.com/opencontainers/runc/releases) - [Changelog](https://github.com/opencontainers/runc/blob/v1.3.3/CHANGELOG.md) - [Commits](https://github.com/opencontainers/runc/compare/v1.3.2...v1.3.3) --- updated-dependencies: - dependency-name: github.com/opencontainers/runc dependency-version: 1.3.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 4 +- go.sum | 12 +- .../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 +- .../runc/internal/pathrs/doc.go | 23 ++++ .../internal/pathrs/mkdirall_pathrslite.go | 99 ++++++++++++++ .../runc/internal/pathrs/path.go | 34 +++++ .../runc/internal/pathrs/procfs_pathrslite.go | 108 +++++++++++++++ .../runc/internal/pathrs/retry.go | 66 +++++++++ .../runc/internal/pathrs/root_pathrslite.go | 72 ++++++++++ .../exeseal/cloned_binary_linux.go | 9 +- .../runc/libcontainer/system/linux.go | 20 +++ .../runc/libcontainer/system/proc.go | 16 ++- .../runc/libcontainer/utils/utils.go | 6 +- .../runc/libcontainer/utils/utils_unix.go | 127 +++--------------- vendor/modules.txt | 5 +- 18 files changed, 525 insertions(+), 139 deletions(-) rename vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/{errors.go => errors_linux.go} (70%) create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/doc.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/path.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/procfs_pathrslite.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/retry.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go diff --git a/go.mod b/go.mod index 74f1a7823..6498da9ca 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ toolchain go1.24.4 require ( github.com/NVIDIA/go-nvlib v0.8.1 github.com/NVIDIA/go-nvml v0.13.0-1 - github.com/cyphar/filepath-securejoin v0.5.0 + github.com/cyphar/filepath-securejoin v0.5.1 github.com/moby/sys/reexec v0.1.0 github.com/moby/sys/symlink v0.3.0 - github.com/opencontainers/runc v1.3.2 + github.com/opencontainers/runc v1.3.3 github.com/opencontainers/runtime-spec v1.2.1 github.com/pelletier/go-toml v1.9.5 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 8daeef0b1..73fee010d 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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= @@ -36,16 +36,16 @@ github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHu github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/opencontainers/runc v1.3.2 h1:GUwgo0Fx9M/pl2utaSYlJfdBcXAB/CZXDxe322lvJ3Y= -github.com/opencontainers/runc v1.3.2/go.mod h1:F7UQQEsxcjUNnFpT1qPLHZBKYP7yWwk6hq8suLy9cl0= +github.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lkFQr4= +github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI= github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 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/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -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/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= +github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= diff --git a/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md b/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md index 6862467c2..3faee0bc5 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 8f0916f76..4b9fcbec1 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 c26e440e9..d0b200f4f 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 230530835..3e937fe3c 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/github.com/opencontainers/runc/internal/pathrs/doc.go b/vendor/github.com/opencontainers/runc/internal/pathrs/doc.go new file mode 100644 index 000000000..496ca5951 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/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/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go new file mode 100644 index 000000000..a9a0157c6 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go @@ -0,0 +1,99 @@ +// 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 retryEAGAIN(func() (*os.File, error) { + 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/vendor/github.com/opencontainers/runc/internal/pathrs/path.go b/vendor/github.com/opencontainers/runc/internal/pathrs/path.go new file mode 100644 index 000000000..1ee7c795d --- /dev/null +++ b/vendor/github.com/opencontainers/runc/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/vendor/github.com/opencontainers/runc/internal/pathrs/procfs_pathrslite.go b/vendor/github.com/opencontainers/runc/internal/pathrs/procfs_pathrslite.go new file mode 100644 index 000000000..37450a0ec --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/procfs_pathrslite.go @@ -0,0 +1,108 @@ +// 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 := retryEAGAIN(func() (*os.File, error) { + return openFn(subpath) + }) + if err != nil { + return nil, err + } + defer handle.Close() + + f, err := 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 := retryEAGAIN(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 := retryEAGAIN(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 := retryEAGAIN(procfs.OpenProcRoot) + if err != nil { + return nil, nil, err + } + defer proc.Close() + + handle, closer, err := retryEAGAIN2(func() (*os.File, procfs.ProcThreadSelfCloser, error) { + return proc.OpenThreadSelf(subpath) + }) + if err != nil { + return nil, nil, err + } + if closer != nil { + defer func() { + if Err != nil { + closer() + } + }() + } + defer handle.Close() + + f, err := 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 retryEAGAIN(func() (*os.File, error) { + return pathrs.Reopen(file, flags) + }) +} diff --git a/vendor/github.com/opencontainers/runc/internal/pathrs/retry.go b/vendor/github.com/opencontainers/runc/internal/pathrs/retry.go new file mode 100644 index 000000000..a51d335c0 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/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/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go b/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go new file mode 100644 index 000000000..899af2703 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go @@ -0,0 +1,72 @@ +// 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 := retryEAGAIN(func() (*os.File, error) { + return pathrs.OpenInRoot(root, subpath) + }) + if err != nil { + return nil, err + } + defer handle.Close() + + return 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/vendor/github.com/opencontainers/runc/libcontainer/exeseal/cloned_binary_linux.go b/vendor/github.com/opencontainers/runc/libcontainer/exeseal/cloned_binary_linux.go index 3bafc96a6..146408f20 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/exeseal/cloned_binary_linux.go +++ b/vendor/github.com/opencontainers/runc/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) } @@ -125,7 +126,7 @@ func getSealableFile(comment, tmpDir string) (file *os.File, sealFn SealFunc, er // First, try an executable memfd (supported since Linux 3.17). file, sealFn, err = Memfd(comment) if err == nil { - return + return file, sealFn, err } logrus.Debugf("memfd cloned binary failed, falling back to O_TMPFILE: %v", err) @@ -154,7 +155,7 @@ func getSealableFile(comment, tmpDir string) (file *os.File, sealFn SealFunc, er file.Close() continue } - return + return file, sealFn, err } logrus.Debugf("O_TMPFILE cloned binary failed, falling back to mktemp(): %v", err) // Finally, try a classic unlinked temporary file. @@ -168,7 +169,7 @@ func getSealableFile(comment, tmpDir string) (file *os.File, sealFn SealFunc, er file.Close() continue } - return + return file, sealFn, err } return nil, nil, fmt.Errorf("could not create sealable file for cloned binary: %w", err) } diff --git a/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go b/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go index e8ce0ecac..5e558c4f9 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go +++ b/vendor/github.com/opencontainers/runc/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/vendor/github.com/opencontainers/runc/libcontainer/system/proc.go b/vendor/github.com/opencontainers/runc/libcontainer/system/proc.go index 774443ec9..34850dd83 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/system/proc.go +++ b/vendor/github.com/opencontainers/runc/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/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go b/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go index 23003e177..17259de98 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go +++ b/vendor/github.com/opencontainers/runc/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 { @@ -111,5 +111,5 @@ func Annotations(labels []string) (bundle string, userAnnotations map[string]str userAnnotations[name] = value } } - return + return bundle, userAnnotations } diff --git a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go b/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go index f6b3fefb1..7dbec54dc 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go +++ b/vendor/github.com/opencontainers/runc/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/vendor/modules.txt b/vendor/modules.txt index b1d5b9ae1..ae954e371 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,7 +12,7 @@ github.com/NVIDIA/go-nvml/pkg/dl github.com/NVIDIA/go-nvml/pkg/nvml github.com/NVIDIA/go-nvml/pkg/nvml/mock github.com/NVIDIA/go-nvml/pkg/nvml/mock/dgxa100 -# 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 @@ -46,8 +46,9 @@ github.com/moby/sys/reexec # github.com/moby/sys/symlink v0.3.0 ## explicit; go 1.17 github.com/moby/sys/symlink -# github.com/opencontainers/runc v1.3.2 +# github.com/opencontainers/runc v1.3.3 ## explicit; go 1.23.0 +github.com/opencontainers/runc/internal/pathrs github.com/opencontainers/runc/libcontainer/exeseal github.com/opencontainers/runc/libcontainer/system github.com/opencontainers/runc/libcontainer/utils