Skip to content

Commit 4650a9e

Browse files
committed
Reopen cgroupRootHandle if it was unexpectedly opened to another file
Signed-off-by: Bin Tang <[email protected]>
1 parent 6a793b6 commit 4650a9e

File tree

2 files changed

+84
-3
lines changed

2 files changed

+84
-3
lines changed

file.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ var (
101101

102102
cgroupRootHandle *os.File
103103
prepOnce sync.Once
104+
resetOnce sync.Once
104105
prepErr error
105106
resolveFlags uint64
106107
)
@@ -134,6 +135,9 @@ func prepareOpenat2() error {
134135
// cgroupv2 has a single mountpoint and no "cpu,cpuacct" symlinks
135136
resolveFlags |= unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_SYMLINKS
136137
}
138+
139+
// Make resetOnce ready for the next time to reset prepOnce.
140+
resetOnce = sync.Once{}
137141
})
138142

139143
return prepErr
@@ -170,11 +174,14 @@ func openFile(dir, file string, flags int) (*os.File, error) {
170174
// (happens when this package is incorrectly used
171175
// across the chroot/pivot_root/mntns boundary, or
172176
// when /sys/fs/cgroup is remounted).
173-
//
174-
// TODO: if such usage will ever be common, amend this
175-
// to reopen cgroupRootHandle and retry openat2.
176177
fdDest, fdErr := os.Readlink("/proc/thread-self/fd/" + strconv.Itoa(int(cgroupRootHandle.Fd())))
177178
if fdErr == nil && fdDest != cgroupfsDir {
179+
// Only reset prepOnce once when the cgroupRootHandle is opened to another location.
180+
resetOnce.Do(func() {
181+
// It is difficult to recover the cgroupRootHandle in this case,
182+
// so we reset prepOnce to reopen the cgroupRootHandle and retry openat2.
183+
prepOnce = sync.Once{}
184+
})
178185
// Wrap the error so it is clear that cgroupRootHandle
179186
// is opened to an unexpected/wrong directory.
180187
err = fmt.Errorf("cgroupRootHandle %d unexpectedly opened to %s != %s: %w",

file_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import (
66
"os"
77
"path/filepath"
88
"strconv"
9+
"strings"
10+
"sync"
11+
"syscall"
912
"testing"
1013
"time"
14+
15+
"golang.org/x/sys/unix"
1116
)
1217

1318
func TestWriteCgroupFileHandlesInterrupt(t *testing.T) {
@@ -70,6 +75,75 @@ func TestOpenat2(t *testing.T) {
7075
}
7176
}
7277

78+
func TestCgroupRootHandleOpenedToAnotherFile(t *testing.T) {
79+
const (
80+
memoryCgroupMount = "/sys/fs/cgroup/memory"
81+
memoryLimit = "memory.limit_in_bytes"
82+
)
83+
if _, err := os.Stat(memoryCgroupMount); err != nil {
84+
// most probably cgroupv2
85+
t.Skip(err)
86+
}
87+
88+
cgroupName := fmt.Sprintf("test-eano-%d", time.Now().Nanosecond())
89+
cgroupPath := filepath.Join(memoryCgroupMount, cgroupName)
90+
if err := os.MkdirAll(cgroupPath, 0o755); err != nil {
91+
t.Fatal(err)
92+
}
93+
defer os.RemoveAll(cgroupPath)
94+
95+
if _, err := os.Stat(filepath.Join(cgroupPath, memoryLimit)); err != nil {
96+
// either cgroupv2, or memory controller is not available
97+
t.Skip(err)
98+
}
99+
100+
// The cgroupRootHandle is opened when the openFile is called.
101+
if _, err := openFile(cgroupfsDir, filepath.Join("memory", cgroupName, memoryLimit), os.O_RDONLY); err != nil {
102+
t.Fatal(err)
103+
}
104+
105+
// Make sure the cgroupRootHandle is opened to another file.
106+
if err := syscall.Close(int(cgroupRootHandle.Fd())); err != nil {
107+
t.Fatal(err)
108+
}
109+
if _, err := unix.Openat2(-1, "/tmp", &unix.OpenHow{Flags: unix.O_DIRECTORY | unix.O_PATH | unix.O_CLOEXEC}); err != nil {
110+
t.Fatal(err)
111+
}
112+
113+
var readErr *error
114+
readErrLock := sync.Mutex{}
115+
errCount := 0
116+
117+
// The openFile returns error (may be multiple times) and the prepOnce is reset only once.
118+
var wg sync.WaitGroup
119+
for i := 0; i < 100; i++ {
120+
wg.Add(1)
121+
go func(i int) {
122+
defer wg.Done()
123+
if _, err := openFile(cgroupfsDir, filepath.Join("memory", cgroupName, memoryLimit), os.O_RDONLY); err != nil {
124+
readErrLock.Lock()
125+
readErr = &err
126+
errCount++
127+
readErrLock.Unlock()
128+
}
129+
}(i)
130+
}
131+
wg.Wait()
132+
133+
if errCount == 0 {
134+
t.Fatal("At least one openFile should fail")
135+
}
136+
137+
if !strings.Contains((*readErr).Error(), "unexpectedly opened to") {
138+
t.Fatalf("openFile should fail with 'cgroupRootHandle %d unexpectedly opened to <another file>'", cgroupRootHandle.Fd())
139+
}
140+
141+
// The openFile should work after prepOnce is reset because the cgroupRootHandle is updated.
142+
if _, err := openFile(cgroupfsDir, filepath.Join("memory", cgroupName, memoryLimit), os.O_RDONLY); err != nil {
143+
t.Fatal(err)
144+
}
145+
}
146+
73147
func BenchmarkWriteFile(b *testing.B) {
74148
TestMode = true
75149
defer func() { TestMode = false }()

0 commit comments

Comments
 (0)