crashpad/util/linux/ptracer.cc
Lei Zhang c63c073d27 Do IWYU for check_op.h
Include check_op.h directly, instead of relying on the transitive
include from logging.h. This transitive include does not exist in
Chromium's //base.

Change-Id: I15962a9cdc26ac206032157b8d2659cf263ad695
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/4950200
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
2023-10-18 20:01:37 +00:00

624 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2017 The Crashpad 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 "util/linux/ptracer.h"
#include <errno.h>
#include <linux/elf.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/uio.h>
#include "base/check_op.h"
#include "base/logging.h"
#include "build/build_config.h"
#include "util/misc/from_pointer_cast.h"
#if defined(ARCH_CPU_X86_FAMILY)
#include <asm/ldt.h>
#endif
namespace crashpad {
namespace {
#if defined(ARCH_CPU_X86_FAMILY)
template <typename Destination>
bool GetRegisterSet(pid_t tid, int set, Destination* dest, bool can_log) {
iovec iov;
iov.iov_base = dest;
iov.iov_len = sizeof(*dest);
if (ptrace(PTRACE_GETREGSET, tid, reinterpret_cast<void*>(set), &iov) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
if (iov.iov_len != sizeof(*dest)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << iov.iov_len
<< " != " << sizeof(*dest);
return false;
}
return true;
}
bool GetFloatingPointRegisters32(pid_t tid,
FloatContext* context,
bool can_log) {
return GetRegisterSet(tid, NT_PRXFPREG, &context->f32.fxsave, can_log);
}
bool GetFloatingPointRegisters64(pid_t tid,
FloatContext* context,
bool can_log) {
return GetRegisterSet(tid, NT_PRFPREG, &context->f64.fxsave, can_log);
}
bool GetThreadArea32(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
size_t index = (context.t32.xgs & 0xffff) >> 3;
user_desc desc;
if (ptrace(
PTRACE_GET_THREAD_AREA, tid, reinterpret_cast<void*>(index), &desc) !=
0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
*address = desc.base_addr;
return true;
}
bool GetThreadArea64(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
*address = context.t64.fs_base;
return true;
}
#elif defined(ARCH_CPU_ARM_FAMILY)
#if defined(ARCH_CPU_ARMEL)
// PTRACE_GETREGSET, introduced in Linux 2.6.34 (2225a122ae26), requires kernel
// support enabled by HAVE_ARCH_TRACEHOOK. This has been set for x86 (including
// x86_64) since Linux 2.6.28 (99bbc4b1e677a), but for ARM only since
// Linux 3.5.0 (0693bf68148c4). Older Linux kernels support PTRACE_GETREGS,
// PTRACE_GETFPREGS, and PTRACE_GETVFPREGS instead, which don't allow checking
// the size of data copied.
//
// Fortunately, 64-bit ARM support only appeared in Linux 3.7.0, so if
// PTRACE_GETREGSET fails on ARM with EIO, indicating that the request is not
// supported, the kernel must be old enough that 64-bit ARM isnt supported
// either.
//
// TODO(mark): Once helpers to interpret the kernel version are available, add
// a DCHECK to ensure that the kernel is older than 3.5.
bool GetGeneralPurposeRegistersLegacy(pid_t tid,
ThreadContext* context,
bool can_log) {
if (ptrace(PTRACE_GETREGS, tid, nullptr, &context->t32) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
return true;
}
bool GetFloatingPointRegistersLegacy(pid_t tid,
FloatContext* context,
bool can_log) {
if (ptrace(PTRACE_GETFPREGS, tid, nullptr, &context->f32.fpregs) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
context->f32.have_fpregs = true;
if (ptrace(PTRACE_GETVFPREGS, tid, nullptr, &context->f32.vfp) != 0) {
switch (errno) {
case EINVAL:
// These registers are optional on 32-bit ARM cpus
break;
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
} else {
context->f32.have_vfp = true;
}
return true;
}
#endif // ARCH_CPU_ARMEL
// Normally, the Linux kernel will copy out register sets according to the size
// of the struct that contains them. Tracing a 32-bit ARM process running in
// compatibility mode on a 64-bit ARM cpu will only copy data for the number of
// registers times the size of the register, which won't include any possible
// trailing padding in the struct. These are the sizes of the register data, not
// including any possible padding.
constexpr size_t kArmVfpSize = 32 * 8 + 4;
// Target is 32-bit
bool GetFloatingPointRegisters32(pid_t tid,
FloatContext* context,
bool can_log) {
context->f32.have_fpregs = false;
context->f32.have_vfp = false;
iovec iov;
iov.iov_base = &context->f32.fpregs;
iov.iov_len = sizeof(context->f32.fpregs);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_PRFPREG), &iov) !=
0) {
switch (errno) {
#if defined(ARCH_CPU_ARMEL)
case EIO:
return GetFloatingPointRegistersLegacy(tid, context, can_log);
#endif // ARCH_CPU_ARMEL
case EINVAL:
// A 32-bit process running on a 64-bit CPU doesn't have this register
// set. It should have a VFP register set instead.
break;
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
} else {
if (iov.iov_len != sizeof(context->f32.fpregs)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << iov.iov_len
<< " != " << sizeof(context->f32.fpregs);
return false;
}
context->f32.have_fpregs = true;
}
iov.iov_base = &context->f32.vfp;
iov.iov_len = sizeof(context->f32.vfp);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_ARM_VFP), &iov) !=
0) {
switch (errno) {
case EINVAL:
// VFP may not be present on 32-bit ARM cpus.
break;
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
} else {
if (iov.iov_len != kArmVfpSize && iov.iov_len != sizeof(context->f32.vfp)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << iov.iov_len
<< " != " << sizeof(context->f32.vfp);
return false;
}
context->f32.have_vfp = true;
}
if (!(context->f32.have_fpregs || context->f32.have_vfp)) {
LOG_IF(ERROR, can_log) << "Unable to collect registers";
return false;
}
return true;
}
bool GetFloatingPointRegisters64(pid_t tid,
FloatContext* context,
bool can_log) {
iovec iov;
iov.iov_base = context;
iov.iov_len = sizeof(*context);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_PRFPREG), &iov) !=
0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
if (iov.iov_len != sizeof(context->f64)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << iov.iov_len
<< " != " << sizeof(context->f64);
return false;
}
return true;
}
bool GetThreadArea32(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
#if defined(ARCH_CPU_ARMEL)
void* result;
if (ptrace(PTRACE_GET_THREAD_AREA, tid, nullptr, &result) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
*address = FromPointerCast<LinuxVMAddress>(result);
return true;
#else
// TODO(jperaza): it doesn't look like there is a way for a 64-bit ARM process
// to get the thread area for a 32-bit ARM process with ptrace.
LOG_IF(WARNING, can_log)
<< "64-bit ARM cannot trace TLS area for a 32-bit process";
return false;
#endif // ARCH_CPU_ARMEL
}
bool GetThreadArea64(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
iovec iov;
iov.iov_base = address;
iov.iov_len = sizeof(*address);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_ARM_TLS), &iov) !=
0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
if (iov.iov_len != 8) {
LOG_IF(ERROR, can_log) << "address size mismatch";
return false;
}
return true;
}
#elif defined(ARCH_CPU_MIPS_FAMILY)
// PTRACE_GETREGSET, introduced in Linux 2.6.34 (2225a122ae26), requires kernel
// support enabled by HAVE_ARCH_TRACEHOOK. This has been set for x86 (including
// x86_64) since Linux 2.6.28 (99bbc4b1e677a), but for MIPS only since
// Linux 3.13 (c0ff3c53d4f99). Older Linux kernels support PTRACE_GETREGS,
// and PTRACE_GETFPREGS instead, which don't allow checking the size of data
// copied. Also, PTRACE_GETREGS assumes register size of 64 bits even for 32 bit
// MIPS CPU (contrary to PTRACE_GETREGSET behavior), so we need buffer
// structure here.
bool GetGeneralPurposeRegistersLegacy(pid_t tid,
ThreadContext* context,
bool can_log) {
ThreadContext context_buffer;
if (ptrace(PTRACE_GETREGS, tid, nullptr, &context_buffer.t64) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
// Bitness of target process can't be determined through ptrace here, so we
// assume target process has the same as current process, making cross-bit
// ptrace unsupported on MIPS for kernels older than 3.13
#if defined(ARCH_CPU_MIPSEL)
#define THREAD_CONTEXT_FIELD t32
#elif defined(ARCH_CPU_MIPS64EL)
#define THREAD_CONTEXT_FIELD t64
#endif
for (size_t reg = 0; reg < 32; ++reg) {
context->THREAD_CONTEXT_FIELD.regs[reg] = context_buffer.t64.regs[reg];
}
context->THREAD_CONTEXT_FIELD.lo = context_buffer.t64.lo;
context->THREAD_CONTEXT_FIELD.hi = context_buffer.t64.hi;
context->THREAD_CONTEXT_FIELD.cp0_epc = context_buffer.t64.cp0_epc;
context->THREAD_CONTEXT_FIELD.cp0_badvaddr = context_buffer.t64.cp0_badvaddr;
context->THREAD_CONTEXT_FIELD.cp0_status = context_buffer.t64.cp0_status;
context->THREAD_CONTEXT_FIELD.cp0_cause = context_buffer.t64.cp0_cause;
#undef THREAD_CONTEXT_FIELD
return true;
}
bool GetFloatingPointRegistersLegacy(pid_t tid,
FloatContext* context,
bool can_log) {
if (ptrace(PTRACE_GETFPREGS, tid, nullptr, &context->f32.fpregs) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
return true;
}
bool GetFloatingPointRegisters32(pid_t tid,
FloatContext* context,
bool can_log) {
iovec iov;
iov.iov_base = &context->f32.fpregs;
iov.iov_len = sizeof(context->f32.fpregs);
if (ptrace(PTRACE_GETFPREGS, tid, nullptr, &context->f32.fpregs) != 0) {
switch (errno) {
case EINVAL:
// fp may not be present
break;
case EIO:
return GetFloatingPointRegistersLegacy(tid, context, can_log);
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
}
return true;
}
bool GetFloatingPointRegisters64(pid_t tid,
FloatContext* context,
bool can_log) {
iovec iov;
iov.iov_base = &context->f64.fpregs;
iov.iov_len = sizeof(context->f64.fpregs);
if (ptrace(PTRACE_GETFPREGS, tid, nullptr, &context->f64.fpregs) != 0) {
switch (errno) {
case EINVAL:
// fp may not be present
break;
case EIO:
return GetFloatingPointRegistersLegacy(tid, context, can_log);
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
}
return true;
}
bool GetThreadArea32(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
#if defined(ARCH_CPU_MIPSEL)
void* result;
if (ptrace(PTRACE_GET_THREAD_AREA, tid, nullptr, &result) != 0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
*address = FromPointerCast<LinuxVMAddress>(result);
return true;
#else
return false;
#endif
}
bool GetThreadArea64(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
void* result;
#if defined(ARCH_CPU_MIPSEL)
if (ptrace(PTRACE_GET_THREAD_AREA_3264, tid, nullptr, &result) != 0) {
#else
if (ptrace(PTRACE_GET_THREAD_AREA, tid, nullptr, &result) != 0) {
#endif
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
*address = FromPointerCast<LinuxVMAddress>(result);
return true;
}
#elif defined(ARCH_CPU_RISCV64)
bool GetFloatingPointRegisters64(pid_t tid,
FloatContext* context,
bool can_log) {
iovec iov;
iov.iov_base = context;
iov.iov_len = sizeof(*context);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_PRFPREG), &iov) !=
0) {
PLOG_IF(ERROR, can_log) << "ptrace";
return false;
}
if (iov.iov_len != sizeof(context->f64)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << iov.iov_len
<< " != " << sizeof(context->f64);
return false;
}
return true;
}
bool GetThreadArea64(pid_t tid,
const ThreadContext& context,
LinuxVMAddress* address,
bool can_log) {
// Thread pointer register
*address = context.t64.regs[3];
return true;
}
#else
#error Port.
#endif // ARCH_CPU_X86_FAMILY
size_t GetGeneralPurposeRegistersAndLength(pid_t tid,
ThreadContext* context,
bool can_log) {
iovec iov;
iov.iov_base = context;
iov.iov_len = sizeof(*context);
if (ptrace(
PTRACE_GETREGSET, tid, reinterpret_cast<void*>(NT_PRSTATUS), &iov) !=
0) {
switch (errno) {
#if defined(ARCH_CPU_ARMEL) || defined(ARCH_CPU_MIPS_FAMILY)
case EIO:
return GetGeneralPurposeRegistersLegacy(tid, context, can_log)
? sizeof(context->t32)
: 0;
#endif // ARCH_CPU_ARMEL
default:
PLOG_IF(ERROR, can_log) << "ptrace";
return 0;
}
}
return iov.iov_len;
}
#if !defined(ARCH_CPU_RISCV64)
bool GetGeneralPurposeRegisters32(pid_t tid,
ThreadContext* context,
bool can_log) {
size_t length = GetGeneralPurposeRegistersAndLength(tid, context, can_log);
if (length != sizeof(context->t32)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << length
<< " != " << sizeof(context->t32);
return false;
}
return true;
}
#endif
bool GetGeneralPurposeRegisters64(pid_t tid,
ThreadContext* context,
bool can_log) {
size_t length = GetGeneralPurposeRegistersAndLength(tid, context, can_log);
if (length != sizeof(context->t64)) {
LOG_IF(ERROR, can_log) << "Unexpected registers size " << length
<< " != " << sizeof(context->t64);
return false;
}
return true;
}
} // namespace
Ptracer::Ptracer(bool can_log)
: is_64_bit_(false), can_log_(can_log), initialized_() {}
Ptracer::Ptracer(bool is_64_bit, bool can_log)
: is_64_bit_(is_64_bit), can_log_(can_log), initialized_() {
INITIALIZATION_STATE_SET_INITIALIZING(initialized_);
INITIALIZATION_STATE_SET_VALID(initialized_);
}
Ptracer::~Ptracer() {}
bool Ptracer::Initialize(pid_t pid) {
INITIALIZATION_STATE_SET_INITIALIZING(initialized_);
ThreadContext context;
size_t length = GetGeneralPurposeRegistersAndLength(pid, &context, can_log_);
if (length == sizeof(context.t64)) {
is_64_bit_ = true;
} else if (length == sizeof(context.t32)) {
is_64_bit_ = false;
} else {
LOG_IF(ERROR, can_log_)
<< "Unexpected registers size " << length
<< " != " << sizeof(context.t64) << ", " << sizeof(context.t32);
return false;
}
INITIALIZATION_STATE_SET_VALID(initialized_);
return true;
}
bool Ptracer::Is64Bit() {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return is_64_bit_;
}
bool Ptracer::GetThreadInfo(pid_t tid, ThreadInfo* info) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
if (is_64_bit_) {
return GetGeneralPurposeRegisters64(tid, &info->thread_context, can_log_) &&
GetFloatingPointRegisters64(tid, &info->float_context, can_log_) &&
GetThreadArea64(tid,
info->thread_context,
&info->thread_specific_data_address,
can_log_);
}
#if !defined(ARCH_CPU_RISCV64)
return GetGeneralPurposeRegisters32(tid, &info->thread_context, can_log_) &&
GetFloatingPointRegisters32(tid, &info->float_context, can_log_) &&
GetThreadArea32(tid,
info->thread_context,
&info->thread_specific_data_address,
can_log_);
#else
return false;
#endif
}
ssize_t Ptracer::ReadUpTo(pid_t pid,
LinuxVMAddress address,
size_t size,
char* buffer) {
size_t bytes_read = 0;
while (size > 0) {
errno = 0;
if (size >= sizeof(long)) {
*reinterpret_cast<long*>(buffer) =
ptrace(PTRACE_PEEKDATA, pid, address, nullptr);
if (errno == EIO) {
ssize_t last_bytes = ReadLastBytes(pid, address, size, buffer);
return last_bytes >= 0 ? bytes_read + last_bytes : -1;
}
if (errno != 0) {
PLOG_IF(ERROR, can_log_) << "ptrace";
return -1;
}
size -= sizeof(long);
buffer += sizeof(long);
address += sizeof(long);
bytes_read += sizeof(long);
} else {
long word = ptrace(PTRACE_PEEKDATA, pid, address, nullptr);
if (errno == 0) {
memcpy(buffer, reinterpret_cast<char*>(&word), size);
return bytes_read + size;
}
if (errno == EIO) {
ssize_t last_bytes = ReadLastBytes(pid, address, size, buffer);
return last_bytes >= 0 ? bytes_read + last_bytes : -1;
}
PLOG_IF(ERROR, can_log_);
return -1;
}
}
return bytes_read;
}
// Handles an EIO by reading at most size bytes from address into buffer if
// address was within a word of a possible page boundary, by aligning to read
// the last word of the page and extracting the desired bytes.
ssize_t Ptracer::ReadLastBytes(pid_t pid,
LinuxVMAddress address,
size_t size,
char* buffer) {
LinuxVMAddress aligned = ((address + 4095) & ~4095) - sizeof(long);
if (aligned >= address || aligned == address - sizeof(long)) {
PLOG_IF(ERROR, can_log_) << "ptrace";
return -1;
}
DCHECK_GT(aligned, address - sizeof(long));
errno = 0;
long word = ptrace(PTRACE_PEEKDATA, pid, aligned, nullptr);
if (errno != 0) {
PLOG_IF(ERROR, can_log_) << "ptrace";
return -1;
}
size_t bytes_read = address - aligned;
size_t last_bytes = std::min(sizeof(long) - bytes_read, size);
memcpy(buffer, reinterpret_cast<char*>(&word) + bytes_read, last_bytes);
return last_bytes;
}
} // namespace crashpad