summaryrefslogtreecommitdiffhomepage
path: root/pkg/sentry/kernel/fd_table_test.go
blob: bf546008379292a163586a53805f07a6e061e0c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package kernel

import (
	"runtime"
	"testing"

	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/sentry/contexttest"
	"gvisor.dev/gvisor/pkg/sentry/fs"
	"gvisor.dev/gvisor/pkg/sentry/fs/filetest"
	"gvisor.dev/gvisor/pkg/sentry/limits"
	"gvisor.dev/gvisor/pkg/sync"
)

const (
	// maxFD is the maximum FD to try to create in the map.
	//
	// This number of open files has been seen in the wild.
	maxFD = 2 * 1024
)

func runTest(t testing.TB, fn func(ctx context.Context, fdTable *FDTable, file *fs.File, limitSet *limits.LimitSet)) {
	t.Helper() // Don't show in stacks.

	// Create the limits and context.
	limitSet := limits.NewLimitSet()
	limitSet.Set(limits.NumberOfFiles, limits.Limit{maxFD, maxFD}, true)
	ctx := contexttest.WithLimitSet(contexttest.Context(t), limitSet)

	// Create a test file.;
	file := filetest.NewTestFile(t)

	// Create the table.
	fdTable := new(FDTable)
	fdTable.init()

	// Run the test.
	fn(ctx, fdTable, file, limitSet)
}

// TestFDTableMany allocates maxFD FDs, i.e. maxes out the FDTable, until there
// is no room, then makes sure that NewFDAt works and also that if we remove
// one and add one that works too.
func TestFDTableMany(t *testing.T) {
	runTest(t, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) {
		for i := 0; i < maxFD; i++ {
			if _, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil {
				t.Fatalf("Allocated %v FDs but wanted to allocate %v", i, maxFD)
			}
		}

		if _, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err == nil {
			t.Fatalf("fdTable.NewFDs(0, r) in full map: got nil, wanted error")
		}

		if err := fdTable.NewFDAt(ctx, 1, file, FDFlags{}); err != nil {
			t.Fatalf("fdTable.NewFDAt(1, r, FDFlags{}): got %v, wanted nil", err)
		}

		i := int32(2)
		fdTable.Remove(ctx, i)
		if fds, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil || fds[0] != i {
			t.Fatalf("Allocated %v FDs but wanted to allocate %v: %v", i, maxFD, err)
		}
	})
}

func TestFDTableOverLimit(t *testing.T) {
	runTest(t, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) {
		if _, err := fdTable.NewFDs(ctx, maxFD, []*fs.File{file}, FDFlags{}); err == nil {
			t.Fatalf("fdTable.NewFDs(maxFD, f): got nil, wanted error")
		}

		if _, err := fdTable.NewFDs(ctx, maxFD-2, []*fs.File{file, file, file}, FDFlags{}); err == nil {
			t.Fatalf("fdTable.NewFDs(maxFD-2, {f,f,f}): got nil, wanted error")
		}

		if fds, err := fdTable.NewFDs(ctx, maxFD-3, []*fs.File{file, file, file}, FDFlags{}); err != nil {
			t.Fatalf("fdTable.NewFDs(maxFD-3, {f,f,f}): got %v, wanted nil", err)
		} else {
			for _, fd := range fds {
				fdTable.Remove(ctx, fd)
			}
		}

		if fds, err := fdTable.NewFDs(ctx, maxFD-1, []*fs.File{file}, FDFlags{}); err != nil || fds[0] != maxFD-1 {
			t.Fatalf("fdTable.NewFDAt(1, r, FDFlags{}): got %v, wanted nil", err)
		}

		if fds, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil {
			t.Fatalf("Adding an FD to a resized map: got %v, want nil", err)
		} else if len(fds) != 1 || fds[0] != 0 {
			t.Fatalf("Added an FD to a resized map: got %v, want {1}", fds)
		}
	})
}

