// 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 <string.h>
#include <sys/mman.h>
#ifdef __linux__
#include <sys/syscall.h>
#endif
#include <unistd.h>

#include <string>

#include "gmock/gmock.h"
#include "absl/strings/string_view.h"
#include "test/util/file_descriptor.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/temp_path.h"
#include "test/util/test_util.h"

using ::testing::_;

namespace gvisor {
namespace testing {

namespace {

// libc mremap isn't guaranteed to be async-signal-safe by signal-safety(7) and
// therefore isn't necessarily safe to call between fork(2) and execve(2);
// provide our own version that is.
void* MremapSafe(void* old_addr, size_t old_size, size_t new_size,
                 unsigned long flags, void* new_address = nullptr) {  // NOLINT
  return reinterpret_cast<void*>(
      syscall(SYS_mremap, old_addr, old_size, new_size, flags, new_address));
}

// Fixture for mremap tests parameterized by mmap flags.
using MremapParamTest = ::testing::TestWithParam<int>;

TEST_P(MremapParamTest, Noop) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));

  ASSERT_THAT(Mremap(m.ptr(), kPageSize, kPageSize, 0, nullptr),
              IsPosixErrorOkAndHolds(m.ptr()));
  EXPECT_TRUE(IsMapped(m.addr()));
}

