blob: 5b7af4a6dab7a4191ac7eda72cdc973318299a0a [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <lib/boot-options/boot-options.h>
#include <lib/fit/defer.h>
#include <lib/fxt/serializer.h>
#include <lib/thread_sampler/per_cpu_state.h>
#include <lib/thread_sampler/thread_sampler.h>
#include <lib/zx/time.h>
#include <fbl/array.h>
#include <kernel/dpc.h>
#include <kernel/event.h>
#include <kernel/mp.h>
#include <kernel/spinlock.h>
#include <lk/init.h>
#include <object/io_buffer_dispatcher.h>
#include <object/process_dispatcher.h>
KernelHandle<sampler::ThreadSamplerDispatcher> sampler::ThreadSamplerDispatcher::gThreadSampler_;
zx::result<> sampler::ThreadSamplerDispatcher::CreateImpl(
const zx_sampler_config_t& config, KernelHandle<ThreadSamplerDispatcher>& read_handle,
KernelHandle<ThreadSamplerDispatcher>& write_handle) {
const size_t num_cpus = percpu::processor_count();
fbl::AllocChecker ac;
// Start by creating the buffer, a la IoBufferDispatcher::Create
auto holder0 = fbl::MakeRefCountedChecked<PeerHolder<IoBufferDispatcher>>(&ac);
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
auto holder1 = holder0;
fbl::RefPtr<SharedIobState> shared_regions = fbl::AdoptRef(new (&ac) SharedIobState{
.regions = nullptr,
});
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
KernelHandle write_dispatcher{fbl::AdoptRef(
new (&ac) ThreadSamplerDispatcher(ktl::move(holder0), IobEndpointId::Ep0, shared_regions))};
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
KernelHandle read_dispatcher{fbl::AdoptRef(
new (&ac) ThreadSamplerDispatcher(ktl::move(holder1), IobEndpointId::Ep1, shared_regions))};
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
IoBufferDispatcher::RegionArray configs{&ac, num_cpus};
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
for (size_t i = 0; i < configs.size(); i++) {
configs[i] =
zx_iob_region_t{.type = ZX_IOB_REGION_TYPE_PRIVATE,
.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ | ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE |
ZX_IOB_ACCESS_EP1_CAN_MAP_READ,
.size = config.buffer_size,
.discipline = zx_iob_discipline_t{.type = ZX_IOB_DISCIPLINE_TYPE_NONE},
.private_region = zx_iob_region_private_t{
.options = 0,
}};
}
{
Guard<CriticalMutex> guard{&shared_regions->state_lock};
zx::result<fbl::Array<IobRegionVariant>> regions = CreateRegions(
configs, write_dispatcher.dispatcher().get(), read_dispatcher.dispatcher().get());
if (regions.is_error()) {
return regions.take_error();
}
shared_regions->regions = ktl::move(*regions);
}
read_dispatcher.dispatcher()->InitPeer(write_dispatcher.dispatcher());
write_dispatcher.dispatcher()->InitPeer(read_dispatcher.dispatcher());
// In addition to the work done a la IoBufferDispatcher::Create, we also need to map and pin
// each of the buffers so that the kernel can safely write to them.
fbl::Array<PinnedVmObject> pinned_buffers = fbl::MakeArray<PinnedVmObject>(&ac, num_cpus);
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
// Allocate and pin each buffer so the kernel can write to it
for (unsigned i = 0; i < num_cpus; i++) {
fbl::RefPtr vmo = write_dispatcher.dispatcher()->GetVmo(i);
const uint64_t vmo_offset = 0;
const size_t size = config.buffer_size;
if (zx_status_t status =
PinnedVmObject::Create(vmo, vmo_offset, size, true, &pinned_buffers[i]);
status != ZX_OK) {
dprintf(INFO, "Failed to make pin: %d\n", status);
return zx::error(status);
}
}
write_dispatcher.dispatcher()->per_cpu_state_ =
fbl::MakeArray<internal::PerCpuState>(&ac, num_cpus);
if (!ac.check()) {
return zx::error(ZX_ERR_NO_MEMORY);
}
// Even though the buffer is per_cpu, we are fine to set up each cpu state here on a single cpu.
// When we start sampling, we call mp_sync_exec which will synchronize the written
// per_cpu_states.
for (unsigned i = 0; i < num_cpus; i++) {
if (zx::result<> setup_result = write_dispatcher.dispatcher()->per_cpu_state_[i].SetUp(
config, ktl::move(pinned_buffers[i]));
setup_result.is_error()) {
return setup_result.take_error();
}
}
read_handle = ktl::move(read_dispatcher);
write_handle = ktl::move(write_dispatcher);
return zx::ok();
}
zx::result<> sampler::ThreadSamplerDispatcher::StartImpl() TA_EXCL(get_lock()) {
Guard<CriticalMutex> guard(get_lock());
if (state_ != SamplingState::Configured) {
return zx::error(ZX_ERR_BAD_STATE);
}
DEBUG_ASSERT(!per_cpu_state_.empty());
DEBUG_ASSERT(GetEndpointId() == IobEndpointId::Ep0);
for (internal::PerCpuState& state : per_cpu_state_) {
state.EnableWrites();
}
mp_sync_exec(
MP_IPI_TARGET_ALL, 0,
[](void* s) { reinterpret_cast<sampler::ThreadSamplerDispatcher*>(s)->SetCurrCpuTimer(); },
this);
state_ = SamplingState::Running;
return zx::ok();
}
zx::result<> sampler::ThreadSamplerDispatcher::StopImpl() TA_EXCL(get_lock()) {
Guard<CriticalMutex> guard(get_lock());
DEBUG_ASSERT(GetEndpointId() == IobEndpointId::Ep0);
if (state_ != SamplingState::Running) {
return zx::error(ZX_ERR_BAD_STATE);
}
StopLocked();
return zx::ok();
}
void sampler::ThreadSamplerDispatcher::StopLocked() TA_REQ(get_lock()) {
DEBUG_ASSERT(GetEndpointId() == IobEndpointId::Ep0);
for (internal::PerCpuState& state : per_cpu_state_) {
state.DisableWrites();
state.CancelTimer();
}
// Some timers may not have not been able to be canceled, so we need to wait for any samples that
// have already started to finish.
zx_time_t deadline = zx_time_add_duration(current_time(), ZX_SEC(30));
for (const internal::PerCpuState& i : per_cpu_state_) {
bool pending_timers;
bool pending_writes;
do {
pending_timers = i.PendingTimer();
pending_writes = i.PendingWrites();
if (pending_timers || pending_writes) {
Thread::Current::SleepRelative(ZX_MSEC(1));
}
} while ((pending_writes || pending_timers) && (current_time() < deadline));
// We'll wait an unreasonable amount of time for the timer to finish. If the timer really
// haven't finished by this point, something has gone terribly wrong.
ZX_ASSERT(!pending_writes || !pending_timers);
}
// At this point, there are no longer pending writes. There may still be threads:
//
// 1) signaled to be sampled but haven't reached ProcessPendingSignals yet, or
// 2) are mid taking a sample but haven't yet reserved a PendingWrite
//
// For 1): Such threads will block on on getting the dispatcher lock which we currently hold to
// read the state. When they acquire it, they will see that the session is no longer running and
// skip taking a sample.
//
// For 2): Threads will check the PerCpuState and see that writes are disabled and will skip
// writing the sample. While taking a sample, threads have taken an fbl::RefPtr to the sampling
// state so that the PerCpuStates are not at risk of being destroyed.
state_ = SamplingState::Configured;
}
zx::result<> sampler::ThreadSamplerDispatcher::SampleThreadImpl(zx_koid_t pid, zx_koid_t tid,
GeneralRegsSource source,
void* gregs,
uint64_t sampler_koid) {
DEBUG_ASSERT(GetEndpointId() == IobEndpointId::Ep0);
// We are going to attempt a usercopy below which might fault, so interrupts cannot be disabled.
DEBUG_ASSERT(!arch_ints_disabled());
// We need to be a little bit careful here because we could be racing with a Stop operation. The
// Stop operation:
//
// 1) Disables Writes
// 2) Cancels each Timer
// 3) Waits for all PendingWrites to finish
//
// It does this while holding the ThreadSamplerDispatcher lock. This means if SetPendingWrite and
// then attempt to obtain the ThreadSamplerDispatcher lock, we could deadlock.
//
// Instead, we'll do a single enabled check here before attempting to read the stack, which will
// take some time. Once we've collected our data and are ready to write out, we'll
// SetPendingWrite to hold onto the buffers for the duration of the write.
//
// If we find that writes are enabled, we are safe to write to the buffers as
// Stop will not destroy them until we lower the PendingWrite bit.
//
// If we find that writes are disabled, we throw away our sample as it's no longer safe to write
// to the buffers.
if (State() != SamplingState::Running || sampler_koid != get_koid()) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
size_t frame_num = 0;
constexpr size_t kMaxUserBacktraceSize = 64;
// We're dropping 512 bytes on the kernel stack here and we need a be careful not to overflow it.
//
// This amount of bytes _should_ be safe because SampleThread is only called during
// Thread::Current::ProcessPendingSignals which occurs directly before returning to usermode. At
// this point, the stack will be shallow.
vaddr_t bt[kMaxUserBacktraceSize]{};
vaddr_t fp = 0;
vaddr_t pc = 0;
switch (source) {
case GeneralRegsSource::None:
break;
case GeneralRegsSource::Iframe:
#ifdef __x86_64__
fp = reinterpret_cast<iframe_t*>(gregs)->rbp;
pc = reinterpret_cast<iframe_t*>(gregs)->ip;
#endif
#ifdef __aarch64__
bt[frame_num++] = (reinterpret_cast<iframe_t*>(gregs)->elr) - 4;
fp = reinterpret_cast<iframe_t*>(gregs)->r[29];
pc = (reinterpret_cast<iframe_t*>(gregs)->lr) - 4;
#endif
#ifdef __riscv
fp = reinterpret_cast<iframe_t*>(gregs)->regs.s0;
pc = reinterpret_cast<iframe_t*>(gregs)->regs.pc;
#endif
break;
#ifdef __x86_64__
case GeneralRegsSource::Syscall:
fp = reinterpret_cast<syscall_regs_t*>(gregs)->rbp;
pc = reinterpret_cast<syscall_regs_t*>(gregs)->rip;
break;
#endif
}
if (pc == 0) {
return zx::error(ZX_ERR_BAD_STATE);
}
bt[frame_num++] = pc;
while (frame_num < kMaxUserBacktraceSize) {
vaddr_t actual_fp = fp;
if (fp == 0) {
// We've reached the top of the frame pointer chain.
break;
}
// RISC-V has a nonstandard frame pointer which points to the CFA instead of
// the previous frame pointer. Since the frame pointer and return address are
// always just below the CFA, subtract 16 bytes to get to the actual frame pointer.
#if __riscv
actual_fp -= 16;
#endif
user_in_ptr<const vaddr_t> user_next_fp{reinterpret_cast<vaddr_t*>(actual_fp)};
user_in_ptr<const vaddr_t> user_pc{reinterpret_cast<vaddr_t*>(actual_fp + 8)};
// A well formed frame pointer chain ends in 0 and should never fail to copy. If a thread's
// stack is not readable or well formatted, we return an error to indicate that sampling should
// be disabled for the offending thread.
zx_status_t copy_res = user_pc.copy_from_user(&pc);
if (copy_res != ZX_OK) {
// We eat the copy_res and return ZX_ERR_NOT_SUPPORTED here and below to indicate that we
// failed to take a sample, but we might still succeed in the future. A thread may not
// necessarily have valid frame pointers at all points in execution, so don't give on this
// thread just yet.
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
if (pc == 0) {
break;
}
bt[frame_num++] = pc;
copy_res = user_next_fp.copy_from_user(&fp);
if (copy_res != ZX_OK) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
}
internal::PerCpuState& cpu_state = GetPerCpuState(arch_curr_cpu_num());
bool enabled = cpu_state.SetPendingWrite();
if (!enabled) {
// Even though we didn't successfully write a sample, we return a success result -- we should
// still try to sample the thread as it may later be scheduled on a different cpu.
return zx::ok();
}
auto d = fit::defer([&cpu_state]() { cpu_state.ResetPendingWrite(); });
constexpr fxt::StringRef<fxt::RefType::kId> empty_string{0};
const fxt::ThreadRef current_thread{pid, tid};
zx_status_t write_result =
fxt::WriteLargeBlobRecordWithMetadata(&cpu_state, current_ticks(), empty_string, empty_string,
current_thread, bt, sizeof(uint64_t) * frame_num);
if (write_result != ZX_OK) {
cpu_state.DisableWrites();
dprintf(INFO, "Buffer full, disabling writes on cpu: %u\n", arch_curr_cpu_num());
}
return zx::ok();
}
void sampler::ThreadSamplerDispatcher::OnPeerZeroHandlesLocked() {
DEBUG_ASSERT(GetEndpointId() == IobEndpointId::Ep0);
// We purposely don't emit a call to IoBufferDispatcher::OnPeerZeroHandlesLocked() here. It's used
// to coordinate and delay ZX_IOB_PEER_CLOSED until any mapped regions have been unmapped. We
// don't need the logic here. Userspace will never see a ZX_IOB_PEER_CLOSED as we will not close
// the endpoint the kernel holds until after userspace closes the last handle to their endpoint.
// When that happens, we end up here and are going to destroy our state anyways.
// The userspace end of the iobuffer has closed. Time to clean up our state
if (state_ == SamplingState::Running) {
StopLocked();
}
// After StopLocked, we have prevented further threads from accessing the per_cpu_states, and then
// waited for any threads that were accessing the states to finish.
//
// It's now safe to destroy our cpu states. This will destroy the mappings and pinnings that the
// kernel keeps to write to, but if userspace has their own mappings, those will remain continue
// to remain valid.
per_cpu_state_.reset();
state_ = SamplingState::Destroyed;
}
void sampler::ThreadSamplerDispatcher::SetCurrCpuTimer() {
GetPerCpuState(arch_curr_cpu_num()).SetTimer();
}
zx::result<KernelHandle<sampler::ThreadSamplerDispatcher>> sampler::ThreadSamplerDispatcher::Create(
const zx_sampler_config_t& config) {
{
Guard<Mutex> guard(ThreadSamplerLock::Get());
if (gThreadSampler_.dispatcher() != nullptr &&
gThreadSampler_.dispatcher()->State() !=
sampler::ThreadSamplerDispatcher::SamplingState::Destroyed) {
return zx::error(ZX_ERR_ALREADY_EXISTS);
}
}
KernelHandle<sampler::ThreadSamplerDispatcher> write_handle;
KernelHandle<sampler::ThreadSamplerDispatcher> read_handle;
zx::result res = sampler::ThreadSamplerDispatcher::CreateImpl(config, read_handle, write_handle);
if (res.is_error()) {
return res.take_error();
}
{
Guard<Mutex> guard(ThreadSamplerLock::Get());
// Ensure that someone hasn't created a new sampler since we created ours
if ((gThreadSampler_.dispatcher() != nullptr &&
gThreadSampler_.dispatcher()->State() !=
sampler::ThreadSamplerDispatcher::SamplingState::Destroyed)) {
return zx::error(ZX_ERR_ALREADY_EXISTS);
}
gThreadSampler_ = ktl::move(write_handle);
}
return zx::ok(ktl::move(read_handle));
}
zx::result<> sampler::ThreadSamplerDispatcher::Stop(const fbl::RefPtr<IoBufferDispatcher>& disp) {
Guard<Mutex> guard(ThreadSamplerLock::Get());
if (gThreadSampler_.dispatcher() == nullptr) {
return zx::error(ZX_ERR_BAD_STATE);
}
if (disp->get_koid() != gThreadSampler_.dispatcher()->get_related_koid()) {
return zx::error(ZX_ERR_BAD_HANDLE);
}
return gThreadSampler_.dispatcher()->StopImpl();
}
zx::result<> sampler::ThreadSamplerDispatcher::Start(const fbl::RefPtr<IoBufferDispatcher>& disp) {
Guard<Mutex> guard(ThreadSamplerLock::Get());
if (gThreadSampler_.dispatcher() == nullptr) {
return zx::error(ZX_ERR_BAD_STATE);
}
if (disp->get_koid() != gThreadSampler_.dispatcher()->get_related_koid()) {
return zx::error(ZX_ERR_BAD_HANDLE);
}
return gThreadSampler_.dispatcher()->StartImpl();
}
zx::result<> sampler::ThreadSamplerDispatcher::AddThread(
const fbl::RefPtr<IoBufferDispatcher>& disp, const fbl::RefPtr<ThreadDispatcher>& thread) {
Guard<Mutex> guard(ThreadSamplerLock::Get());
if (gThreadSampler_.dispatcher() == nullptr) {
return zx::error(ZX_ERR_BAD_STATE);
}
if (disp->get_koid() != gThreadSampler_.dispatcher()->get_related_koid()) {
return zx::error(ZX_ERR_BAD_HANDLE);
}
return zx::make_result(thread->EnableStackSampling(gThreadSampler_.dispatcher()->get_koid()));
}
zx::result<> sampler::ThreadSamplerDispatcher::SampleThread(zx_koid_t pid, zx_koid_t tid,
GeneralRegsSource source, void* gregs,
uint64_t sampler_koid) {
if (sampler_koid == ZX_KOID_INVALID) {
// Whatever thread this sampler_koid came from doesn't have sampling enabled.
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
fbl::RefPtr<sampler::ThreadSamplerDispatcher> sampler_ref;
{
// We hold the global ThreadSamplerLock only long enough to increase the reference count on
// the current sampler if it exists.
//
// The sampling is relatively slow and we don't want to prevent other threads on a different
// core from making progress.
//
// Once we release the ThreadSamplerLock, it may be that the global sampler is replaced with a
// new one, however the ThreadSamplerDispatcher maintains enough state that the SampleThread
// and SetCurrCpuTimers will simply short circuit and return early if that is the case.
Guard<Mutex> guard(ThreadSamplerLock::Get());
if (gThreadSampler_.dispatcher() == nullptr ||
gThreadSampler_.dispatcher()->State() !=
sampler::ThreadSamplerDispatcher::SamplingState::Running) {
return zx::error(ZX_ERR_BAD_STATE);
}
sampler_ref = gThreadSampler_.dispatcher();
}
return sampler_ref->SampleThreadImpl(pid, tid, source, gregs, sampler_koid);
}