// Copyright 2019 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 <linux/membarrier.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "gtest/gtest.h"
#include "test/syscalls/linux/rseq/test.h"
#include "test/syscalls/linux/rseq/uapi.h"
#include "test/util/logging.h"
#include "test/util/multiprocess_util.h"
#include "test/util/posix_error.h"
#include "test/util/test_util.h"

namespace gvisor {
namespace testing {

namespace {

using ::testing::AnyOf;
using ::testing::Eq;

// Syscall test for rseq (restartable sequences).
//
// We must be very careful about how these tests are written. Each thread may
// only have one struct rseq registration, which may be done automatically at
// thread start (as of 2019-11-13, glibc does *not* support rseq and thus does
// not do so, but other libraries do).
//
// Testing of rseq is thus done primarily in a child process with no
// registration. This means exec'ing a nostdlib binary, as rseq registration can
// only be cleared by execve (or knowing the old rseq address), and glibc (based
// on the current unmerged patches) register rseq before calling main()).

int RSeq(struct rseq* rseq, uint32_t rseq_len, int flags, uint32_t sig) {
  return syscall(kRseqSyscall, rseq, rseq_len, flags, sig);
}

// Returns true if this kernel supports the rseq syscall.
PosixErrorOr<bool> RSeqSupported() {
  // We have to be careful here, there are three possible cases:
  //
  // 1. rseq is not supported -> ENOSYS
  // 2. rseq is supported and not registered -> success, but we should
  //    unregister.
  // 3. rseq is supported and registered -> EINVAL (most likely).

  // The only validation done on new registrations is that rseq is aligned and
  // writable.
  rseq rseq = {};
  int ret = RSeq(&rseq, sizeof(rseq), 0, 0);
  if (ret == 0) {
    // Successfully registered, rseq is supported. Unregister.
    ret = RSeq(&rseq, sizeof(rseq), kRseqFlagUnregister, 0);
    if (ret != 0) {
      return PosixError(errno);
    }
    return true;
  }

  switch (errno) {
    case ENOSYS:
      // Not supported.
      return false;
    case EINVAL:
      // Supported, but already registered. EINVAL returned because we provided
      // a different address.
      return true;
    default:
      // Unknown error.
      return PosixError(errno);
  }
}

constexpr char kRseqBinary[] = "test/syscalls/linux/rseq/rseq";

void RunChildTest(std::string test_case, int want_status) {
  std::string path = RunfilePath(kRseqBinary);

  pid_t child_pid = -1;
  int execve_errno = 0;
  auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
      ForkAndExec(path, {path, test_case}, {}, &child_pid, &execve_errno));

  ASSERT_GT(child_pid, 0);
  ASSERT_EQ(execve_errno, 0);

  int status = 0;
  ASSERT_THAT(RetryEINTR(waitpid)(child_pid, &status, 0), SyscallSucceeds());
  ASSERT_THAT(status, AnyOf(Eq(want_status), Eq(128 + want_status)));
}

// Test that rseq must be aligned.
TEST(RseqTest, Unaligned) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestUnaligned, 0);
}

// Sanity test that registration works.
TEST(RseqTest, Register) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestRegister, 0);
}

// Registration can't be done twice.
TEST(RseqTest, DoubleRegister) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestDoubleRegister, 0);
}

// Registration can be done again after unregister.
TEST(RseqTest, RegisterUnregister) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestRegisterUnregister, 0);
}

// The pointer to rseq must match on register/unregister.
TEST(RseqTest, UnregisterDifferentPtr) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestUnregisterDifferentPtr, 0);
}

// The signature must match on register/unregister.
TEST(RseqTest, UnregisterDifferentSignature) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestUnregisterDifferentSignature, 0);
}

// The CPU ID is initialized.
TEST(RseqTest, CPU) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));

  RunChildTest(kRseqTestCPU, 0);
}

// Critical section is eventually aborted.
TEST(RseqTest, Abort) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  // TODO(b/456832928): Re-enable after making this test compatible with
  // platform/KVM
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestAbort, 0);
}

// Abort may be before the critical section.
TEST(RseqTest, AbortBefore) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  // TODO(b/456832928): Re-enable after making this test compatible with
  // platform/KVM
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestAbortBefore, 0);
}

// Signature must match.
TEST(RseqTest, AbortSignature) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  // TODO(b/456832928): Re-enable after making this test compatible with
  // platform/KVM
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestAbortSignature, SIGSEGV);
}

// Abort must not be in the critical section.
TEST(RseqTest, AbortPreCommit) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  // TODO(b/456832928): Re-enable after making this test compatible with
  // platform/KVM
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestAbortPreCommit, SIGSEGV);
}

// rseq.rseq_cs is cleared on abort.
TEST(RseqTest, AbortClearsCS) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  // TODO(b/456832928): Re-enable after making this test compatible with
  // platform/KVM
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestAbortClearsCS, 0);
}

// rseq.rseq_cs is cleared on abort outside of critical section.
TEST(RseqTest, InvalidAbortClearsCS) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  SKIP_IF(GvisorPlatform() == Platform::kKVM);

  RunChildTest(kRseqTestInvalidAbortClearsCS, 0);
}

// NOTE(b/326665974): Needs implementation of clone() in rseq/start_arm64.S.
#if defined(__x86_64__)
TEST(RseqTest, MembarrierResetsCpuIdStart) {
  SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(RSeqSupported()));
  int cmds = syscall(SYS_membarrier, MEMBARRIER_CMD_QUERY, 0);
  SKIP_IF(cmds < 0);
  SKIP_IF((cmds & MEMBARRIER_CMD_PRIVATE_EXPEDITED_RSEQ) == 0);

  RunChildTest(kRseqTestMembarrierResetsCpuIdStart, 0);
}
#endif

}  // namespace

}  // namespace testing
}  // namespace gvisor
