blob: 5755f545a1469ed91e8795d8dc7033401d5d09f5 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <crashsvc/crashsvc.h>
#include <threads.h>
#include <zircon/syscalls/exception.h>
#include <zircon/syscalls/object.h>
#include <memory>
#include <fs/pseudo-dir.h>
#include <fs/service.h>
#include <fs/synchronous-vfs.h>
#include <fuchsia/crash/c/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/wait.h>
#include <lib/fidl-async/bind.h>
#include <lib/zx/event.h>
#include <lib/zx/job.h>
#include <lib/zx/process.h>
#include <lib/zx/thread.h>
#include <lib/zx/vmar.h>
#include <mini-process/mini-process.h>
#include <zxtest/zxtest.h>
namespace {
TEST(crashsvc, StartAndStop) {
zx::job job;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &job));
thrd_t thread;
zx::job job_copy;
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), ZX_HANDLE_INVALID, &thread));
ASSERT_OK(job.kill());
int exit_code = -1;
EXPECT_EQ(thrd_join(thread, &exit_code), thrd_success);
EXPECT_EQ(exit_code, 0);
}
constexpr char kTaskName[] = "crashsvc-test";
constexpr uint32_t kTaskNameLen = sizeof(kTaskName) - 1;
// Creates a mini-process under |job|.
void CreateMiniProcess(const zx::job& job, zx::process* process, zx::thread* thread,
zx::channel* command_channel) {
zx::vmar vmar;
ASSERT_OK(zx::process::create(job, kTaskName, kTaskNameLen, 0, process, &vmar));
ASSERT_OK(zx::thread::create(*process, kTaskName, kTaskNameLen, 0, thread));
zx::event event;
ASSERT_OK(zx::event::create(0, &event));
ASSERT_OK(start_mini_process_etc(process->get(), thread->get(), vmar.get(), event.release(),
command_channel->reset_and_get_address()));
}
// Creates a mini-process under |job| and tells it to crash.
void CreateAndCrashProcess(const zx::job& job, zx::process* process, zx::thread* thread) {
zx::channel command_channel;
ASSERT_NO_FATAL_FAILURES(CreateMiniProcess(job, process, thread, &command_channel));
// Use mini_process_cmd_send() here to send but not wait for a response
// so we can handle the exception.
printf("Intentionally crashing test thread '%s', the following dump is expected\n", kTaskName);
ASSERT_OK(mini_process_cmd_send(command_channel.get(), MINIP_CMD_BUILTIN_TRAP));
}
// Creates a mini-process under |job| and tells it to request a backtrace.
// Blocks until the mini-process thread has successfully resumed.
void CreateAndBacktraceProcess(const zx::job& job, zx::process* process, zx::thread* thread) {
zx::channel command_channel;
ASSERT_NO_FATAL_FAILURES(CreateMiniProcess(job, process, thread, &command_channel));
// Use mini_process_cmd() here to send and block until we get a response.
printf("Intentionally dumping test thread '%s', the following dump is expected\n", kTaskName);
ASSERT_OK(mini_process_cmd(command_channel.get(), MINIP_CMD_BACKTRACE_REQUEST, nullptr));
}
TEST(crashsvc, ThreadCrashNoAnalyzer) {
zx::job parent_job, job;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
// Catch exceptions on |parent_job| so that the crashing thread doesn't go
// all the way up to the system crashsvc when our local crashsvc is done.
zx::channel exception_channel;
ASSERT_OK(parent_job.create_exception_channel(0, &exception_channel));
thrd_t cthread;
zx::job job_copy;
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), ZX_HANDLE_INVALID, &cthread));
zx::process process;
zx::thread thread;
ASSERT_NO_FATAL_FAILURES(CreateAndCrashProcess(job, &process, &thread));
// crashsvc should pass exception handling up the chain when done. Once we
// get the exception, kill the job which will stop exception handling and
// cause the crashsvc thread to exit.
ASSERT_OK(exception_channel.wait_one(ZX_CHANNEL_READABLE, zx::time::infinite(), nullptr));
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
TEST(crashsvc, ThreadBacktraceNoAnalyzer) {
zx::job parent_job, job;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
zx::channel exception_channel;
ASSERT_OK(parent_job.create_exception_channel(0, &exception_channel));
thrd_t cthread;
zx::job job_copy;
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), ZX_HANDLE_INVALID, &cthread));
zx::process process;
zx::thread thread;
ASSERT_NO_FATAL_FAILURES(CreateAndBacktraceProcess(job, &process, &thread));
// The backtrace request exception should not make it out of crashsvc.
ASSERT_EQ(exception_channel.wait_one(ZX_CHANNEL_READABLE, zx::time(0), nullptr),
ZX_ERR_TIMED_OUT);
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
// Returns the object's koid, or ZX_KOID_INVALID and marks test failure if
// get_info() fails.
template <typename T>
zx_koid_t GetKoid(const zx::object<T>& object) {
zx_info_handle_basic_t info;
info.koid = ZX_KOID_INVALID;
EXPECT_OK(object.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr));
return info.koid;
}
// Provides FIDL stubs for fuchsia::crash::Analyzer.
class CrashAnalyzerStub {
public:
enum class Behavior {
kSuccess, // Return ZX_OK.
kError // Simulate analyzer failure by returning an error.
};
// Sets the behavior to use on the next OnNativeException() call. |process|
// and |thread| are the tasks we expect to be given from crashsvc.
void SetBehavior(Behavior behavior, const zx::process& process, const zx::thread& thread) {
behavior_ = behavior;
process_koid_ = GetKoid(process);
thread_koid_ = GetKoid(thread);
ASSERT_NE(process_koid_, ZX_KOID_INVALID);
ASSERT_NE(thread_koid_, ZX_KOID_INVALID);
}
// Creates a virtual file system serving this analyzer at the appropriate path.
void Serve(async_dispatcher_t* dispatcher, std::unique_ptr<fs::SynchronousVfs>* vfs,
zx::channel* client) {
auto directory = fbl::MakeRefCounted<fs::PseudoDir>();
auto node = fbl::MakeRefCounted<fs::Service>([dispatcher, this](zx::channel channel) {
auto dispatch = reinterpret_cast<fidl_dispatch_t*>(fuchsia_crash_Analyzer_dispatch);
return fidl_bind(dispatcher, channel.release(), dispatch, this,
&CrashAnalyzerStub::kOps);
});
ASSERT_OK(directory->AddEntry(fuchsia_crash_Analyzer_Name, std::move(node)));
zx::channel server;
ASSERT_OK(zx::channel::create(0u, client, &server));
*vfs = std::make_unique<fs::SynchronousVfs>(dispatcher);
ASSERT_OK((*vfs)->ServeDirectory(std::move(directory), std::move(server)));
}
// Returns the number of times OnNativeException() has fired.
int on_native_exception_count() const { return on_native_exception_count_; }
private:
static zx_status_t OnNativeExceptionWrapper(void* ctx, zx_handle_t process, zx_handle_t thread,
fidl_txn_t* txn) {
return reinterpret_cast<CrashAnalyzerStub*>(ctx)->OnNativeException(zx::process(process),
zx::thread(thread),
txn);
}
zx_status_t OnNativeException(zx::process process, zx::thread thread, fidl_txn_t* txn) {
++on_native_exception_count_;
// Make sure crashsvc passed us the correct task handles.
EXPECT_EQ(process_koid_, GetKoid(process));
EXPECT_EQ(thread_koid_, GetKoid(thread));
// Build a reply corresponding to our desired behavior.
fuchsia_crash_Analyzer_OnNativeException_Result result;
if (behavior_ == Behavior::kSuccess) {
result.tag = fuchsia_crash_Analyzer_OnNativeException_ResultTag_response;
} else {
result.tag = fuchsia_crash_Analyzer_OnNativeException_ResultTag_err;
result.err = ZX_ERR_BAD_STATE;
}
zx_status_t status = fuchsia_crash_AnalyzerOnNativeException_reply(txn, &result);
EXPECT_EQ(status, ZX_OK);
return status;
}
static constexpr fuchsia_crash_Analyzer_ops_t kOps = {
.OnNativeException = OnNativeExceptionWrapper,
.OnManagedRuntimeException = nullptr,
.OnKernelPanicCrashLog = nullptr};
Behavior behavior_;
zx_koid_t process_koid_ = ZX_KOID_INVALID;
zx_koid_t thread_koid_ = ZX_KOID_INVALID;
int on_native_exception_count_ = 0;
};
// Creates a new thread, crashes it, and processes the resulting Analyzer FIDL
// message from crashsvc according to |behavior|.
//
// |parent_job| is used to catch exceptions after they've been analyzed on |job|
// so that they don't bubble up to the real crashsvc.
void AnalyzeCrash(CrashAnalyzerStub* analyzer, async::Loop* loop, const zx::job& parent_job,
const zx::job& job, CrashAnalyzerStub::Behavior behavior) {
zx::channel exception_channel;
ASSERT_OK(parent_job.create_exception_channel(0, &exception_channel));
zx::process process;
zx::thread thread;
ASSERT_NO_FATAL_FAILURES(CreateAndCrashProcess(job, &process, &thread));
ASSERT_NO_FATAL_FAILURES(analyzer->SetBehavior(behavior, process, thread));
// Run the loop until the exception filters up to our job handler.
async::Wait wait(exception_channel.get(), ZX_CHANNEL_READABLE, [&loop](...) {
loop->Quit();
});
ASSERT_OK(wait.Begin(loop->dispatcher()));
ASSERT_EQ(loop->Run(), ZX_ERR_CANCELED);
ASSERT_OK(loop->ResetQuit());
// The exception is now waiting in |exception_channel|, kill the process
// before the channel closes to keep it from propagating further.
ASSERT_OK(process.kill());
ASSERT_OK(process.wait_one(ZX_PROCESS_TERMINATED, zx::time::infinite(), nullptr));
}
TEST(crashsvc, ThreadCrashAnalyzerSuccess) {
async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
std::unique_ptr<fs::SynchronousVfs> vfs;
zx::channel client;
CrashAnalyzerStub analyzer;
ASSERT_NO_FATAL_FAILURES(analyzer.Serve(loop.dispatcher(), &vfs, &client));
zx::job parent_job, job, job_copy;
thrd_t cthread;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), client.get(), &cthread));
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kSuccess));
EXPECT_EQ(1, analyzer.on_native_exception_count());
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
TEST(crashsvc, ThreadCrashAnalyzerFailure) {
async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
std::unique_ptr<fs::SynchronousVfs> vfs;
zx::channel client;
CrashAnalyzerStub analyzer;
ASSERT_NO_FATAL_FAILURES(analyzer.Serve(loop.dispatcher(), &vfs, &client));
zx::job parent_job, job, job_copy;
thrd_t cthread;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), client.get(), &cthread));
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kError));
EXPECT_EQ(1, analyzer.on_native_exception_count());
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
TEST(crashsvc, MultipleThreadCrashAnalyzer) {
async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
std::unique_ptr<fs::SynchronousVfs> vfs;
zx::channel client;
CrashAnalyzerStub analyzer;
ASSERT_NO_FATAL_FAILURES(analyzer.Serve(loop.dispatcher(), &vfs, &client));
zx::job parent_job, job, job_copy;
thrd_t cthread;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), client.get(), &cthread));
// Make sure crashsvc continues to loop no matter what the analyzer does.
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kSuccess));
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kError));
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kSuccess));
ASSERT_NO_FATAL_FAILURES(AnalyzeCrash(&analyzer, &loop, parent_job, job,
CrashAnalyzerStub::Behavior::kError));
EXPECT_EQ(4, analyzer.on_native_exception_count());
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
TEST(crashsvc, ThreadBacktraceAnalyzer) {
async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
std::unique_ptr<fs::SynchronousVfs> vfs;
zx::channel client;
CrashAnalyzerStub analyzer;
ASSERT_NO_FATAL_FAILURES(analyzer.Serve(loop.dispatcher(), &vfs, &client));
zx::job parent_job, job, job_copy;
thrd_t cthread;
ASSERT_OK(zx::job::create(*zx::job::default_job(), 0, &parent_job));
ASSERT_OK(zx::job::create(parent_job, 0, &job));
ASSERT_OK(job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_copy));
ASSERT_OK(start_crashsvc(std::move(job_copy), client.get(), &cthread));
zx::process process;
zx::thread thread;
ASSERT_NO_FATAL_FAILURES(CreateAndBacktraceProcess(job, &process, &thread));
// Thread backtrace requests shouldn't be sent out to the analyzer.
EXPECT_EQ(0, analyzer.on_native_exception_count());
ASSERT_OK(job.kill());
EXPECT_EQ(thrd_join(cthread, nullptr), thrd_success);
}
} // namespace