Skip to content

Commit cdee962

Browse files
committed
merge private security patches into ghsa-release-1.2.8
Aleksa Sarai (21): 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 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>
2 parents 2462b68 + b4cb2f5 commit cdee962

File tree

125 files changed

+9551
-1517
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+9551
-1517
lines changed

go.mod

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ toolchain go1.22.4
1010
require (
1111
github.com/checkpoint-restore/go-criu/v6 v6.3.0
1212
github.com/cilium/ebpf v0.16.0
13-
github.com/containerd/console v1.0.4
13+
github.com/containerd/console v1.0.5
1414
github.com/coreos/go-systemd/v22 v22.5.0
15-
github.com/cyphar/filepath-securejoin v0.4.1
15+
github.com/cyphar/filepath-securejoin v0.5.1
1616
github.com/docker/go-units v0.5.0
1717
github.com/godbus/dbus/v5 v5.1.0
1818
github.com/moby/sys/mountinfo v0.7.1
1919
github.com/moby/sys/user v0.3.0
2020
github.com/moby/sys/userns v0.1.0
2121
github.com/mrunalp/fileutils v0.5.1
2222
github.com/opencontainers/runtime-spec v1.2.0
23-
github.com/opencontainers/selinux v1.11.0
23+
github.com/opencontainers/selinux v1.12.0
2424
github.com/seccomp/libseccomp-golang v0.10.0
2525
github.com/sirupsen/logrus v1.9.3
2626
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
@@ -37,3 +37,8 @@ require (
3737
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
3838
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect
3939
)
40+
41+
// FIXME: This is only intended as a short-term solution to include a patch for
42+
// CVE-2025-52881 in go-selinux without pushing the patches upstream. This
43+
// should be removed as soon as possible after the embargo is lifted.
44+
replace github.com/opencontainers/selinux => ./internal/third_party/selinux

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3
33
github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI=
44
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
55
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
6-
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
7-
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
6+
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
7+
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
88
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
99
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
1010
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
1111
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
12-
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
13-
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
12+
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
13+
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
1414
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1515
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1616
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -48,8 +48,6 @@ github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm
4848
github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
4949
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
5050
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
51-
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
52-
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
5351
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5452
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5553
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=

internal/linux/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package linux provides minimal wrappers around Linux system calls, primarily
2+
// to provide support for automatic EINTR-retries.
3+
package linux

internal/linux/linux.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package linux
2+
3+
import (
4+
"os"
5+
6+
"golang.org/x/sys/unix"
7+
)
8+
9+
// Readlinkat wraps [unix.Readlinkat].
10+
func Readlinkat(dir *os.File, path string) (string, error) {
11+
size := 4096
12+
for {
13+
linkBuf := make([]byte, size)
14+
n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf)
15+
if err != nil {
16+
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
17+
}
18+
if n != size {
19+
return string(linkBuf[:n]), nil
20+
}
21+
// Possible truncation, resize the buffer.
22+
size *= 2
23+
}
24+
}
25+
26+
// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER).
27+
func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) {
28+
// Make sure O_NOCTTY is always set -- otherwise runc might accidentally
29+
// gain it as a controlling terminal. O_CLOEXEC also needs to be set to
30+
// make sure we don't leak the handle either.
31+
flags |= unix.O_NOCTTY | unix.O_CLOEXEC
32+
33+
// There is no nice wrapper for this kind of ioctl in unix.
34+
peerFd, _, errno := unix.Syscall(
35+
unix.SYS_IOCTL,
36+
ptyFd,
37+
uintptr(unix.TIOCGPTPEER),
38+
uintptr(flags),
39+
)
40+
if errno != 0 {
41+
return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno)
42+
}
43+
return os.NewFile(peerFd, unsafePeerPath), nil
44+
}

