blob: d80f5cf064876fc16fc437d2cef02970e6b67a0c [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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 "src/profiling/memory/client.h"
#include <signal.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cinttypes>
#include <new>
#include <unwindstack/Regs.h>
#include <unwindstack/RegsGetLocal.h>
#include "perfetto/base/compiler.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/thread_utils.h"
#include "perfetto/base/time.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/unix_socket.h"
#include "perfetto/ext/base/utils.h"
#include "src/profiling/memory/sampler.h"
#include "src/profiling/memory/scoped_spinlock.h"
#include "src/profiling/memory/shared_ring_buffer.h"
#include "src/profiling/memory/wire_protocol.h"
namespace perfetto {
namespace profiling {
namespace {
const char kSingleByte[1] = {'x'};
constexpr auto kResendBackoffUs = 100;
inline bool IsMainThread() {
return getpid() == base::GetThreadId();
}
int UnsetDumpable(int) {
prctl(PR_SET_DUMPABLE, 0);
return 0;
}
bool Contained(const StackRange& base, const char* ptr) {
return (ptr >= base.begin && ptr < base.end);
}
} // namespace
uint64_t GetMaxTries(const ClientConfiguration& client_config) {
if (!client_config.block_client)
return 1u;
if (client_config.block_client_timeout_us == 0)
return kInfiniteTries;
return std::max<uint64_t>(
1ul, client_config.block_client_timeout_us / kResendBackoffUs);
}
StackRange GetThreadStackRange() {
// In glibc pthread_getattr_np can call realloc, even for a non-main-thread.
// This is fine, because the heapprofd wrapper for glibc prevents re-entering
// malloc.
pthread_attr_t attr;
if (pthread_getattr_np(pthread_self(), &attr) != 0)
return {nullptr, nullptr};
base::ScopedResource<pthread_attr_t*, pthread_attr_destroy, nullptr> cleanup(
&attr);
char* stackaddr;
size_t stacksize;
if (pthread_attr_getstack(&attr, reinterpret_cast<void**>(&stackaddr),
&stacksize) != 0)
return {nullptr, nullptr};
return {stackaddr, stackaddr + stacksize};
}
StackRange GetSigAltStackRange() {
stack_t altstack;
if (sigaltstack(nullptr, &altstack) == -1) {
PERFETTO_PLOG("sigaltstack");
return {nullptr, nullptr};
}
if ((altstack.ss_flags & SS_ONSTACK) == 0) {
return {nullptr, nullptr};
}
return {static_cast<char*>(altstack.ss_sp),
static_cast<char*>(altstack.ss_sp) + altstack.ss_size};
}
// The implementation of pthread_getattr_np for the main thread on bionic uses
// malloc, so we cannot use it in GetStackEnd, which we use inside of
// RecordMalloc (which is called from malloc). We would re-enter malloc if we
// used it.
//
// This is why we find the stack base for the main-thread when constructing
// the client and remember it.
StackRange GetMainThreadStackRange() {
base::ScopedFstream maps(fopen("/proc/self/maps", "re"));
if (!maps) {
return {nullptr, nullptr};
}
while (!feof(*maps)) {
char line[1024];
char* data = fgets(line, sizeof(line), *maps);
if (data != nullptr && strstr(data, "[stack]")) {
char* sep = strstr(data, "-");
if (sep == nullptr)
continue;
char* min = reinterpret_cast<char*>(strtoll(data, nullptr, 16));
char* max = reinterpret_cast<char*>(strtoll(sep + 1, nullptr, 16));
return {min, max};
}
}
return {nullptr, nullptr};
}
// static
std::optional<base::UnixSocketRaw> Client::ConnectToHeapprofd(
const std::string& sock_name) {
auto sock = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kUnix,
base::SockType::kStream);
if (!sock || !sock.Connect(sock_name)) {
PERFETTO_PLOG("Failed to connect to %s", sock_name.c_str());
return std::nullopt;
}
if (!sock.SetTxTimeout(kClientSockTimeoutMs)) {
PERFETTO_PLOG("Failed to set send timeout for %s", sock_name.c_str());
return std::nullopt;
}
if (!sock.SetRxTimeout(kClientSockTimeoutMs)) {
PERFETTO_PLOG("Failed to set receive timeout for %s", sock_name.c_str());
return std::nullopt;
}
return std::move(sock);
}
// static
std::shared_ptr<Client> Client::CreateAndHandshake(
base::UnixSocketRaw sock,
UnhookedAllocator<Client> unhooked_allocator) {
if (!sock) {
PERFETTO_DFATAL_OR_ELOG("Socket not connected.");
return nullptr;
}
sock.DcheckIsBlocking(true);
// We might be running in a process that is not dumpable (such as app
// processes on user builds), in which case the /proc/self/mem will be chown'd
// to root:root, and will not be accessible even to the process itself (see
// man 5 proc). In such situations, temporarily mark the process dumpable to
// be able to open the files, unsetting dumpability immediately afterwards.
int orig_dumpable = prctl(PR_GET_DUMPABLE);
enum { kNop, kDoUnset };
base::ScopedResource<int, UnsetDumpable, kNop, false> unset_dumpable(kNop);
if (orig_dumpable == 0) {
unset_dumpable.reset(kDoUnset);
prctl(PR_SET_DUMPABLE, 1);
}
base::ScopedFile maps(base::OpenFile("/proc/self/maps", O_RDONLY));
if (!maps) {
PERFETTO_DFATAL_OR_ELOG("Failed to open /proc/self/maps");
return nullptr;
}
base::ScopedFile mem(base::OpenFile("/proc/self/mem", O_RDONLY));
if (!mem) {
PERFETTO_DFATAL_OR_ELOG("Failed to open /proc/self/mem");
return nullptr;
}
// Restore original dumpability value if we overrode it.
unset_dumpable.reset();
int fds[kHandshakeSize];
fds[kHandshakeMaps] = *maps;
fds[kHandshakeMem] = *mem;
// Send an empty record to transfer fds for /proc/self/maps and
// /proc/self/mem.
if (sock.Send(kSingleByte, sizeof(kSingleByte), fds, kHandshakeSize) !=
sizeof(kSingleByte)) {
PERFETTO_DFATAL_OR_ELOG("Failed to send file descriptors.");
return nullptr;
}
ClientConfiguration client_config;
base::ScopedFile shmem_fd;
size_t recv = 0;
while (recv < sizeof(client_config)) {
size_t num_fds = 0;
base::ScopedFile* fd = nullptr;
if (!shmem_fd) {
num_fds = 1;
fd = &shmem_fd;
}
ssize_t rd = sock.Receive(reinterpret_cast<char*>(&client_config) + recv,
sizeof(client_config) - recv, fd, num_fds);
if (rd == -1) {
PERFETTO_PLOG("Failed to receive ClientConfiguration.");
return nullptr;
}
if (rd == 0) {
PERFETTO_LOG("Server disconnected while sending ClientConfiguration.");
return nullptr;
}
recv += static_cast<size_t>(rd);
}
if (!shmem_fd) {
PERFETTO_DFATAL_OR_ELOG("Did not receive shmem fd.");
return nullptr;
}
auto shmem = SharedRingBuffer::Attach(std::move(shmem_fd));
if (!shmem || !shmem->is_valid()) {
PERFETTO_DFATAL_OR_ELOG("Failed to attach to shmem.");
return nullptr;
}
sock.SetBlocking(false);
// note: the shared_ptr will retain a copy of the unhooked_allocator
return std::allocate_shared<Client>(unhooked_allocator, std::move(sock),
client_config, std::move(shmem.value()),
getpid(), GetMainThreadStackRange());
}
Client::Client(base::UnixSocketRaw sock,
ClientConfiguration client_config,
SharedRingBuffer shmem,
pid_t pid_at_creation,
StackRange main_thread_stack_range)
: client_config_(client_config),
max_shmem_tries_(GetMaxTries(client_config_)),
sock_(std::move(sock)),
main_thread_stack_range_(main_thread_stack_range),
shmem_(std::move(shmem)),
pid_at_creation_(pid_at_creation) {}
Client::~Client() {
// This is work-around for code like the following:
// https://android.googlesource.com/platform/libcore/+/4ecb71f94378716f88703b9f7548b5d24839262f/ojluni/src/main/native/UNIXProcess_md.c#427
// They fork, close all fds by iterating over /proc/self/fd using opendir.
// Unfortunately closedir calls free, which detects the fork, and then tries
// to destruct this Client.
//
// ScopedResource crashes on failure to close, so we explicitly ignore
// failures here.
int fd = sock_.ReleaseFd().release();
if (fd != -1)
close(fd);
}
const char* Client::GetStackEnd(const char* stackptr) {
StackRange thread_stack_range;
bool is_main_thread = IsMainThread();
if (is_main_thread) {
thread_stack_range = main_thread_stack_range_;
} else {
thread_stack_range = GetThreadStackRange();
}
if (Contained(thread_stack_range, stackptr)) {
return thread_stack_range.end;
}
StackRange sigalt_stack_range = GetSigAltStackRange();
if (Contained(sigalt_stack_range, stackptr)) {
return sigalt_stack_range.end;
}
// The main thread might have expanded since we read its bounds. We now know
// it is not the sigaltstack, so it has to be the main stack.
// TODO(fmayer): We should reparse maps here, because now we will keep
// hitting the slow-path that calls the sigaltstack syscall.
if (is_main_thread && stackptr < thread_stack_range.end) {
return thread_stack_range.end;
}
return nullptr;
}
// Best-effort detection of whether we're continuing work in a forked child of
// the profiled process, in which case we want to stop. Note that due to
// malloc_hooks.cc's atfork handler, the proper fork calls should leak the child
// before reaching this point. Therefore this logic exists primarily to handle
// clone and vfork.
// TODO(rsavitski): rename/delete |disable_fork_teardown| config option if this
// logic sticks, as the option becomes more clone-specific, and quite narrow.
bool Client::IsPostFork() {
if (PERFETTO_UNLIKELY(getpid() != pid_at_creation_)) {
// Only print the message once, even if we do not shut down the client.
if (!detected_fork_) {
detected_fork_ = true;
const char* vfork_detected = "";
// We use the fact that vfork does not update Bionic's TID cache, so
// we will have a mismatch between the actual TID (from the syscall)
// and the cached one.
//
// What we really want to check is if we are sharing virtual memory space
// with the original process. This would be
// syscall(__NR_kcmp, syscall(__NR_getpid), pid_at_creation_,
// KCMP_VM, 0, 0),
// but that is not compiled into our kernels and disallowed by seccomp.
if (!client_config_.disable_vfork_detection &&
syscall(__NR_gettid) != base::GetThreadId()) {
postfork_return_value_ = true;
vfork_detected = " (vfork detected)";
} else {
postfork_return_value_ = client_config_.disable_fork_teardown;
}
const char* action =
postfork_return_value_ ? "Not shutting down" : "Shutting down";
const char* force =
postfork_return_value_ ? " (fork teardown disabled)" : "";
PERFETTO_LOG(
"Detected post-fork child situation. Not profiling the child. "
"%s client%s%s",
action, force, vfork_detected);
}
return true;
}
return false;
}
// The stack grows towards numerically smaller addresses, so the stack layout
// of main calling malloc is as follows.
//
// +------------+
// |SendWireMsg |
// stackptr +--> +------------+ 0x1000
// |RecordMalloc| +
// +------------+ |
// | malloc | |
// +------------+ |
// | main | v
// stackend +-> +------------+ 0xffff
bool Client::RecordMalloc(uint32_t heap_id,
uint64_t sample_size,
uint64_t alloc_size,
uint64_t alloc_address) {
if (PERFETTO_UNLIKELY(IsPostFork())) {
return postfork_return_value_;
}
AllocMetadata metadata;
const char* stackptr = reinterpret_cast<char*>(__builtin_frame_address(0));
unwindstack::AsmGetRegs(metadata.register_data);
const char* stackend = GetStackEnd(stackptr);
if (!stackend) {
PERFETTO_ELOG("Failed to find stackend.");
shmem_.SetErrorState(SharedRingBuffer::kInvalidStackBounds);
return false;
}
uint64_t stack_size = static_cast<uint64_t>(stackend - stackptr);
metadata.sample_size = sample_size;
metadata.alloc_size = alloc_size;
metadata.alloc_address = alloc_address;
metadata.stack_pointer = reinterpret_cast<uint64_t>(stackptr);
metadata.arch = unwindstack::Regs::CurrentArch();
metadata.sequence_number =
1 + sequence_number_[heap_id].fetch_add(1, std::memory_order_acq_rel);
metadata.heap_id = heap_id;
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC_COARSE, &ts) == 0) {
metadata.clock_monotonic_coarse_timestamp =
static_cast<uint64_t>(base::FromPosixTimespec(ts).count());
} else {
metadata.clock_monotonic_coarse_timestamp = 0;
}
WireMessage msg{};
msg.record_type = RecordType::Malloc;
msg.alloc_header = &metadata;
msg.payload = const_cast<char*>(stackptr);
msg.payload_size = static_cast<size_t>(stack_size);
if (SendWireMessageWithRetriesIfBlocking(msg) == -1)
return false;
if (!shmem_.GetAndResetReaderPaused())
return true;
return SendControlSocketByte();
}
int64_t Client::SendWireMessageWithRetriesIfBlocking(const WireMessage& msg) {
for (uint64_t i = 0;
max_shmem_tries_ == kInfiniteTries || i < max_shmem_tries_; ++i) {
if (shmem_.shutting_down())
return -1;
int64_t res = SendWireMessage(&shmem_, msg);
if (PERFETTO_LIKELY(res >= 0))
return res;
// retry if in blocking mode and still connected
if (client_config_.block_client && base::IsAgain(errno) && IsConnected()) {
usleep(kResendBackoffUs);
} else {
break;
}
}
if (IsConnected())
shmem_.SetErrorState(SharedRingBuffer::kHitTimeout);
PERFETTO_PLOG("Failed to write to shared ring buffer. Disconnecting.");
return -1;
}
bool Client::RecordFree(uint32_t heap_id, const uint64_t alloc_address) {
if (PERFETTO_UNLIKELY(IsPostFork())) {
return postfork_return_value_;
}
FreeEntry current_entry;
current_entry.sequence_number =
1 + sequence_number_[heap_id].fetch_add(1, std::memory_order_acq_rel);
current_entry.addr = alloc_address;
current_entry.heap_id = heap_id;
WireMessage msg = {};
msg.record_type = RecordType::Free;
msg.free_header = &current_entry;
// Do not send control socket byte, as frees are very cheap to handle, so we
// just delay to the next alloc. Sending the control socket byte is ~10x the
// rest of the client overhead.
int64_t bytes_free = SendWireMessageWithRetriesIfBlocking(msg);
if (bytes_free == -1)
return false;
// Seems like we are filling up the shmem with frees. Flush.
if (static_cast<uint64_t>(bytes_free) < shmem_.size() / 2 &&
shmem_.GetAndResetReaderPaused()) {
return SendControlSocketByte();
}
return true;
}
bool Client::RecordHeapInfo(uint32_t heap_id,
const char* heap_name,
uint64_t interval) {
if (PERFETTO_UNLIKELY(IsPostFork())) {
return postfork_return_value_;
}
HeapName hnr;
hnr.heap_id = heap_id;
base::StringCopy(&hnr.heap_name[0], heap_name, sizeof(hnr.heap_name));
hnr.sample_interval = interval;
WireMessage msg = {};
msg.record_type = RecordType::HeapName;
msg.heap_name_header = &hnr;
return SendWireMessageWithRetriesIfBlocking(msg);
}
bool Client::IsConnected() {
sock_.DcheckIsBlocking(false);
char buf[1];
ssize_t recv_bytes = sock_.Receive(buf, sizeof(buf), nullptr, 0);
if (recv_bytes == 0)
return false;
// This is not supposed to happen because currently heapprofd does not send
// data to the client. Here for generality's sake.
if (recv_bytes > 0)
return true;
return base::IsAgain(errno);
}
bool Client::SendControlSocketByte() {
// If base::IsAgain(errno), the socket buffer is full, so the service will
// pick up the notification even without adding another byte.
// In other error cases (usually EPIPE) we want to disconnect, because that
// is how the service signals the tracing session was torn down.
if (sock_.Send(kSingleByte, sizeof(kSingleByte)) == -1 &&
!base::IsAgain(errno)) {
if (shmem_.shutting_down()) {
PERFETTO_LOG("Profiling session ended.");
} else {
PERFETTO_PLOG("Failed to send control socket byte.");
}
return false;
}
return true;
}
} // namespace profiling
} // namespace perfetto