Skip to content

Commit 243957d

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

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-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: 76 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,77 @@ 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+
_, err := openFile(cgroupfsDir, filepath.Join("memory", cgroupName, memoryLimit), os.O_RDONLY)
124+
t.Logf("openFile attempt %d: %v\n", i, err)
125+
if err != nil {
126+
readErrLock.Lock()
127+
readErr = &err
128+
errCount++
129+
readErrLock.Unlock()
130+
}
131+
}(i)
132+
}
133+
wg.Wait()
134+
135+
if errCount == 0 {
136+
t.Fatal("At least one openFile should fail")
137+
}
138+
139+
if !strings.Contains((*readErr).Error(), "unexpectedly opened to") {
140+
t.Fatalf("openFile should fail with 'cgroupRootHandle %d unexpectedly opened to <another file>'", cgroupRootHandle.Fd())
141+
}
142+
143+
// The openFile should work after prepOnce is reset because the cgroupRootHandle is updated.
144+
if _, err := openFile(cgroupfsDir, filepath.Join("memory", cgroupName, memoryLimit), os.O_RDONLY); err != nil {
145+
t.Fatal(err)
146+
}
147+
}
148+
73149
func BenchmarkWriteFile(b *testing.B) {
74150
TestMode = true
75151
defer func() { TestMode = false }()

0 commit comments

Comments
 (0)