Skip to content

Commit 2f74f4a

Browse files
committed
merge private security patches into opencontainers/runc:main
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 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/[email protected] 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 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 <[email protected]> Reported-by: Li Fubang <[email protected]> Reported-by: Tõnis Tiigi <[email protected]> Reported-by: Aleksa Sarai <[email protected]> Signed-off-by: Aleksa Sarai <[email protected]>
2 parents fb01482 + 3f92552 commit 2f74f4a

File tree

111 files changed

+9452
-1338
lines changed

Some content is hidden

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

111 files changed

+9452
-1338
lines changed

.github/workflows/validate.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,12 @@ jobs:
152152
- name: no toolchain in go.mod # See https://github.com/opencontainers/runc/pull/4717, https://github.com/dependabot/dependabot-core/issues/11933.
153153
run: |
154154
if grep -q '^toolchain ' go.mod; then echo "Error: go.mod must not have toolchain directive, please fix"; exit 1; fi
155-
- name: no exclude nor replace in go.mod
156-
run: |
157-
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
155+
# FIXME: This check needed to be disabled for the go-selinux patch addded
156+
# when patching CVE-2025-52881. This needs to be removed as soon as
157+
# the embargo is lifted, along with the replace directive in go.mod.
158+
#- name: no exclude nor replace in go.mod
159+
# run: |
160+
# 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
158161

159162

160163
commit:

.golangci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ formatters:
1111
linters:
1212
enable:
1313
- errorlint
14+
- forbidigo
1415
- nolintlint
1516
- unconvert
1617
- unparam
@@ -25,6 +26,20 @@ linters:
2526
- -ST1003 # https://staticcheck.dev/docs/checks/#ST1003 Poorly chosen identifier.
2627
- -ST1005 # https://staticcheck.dev/docs/checks/#ST1005 Incorrectly formatted error string.
2728
- -QF1008 # https://staticcheck.dev/docs/checks/#QF1008 Omit embedded fields from selector expression.
29+
forbidigo:
30+
forbid:
31+
# os.Create implies O_TRUNC without O_CREAT|O_EXCL, which can lead to
32+
# an even more severe attacks than CVE-2024-45310, where host files
33+
# could be wiped. Always use O_EXCL or otherwise ensure we are not
34+
# going to be tricked into overwriting host files.
35+
- pattern: ^os\.Create$
36+
pkg: ^os$
37+
analyze-types: true
2838
exclusions:
39+
rules:
40+
# forbidigo lints are only relevant for main code.
41+
- path: '(.+)_test\.go'
42+
linters:
43+
- forbidigo
2944
presets:
3045
- std-error-handling

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/checkpoint-restore/go-criu/v7 v7.2.0
77
github.com/containerd/console v1.0.5
88
github.com/coreos/go-systemd/v22 v22.6.0
9-
github.com/cyphar/filepath-securejoin v0.4.1
9+
github.com/cyphar/filepath-securejoin v0.5.1
1010
github.com/docker/go-units v0.5.0
1111
github.com/godbus/dbus/v5 v5.1.0
1212
github.com/moby/sys/capability v0.4.0
@@ -32,3 +32,8 @@ require (
3232
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
3333
github.com/russross/blackfriday/v2 v2.1.0 // indirect
3434
)
35+
36+
// FIXME: This is only intended as a short-term solution to include a patch for
37+
// CVE-2025-52881 in go-selinux without pushing the patches upstream. This
38+
// should be removed as soon as possible after the embargo is lifted.
39+
replace github.com/opencontainers/selinux => ./internal/third_party/selinux

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z
99
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
1010
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
1111
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/opencontainers/cgroups v0.0.5 h1:DRITAqcOnY0uSBzIpt1RYWLjh5DPDiqUs4fY
4848
github.com/opencontainers/cgroups v0.0.5/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs=
4949
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0 h1:RLn0YfUWkiqPGtgUANvJrcjIkCHGRl3jcz/c557M28M=
5050
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
51-
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
52-
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
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/linux.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,42 @@ func SetMempolicy(mode uint, mask *unix.CPUSet) error {
8585
})
8686
return os.NewSyscallError("set_mempolicy", err)
8787
}
88+
89+
// Readlinkat wraps [unix.Readlinkat].
90+
func Readlinkat(dir *os.File, path string) (string, error) {
91+
size := 4096
92+
for {
93+
linkBuf := make([]byte, size)
94+
n, err := retryOnEINTR2(func() (int, error) {
95+
return unix.Readlinkat(int(dir.Fd()), path, linkBuf)
96+
})
97+
if err != nil {
98+
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
99+
}
100+
if n != size {
101+
return string(linkBuf[:n]), nil
102+
}
103+
// Possible truncation, resize the buffer.
104+
size *= 2
105+
}
106+
}
107+
108+
// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER).
109+
func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) {
110+
// Make sure O_NOCTTY is always set -- otherwise runc might accidentally
111+
// gain it as a controlling terminal. O_CLOEXEC also needs to be set to
112+
// make sure we don't leak the handle either.
113+
flags |= unix.O_NOCTTY | unix.O_CLOEXEC
114+
115+
// There is no nice wrapper for this kind of ioctl in unix.
116+
peerFd, _, errno := unix.Syscall(
117+
unix.SYS_IOCTL,
118+
ptyFd,
119+
uintptr(unix.TIOCGPTPEER),
120+
uintptr(flags),
121+
)
122+
if errno != 0 {
123+
return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno)
124+
}
125+
return os.NewFile(peerFd, unsafePeerPath), nil
126+
}

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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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)