Skip to content

Commit 7fcfb3f

Browse files
authored
Merge pull request opencontainers#3030 from kolyshkin/openat2-improve
libct/cg/OpenFile: fix/improve openat2 handling
2 parents bd75bc2 + bd50e7c commit 7fcfb3f

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

libcontainer/cgroups/file.go

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"path"
9+
"strconv"
810
"strings"
911
"sync"
1012

@@ -13,7 +15,11 @@ import (
1315
)
1416

1517
// OpenFile opens a cgroup file in a given dir with given flags.
16-
// It is supposed to be used for cgroup files only.
18+
// It is supposed to be used for cgroup files only, and returns
19+
// an error if the file is not a cgroup file.
20+
//
21+
// Arguments dir and file are joined together to form an absolute path
22+
// to a file being opened.
1723
func OpenFile(dir, file string, flags int) (*os.File, error) {
1824
if dir == "" {
1925
return nil, fmt.Errorf("no directory specified for %s", file)
@@ -109,43 +115,59 @@ func prepareOpenat2() error {
109115
return prepErr
110116
}
111117

112-
// OpenFile opens a cgroup file in a given dir with given flags.
113-
// It is supposed to be used for cgroup files only.
114118
func openFile(dir, file string, flags int) (*os.File, error) {
115119
mode := os.FileMode(0)
116120
if TestMode && flags&os.O_WRONLY != 0 {
117121
// "emulate" cgroup fs for unit tests
118122
flags |= os.O_TRUNC | os.O_CREATE
119123
mode = 0o600
120124
}
125+
path := path.Join(dir, file)
121126
if prepareOpenat2() != nil {
122-
return openFallback(dir, file, flags, mode)
127+
return openFallback(path, flags, mode)
123128
}
124-
reldir := strings.TrimPrefix(dir, cgroupfsPrefix)
125-
if len(reldir) == len(dir) { // non-standard path, old system?
126-
return openFallback(dir, file, flags, mode)
129+
relPath := strings.TrimPrefix(path, cgroupfsPrefix)
130+
if len(relPath) == len(path) { // non-standard path, old system?
131+
return openFallback(path, flags, mode)
127132
}
128133

129-
relname := reldir + "/" + file
130-
fd, err := unix.Openat2(cgroupFd, relname,
134+
fd, err := unix.Openat2(cgroupFd, relPath,
131135
&unix.OpenHow{
132136
Resolve: resolveFlags,
133137
Flags: uint64(flags) | unix.O_CLOEXEC,
134138
Mode: uint64(mode),
135139
})
136140
if err != nil {
137-
return nil, &os.PathError{Op: "openat2", Path: dir + "/" + file, Err: err}
141+
err = &os.PathError{Op: "openat2", Path: path, Err: err}
142+
// Check if cgroupFd is still opened to cgroupfsDir
143+
// (happens when this package is incorrectly used
144+
// across the chroot/pivot_root/mntns boundary, or
145+
// when /sys/fs/cgroup is remounted).
146+
//
147+
// TODO: if such usage will ever be common, amend this
148+
// to reopen cgroupFd and retry openat2.
149+
fdStr := strconv.Itoa(cgroupFd)
150+
fdDest, _ := os.Readlink("/proc/self/fd/" + fdStr)
151+
if fdDest != cgroupfsDir {
152+
// Wrap the error so it is clear that cgroupFd
153+
// is opened to an unexpected/wrong directory.
154+
err = fmt.Errorf("cgroupFd %s unexpectedly opened to %s != %s: %w",
155+
fdStr, fdDest, cgroupfsDir, err)
156+
}
157+
return nil, err
138158
}
139159

140-
return os.NewFile(uintptr(fd), cgroupfsPrefix+relname), nil
160+
return os.NewFile(uintptr(fd), path), nil
141161
}
142162

143163
var errNotCgroupfs = errors.New("not a cgroup file")
144164

145-
// openFallback is used when openat2(2) is not available. It checks the opened
165+
// Can be changed by unit tests.
166+
var openFallback = openAndCheck
167+
168+
// openAndCheck is used when openat2(2) is not available. It checks the opened
146169
// file is on cgroupfs, returning an error otherwise.
147-
func openFallback(dir, file string, flags int, mode os.FileMode) (*os.File, error) {
148-
path := dir + "/" + file
170+
func openAndCheck(path string, flags int, mode os.FileMode) (*os.File, error) {
149171
fd, err := os.OpenFile(path, flags, mode)
150172
if err != nil {
151173
return nil, err

libcontainer/cgroups/file_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cgroups
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -38,3 +39,35 @@ func TestWriteCgroupFileHandlesInterrupt(t *testing.T) {
3839
}
3940
}
4041
}
42+
43+
func TestOpenat2(t *testing.T) {
44+
if !IsCgroup2UnifiedMode() {
45+
// The reason is many test cases below test opening files from
46+
// the top-level directory, where cgroup v1 has no files.
47+
t.Skip("test requires cgroup v2")
48+
}
49+
50+
// Make sure we test openat2, not its fallback.
51+
openFallback = func(_ string, _ int, _ os.FileMode) (*os.File, error) {
52+
return nil, errors.New("fallback")
53+
}
54+
defer func() { openFallback = openAndCheck }()
55+
56+
for _, tc := range []struct{ dir, file string }{
57+
{"/sys/fs/cgroup", "cgroup.controllers"},
58+
{"/sys/fs/cgroup", "/cgroup.controllers"},
59+
{"/sys/fs/cgroup/", "cgroup.controllers"},
60+
{"/sys/fs/cgroup/", "/cgroup.controllers"},
61+
{"/sys/fs/cgroup/user.slice", "cgroup.controllers"},
62+
{"/sys/fs/cgroup/user.slice/", "/cgroup.controllers"},
63+
{"/", "/sys/fs/cgroup/cgroup.controllers"},
64+
{"/", "sys/fs/cgroup/cgroup.controllers"},
65+
{"/sys/fs/cgroup/cgroup.controllers", ""},
66+
} {
67+
fd, err := OpenFile(tc.dir, tc.file, os.O_RDONLY)
68+
if err != nil {
69+
t.Errorf("case %+v: %v", tc, err)
70+
}
71+
fd.Close()
72+
}
73+
}

0 commit comments

Comments
 (0)