internal/pathrs/doc.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
4+
* Copyright (C) 2024-2025 SUSE LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
// Package pathrs provides wrappers around filepath-securejoin to add the
20+
// minimum set of features needed from libpathrs that are not provided by
21+
// filepath-securejoin, with the eventual goal being that these can be used to
22+
// ease the transition by converting them stubs when enabling libpathrs builds.
23+
package pathrs
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
4+
* Copyright (C) 2024-2025 SUSE LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package pathrs
20+
21+
import (
22+
"fmt"
23+
"os"
24+
"path/filepath"
25+
26+
"github.com/cyphar/filepath-securejoin/pathrs-lite"
27+
"github.com/sirupsen/logrus"
28+
"golang.org/x/sys/unix"
29+
)
30+
31+
// MkdirAllInRootOpen attempts to make
32+
//
33+
// path, _ := securejoin.SecureJoin(root, unsafePath)
34+
// os.MkdirAll(path, mode)
35+
// os.Open(path)
36+
//
37+
// safer against attacks where components in the path are changed between
38+
// SecureJoin returning and MkdirAll (or Open) being called. In particular, we
39+
// try to detect any symlink components in the path while we are doing the
40+
// MkdirAll.
41+
//
42+
// NOTE: If unsafePath is a subpath of root, we assume that you have already
43+
// called SecureJoin and so we use the provided path verbatim without resolving
44+
// any symlinks (this is done in a way that avoids symlink-exchange races).
45+
// This means that the path also must not contain ".." elements, otherwise an
46+
// error will occur.
47+
//
48+
// This uses (pathrs-lite).MkdirAllHandle under the hood, but it has special
49+
// handling if unsafePath has already been scoped within the rootfs (this is
50+
// needed for a lot of runc callers and fixing this would require reworking a
51+
// lot of path logic).
52+
func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) {
53+
// If the path is already "within" the root, get the path relative to the
54+
// root and use that as the unsafe path. This is necessary because a lot of
55+
// MkdirAllInRootOpen callers have already done SecureJoin, and refactoring
56+
// all of them to stop using these SecureJoin'd paths would require a fair
57+
// amount of work.
58+
// TODO(cyphar): Do the refactor to libpathrs once it's ready.
59+
if IsLexicallyInRoot(root, unsafePath) {
60+
subPath, err := filepath.Rel(root, unsafePath)
61+
if err != nil {
62+
return nil, err
63+
}
64+
unsafePath = subPath
65+
}
66+
67+
// Check for any silly mode bits.
68+
if mode&^0o7777 != 0 {
69+
return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode)
70+
}
71+
// Linux (and thus os.MkdirAll) silently ignores the suid and sgid bits if
72+
// passed. While it would make sense to return an error in that case (since
73+
// the user has asked for a mode that won't be applied), for compatibility
74+
// reasons we have to ignore these bits.
75+
if ignoredBits := mode &^ 0o1777; ignoredBits != 0 {
76+
logrus.Warnf("MkdirAll called with no-op mode bits that are ignored by Linux: 0o%.3o", ignoredBits)
77+
mode &= 0o1777
78+
}
79+
80+
rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
81+
if err != nil {
82+
return nil, fmt.Errorf("open root handle: %w", err)
83+
}
84+
defer rootDir.Close()
85+
86+
return retryEAGAIN(func() (*os.File, error) {
87+
return pathrs.MkdirAllHandle(rootDir, unsafePath, mode)
88+
})
89+
}
90+
91+
// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the
92+
// returned handle, for callers that don't need to use it.
93+
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error {
94+
f, err := MkdirAllInRootOpen(root, unsafePath, mode)
95+
if err == nil {
96+
_ = f.Close()
97+
}
98+
return err
99+
}

internal/pathrs/path.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
4+
* Copyright (C) 2024-2025 SUSE LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package pathrs
20+
21+
import (
22+
"strings"
23+
)
24+
25+
// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"),
26+
// but properly handling the case where path or root have a "/" suffix.
27+
//
28+
// NOTE: The return value only make sense if the path is already mostly cleaned
29+
// (i.e., doesn't contain "..", ".", nor unneeded "/"s).
30+
func IsLexicallyInRoot(root, path string) bool {
31+
root = strings.TrimRight(root, "/")
32+
path = strings.TrimRight(path, "/")
33+
return strings.HasPrefix(path+"/", root+"/")
34+
}

internal/pathrs/path_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
4+
* Copyright (C) 2024-2025 SUSE LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package pathrs
20+
21+
import "testing"
22+
23+
func TestIsLexicallyInRoot(t *testing.T) {
24+
for _, test := range []struct {
25+
name string
26+
root, path string
27+
expected bool
28+
}{
29+
{"Equal1", "/foo", "/foo", true},
30+
{"Equal2", "/bar/baz", "/bar/baz", true},
31+
{"Equal3", "/bar/baz/", "/bar/baz/", true},
32+
{"Root", "/", "/foo/bar", true},
33+
{"Root-Equal", "/", "/", true},
34+
{"InRoot-Basic1", "/foo/bar", "/foo/bar/baz/abcd", true},
35+
{"InRoot-Basic2", "/a/b/c/d", "/a/b/c/d/e/f/g/h", true},
36+
{"InRoot-Long", "/var/lib/docker/container/1234abcde/rootfs", "/var/lib/docker/container/1234abcde/rootfs/a/b/c", true},
37+
{"InRoot-TrailingSlash1", "/foo/bar/", "/foo/bar", true},
38+
{"InRoot-TrailingSlash2", "/foo/", "/foo/bar/baz/boop", true},
39+
{"NotInRoot-Basic1", "/foo", "/bar", false},
40+
{"NotInRoot-Basic2", "/foo", "/bar", false},
41+
{"NotInRoot-Basic3", "/foo/bar/baz", "/foo/boo/baz/abc", false},
42+
{"NotInRoot-Long", "/var/lib/docker/container/1234abcde/rootfs", "/a/b/c", false},
43+
{"NotInRoot-Tricky1", "/foo/bar", "/foo/bara", false},
44+
{"NotInRoot-Tricky2", "/foo/bar", "/foo/ba/r", false},
45+
} {
46+
t.Run(test.name, func(t *testing.T) {
47+
got := IsLexicallyInRoot(test.root, test.path)
48+
if test.expected != got {
49+
t.Errorf("IsLexicallyInRoot(%q, %q) = %v (expected %v)", test.root, test.path, got, test.expected)
50+
}
51+
})
52+
}
53+
}

0 commit comments

Comments
 (0)