TEST_P(MremapParamTest, InPlace_ShrinkingWholeVMA) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // N.B. we must be in a single-threaded subprocess to ensure a
    // background thread doesn't concurrently map the second page.
    void* addr = MremapSafe(m.ptr(), 2 * kPageSize, kPageSize, 0, nullptr);
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == m.ptr());
    MaybeSave();

    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(!IsMapped(m.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, InPlace_ShrinkingPartialVMA) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    void* addr = MremapSafe(m.ptr(), 2 * kPageSize, kPageSize, 0, nullptr);
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == m.ptr());
    MaybeSave();

    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(!IsMapped(m.addr() + kPageSize));
    TEST_CHECK(IsMapped(m.addr() + 2 * kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, InPlace_ShrinkingAcrossVMAs) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_READ, GetParam()));
  // Changing permissions on the first page forces it to become a separate vma.
  ASSERT_THAT(mprotect(m.ptr(), kPageSize, PROT_NONE), SyscallSucceeds());

  const auto rest = [&] {
    // Both old_size and new_size now span two vmas; mremap
    // shouldn't care.
    void* addr = MremapSafe(m.ptr(), 3 * kPageSize, 2 * kPageSize, 0, nullptr);
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == m.ptr());
    MaybeSave();

    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(IsMapped(m.addr() + kPageSize));
    TEST_CHECK(!IsMapped(m.addr() + 2 * kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, InPlace_ExpansionSuccess) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap the second page so that the first can be expanded back into it.
    //
    // N.B. we must be in a single-threaded subprocess to ensure a
    // background thread doesn't concurrently map this page.
    TEST_PCHECK(MunmapSafe(reinterpret_cast<void*>(m.addr() + kPageSize),
                           kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(m.ptr(), kPageSize, 2 * kPageSize, 0, nullptr);
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == m.ptr());
    MaybeSave();

    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(IsMapped(m.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, InPlace_ExpansionFailure) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap the second page, leaving a one-page hole. Trying to expand the
    // first page to three pages should fail since the original third page
    // is still mapped.
    TEST_PCHECK(MunmapSafe(reinterpret_cast<void*>(m.addr() + kPageSize),
                           kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(m.ptr(), kPageSize, 3 * kPageSize, 0, nullptr);
    TEST_CHECK_MSG(addr == MAP_FAILED, "mremap unexpectedly succeeded");
    TEST_PCHECK_MSG(errno == ENOMEM, "mremap failed with wrong errno");
    MaybeSave();

    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(!IsMapped(m.addr() + kPageSize));
    TEST_CHECK(IsMapped(m.addr() + 2 * kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, MayMove_Expansion) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap the second page, leaving a one-page hole. Trying to expand the
    // first page to three pages with MREMAP_MAYMOVE should force the
    // mapping to be relocated since the original third page is still
    // mapped.
    TEST_PCHECK(MunmapSafe(reinterpret_cast<void*>(m.addr() + kPageSize),
                           kPageSize) == 0);
    MaybeSave();

    void* addr2 =
        MremapSafe(m.ptr(), kPageSize, 3 * kPageSize, MREMAP_MAYMOVE, nullptr);
    TEST_PCHECK_MSG(addr2 != MAP_FAILED, "mremap failed");
    MaybeSave();

    const Mapping m2 = Mapping(addr2, 3 * kPageSize);
    TEST_CHECK(m.addr() != m2.addr());

    TEST_CHECK(!IsMapped(m.addr()));
    TEST_CHECK(!IsMapped(m.addr() + kPageSize));
    TEST_CHECK(IsMapped(m.addr() + 2 * kPageSize));
    TEST_CHECK(IsMapped(m2.addr()));
    TEST_CHECK(IsMapped(m2.addr() + kPageSize));
    TEST_CHECK(IsMapped(m2.addr() + 2 * kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_SourceAndDestinationCannotOverlap) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));

  ASSERT_THAT(Mremap(m.ptr(), kPageSize, kPageSize,
                     MREMAP_MAYMOVE | MREMAP_FIXED, m.ptr()),
              PosixErrorIs(EINVAL, _));
  EXPECT_TRUE(IsMapped(m.addr()));
}

TEST_P(MremapParamTest, Fixed_SameSize) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap dst to create a hole.
    TEST_PCHECK(MunmapSafe(dst.ptr(), kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(src.ptr(), kPageSize, kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == dst.ptr());
    MaybeSave();

    TEST_CHECK(!IsMapped(src.addr()));
    TEST_CHECK(IsMapped(dst.addr()));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_SameSize_Unmapping) {
  // Like the Fixed_SameSize case, but expect mremap to unmap the destination
  // automatically.
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    void* addr = MremapSafe(src.ptr(), kPageSize, kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == dst.ptr());
    MaybeSave();

    TEST_CHECK(!IsMapped(src.addr()));
    TEST_CHECK(IsMapped(dst.addr()));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_ShrinkingWholeVMA) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap dst so we can check that mremap does not keep the
    // second page.
    TEST_PCHECK(MunmapSafe(dst.ptr(), 2 * kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(src.ptr(), 2 * kPageSize, kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == dst.ptr());
    MaybeSave();

    TEST_CHECK(!IsMapped(src.addr()));
    TEST_CHECK(!IsMapped(src.addr() + kPageSize));
    TEST_CHECK(IsMapped(dst.addr()));
    TEST_CHECK(!IsMapped(dst.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_ShrinkingPartialVMA) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_NONE, GetParam()));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap dst so we can check that mremap does not keep the
    // second page.
    TEST_PCHECK(MunmapSafe(dst.ptr(), 2 * kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(src.ptr(), 2 * kPageSize, kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == dst.ptr());
    MaybeSave();

    TEST_CHECK(!IsMapped(src.addr()));
    TEST_CHECK(!IsMapped(src.addr() + kPageSize));
    TEST_CHECK(IsMapped(src.addr() + 2 * kPageSize));
    TEST_CHECK(IsMapped(dst.addr()));
    TEST_CHECK(!IsMapped(dst.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_ShrinkingAcrossVMAs) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(3 * kPageSize, PROT_READ, GetParam()));
  // Changing permissions on the first page forces it to become a separate vma.
  ASSERT_THAT(mprotect(src.ptr(), kPageSize, PROT_NONE), SyscallSucceeds());
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unlike flags=0, MREMAP_FIXED requires that [old_address,
    // old_address+new_size) only spans a single vma.
    void* addr = MremapSafe(src.ptr(), 3 * kPageSize, 2 * kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_CHECK_MSG(addr == MAP_FAILED, "mremap unexpectedly succeeded");
    TEST_PCHECK_MSG(errno == EFAULT, "mremap failed with wrong errno");
    MaybeSave();

    TEST_CHECK(IsMapped(src.addr()));
    TEST_CHECK(IsMapped(src.addr() + kPageSize));
    // Despite failing, mremap should have unmapped [old_address+new_size,
    // old_address+old_size) (i.e. the third page).
    TEST_CHECK(!IsMapped(src.addr() + 2 * kPageSize));
    // Despite failing, mremap should have unmapped the destination pages.
    TEST_CHECK(!IsMapped(dst.addr()));
    TEST_CHECK(!IsMapped(dst.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST_P(MremapParamTest, Fixed_Expansion) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, GetParam()));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(2 * kPageSize, PROT_NONE, GetParam()));

  const auto rest = [&] {
    // Unmap dst so we can check that mremap actually maps all pages
    // at the destination.
    TEST_PCHECK(MunmapSafe(dst.ptr(), 2 * kPageSize) == 0);
    MaybeSave();

    void* addr = MremapSafe(src.ptr(), kPageSize, 2 * kPageSize,
                            MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    TEST_PCHECK_MSG(addr != MAP_FAILED, "mremap failed");
    TEST_CHECK(addr == dst.ptr());
    MaybeSave();

    TEST_CHECK(!IsMapped(src.addr()));
    TEST_CHECK(IsMapped(dst.addr()));
    TEST_CHECK(IsMapped(dst.addr() + kPageSize));
  };

  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

INSTANTIATE_TEST_SUITE_P(PrivateShared, MremapParamTest,
                         ::testing::Values(MAP_PRIVATE, MAP_SHARED));

// mremap with old_size == 0 only works with MAP_SHARED after Linux 4.14
// (dba58d3b8c50 "mm/mremap: fail map duplication attempts for private
// mappings").

TEST(MremapTest, InPlace_Copy) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, MAP_SHARED));
  EXPECT_THAT(Mremap(m.ptr(), 0, kPageSize, 0, nullptr),
              PosixErrorIs(ENOMEM, _));
}

TEST(MremapTest, MayMove_Copy) {
  Mapping const m =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, MAP_SHARED));

  // Remainder of this test executes in a subprocess to ensure that if mremap
  // incorrectly removes m, it is not remapped by another thread.
  const auto rest = [&] {
    void* ptr = MremapSafe(m.ptr(), 0, kPageSize, MREMAP_MAYMOVE, nullptr);
    MaybeSave();
    TEST_PCHECK_MSG(ptr != MAP_FAILED, "mremap failed");
    TEST_CHECK(ptr != m.ptr());
    TEST_CHECK(IsMapped(m.addr()));
    TEST_CHECK(IsMapped(reinterpret_cast<uintptr_t>(ptr)));
  };
  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST(MremapTest, MustMove_Copy) {
  Mapping const src =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, MAP_SHARED));
  Mapping const dst =
      ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, MAP_PRIVATE));

  // Remainder of this test executes in a subprocess to ensure that if mremap
  // incorrectly removes src, it is not remapped by another thread.
  const auto rest = [&] {
    void* ptr = MremapSafe(src.ptr(), 0, kPageSize,
                           MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr());
    MaybeSave();
    TEST_PCHECK_MSG(ptr != MAP_FAILED, "mremap failed");
    TEST_CHECK(ptr == dst.ptr());
    TEST_CHECK(IsMapped(src.addr()));
    TEST_CHECK(IsMapped(dst.addr()));
  };
  EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

void ExpectAllBytesAre(absl::string_view v, char c) {
  for (size_t i = 0; i < v.size(); i++) {
    ASSERT_EQ(v[i], c) << "at offset " << i;
  }
}

TEST(MremapTest, ExpansionPreservesCOWPagesAndExposesNewFilePages) {
  // Create a file with 3 pages. The first is filled with 'a', the second is
  // filled with 'b', and the third is filled with 'c'.
  TempPath const file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
  const FileDescriptor fd =
      ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR));
  ASSERT_THAT(WriteFd(fd.get(), std::string(kPageSize, 'a').c_str(), kPageSize),
              SyscallSucceedsWithValue(kPageSize));
  ASSERT_THAT(WriteFd(fd.get(), std::string(kPageSize, 'b').c_str(), kPageSize),
              SyscallSucceedsWithValue(kPageSize));
  ASSERT_THAT(WriteFd(fd.get(), std::string(kPageSize, 'c').c_str(), kPageSize),
              SyscallSucceedsWithValue(kPageSize));

  // Create a private mapping of the first 2 pages, and fill the second page
  // with 'd'.
  Mapping const src = ASSERT_NO_ERRNO_AND_VALUE(Mmap(nullptr, 2 * kPageSize,
                                                     PROT_READ | PROT_WRITE,
                                                     MAP_PRIVATE, fd.get(), 0));
  memset(reinterpret_cast<void*>(src.addr() + kPageSize), 'd', kPageSize);
  MaybeSave();

  // Move the mapping while expanding it to 3 pages. The resulting mapping
  // should contain the original first page of the file (filled with 'a'),
  // followed by the private copy of the second page (filled with 'd'), followed
  // by the newly-mapped third page of the file (filled with 'c').
  Mapping const dst = ASSERT_NO_ERRNO_AND_VALUE(
      MmapAnon(3 * kPageSize, PROT_NONE, MAP_PRIVATE));
  ASSERT_THAT(Mremap(src.ptr(), 2 * kPageSize, 3 * kPageSize,
                     MREMAP_MAYMOVE | MREMAP_FIXED, dst.ptr()),
              IsPosixErrorOkAndHolds(dst.ptr()));
  auto const v = dst.view();
  ExpectAllBytesAre(v.substr(0, kPageSize), 'a');
  ExpectAllBytesAre(v.substr(kPageSize, kPageSize), 'd');
  ExpectAllBytesAre(v.substr(2 * kPageSize, kPageSize), 'c');
}

TEST(MremapDeathTest, SharedAnon) {
  SetupGvisorDeathTest();

  // Reserve 4 pages of address space.
  Mapping const reserved = ASSERT_NO_ERRNO_AND_VALUE(
      MmapAnon(4 * kPageSize, PROT_NONE, MAP_PRIVATE));

  // Create a 2-page shared anonymous mapping at the beginning of the
  // reservation. Fill the first page with 'a' and the second with 'b'.
  Mapping const m = ASSERT_NO_ERRNO_AND_VALUE(
      Mmap(reserved.ptr(), 2 * kPageSize, PROT_READ | PROT_WRITE,
           MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
  memset(m.ptr(), 'a', kPageSize);
  memset(reinterpret_cast<void*>(m.addr() + kPageSize), 'b', kPageSize);
  MaybeSave();

  // Shrink the mapping to 1 page in-place.
  ASSERT_THAT(Mremap(m.ptr(), 2 * kPageSize, kPageSize, 0, m.ptr()),
              IsPosixErrorOkAndHolds(m.ptr()));

  // Expand the mapping to 3 pages, moving it forward by 1 page in the process
  // since the old and new mappings can't overlap.
  void* const new_m = reinterpret_cast<void*>(m.addr() + kPageSize);
  ASSERT_THAT(Mremap(m.ptr(), kPageSize, 3 * kPageSize,
                     MREMAP_MAYMOVE | MREMAP_FIXED, new_m),
              IsPosixErrorOkAndHolds(new_m));

  // The first 2 pages of the mapping should still contain the data we wrote
  // (i.e. shrinking should not have discarded the second page's data), while
  // touching the third page should raise SIGBUS.
  auto const v =
      absl::string_view(static_cast<char const*>(new_m), 3 * kPageSize);
  ExpectAllBytesAre(v.substr(0, kPageSize), 'a');
  ExpectAllBytesAre(v.substr(kPageSize, kPageSize), 'b');
  EXPECT_EXIT(ExpectAllBytesAre(v.substr(2 * kPageSize, kPageSize), '\0'),
              ::testing::KilledBySignal(SIGBUS), "");
}

}  // namespace

}  // namespace testing
}  // namespace gvisor
