gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/syscalls/linux/chroot.cc (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 #include <errno.h> 16 #include <fcntl.h> 17 #include <stddef.h> 18 #include <sys/mman.h> 19 #include <sys/stat.h> 20 #include <syscall.h> 21 #include <unistd.h> 22 23 #include <algorithm> 24 #include <string> 25 #include <vector> 26 27 #include "gmock/gmock.h" 28 #include "gtest/gtest.h" 29 #include "absl/cleanup/cleanup.h" 30 #include "absl/strings/str_cat.h" 31 #include "absl/strings/str_split.h" 32 #include "absl/strings/string_view.h" 33 #include "test/util/capability_util.h" 34 #include "test/util/file_descriptor.h" 35 #include "test/util/fs_util.h" 36 #include "test/util/logging.h" 37 #include "test/util/mount_util.h" 38 #include "test/util/multiprocess_util.h" 39 #include "test/util/temp_path.h" 40 #include "test/util/test_util.h" 41 42 using ::testing::HasSubstr; 43 using ::testing::Not; 44 45 namespace gvisor { 46 namespace testing { 47 48 namespace { 49 50 // Async-signal-safe conversion from integer to string, appending the string 51 // (including a terminating NUL) to buf, which is a buffer of size len bytes. 52 // Returns the number of bytes written, or 0 if the buffer is too small. 53 // 54 // Preconditions: 2 <= radix <= 16. 55 template <typename T> 56 size_t SafeItoa(T val, char* buf, size_t len, int radix) { 57 size_t n = 0; 58 #define _WRITE_OR_FAIL(c) \ 59 do { \ 60 if (len == 0) { \ 61 return 0; \ 62 } \ 63 buf[n] = (c); \ 64 n++; \ 65 len--; \ 66 } while (false) 67 if (val == 0) { 68 _WRITE_OR_FAIL('0'); 69 } else { 70 // Write digits in reverse order, then reverse them at the end. 71 bool neg = val < 0; 72 while (val != 0) { 73 // C/C++ define modulo such that the result is negative if exactly one of 74 // the dividend or divisor is negative, so this handles both positive and 75 // negative values. 76 char c = "fedcba9876543210123456789abcdef"[val % radix + 15]; 77 _WRITE_OR_FAIL(c); 78 val /= 10; 79 } 80 if (neg) { 81 _WRITE_OR_FAIL('-'); 82 } 83 std::reverse(buf, buf + n); 84 } 85 _WRITE_OR_FAIL('\0'); 86 return n; 87 #undef _WRITE_OR_FAIL 88 } 89 90 TEST(ChrootTest, Success) { 91 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 92 auto temp_dir = TempPath::CreateDir().ValueOrDie(); 93 const std::string temp_dir_path = temp_dir.path(); 94 95 const auto rest = [&] { TEST_CHECK_SUCCESS(chroot(temp_dir_path.c_str())); }; 96 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 97 } 98 99 TEST(ChrootTest, PermissionDenied) { 100 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 101 102 // CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission 103 // on directories. 104 AutoCapability cap_search(CAP_DAC_READ_SEARCH, false); 105 AutoCapability cap_override(CAP_DAC_OVERRIDE, false); 106 107 auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE( 108 TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0666 /* mode */)); 109 EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EACCES)); 110 } 111 112 TEST(ChrootTest, NotDir) { 113 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 114 115 auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); 116 EXPECT_THAT(chroot(temp_file.path().c_str()), SyscallFailsWithErrno(ENOTDIR)); 117 } 118 119 TEST(ChrootTest, NotExist) { 120 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 121 122 EXPECT_THAT(chroot("/foo/bar"), SyscallFailsWithErrno(ENOENT)); 123 } 124 125 TEST(ChrootTest, WithoutCapability) { 126 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETPCAP))); 127 128 // Unset CAP_SYS_CHROOT. 129 AutoCapability cap(CAP_SYS_CHROOT, false); 130 131 auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 132 EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EPERM)); 133 } 134 135 TEST(ChrootTest, CreatesNewRoot) { 136 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 137 138 // Grab the initial cwd. 139 char initial_cwd[1024]; 140 ASSERT_THAT(syscall(__NR_getcwd, initial_cwd, sizeof(initial_cwd)), 141 SyscallSucceeds()); 142 143 auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 144 const std::string new_root_path = new_root.path(); 145 auto file_in_new_root = 146 ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(new_root.path())); 147 const std::string file_in_new_root_path = file_in_new_root.path(); 148 149 const auto rest = [&] { 150 // chroot into new_root. 151 TEST_CHECK_SUCCESS(chroot(new_root_path.c_str())); 152 153 // getcwd should return "(unreachable)" followed by the initial_cwd. 154 char buf[1024]; 155 TEST_CHECK_SUCCESS(syscall(__NR_getcwd, buf, sizeof(buf))); 156 constexpr char kUnreachablePrefix[] = "(unreachable)"; 157 TEST_CHECK( 158 strncmp(buf, kUnreachablePrefix, sizeof(kUnreachablePrefix) - 1) == 0); 159 TEST_CHECK(strcmp(buf + sizeof(kUnreachablePrefix) - 1, initial_cwd) == 0); 160 161 // Should not be able to stat file by its full path. 162 struct stat statbuf; 163 TEST_CHECK_ERRNO(stat(file_in_new_root_path.c_str(), &statbuf), ENOENT); 164 165 // Should be able to stat file at new rooted path. 166 buf[0] = '/'; 167 absl::string_view basename = Basename(file_in_new_root_path); 168 TEST_CHECK(basename.length() < (sizeof(buf) - 2)); 169 memcpy(buf + 1, basename.data(), basename.length()); 170 buf[basename.length() + 1] = '\0'; 171 TEST_CHECK_SUCCESS(stat(buf, &statbuf)); 172 173 // Should be able to stat cwd at '.' even though it's outside root. 174 TEST_CHECK_SUCCESS(stat(".", &statbuf)); 175 176 // chdir into new root. 177 TEST_CHECK_SUCCESS(chdir("/")); 178 179 // getcwd should return "/". 180 TEST_CHECK_SUCCESS(syscall(__NR_getcwd, buf, sizeof(buf))); 181 TEST_PCHECK(strcmp(buf, "/") == 0); 182 183 // Statting '.', '..', '/', and '/..' all return the same dev and inode. 184 struct stat statbuf_dot; 185 TEST_CHECK_SUCCESS(stat(".", &statbuf_dot)); 186 struct stat statbuf_dotdot; 187 TEST_CHECK_SUCCESS(stat("..", &statbuf_dotdot)); 188 TEST_CHECK(statbuf_dot.st_dev == statbuf_dotdot.st_dev); 189 TEST_CHECK(statbuf_dot.st_ino == statbuf_dotdot.st_ino); 190 struct stat statbuf_slash; 191 TEST_CHECK_SUCCESS(stat("/", &statbuf_slash)); 192 TEST_CHECK(statbuf_dot.st_dev == statbuf_slash.st_dev); 193 TEST_CHECK(statbuf_dot.st_ino == statbuf_slash.st_ino); 194 struct stat statbuf_slashdotdot; 195 TEST_CHECK_SUCCESS(stat("/..", &statbuf_slashdotdot)); 196 TEST_CHECK(statbuf_dot.st_dev == statbuf_slashdotdot.st_dev); 197 TEST_CHECK(statbuf_dot.st_ino == statbuf_slashdotdot.st_ino); 198 }; 199 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 200 } 201 202 TEST(ChrootTest, DotDotFromOpenFD) { 203 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 204 205 auto dir_outside_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 206 auto fd = ASSERT_NO_ERRNO_AND_VALUE( 207 Open(dir_outside_root.path(), O_RDONLY | O_DIRECTORY)); 208 auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 209 const std::string new_root_path = new_root.path(); 210 211 const auto rest = [&] { 212 // chroot into new_root. 213 TEST_CHECK_SUCCESS(chroot(new_root_path.c_str())); 214 215 // openat on fd with path .. will succeed. 216 int other_fd; 217 TEST_CHECK_SUCCESS(other_fd = openat(fd.get(), "..", O_RDONLY)); 218 TEST_CHECK_SUCCESS(close(other_fd)); 219 220 // getdents on fd should not error. 221 char buf[1024]; 222 TEST_CHECK_SUCCESS(syscall(SYS_getdents64, fd.get(), buf, sizeof(buf))); 223 }; 224 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 225 } 226 227 // Test that link resolution in a chroot can escape the root by following an 228 // open proc fd. Regression test for b/32316719. 229 TEST(ChrootTest, ProcFdLinkResolutionInChroot) { 230 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 231 232 const TempPath file_outside_chroot = 233 ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); 234 const std::string file_outside_chroot_path = file_outside_chroot.path(); 235 const FileDescriptor fd = 236 ASSERT_NO_ERRNO_AND_VALUE(Open(file_outside_chroot.path(), O_RDONLY)); 237 238 const FileDescriptor proc_fd = ASSERT_NO_ERRNO_AND_VALUE( 239 Open("/proc", O_DIRECTORY | O_RDONLY | O_CLOEXEC)); 240 241 auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 242 const std::string temp_dir_path = temp_dir.path(); 243 244 const auto rest = [&] { 245 TEST_CHECK_SUCCESS(chroot(temp_dir_path.c_str())); 246 247 // Opening relative to an already open fd to a node outside the chroot 248 // works. 249 const FileDescriptor proc_self_fd = TEST_CHECK_NO_ERRNO_AND_VALUE( 250 OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC)); 251 252 // Proc fd symlinks can escape the chroot if the fd the symlink refers to 253 // refers to an object outside the chroot. 254 char fd_buf[11]; 255 TEST_CHECK(SafeItoa(fd.get(), fd_buf, sizeof(fd_buf), 10)); 256 struct stat s = {}; 257 TEST_CHECK_SUCCESS(fstatat(proc_self_fd.get(), fd_buf, &s, 0)); 258 259 // Try to stat the stdin fd. Internally, this is handled differently from a 260 // proc fd entry pointing to a file, since stdin is backed by a host fd, and 261 // isn't a walkable path on the filesystem inside the sandbox. 262 TEST_CHECK_SUCCESS(fstatat(proc_self_fd.get(), "0", &s, 0)); 263 }; 264 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 265 } 266 267 // This test will verify that when you hold a fd to proc before entering 268 // a chroot that any files inside the chroot will appear rooted to the 269 // base chroot when examining /proc/self/fd/{num}. 270 TEST(ChrootTest, ProcMemSelfFdsNoEscapeProcOpen) { 271 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 272 273 // Get a FD to /proc before we enter the chroot. 274 const FileDescriptor proc = 275 ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); 276 277 const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 278 const std::string temp_dir_path = temp_dir.path(); 279 280 const auto rest = [&] { 281 // Enter the chroot directory. 282 TEST_CHECK_SUCCESS(chroot(temp_dir_path.c_str())); 283 284 // Open a file inside the chroot at /foo. 285 const FileDescriptor foo = 286 TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); 287 288 // Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're 289 // inside a chroot, the path should be /foo and NOT {chroot_dir}/foo. 290 constexpr char kSelfFdRelpath[] = "self/fd/"; 291 char path_buf[20]; 292 strcpy(path_buf, kSelfFdRelpath); // NOLINT: need async-signal-safety 293 TEST_CHECK(SafeItoa(foo.get(), path_buf + sizeof(kSelfFdRelpath) - 1, 294 sizeof(path_buf) - (sizeof(kSelfFdRelpath) - 1), 10)); 295 char buf[1024] = {}; 296 size_t bytes_read = 0; 297 TEST_CHECK_SUCCESS( 298 bytes_read = readlinkat(proc.get(), path_buf, buf, sizeof(buf) - 1)); 299 300 // The link should resolve to something. 301 TEST_CHECK(bytes_read > 0); 302 303 // Assert that the link doesn't contain the chroot path and is only /foo. 304 TEST_CHECK(strcmp(buf, "/foo") == 0); 305 }; 306 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 307 } 308 309 // This test will verify that when you hold a fd to proc before entering 310 // a chroot that any files outside the chroot will appear rooted outside the 311 // chroot when examining /proc/self/fd/{num}. 312 TEST(ChrootTest, ProcMemSelfFdsYesEscapeProcOpen) { 313 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 314 315 // Get a FD to /proc before we enter the chroot. 316 const FileDescriptor proc = 317 ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); 318 319 const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 320 const std::string temp_dir_path = temp_dir.path(); 321 322 auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); 323 const FileDescriptor temp_fd = 324 ASSERT_NO_ERRNO_AND_VALUE(Open(temp_file.path(), O_RDONLY)); 325 326 const auto rest = [&] { 327 // Enter the chroot directory. 328 TEST_CHECK_SUCCESS(chroot(temp_dir_path.c_str())); 329 330 // Examine /proc/self/fd/{temp_fd} to see if it exposes the fact that we're 331 // inside a chroot, the path should be outside the chroot. 332 constexpr char kSelfFdRelpath[] = "self/fd/"; 333 char path_buf[20]; 334 strcpy(path_buf, kSelfFdRelpath); // NOLINT: need async-signal-safety 335 TEST_CHECK(SafeItoa(temp_fd.get(), path_buf + sizeof(kSelfFdRelpath) - 1, 336 sizeof(path_buf) - (sizeof(kSelfFdRelpath) - 1), 10)); 337 char buf[1024] = {}; 338 size_t bytes_read = 0; 339 TEST_CHECK_SUCCESS( 340 bytes_read = readlinkat(proc.get(), path_buf, buf, sizeof(buf) - 1)); 341 342 // The link should resolve to something. 343 TEST_CHECK(bytes_read > 0); 344 345 // Assert that the link contains full path. 346 TEST_CHECK(strcmp(buf, temp_file.path().c_str()) == 0); 347 }; 348 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 349 } 350 351 // This test will verify that a file inside a chroot when mmapped will not 352 // expose the full file path via /proc/self/maps and instead honor the chroot. 353 TEST(ChrootTest, ProcMemSelfMapsNoEscapeProcOpen) { 354 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 355 // TODO(b/264306751): Remove once FUSE implements mmap. 356 SKIP_IF(getenv("GVISOR_FUSE_TEST")); 357 358 // Get a FD to /proc before we enter the chroot. 359 const FileDescriptor proc = 360 ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); 361 362 const auto temp_dir = TEST_CHECK_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 363 const std::string temp_dir_path = temp_dir.path(); 364 365 const auto rest = [&] { 366 // Enter the chroot directory. 367 TEST_CHECK_SUCCESS(chroot(temp_dir_path.c_str())); 368 369 // Open a file inside the chroot at /foo. 370 const FileDescriptor foo = 371 TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); 372 373 // Mmap the newly created file. 374 void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, 375 MAP_PRIVATE, foo.get(), 0); 376 TEST_CHECK_SUCCESS(reinterpret_cast<int64_t>(foo_map)); 377 378 // Always unmap. Since this function is called between fork() and execve(), 379 // we can't use gvisor::testing::Cleanup, which uses std::function 380 // and thus may heap-allocate (which is async-signal-unsafe); instead, use 381 // absl::Cleanup, which is templated on the callback type. 382 auto cleanup_map = absl::MakeCleanup( 383 [&] { TEST_CHECK_SUCCESS(munmap(foo_map, kPageSize)); }); 384 385 // Examine /proc/self/maps to be sure that /foo doesn't appear to be 386 // mapped with the full chroot path. 387 const FileDescriptor maps = TEST_CHECK_NO_ERRNO_AND_VALUE( 388 OpenAt(proc.get(), "self/maps", O_RDONLY)); 389 390 size_t bytes_read = 0; 391 char buf[8 * 1024] = {}; 392 TEST_CHECK_SUCCESS(bytes_read = ReadFd(maps.get(), buf, sizeof(buf))); 393 394 // The maps file should have something. 395 TEST_CHECK(bytes_read > 0); 396 397 // Finally we want to make sure the maps don't contain the chroot path 398 TEST_CHECK( 399 !absl::StrContains(absl::string_view(buf, bytes_read), temp_dir_path)); 400 }; 401 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 402 } 403 404 // Test that mounts outside the chroot will not appear in /proc/self/mounts or 405 // /proc/self/mountinfo. 406 TEST(ChrootTest, ProcMountsMountinfoNoEscape) { 407 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); 408 SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); 409 410 // Create nested tmpfs mounts. 411 const auto outer_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); 412 const std::string outer_dir_path = outer_dir.path(); 413 const auto outer_mount = ASSERT_NO_ERRNO_AND_VALUE( 414 Mount("none", outer_dir_path, "tmpfs", 0, "mode=0700", 0)); 415 416 const auto inner_dir = 417 ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(outer_dir_path)); 418 const std::string inner_dir_path = inner_dir.path(); 419 const auto inner_mount = ASSERT_NO_ERRNO_AND_VALUE( 420 Mount("none", inner_dir_path, "tmpfs", 0, "mode=0700", 0)); 421 const std::string inner_dir_in_outer_chroot_path = 422 absl::StrCat("/", Basename(inner_dir_path)); 423 424 // Filenames that will be checked for mounts, all relative to /proc dir. 425 std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"}; 426 427 for (const std::string& path : paths) { 428 // We should have both inner and outer mounts. 429 const std::string contents = 430 ASSERT_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path))); 431 EXPECT_THAT(contents, 432 AllOf(HasSubstr(outer_dir_path), HasSubstr(inner_dir_path))); 433 // We better have at least two mounts: the mounts we created plus the 434 // root. 435 std::vector<absl::string_view> submounts = 436 absl::StrSplit(contents, '\n', absl::SkipWhitespace()); 437 ASSERT_GT(submounts.size(), 2); 438 } 439 440 // Get a FD to /proc before we enter the chroot. 441 const FileDescriptor proc = 442 ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); 443 444 const auto rest = [&] { 445 // Chroot to outer mount. 446 TEST_CHECK_SUCCESS(chroot(outer_dir_path.c_str())); 447 448 char buf[8 * 1024]; 449 for (const std::string& path : paths) { 450 const FileDescriptor proc_file = 451 TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); 452 453 // Only two mounts visible from this chroot: the inner and outer. Both 454 // paths should be relative to the new chroot. 455 ssize_t n = ReadFd(proc_file.get(), buf, sizeof(buf)); 456 TEST_PCHECK(n >= 0); 457 buf[n] = '\0'; 458 TEST_CHECK(absl::StrContains(buf, Basename(inner_dir_path))); 459 TEST_CHECK(!absl::StrContains(buf, outer_dir_path)); 460 TEST_CHECK(!absl::StrContains(buf, inner_dir_path)); 461 TEST_CHECK(std::count(buf, buf + n, '\n') == 2); 462 } 463 464 // Chroot to inner mount. We must use an absolute path accessible to our 465 // chroot. 466 TEST_CHECK_SUCCESS(chroot(inner_dir_in_outer_chroot_path.c_str())); 467 468 for (const std::string& path : paths) { 469 const FileDescriptor proc_file = 470 TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); 471 472 // Only the inner mount visible from this chroot. 473 ssize_t n = ReadFd(proc_file.get(), buf, sizeof(buf)); 474 TEST_PCHECK(n >= 0); 475 buf[n] = '\0'; 476 TEST_CHECK(std::count(buf, buf + n, '\n') == 1); 477 } 478 }; 479 EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); 480 } 481 482 } // namespace 483 484 } // namespace testing 485 } // namespace gvisor