diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 23f4648986..e9eba54a9d 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -396,6 +396,12 @@ syscall_test( test = "//test/syscalls/linux:mlock_test", ) +syscall_test( + timeout = "eternal", # YES_I_REALLY_NEED_AN_ETERNAL_TEST + save = False, # save tests incorrectly shorten timeout to "long" + test = "//test/syscalls/linux:mmap_eternal_test", +) + syscall_test( size = "medium", shard_count = more_shards, diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 8fb158955d..4cf7ef6293 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -1381,6 +1381,23 @@ cc_binary( ], ) +cc_binary( + name = "mmap_eternal_test", + testonly = 1, + srcs = ["mmap_eternal.cc"], + linkstatic = 1, + malloc = "//test/util:errno_safe_allocator", + deps = select_gtest() + [ + "//test/util:logging", + "//test/util:memory_util", + "//test/util:multiprocess_util", + "//test/util:posix_error", + "//test/util:save_util", + "//test/util:test_main", + "//test/util:test_util", + ], +) + cc_binary( name = "mmap_test", testonly = 1, diff --git a/test/syscalls/linux/mmap_eternal.cc b/test/syscalls/linux/mmap_eternal.cc new file mode 100644 index 0000000000..bddc01f26e --- /dev/null +++ b/test/syscalls/linux/mmap_eternal.cc @@ -0,0 +1,85 @@ +// Copyright 2024 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. + +// mmap tests that often take longer than 900s to run and thus may be skipped +// by test automation. + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "test/util/logging.h" +#include "test/util/memory_util.h" +#include "test/util/multiprocess_util.h" +#include "test/util/posix_error.h" +#include "test/util/save_util.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +// Tests that when using one entry per page table leaf page at a time, page +// table pages that become empty do not accumulate. +TEST(MmapEternalTest, PageTableLeak) { + // Skip this test on platforms where app page tables are managed as for + // ordinary processes by the host kernel, both because there's relatively + // little value in exercising this behavior (separately from + // Platform::kNative) and because MM can be slow enough on such platforms to + // cause the test to time out. + SKIP_IF(GvisorPlatform() == Platform::kPtrace || + GvisorPlatform() == Platform::kSystrap); + + // Guess how much virtual address space we need. + constexpr size_t kMemoryLimitBytes = 12L << 30; + const size_t kMemoryLimitPages = kMemoryLimitBytes / kPageSize; + const size_t kEntriesPerPageTablePage = kPageSize / sizeof(void*); + const size_t kMemoryPerPageTableLeafPage = + kPageSize * kEntriesPerPageTablePage; + const size_t kMemorySizeBytes = + kMemoryLimitPages * kMemoryPerPageTableLeafPage; + + // Reserve virtual address space. + Mapping m = ASSERT_NO_ERRNO_AND_VALUE( + MmapAnon(kMemorySizeBytes, PROT_NONE, MAP_PRIVATE)); + + // Map and unmap one page at a time. This uses a subprocess since the + // existence of our reservation VMA interferes with page table freeing; + // forking ensures that there are no other threads in the subprocess, + // allowing us to safely unmap the reservation. + const DisableSave ds; + const auto rest = [&] { + char* ptr = static_cast(m.ptr()); + char const* const end = static_cast(m.endptr()); + m.reset(); + while (ptr < end) { + TEST_PCHECK( + MmapSafe(ptr, kPageSize, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_POPULATE, -1, + 0) == ptr); + TEST_PCHECK(MunmapSafe(ptr, kPageSize) == 0); + ptr += kMemoryPerPageTableLeafPage; + } + }; + + // The test passes if this does not result in an OOM kill (of the subprocess + // or the sandbox). + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); +} + +} // namespace + +} // namespace testing +} // namespace gvisor diff --git a/test/util/memory_util.h b/test/util/memory_util.h index e864339cfb..535ad7d113 100644 --- a/test/util/memory_util.h +++ b/test/util/memory_util.h @@ -32,6 +32,13 @@ namespace gvisor { namespace testing { +// Async-signal-safe version of mmap(2). +inline void* MmapSafe(void* addr, size_t length, int prot, int flags, int fd, + off_t offset) { + return reinterpret_cast( + syscall(SYS_mmap, addr, length, prot, flags, fd, offset)); +} + // Async-signal-safe version of munmap(2). inline int MunmapSafe(void* addr, size_t length) { return syscall(SYS_munmap, addr, length); @@ -110,7 +117,7 @@ class Mapping { // Wrapper around mmap(2) that returns a Mapping. inline PosixErrorOr Mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset) { - void* ptr = mmap(addr, length, prot, flags, fd, offset); + void* ptr = MmapSafe(addr, length, prot, flags, fd, offset); if (ptr == MAP_FAILED) { return PosixError( errno, absl::StrFormat("mmap(%p, %d, %x, %x, %d, %d)", addr, length,