mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-30 16:46:27 +00:00
129 lines
3.8 KiB
Go
129 lines
3.8 KiB
Go
|
//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"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
|
||
|
"golang.org/x/sys/unix"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
hasOpenat2Bool bool
|
||
|
hasOpenat2Once sync.Once
|
||
|
|
||
|
testingForceHasOpenat2 *bool
|
||
|
)
|
||
|
|
||
|
func hasOpenat2() bool {
|
||
|
if testing.Testing() && testingForceHasOpenat2 != nil {
|
||
|
return *testingForceHasOpenat2
|
||
|
}
|
||
|
hasOpenat2Once.Do(func() {
|
||
|
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 {
|
||
|
hasOpenat2Bool = true
|
||
|
_ = unix.Close(fd)
|
||
|
}
|
||
|
})
|
||
|
return hasOpenat2Bool
|
||
|
}
|
||
|
|
||
|
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}
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
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:], nil
|
||
|
}
|
||
|
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, '/')
|
||
|
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, nil
|
||
|
}
|