// Copyright 2014 The Crashpad Authors. All rights reserved.
//
// 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 "client/simulate_crash.h"

#include <mach/mach.h>
#include <string.h>

#include "base/basictypes.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "gtest/gtest.h"
#include "test/mac/mach_errors.h"
#include "test/mac/mach_multiprocess.h"
#include "util/mach/exc_server_variants.h"
#include "util/mach/exception_behaviors.h"
#include "util/mach/exception_ports.h"
#include "util/mach/mach_extensions.h"
#include "util/mach/mach_message.h"
#include "util/mach/mach_message_server.h"
#include "util/mach/symbolic_constants_mach.h"

namespace crashpad {
namespace test {
namespace {

class TestSimulateCrashMac final : public MachMultiprocess,
                                   public UniversalMachExcServer::Interface {
 public:
  // Defines which targets the child should set an EXC_CRASH exception handler
  // for.
  enum ExceptionPortsTarget {
    // The child should clear its EXC_CRASH handler for both its task and thread
    // targets. SimulateCrash() will attempt to deliver the exception to the
    // host target, which will fail if not running as root. In any case, the
    // parent should not expect to receive any exception message from the child.
    kExceptionPortsTargetNone = 0,

    // The child will set an EXC_CRASH handler for its task target, and clear it
    // for its thread target. The parent runs an exception server to receive
    // the child’s simulated crash message.
    kExceptionPortsTargetTask,

    // The child will set an EXC_CRASH handler for its thread target, and clear
    // it for its task target. The parent runs an exception server to receive
    // the child’s simulated crash message.
    kExceptionPortsTargetThread,

    // The child sets an EXC_CRASH handler for both its task and thread targets.
    // The parent runs an exception server to receive the message expected to be
    // delivered to the thread target, but returns an error code. The child will
    // then fall back to trying the server registered for the task target,
    // sending a second message to the parent. The server in the parent will
    // handle this one successfully.
    kExceptionPortsTargetBoth,
  };

  TestSimulateCrashMac(ExceptionPortsTarget target,
                       exception_behavior_t behavior,
                       thread_state_flavor_t flavor)
      : MachMultiprocess(),
        UniversalMachExcServer::Interface(),
        target_(target),
        behavior_(behavior),
        flavor_(flavor),
        succeed_(true) {
  }

  ~TestSimulateCrashMac() {}

  // UniversalMachExcServer::Interface:
  kern_return_t CatchMachException(exception_behavior_t behavior,
                                   exception_handler_t exception_port,
                                   thread_t thread,
                                   task_t task,
                                   exception_type_t exception,
                                   const mach_exception_data_type_t* code,
                                   mach_msg_type_number_t code_count,
                                   thread_state_flavor_t* flavor,
                                   const natural_t* old_state,
                                   mach_msg_type_number_t old_state_count,
                                   thread_state_t new_state,
                                   mach_msg_type_number_t* new_state_count,
                                   const mach_msg_trailer_t* trailer,
                                   bool* destroy_complex_request) override {
    *destroy_complex_request = true;

    // Check the entire exception message, because most or all of it was
    // generated by SimulateCrash() instead of the kernel.

    EXPECT_EQ(behavior_, behavior);
    EXPECT_EQ(LocalPort(), exception_port);
    if (ExceptionBehaviorHasIdentity(behavior)) {
      EXPECT_NE(THREAD_NULL, thread);
      EXPECT_EQ(ChildTask(), task);
    } else {
      EXPECT_EQ(THREAD_NULL, thread);
      EXPECT_EQ(TASK_NULL, task);
    }
    EXPECT_EQ(kMachExceptionSimulated, exception);
    EXPECT_EQ(2u, code_count);
    if (code_count >= 1) {
      EXPECT_EQ(0, code[0]);
    }
    if (code_count >= 2) {
      EXPECT_EQ(0, code[1]);
    }
    if (!ExceptionBehaviorHasState(behavior)) {
      EXPECT_EQ(THREAD_STATE_NONE, *flavor);
    } else {
      EXPECT_EQ(flavor_, *flavor);
      switch (*flavor) {
#if defined(ARCH_CPU_X86_FAMILY)
        case x86_THREAD_STATE: {
          EXPECT_EQ(x86_THREAD_STATE_COUNT, old_state_count);
          const x86_thread_state* state =
              reinterpret_cast<const x86_thread_state*>(old_state);
          switch (state->tsh.flavor) {
            case x86_THREAD_STATE32:
              EXPECT_EQ(implicit_cast<int>(x86_THREAD_STATE32_COUNT),
                        state->tsh.count);
              break;
            case x86_THREAD_STATE64:
              EXPECT_EQ(implicit_cast<int>(x86_THREAD_STATE64_COUNT),
                        state->tsh.count);
              break;
            default:
              ADD_FAILURE() << "unexpected tsh.flavor " << state->tsh.flavor;
              break;
          }
          break;
        }
        case x86_FLOAT_STATE: {
          EXPECT_EQ(x86_FLOAT_STATE_COUNT, old_state_count);
          const x86_float_state* state =
              reinterpret_cast<const x86_float_state*>(old_state);
          switch (state->fsh.flavor) {
            case x86_FLOAT_STATE32:
              EXPECT_EQ(implicit_cast<int>(x86_FLOAT_STATE32_COUNT),
                        state->fsh.count);
              break;
            case x86_FLOAT_STATE64:
              EXPECT_EQ(implicit_cast<int>(x86_FLOAT_STATE64_COUNT),
                        state->fsh.count);
              break;
            default:
              ADD_FAILURE() << "unexpected fsh.flavor " << state->fsh.flavor;
              break;
          }
          break;
        }
        case x86_DEBUG_STATE: {
          EXPECT_EQ(x86_DEBUG_STATE_COUNT, old_state_count);
          const x86_debug_state* state =
              reinterpret_cast<const x86_debug_state*>(old_state);
          switch (state->dsh.flavor) {
            case x86_DEBUG_STATE32:
              EXPECT_EQ(implicit_cast<int>(x86_DEBUG_STATE32_COUNT),
                        state->dsh.count);
              break;
            case x86_DEBUG_STATE64:
              EXPECT_EQ(implicit_cast<int>(x86_DEBUG_STATE64_COUNT),
                        state->dsh.count);
              break;
            default:
              ADD_FAILURE() << "unexpected dsh.flavor " << state->dsh.flavor;
              break;
          }
          break;
        }
        case x86_THREAD_STATE32:
          EXPECT_EQ(x86_THREAD_STATE32_COUNT, old_state_count);
          break;
        case x86_FLOAT_STATE32:
          EXPECT_EQ(x86_FLOAT_STATE32_COUNT, old_state_count);
          break;
        case x86_DEBUG_STATE32:
          EXPECT_EQ(x86_DEBUG_STATE32_COUNT, old_state_count);
          break;
        case x86_THREAD_STATE64:
          EXPECT_EQ(x86_THREAD_STATE64_COUNT, old_state_count);
          break;
        case x86_FLOAT_STATE64:
          EXPECT_EQ(x86_FLOAT_STATE64_COUNT, old_state_count);
          break;
        case x86_DEBUG_STATE64:
          EXPECT_EQ(x86_DEBUG_STATE64_COUNT, old_state_count);
          break;
#else
#error Port to your CPU architecture
#endif
        default:
          ADD_FAILURE() << "unexpected flavor " << *flavor;
          break;
      }

      // Attempt to set a garbage thread state, which would cause the child to
      // crash inside SimulateCrash() if it actually succeeded. This tests that
      // SimulateCrash() ignores new_state instead of attempting to set the
      // state as the kernel would do. This operates in conjunction with the
      // |true| argument to ExcServerSuccessfulReturnValue() below.
      *new_state_count = old_state_count;
      size_t new_state_size = sizeof(natural_t) * old_state_count;
      memset(new_state, 0xa5, new_state_size);
    }

    if (!succeed_) {
      // The client has registered EXC_CRASH handlers for both its thread and
      // task targets, and sent a simulated exception message to its
      // thread-level EXC_CRASH handler. To test that it will fall back to
      // trying the task-level EXC_CRASH handler, return a failure code, which
      // should cause SimulateCrash() to try the next target.
      EXPECT_EQ(kExceptionPortsTargetBoth, target_);
      return KERN_ABORTED;
    }

    return ExcServerSuccessfulReturnValue(behavior, true);
  }

 private:
  // MachMultiprocess:

  void MachMultiprocessParent() override {
    if (target_ == kExceptionPortsTargetNone) {
      // The child does not have any EXC_CRASH handlers registered for its
      // thread or task targets, so no exception message is expected to be
      // generated. Don’t run the server at all.
      return;
    }

    UniversalMachExcServer universal_mach_exc_server(this);

    mach_msg_return_t mr;
    if (target_ == kExceptionPortsTargetBoth) {
      // The client has registered EXC_CRASH handlers for both its thread and
      // task targets. Run a server that will return a failure code when the
      // exception message is sent to the thread target, which will cause the
      // client to fall back to the task target and send another message.
      succeed_ = false;
      mr = MachMessageServer::Run(&universal_mach_exc_server,
                                  LocalPort(),
                                  MACH_MSG_OPTION_NONE,
                                  MachMessageServer::kOneShot,
                                  MachMessageServer::kReceiveLargeError,
                                  kMachMessageTimeoutWaitIndefinitely);
      EXPECT_EQ(MACH_MSG_SUCCESS, mr)
          << MachErrorMessage(mr, "MachMessageServer::Run");
    }

    succeed_ = true;
    mr = MachMessageServer::Run(&universal_mach_exc_server,
                                LocalPort(),
                                MACH_MSG_OPTION_NONE,
                                MachMessageServer::kOneShot,
                                MachMessageServer::kReceiveLargeError,
                                kMachMessageTimeoutWaitIndefinitely);
    EXPECT_EQ(MACH_MSG_SUCCESS, mr)
        << MachErrorMessage(mr, "MachMessageServer::Run");
  }

  void MachMultiprocessChild() override {
    bool task_valid = target_ == kExceptionPortsTargetTask ||
                      target_ == kExceptionPortsTargetBoth;
    ExceptionPorts task_exception_ports(ExceptionPorts::kTargetTypeTask,
                                        TASK_NULL);
    ASSERT_TRUE(task_exception_ports.SetExceptionPort(
        EXC_MASK_CRASH,
        task_valid ? RemotePort() : MACH_PORT_NULL,
        behavior_,
        flavor_));

    bool thread_valid = target_ == kExceptionPortsTargetThread ||
                        target_ == kExceptionPortsTargetBoth;
    ExceptionPorts thread_exception_ports(ExceptionPorts::kTargetTypeThread,
                                          THREAD_NULL);
    ASSERT_TRUE(thread_exception_ports.SetExceptionPort(
        EXC_MASK_CRASH,
        thread_valid ? RemotePort() : MACH_PORT_NULL,
        behavior_,
        flavor_));

    CRASHPAD_SIMULATE_CRASH();
  }

  ExceptionPortsTarget target_;
  exception_behavior_t behavior_;
  thread_state_flavor_t flavor_;
  bool succeed_;

  DISALLOW_COPY_AND_ASSIGN(TestSimulateCrashMac);
};

TEST(SimulateCrash, SimulateCrash) {
  const TestSimulateCrashMac::ExceptionPortsTarget kTargets[] = {
      TestSimulateCrashMac::kExceptionPortsTargetNone,
      TestSimulateCrashMac::kExceptionPortsTargetTask,
      TestSimulateCrashMac::kExceptionPortsTargetThread,
      TestSimulateCrashMac::kExceptionPortsTargetBoth,
  };

  const exception_behavior_t kBehaviors[] = {
      EXCEPTION_DEFAULT,
      EXCEPTION_STATE,
      EXCEPTION_STATE_IDENTITY,
      EXCEPTION_DEFAULT | kMachExceptionCodes,
      EXCEPTION_STATE | kMachExceptionCodes,
      EXCEPTION_STATE_IDENTITY | kMachExceptionCodes,
  };

  const thread_state_flavor_t kFlavors[] = {
#if defined(ARCH_CPU_X86_FAMILY)
      x86_THREAD_STATE,
      x86_FLOAT_STATE,
      x86_DEBUG_STATE,
#if defined(ARCH_CPU_X86)
      x86_THREAD_STATE32,
      x86_FLOAT_STATE32,
      x86_DEBUG_STATE32,
#elif defined(ARCH_CPU_X86_64)
      x86_THREAD_STATE64,
      x86_FLOAT_STATE64,
      x86_DEBUG_STATE64,
#endif
#else
#error Port to your CPU architecture
#endif
  };

  for (size_t target_index = 0;
       target_index < arraysize(kTargets);
       ++target_index) {
    TestSimulateCrashMac::ExceptionPortsTarget target = kTargets[target_index];
    SCOPED_TRACE(base::StringPrintf(
        "target_index %zu, target %d", target_index, target));

    for (size_t behavior_index = 0;
         behavior_index < arraysize(kBehaviors);
         ++behavior_index) {
      exception_behavior_t behavior = kBehaviors[behavior_index];
      SCOPED_TRACE(base::StringPrintf(
          "behavior_index %zu, behavior %s",
          behavior_index,
          ExceptionBehaviorToString(behavior, kUseFullName | kUnknownIsNumeric)
              .c_str()));

      if (!ExceptionBehaviorHasState(behavior)) {
        TestSimulateCrashMac test_simulate_crash_mac(
            target, behavior, THREAD_STATE_NONE);
        test_simulate_crash_mac.Run();
      } else {
        for (size_t flavor_index = 0;
             flavor_index < arraysize(kFlavors);
             ++flavor_index) {
          thread_state_flavor_t flavor = kFlavors[flavor_index];
          SCOPED_TRACE(base::StringPrintf(
              "flavor_index %zu, flavor %s",
              flavor_index,
              ThreadStateFlavorToString(
                  flavor, kUseFullName | kUnknownIsNumeric).c_str()));

          TestSimulateCrashMac test_simulate_crash_mac(
              target, behavior, flavor);
          test_simulate_crash_mac.Run();
        }
      }
    }
  }
}

}  // namespace
}  // namespace test
}  // namespace crashpad