mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
merge private security patches into ghsa-release-1.4.0-rc.3
Aleksa Sarai (22): rootfs: re-allow dangling symlinks in mount targets openat2: improve resilience on busy systems selinux: use safe procfs API for labels rootfs: switch to fd-based handling of mountpoint targets libct/system: use securejoin for /proc/$pid/stat init: use securejoin for /proc/self/setgroups init: write sysctls using safe procfs API utils: remove unneeded EnsureProcHandle utils: use safe procfs for /proc/self/fd loop code apparmor: use safe procfs API for labels ci: add lint to forbid the usage of os.Create rootfs: avoid using os.Create for new device inodes internal: add wrappers for securejoin.Proc* go.mod: update to github.com/cyphar/filepath-securejoin@v0.5.0 console: verify /dev/pts/ptmx before use console: avoid trivial symlink attacks for /dev/console console: add fallback for pre-TIOCGPTPEER kernels console: use TIOCGPTPEER when allocating peer PTY *: switch to safer securejoin.Reopen internal: move utils.MkdirAllInRoot to internal/pathrs internal/sys: add VerifyInode helper internal: linux: add package doc-comment Li Fubang (1): libct: align param type for mountCgroupV1/V2 functions Kir Kolyshkin (3): libct: maskPaths: don't rely on ENOTDIR for mount libct: maskPaths: only ignore ENOENT on mount dest libct: add/use isDevNull, verifyDevNull Fixes: CVE-2025-31133 GHSA-9493-h29p-rfm2 Fixes: CVE-2025-52565 GHSA-qw9x-cqr3-wc7r Fixes: CVE-2025-52881 GHSA-cgrx-mc8f-2prm Reported-by: Lei Wang <ssst0n3@gmail.com> Reported-by: Li Fubang <lifubang@acmcoder.com> Reported-by: Tõnis Tiigi <tonistiigi@gmail.com> Reported-by: Aleksa Sarai <cyphar@cyphar.com> Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
@@ -152,9 +152,12 @@ jobs:
|
||||
- name: no toolchain in go.mod # See https://github.com/opencontainers/runc/pull/4717, https://github.com/dependabot/dependabot-core/issues/11933.
|
||||
run: |
|
||||
if grep -q '^toolchain ' go.mod; then echo "Error: go.mod must not have toolchain directive, please fix"; exit 1; fi
|
||||
- name: no exclude nor replace in go.mod
|
||||
run: |
|
||||
if grep -Eq '^\s*(exclude|replace) ' go.mod; then echo "Error: go.mod must not have exclude/replace directive, it breaks go install. Please fix"; exit 1; fi
|
||||
# FIXME: This check needed to be disabled for the go-selinux patch addded
|
||||
# when patching CVE-2025-52881. This needs to be removed as soon as
|
||||
# the embargo is lifted, along with the replace directive in go.mod.
|
||||
#- name: no exclude nor replace in go.mod
|
||||
# run: |
|
||||
# if grep -Eq '^\s*(exclude|replace) ' go.mod; then echo "Error: go.mod must not have exclude/replace directive, it breaks go install. Please fix"; exit 1; fi
|
||||
|
||||
|
||||
commit:
|
||||
|
||||
@@ -11,6 +11,7 @@ formatters:
|
||||
linters:
|
||||
enable:
|
||||
- errorlint
|
||||
- forbidigo
|
||||
- nolintlint
|
||||
- unconvert
|
||||
- unparam
|
||||
@@ -25,6 +26,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
|
||||
|
||||
@@ -6,7 +6,7 @@ require (
|
||||
github.com/checkpoint-restore/go-criu/v7 v7.2.0
|
||||
github.com/containerd/console v1.0.5
|
||||
github.com/coreos/go-systemd/v22 v22.6.0
|
||||
github.com/cyphar/filepath-securejoin v0.4.1
|
||||
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
|
||||
@@ -32,3 +32,8 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // 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
|
||||
|
||||
@@ -9,8 +9,8 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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.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=
|
||||
@@ -48,8 +48,6 @@ github.com/opencontainers/cgroups v0.0.5 h1:DRITAqcOnY0uSBzIpt1RYWLjh5DPDiqUs4fY
|
||||
github.com/opencontainers/cgroups v0.0.5/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs=
|
||||
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0 h1:RLn0YfUWkiqPGtgUANvJrcjIkCHGRl3jcz/c557M28M=
|
||||
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
|
||||
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package linux provides minimal wrappers around Linux system calls, primarily
|
||||
// to provide support for automatic EINTR-retries.
|
||||
package linux
|
||||
@@ -85,3 +85,42 @@ func SetMempolicy(mode uint, mask *unix.CPUSet) error {
|
||||
})
|
||||
return os.NewSyscallError("set_mempolicy", err)
|
||||
}
|
||||
|
||||
// Readlinkat wraps [unix.Readlinkat].
|
||||
func Readlinkat(dir *os.File, path string) (string, error) {
|
||||
size := 4096
|
||||
for {
|
||||
linkBuf := make([]byte, size)
|
||||
n, err := retryOnEINTR2(func() (int, error) {
|
||||
return 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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 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
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 (
|
||||
"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+"/")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 "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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/*
|
||||
* Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
* 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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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 (
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
)
|
||||
|
||||
// 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 := linux.Openat(int(dirFd.Fd()), filename, flags, fileMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(fd), root+"/"+subpath), nil
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[codespell]
|
||||
skip = ./.git,./go.sum,./go-selinux/testdata
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
build
|
||||
+44
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah
|
||||
+119
@@ -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 <joe@gmail.com>
|
||||
|
||||
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`.
|
||||
Vendored
+201
@@ -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.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Antonio Murdaca <runcom@redhat.com> (@runcom)
|
||||
Daniel J Walsh <dwalsh@redhat.com> (@rhatdan)
|
||||
Mrunal Patel <mpatel@redhat.com> (@mrunalp)
|
||||
Sebastiaan van Stijn <github@gone.nl> (@thaJeztah)
|
||||
Kirill Kolyshikin <kolyshkin@gmail.com> (@kolyshkin)
|
||||
+37
@@ -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
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# selinux
|
||||
|
||||
[](https://godoc.org/github.com/opencontainers/selinux) [](https://goreportcard.com/report/github.com/opencontainers/selinux) [](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
|
||||
+13
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
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) {
|
||||
// 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 {
|
||||
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/<username> 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package selinux
|
||||
|
||||
func attrPath(string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConThreadSelf(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func readConSelf(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func writeConThreadSelf(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConSelf(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 (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 ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -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
|
||||
)
|
||||
Vendored
+8
@@ -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=
|
||||
@@ -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.
|
||||
+131
@@ -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
|
||||
}
|
||||
@@ -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++
|
||||
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 += c
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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++
|
||||
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 += c
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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/<tid>/, 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
|
||||
}
|
||||
|
||||
+146
-22
@@ -1,40 +1,164 @@
|
||||
package libcontainer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"golang.org/x/sys/unix"
|
||||
"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
|
||||
}
|
||||
if f != nil {
|
||||
// Ensure permission bits (can be different because of umask).
|
||||
if err := f.Chmod(0o666); err != nil {
|
||||
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 <linux/major.h>
|
||||
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)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
return mount(slavePath, "/dev/console", "bind", unix.MS_BIND, "")
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 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 := linux.Open(slavePath, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
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
|
||||
}
|
||||
|
||||
// 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 <linux/major.h>
|
||||
)
|
||||
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))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pathrs.Reopen(peerHandle, flags)
|
||||
}
|
||||
|
||||
// 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 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 := linux.Dup3(fd, i, 0); err != nil {
|
||||
if err := linux.Dup3(int(peerPty.Fd()), i, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
runtime.KeepAlive(peerPty)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -569,8 +569,12 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
|
||||
if isOnTmpfs(m.Destination, mounts) {
|
||||
continue
|
||||
}
|
||||
if _, err := createMountpoint(c.config.Rootfs, mountEntry{Mount: m}); err != nil {
|
||||
return fmt.Errorf("create criu restore mountpoint for %s mount: %w", m.Destination, err)
|
||||
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
|
||||
@@ -582,13 +586,20 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1077,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+16
-11
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
"github.com/opencontainers/cgroups"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/libcontainer/capabilities"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/system"
|
||||
@@ -377,12 +379,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{
|
||||
@@ -396,7 +399,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
|
||||
}
|
||||
}
|
||||
@@ -407,7 +410,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
|
||||
@@ -469,7 +472,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
|
||||
}
|
||||
@@ -505,19 +513,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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,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"
|
||||
@@ -1683,11 +1684,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)
|
||||
@@ -1726,8 +1725,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 {
|
||||
@@ -1736,7 +1737,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
|
||||
|
||||
+294
-136
@@ -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"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
devices "github.com/opencontainers/cgroups/devices/config"
|
||||
"github.com/opencontainers/cgroups/fs2"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/internal/sys"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
@@ -44,6 +47,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.
|
||||
@@ -295,8 +299,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
|
||||
}
|
||||
@@ -327,7 +331,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 {
|
||||
@@ -366,8 +370,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)) {
|
||||
@@ -396,14 +400,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 {
|
||||
@@ -416,13 +420,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() {
|
||||
@@ -433,7 +443,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.
|
||||
@@ -495,72 +505,87 @@ 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)
|
||||
}
|
||||
|
||||
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 !errors.Is(err, unix.ENOENT) {
|
||||
return fmt.Errorf("lookup mountpoint target: %w", 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)
|
||||
// Error out if the source of a bind mount does not exist as we
|
||||
// will be unable to bind anything to it.
|
||||
return 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)
|
||||
}
|
||||
}
|
||||
// Nothing left to do.
|
||||
return dest, nil
|
||||
dstIsFile = !fi.IsDir()
|
||||
}
|
||||
|
||||
case "tmpfs":
|
||||
// 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 {
|
||||
dstFile, err = pathrs.MkdirAllInRootOpen(rootfs, unsafePath, 0o755)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("make mountpoint %q: %w", m.Destination, err)
|
||||
}
|
||||
}
|
||||
|
||||
if m.Device == "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
|
||||
|
||||
// Nothing left to do.
|
||||
return dest, nil
|
||||
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)
|
||||
}
|
||||
return dest, nil
|
||||
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)
|
||||
}
|
||||
// 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 {
|
||||
@@ -576,7 +601,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)
|
||||
}
|
||||
@@ -590,36 +615,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
|
||||
}
|
||||
|
||||
@@ -633,7 +669,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// contrast to mount(8)'s current behaviour, but is what users probably
|
||||
// expect. See <https://github.com/util-linux/util-linux/issues/2433>.
|
||||
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
|
||||
@@ -734,14 +770,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,20 +924,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()
|
||||
if err := unix.Fstat(int(file.Fd()), &devNullStat); err != nil {
|
||||
return &os.PathError{Op: "fstat", Path: file.Name(), Err: err}
|
||||
if err := verifyDevNull(file); err != nil {
|
||||
return fmt.Errorf("can't reopen /dev/null: %w", err)
|
||||
}
|
||||
for fd := range 3 {
|
||||
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 := linux.Dup3(int(file.Fd()), fd, 0); err != nil {
|
||||
return err
|
||||
@@ -930,16 +966,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
|
||||
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)
|
||||
}
|
||||
if f != nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
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, "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -949,31 +984,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:
|
||||
@@ -989,14 +1026,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.
|
||||
@@ -1250,31 +1317,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))
|
||||
}
|
||||
return err
|
||||
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 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
|
||||
@@ -1287,19 +1434,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
|
||||
@@ -1312,11 +1470,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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"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/apparmor"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/keys"
|
||||
@@ -131,20 +133,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 {
|
||||
@@ -259,19 +258,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 := linux.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 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
|
||||
@@ -280,6 +277,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 {
|
||||
|
||||
@@ -160,3 +160,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,28 +9,16 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"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
|
||||
@@ -60,19 +48,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
|
||||
@@ -171,8 +153,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)
|
||||
}
|
||||
@@ -181,7 +163,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)
|
||||
}
|
||||
@@ -191,13 +173,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 (
|
||||
@@ -269,88 +262,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
|
||||
|
||||
+7
-2
@@ -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"
|
||||
@@ -241,10 +242,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)
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
# 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
|
||||
+150
-3
@@ -4,7 +4,152 @@ 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 ##
|
||||
|
||||
> 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 ##
|
||||
|
||||
@@ -173,7 +318,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 +383,9 @@ 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 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
|
||||
[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6
|
||||
|
||||
+447
@@ -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.
|
||||
```
|
||||
Generated
Vendored
+373
@@ -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.
|
||||
+18
-3
@@ -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 ###
|
||||
### <a name="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
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.4.1
|
||||
0.5.1
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
# 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
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
)
|
||||
+21
-13
@@ -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
|
||||
|
||||
-32
@@ -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)
|
||||
}
|
||||
-124
@@ -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
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
+13
-10
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
-127
@@ -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
|
||||
}
|
||||
-59
@@ -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
|
||||
}
|
||||
}
|
||||
+33
@@ -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 <cyphar@cyphar.com>
|
||||
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
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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...))
|
||||
}
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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"
|
||||
|
||||
"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 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 error = xdevErrorish{"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")
|
||||
)
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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} }
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 {
|
||||
fd, err := unix.Openat2(dirFd, path, how)
|
||||
if err != nil {
|
||||
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.
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return os.NewFile(uintptr(fd), fullPath), nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+10
@@ -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.
|
||||
Generated
Vendored
+13
@@ -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
|
||||
Generated
Vendored
+4
-3
@@ -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)
|
||||
}
|
||||
+5
-3
@@ -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,
|
||||
Generated
Vendored
+53
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+187
@@ -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
|
||||
}
|
||||
Generated
Vendored
+123
@@ -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
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
|
||||
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
|
||||
})
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
})
|
||||
Generated
Vendored
+544
@@ -0,0 +1,544 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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/<subpath>").
|
||||
ProcRoot procfsBase = "/proc"
|
||||
// ProcSelf refers to the current process' subdirectory (i.e.,
|
||||
// "/proc/self/<subpath>").
|
||||
ProcSelf procfsBase = "/proc/self"
|
||||
// ProcThreadSelf refers to the current thread's subdirectory (i.e.,
|
||||
// "/proc/thread-self/<subpath>"). 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/<base>/<subpath>". 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/<subpath>" (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/<subpath>.
|
||||
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/<subpath>.
|
||||
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/<subpath> (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/<base>/<subpath>" 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/<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 }
|
||||
Generated
Vendored
+222
@@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// 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
|
||||
// <https://github.com/opensuse/libpathrs/blob/v0.1.3/src/resolvers/procfs.rs>.
|
||||
// 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user