// TestFDTable does a set of simple tests to make sure simple adds, removes,
// GetRefs, and DecRefs work. The ordering is just weird enough that a
// table-driven approach seemed clumsy.
func TestFDTable(t *testing.T) {
	runTest(t, func(ctx context.Context, fdTable *FDTable, file *fs.File, limitSet *limits.LimitSet) {
		// Cap the limit at one.
		limitSet.Set(limits.NumberOfFiles, limits.Limit{1, maxFD}, true)

		if _, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil {
			t.Fatalf("Adding an FD to an empty 1-size map: got %v, want nil", err)
		}

		if _, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err == nil {
			t.Fatalf("Adding an FD to a filled 1-size map: got nil, wanted an error")
		}

		// Remove the previous limit.
		limitSet.Set(limits.NumberOfFiles, limits.Limit{maxFD, maxFD}, true)

		if fds, err := fdTable.NewFDs(ctx, 0, []*fs.File{file}, FDFlags{}); err != nil {
			t.Fatalf("Adding an FD to a resized map: got %v, want nil", err)
		} else if len(fds) != 1 || fds[0] != 1 {
			t.Fatalf("Added an FD to a resized map: got %v, want {1}", fds)
		}

		if err := fdTable.NewFDAt(ctx, 1, file, FDFlags{}); err != nil {
			t.Fatalf("Replacing FD 1 via fdTable.NewFDAt(1, r, FDFlags{}): got %v, wanted nil", err)
		}

		if err := fdTable.NewFDAt(ctx, maxFD+1, file, FDFlags{}); err == nil {
			t.Fatalf("Using an FD that was too large via fdTable.NewFDAt(%v, r, FDFlags{}): got nil, wanted an error", maxFD+1)
		}

		if ref, _ := fdTable.Get(1); ref == nil {
			t.Fatalf("fdTable.Get(1): got nil, wanted %v", file)
		}

		if ref, _ := fdTable.Get(2); ref != nil {
			t.Fatalf("fdTable.Get(2): got a %v, wanted nil", ref)
		}

		ref, _ := fdTable.Remove(ctx, 1)
		if ref == nil {
			t.Fatalf("fdTable.Remove(1) for an existing FD: failed, want success")
		}
		ref.DecRef(ctx)

		if ref, _ := fdTable.Remove(ctx, 1); ref != nil {
			t.Fatalf("r.Remove(1) for a removed FD: got success, want failure")
		}
	})
}

func TestDescriptorFlags(t *testing.T) {
	runTest(t, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) {
		if err := fdTable.NewFDAt(ctx, 2, file, FDFlags{CloseOnExec: true}); err != nil {
			t.Fatalf("fdTable.NewFDAt(2, r, FDFlags{}): got %v, wanted nil", err)
		}

		newFile, flags := fdTable.Get(2)
		if newFile == nil {
			t.Fatalf("fdTable.Get(2): got a %v, wanted nil", newFile)
		}

		if !flags.CloseOnExec {
			t.Fatalf("new File flags %v don't match original %d\n", flags, 0)
		}
	})
}

func BenchmarkFDLookupAndDecRef(b *testing.B) {
	b.StopTimer() // Setup.

	runTest(b, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) {
		fds, err := fdTable.NewFDs(ctx, 0, []*fs.File{file, file, file, file, file}, FDFlags{})
		if err != nil {
			b.Fatalf("fdTable.NewFDs: got %v, wanted nil", err)
		}

		b.StartTimer() // Benchmark.
		for i := 0; i < b.N; i++ {
			tf, _ := fdTable.Get(fds[i%len(fds)])
			tf.DecRef(ctx)
		}
	})
}

func BenchmarkFDLookupAndDecRefConcurrent(b *testing.B) {
	b.StopTimer() // Setup.

	runTest(b, func(ctx context.Context, fdTable *FDTable, file *fs.File, _ *limits.LimitSet) {
		fds, err := fdTable.NewFDs(ctx, 0, []*fs.File{file, file, file, file, file}, FDFlags{})
		if err != nil {
			b.Fatalf("fdTable.NewFDs: got %v, wanted nil", err)
		}

		concurrency := runtime.GOMAXPROCS(0)
		if concurrency < 4 {
			concurrency = 4
		}
		each := b.N / concurrency

		b.StartTimer() // Benchmark.
		var wg sync.WaitGroup
		for i := 0; i < concurrency; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				for i := 0; i < each; i++ {
					tf, _ := fdTable.Get(fds[i%len(fds)])
					tf.DecRef(ctx)
				}
			}()
		}
		wg.Wait()
	})
}