mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
a41366e740
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. Tested-by: Phil Estes <estesp@gmail.com> Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
/*
|
|
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
* 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
|
|
}
|