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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
|
// 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.
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <syscall.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "gtest/gtest.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "test/util/capability_util.h"
#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/mount_util.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
using ::testing::HasSubstr;
using ::testing::Not;
namespace gvisor {
namespace testing {
namespace {
TEST(ChrootTest, Success) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
}
TEST(ChrootTest, PermissionDenied) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
// CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission on
// directories.
ASSERT_NO_ERRNO(SetCapability(CAP_DAC_READ_SEARCH, false));
ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0666 /* mode */));
EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EACCES));
}
TEST(ChrootTest, NotDir) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
EXPECT_THAT(chroot(temp_file.path().c_str()), SyscallFailsWithErrno(ENOTDIR));
}
TEST(ChrootTest, NotExist) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
EXPECT_THAT(chroot("/foo/bar"), SyscallFailsWithErrno(ENOENT));
}
TEST(ChrootTest, WithoutCapability) {
// Unset CAP_SYS_CHROOT.
ASSERT_NO_ERRNO(SetCapability(CAP_SYS_CHROOT, false));
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EPERM));
}
TEST(ChrootTest, CreatesNewRoot) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
// Grab the initial cwd.
char initial_cwd[1024];
ASSERT_THAT(syscall(__NR_getcwd, initial_cwd, sizeof(initial_cwd)),
SyscallSucceeds());
auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
auto file_in_new_root =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(new_root.path()));
// chroot into new_root.
ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds());
// getcwd should return "(unreachable)" followed by the initial_cwd.
char cwd[1024];
ASSERT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds());
std::string expected_cwd = "(unreachable)";
expected_cwd += initial_cwd;
EXPECT_STREQ(cwd, expected_cwd.c_str());
// Should not be able to stat file by its full path.
struct stat statbuf;
EXPECT_THAT(stat(file_in_new_root.path().c_str(), &statbuf),
SyscallFailsWithErrno(ENOENT));
// Should be able to stat file at new rooted path.
auto basename = std::string(Basename(file_in_new_root.path()));
auto rootedFile = "/" + basename;
ASSERT_THAT(stat(rootedFile.c_str(), &statbuf), SyscallSucceeds());
// Should be able to stat cwd at '.' even though it's outside root.
ASSERT_THAT(stat(".", &statbuf), SyscallSucceeds());
// chdir into new root.
ASSERT_THAT(chdir("/"), SyscallSucceeds());
// getcwd should return "/".
EXPECT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds());
EXPECT_STREQ(cwd, "/");
// Statting '.', '..', '/', and '/..' all return the same dev and inode.
struct stat statbuf_dot;
ASSERT_THAT(stat(".", &statbuf_dot), SyscallSucceeds());
struct stat statbuf_dotdot;
ASSERT_THAT(stat("..", &statbuf_dotdot), SyscallSucceeds());
EXPECT_EQ(statbuf_dot.st_dev, statbuf_dotdot.st_dev);
EXPECT_EQ(statbuf_dot.st_ino, statbuf_dotdot.st_ino);
struct stat statbuf_slash;
ASSERT_THAT(stat("/", &statbuf_slash), SyscallSucceeds());
EXPECT_EQ(statbuf_dot.st_dev, statbuf_slash.st_dev);
EXPECT_EQ(statbuf_dot.st_ino, statbuf_slash.st_ino);
struct stat statbuf_slashdotdot;
ASSERT_THAT(stat("/..", &statbuf_slashdotdot), SyscallSucceeds());
EXPECT_EQ(statbuf_dot.st_dev, statbuf_slashdotdot.st_dev);
EXPECT_EQ(statbuf_dot.st_ino, statbuf_slashdotdot.st_ino);
}
TEST(ChrootTest, DotDotFromOpenFD) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
auto dir_outside_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
auto fd = ASSERT_NO_ERRNO_AND_VALUE(
Open(dir_outside_root.path(), O_RDONLY | O_DIRECTORY));
auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
// chroot into new_root.
ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds());
// openat on fd with path .. will succeed.
int other_fd;
ASSERT_THAT(other_fd = openat(fd.get(), "..", O_RDONLY), SyscallSucceeds());
EXPECT_THAT(close(other_fd), SyscallSucceeds());
// getdents on fd should not error.
char buf[1024];
ASSERT_THAT(syscall(SYS_getdents, fd.get(), buf, sizeof(buf)),
SyscallSucceeds());
}
// Test that link resolution in a chroot can escape the root by following an
// open proc fd.
TEST(ChrootTest, ProcFdLinkResolutionInChroot) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
const TempPath file_outside_chroot =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(file_outside_chroot.path(), O_RDONLY));
const FileDescriptor proc_fd = ASSERT_NO_ERRNO_AND_VALUE(
Open("/proc", O_DIRECTORY | O_RDONLY | O_CLOEXEC));
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
// Opening relative to an already open fd to a node outside the chroot works.
const FileDescriptor proc_self_fd = ASSERT_NO_ERRNO_AND_VALUE(
OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC));
// Proc fd symlinks can escape the chroot if the fd the symlink refers to
// refers to an object outside the chroot.
struct stat s = {};
EXPECT_THAT(
fstatat(proc_self_fd.get(), absl::StrCat(fd.get()).c_str(), &s, 0),
SyscallSucceeds());
// Try to stat the stdin fd. Internally, this is handled differently from a
// proc fd entry pointing to a file, since stdin is backed by a host fd, and
// isn't a walkable path on the filesystem inside the sandbox.
EXPECT_THAT(fstatat(proc_self_fd.get(), "0", &s, 0), SyscallSucceeds());
}
// This test will verify that when you hold a fd to proc before entering
// a chroot that any files inside the chroot will appear rooted to the
// base chroot when examining /proc/self/fd/{num}.
TEST(ChrootTest, ProcMemSelfFdsNoEscapeProcOpen) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
// Get a FD to /proc before we enter the chroot.
const FileDescriptor proc =
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
// Create and enter a chroot directory.
const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
// Open a file inside the chroot at /foo.
const FileDescriptor foo =
ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
// Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're
// inside a chroot, the path should be /foo and NOT {chroot_dir}/foo.
const std::string fd_path = absl::StrCat("self/fd/", foo.get());
char buf[1024] = {};
size_t bytes_read = 0;
ASSERT_THAT(bytes_read =
readlinkat(proc.get(), fd_path.c_str(), buf, sizeof(buf) - 1),
SyscallSucceeds());
// The link should resolve to something.
ASSERT_GT(bytes_read, 0);
// Assert that the link doesn't contain the chroot path and is only /foo.
EXPECT_STREQ(buf, "/foo");
}
// This test will verify that a file inside a chroot when mmapped will not
// expose the full file path via /proc/self/maps and instead honor the chroot.
TEST(ChrootTest, ProcMemSelfMapsNoEscapeProcOpen) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
// Get a FD to /proc before we enter the chroot.
const FileDescriptor proc =
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
// Create and enter a chroot directory.
const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
// Open a file inside the chroot at /foo.
const FileDescriptor foo =
ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
// Mmap the newly created file.
void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE,
foo.get(), 0);
ASSERT_THAT(reinterpret_cast<int64_t>(foo_map), SyscallSucceeds());
// Always unmap.
auto cleanup_map = Cleanup(
[&] { EXPECT_THAT(munmap(foo_map, kPageSize), SyscallSucceeds()); });
// Examine /proc/self/maps to be sure that /foo doesn't appear to be
// mapped with the full chroot path.
const FileDescriptor maps =
ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), "self/maps", O_RDONLY));
size_t bytes_read = 0;
char buf[8 * 1024] = {};
ASSERT_THAT(bytes_read = ReadFd(maps.get(), buf, sizeof(buf)),
SyscallSucceeds());
// The maps file should have something.
ASSERT_GT(bytes_read, 0);
// Finally we want to make sure the maps don't contain the chroot path
ASSERT_EQ(std::string(buf, bytes_read).find(temp_dir.path()),
std::string::npos);
}
// Test that mounts outside the chroot will not appear in /proc/self/mounts or
// /proc/self/mountinfo.
TEST(ChrootTest, ProcMountsMountinfoNoEscape) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
// We are going to create some mounts and then chroot. In order to be able to
// unmount the mounts after the test run, we must chdir to the root and use
// relative paths for all mounts. That way, as long as we never chdir into
// the new root, we can access the mounts via relative paths and unmount them.
ASSERT_THAT(chdir("/"), SyscallSucceeds());
// Create nested tmpfs mounts. Note the use of relative paths in Mount calls.
auto const outer_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
auto const outer_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount(
"none", JoinPath(".", outer_dir.path()), "tmpfs", 0, "mode=0700", 0));
auto const inner_dir =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(outer_dir.path()));
auto const inner_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount(
"none", JoinPath(".", inner_dir.path()), "tmpfs", 0, "mode=0700", 0));
// Filenames that will be checked for mounts, all relative to /proc dir.
std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"};
for (const std::string& path : paths) {
// We should have both inner and outer mounts.
const std::string contents =
ASSERT_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path)));
EXPECT_THAT(contents, AllOf(HasSubstr(outer_dir.path()),
HasSubstr(inner_dir.path())));
// We better have at least two mounts: the mounts we created plus the root.
std::vector<absl::string_view> submounts =
absl::StrSplit(contents, '\n', absl::SkipWhitespace());
EXPECT_GT(submounts.size(), 2);
}
// Get a FD to /proc before we enter the chroot.
const FileDescriptor proc =
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
// Chroot to outer mount.
ASSERT_THAT(chroot(outer_dir.path().c_str()), SyscallSucceeds());
for (const std::string& path : paths) {
const FileDescriptor proc_file =
ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
// Only two mounts visible from this chroot: the inner and outer. Both
// paths should be relative to the new chroot.
const std::string contents =
ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
EXPECT_THAT(contents,
AllOf(HasSubstr(absl::StrCat(Basename(inner_dir.path()))),
Not(HasSubstr(outer_dir.path())),
Not(HasSubstr(inner_dir.path()))));
std::vector<absl::string_view> submounts =
absl::StrSplit(contents, '\n', absl::SkipWhitespace());
EXPECT_EQ(submounts.size(), 2);
}
// Chroot to inner mount. We must use an absolute path accessible to our
// chroot.
const std::string inner_dir_basename =
absl::StrCat("/", Basename(inner_dir.path()));
ASSERT_THAT(chroot(inner_dir_basename.c_str()), SyscallSucceeds());
for (const std::string& path : paths) {
const FileDescriptor proc_file =
ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
const std::string contents =
ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
// Only the inner mount visible from this chroot.
std::vector<absl::string_view> submounts =
absl::StrSplit(contents, '\n', absl::SkipWhitespace());
EXPECT_EQ(submounts.size(), 1);
}
// Chroot back to ".".
ASSERT_THAT(chroot("."), SyscallSucceeds());
}
} // namespace
} // namespace testing
} // namespace gvisor
|