Skip to content

Commit ad4e8f8

Browse files
committed
pkg/cdi: add test case for EMFILE crash (avoidance).
Signed-off-by: Krisztian Litkey <[email protected]>
1 parent 039c460 commit ad4e8f8

File tree

1 file changed

+142
-0
lines changed

1 file changed

+142
-0
lines changed

pkg/cdi/cache_linux_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copyright © The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"strings"
24+
"syscall"
25+
"testing"
26+
27+
oci "github.com/opencontainers/runtime-spec/specs-go"
28+
"github.com/stretchr/testify/require"
29+
)
30+
31+
func TestTooManyOpenFiles(t *testing.T) {
32+
em, err := triggerEmfile()
33+
require.NoError(t, err)
34+
require.NotNil(t, em)
35+
defer func() {
36+
require.NoError(t, em.undo())
37+
}()
38+
39+
_, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
40+
require.Equal(t, syscall.EMFILE, err)
41+
42+
cache := newCache(
43+
WithAutoRefresh(true),
44+
)
45+
require.NotNil(t, cache)
46+
47+
// try to trigger original crash with a nil fsnotify.Watcher
48+
_, _ = cache.InjectDevices(&oci.Spec{}, "vendor1.com/device=dev1")
49+
}
50+
51+
type emfile struct {
52+
limit syscall.Rlimit
53+
fds []int
54+
undone bool
55+
}
56+
57+
// getFdTableSize reads the process' FD table size.
58+
func getFdTableSize() (uint64, error) {
59+
status, err := os.ReadFile("/proc/self/status")
60+
if err != nil {
61+
return 0, err
62+
}
63+
64+
const fdSizeTag = "FDSize:"
65+
66+
for _, line := range strings.Split(string(status), "\n") {
67+
if strings.HasPrefix(line, fdSizeTag) {
68+
value := strings.TrimSpace(strings.TrimPrefix(line, fdSizeTag))
69+
size, err := strconv.ParseUint(value, 10, 64)
70+
if err != nil {
71+
return 0, err
72+
}
73+
return size, nil
74+
}
75+
}
76+
77+
return 0, fmt.Errorf("tag %s not found in /proc/self/status", fdSizeTag)
78+
}
79+
80+
// triggerEmfile exhausts the file descriptors of the process and triggers
81+
// a failure with an EMFILE for any syscall that needs to create a new fd.
82+
// On success, the returned emfile's undo() method can be used to undo the
83+
// exhausted table and restore everything to a working state.
84+
func triggerEmfile() (*emfile, error) {
85+
// We exhaust our file descriptors by
86+
// - checking the size of our current fd table
87+
// - setting our soft RLIMIT_NOFILE limit to the table size
88+
// - ensuring the fd table is full by creating new fd's
89+
//
90+
// We also save our original RLIMIT_NOFILE limit and any fd's we
91+
// might need to create, so we can eventually restore everything
92+
// to its original state.
93+
94+
fdsize, err := getFdTableSize()
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
em := &emfile{}
100+
101+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &em.limit); err != nil {
102+
return nil, err
103+
}
104+
105+
limit := em.limit
106+
limit.Cur = fdsize
107+
108+
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
109+
return nil, err
110+
}
111+
112+
for i := uint64(0); i < fdsize; i++ {
113+
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
114+
if err != nil {
115+
return em, nil
116+
}
117+
em.fds = append(em.fds, fd)
118+
}
119+
120+
return nil, fmt.Errorf("failed to trigger EMFILE")
121+
}
122+
123+
// undo restores the process' state to its pre-EMFILE condition.
124+
func (em *emfile) undo() error {
125+
if em == nil || em.undone {
126+
return nil
127+
}
128+
129+
// we restore the process' state to pre-EMFILE condition by
130+
// - restoring our saved RLIMIT_NOFILE
131+
// - closing any extra file descriptors we might have created
132+
133+
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &em.limit); err != nil {
134+
return err
135+
}
136+
for _, fd := range em.fds {
137+
syscall.Close(fd)
138+
}
139+
em.undone = true
140+
141+
return nil
142+
}

0 commit comments

Comments
 (0)