blob: 28b36db56750deae37c6e479b0a81ee69fc14269 [file] [log] [blame]
// Copyright 2016 The Chromium OS 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 "crash-reporter/core-collector/coredump_writer.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <sysexits.h>
#include <unistd.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <string>
#include <common/linux/elf_core_dump.h>
#include "crash-reporter/core-collector/logging.h"
using google_breakpad::ElfCoreDump;
namespace {
const size_t kMaxAbsCoredumpSize = 256 * 1024 * 1024;
const double kMaxRelCoredumpSize = 0.05; // Percentage of free disk space.
struct ScopedFd {
explicit ScopedFd(int fd) : fd(fd) {}
~ScopedFd() { close(fd); }
bool IsValid() const { return fd >= 0; }
operator int() const { return fd; }
const int fd;
};
template <typename DataPtr>
using FdFunctionPtr = ssize_t (*)(int, DataPtr, size_t);
// Reads or writes exactly |count| bytes of |data| using |fd| as the source or
// destination, respectively.
template <typename DataPtr, typename BytePtr, FdFunctionPtr<DataPtr> op>
struct FdOperation {
bool operator ()(int fd, DataPtr data, size_t count) {
BytePtr ptr = reinterpret_cast<BytePtr>(data);
while (count > 0) {
const ssize_t n = TEMP_FAILURE_RETRY(op(fd, ptr, count));
if (n < 0)
return false;
if (n == 0) {
errno = EIO;
return false;
}
ptr += n;
count -= n;
}
return true;
}
};
FdOperation<void *, uint8_t *, &read> ReadAllBlocking;
FdOperation<const void *, const uint8_t *, &write> WriteAllBlocking;
inline bool Seek(int fd, off_t offset) {
return lseek(fd, offset, SEEK_SET) == offset;
}
inline int64_t GetFreeDiskSpace(const char *path) {
struct statvfs stats;
return TEMP_FAILURE_RETRY(statvfs(path, &stats)) != 0 ? -1 :
static_cast<int64_t>(stats.f_bavail) * stats.f_frsize;
}
} // namespace
class CoredumpWriter::Reader {
public:
explicit Reader(int fd) : fd_(fd) {}
bool Read(void *buf, size_t count) {
if (!ReadAllBlocking(fd_, buf, count))
return false;
bytes_read_ += count;
return true;
}
bool CopyTo(int dest_fd, size_t count) {
static const size_t kBufSize = 32 * 1024;
char buf[kBufSize];
while (count > 0) {
const ssize_t n = TEMP_FAILURE_RETRY(
read(fd_, buf, std::min(kBufSize, count)));
if (n < 0)
return false;
if (n == 0) {
errno = 0;
break;
}
if (dest_fd >= 0 && !WriteAllBlocking(dest_fd, buf, n))
return false;
count -= n;
bytes_read_ += n;
}
return count == 0;
}
bool Seek(size_t offset) {
if (offset < bytes_read_) // Cannot move backward.
return false;
return CopyTo(-1, offset - bytes_read_);
}
private:
const int fd_;
size_t bytes_read_ = 0;
DISALLOW_COPY_AND_ASSIGN(Reader);
};
CoredumpWriter::CoredumpWriter(int fd,
const char *coredump_path,
const char *container_dir)
: fd_(fd),
coredump_path_(coredump_path),
container_dir_(container_dir) {
}
int CoredumpWriter::WriteCoredump() {
const ScopedFd dest(TEMP_FAILURE_RETRY(
open(coredump_path_, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)));
if (!dest.IsValid()) {
PLOG_ERROR << "Failed to open '" << coredump_path_ << "'";
return EX_CANTCREAT;
}
// Input core dump is generated by kernel's fs/binfmt_elf.c using this format:
//
// ELF Header
// Program Header 1
// Program Header 2
// ...
// Program Header n
// Segment 1 (This segment's type should be PT_NOTE)
// Segment 2
// ...
// Segment n
// Read ELF header, all program headers, and the first segment whose type is
// PT_NOTE.
Reader reader(fd_);
Ehdr elf_header;
std::vector<Phdr> program_headers;
std::vector<char> note_buf;
int error = ReadUntilNote(&reader, &elf_header, &program_headers, &note_buf);
if (error != EX_OK)
return error;
// Get a set of address ranges occupied by mapped files from PT_NOTE segment.
FileMappings file_mappings;
if (!GetFileMappings(note_buf, &file_mappings))
return EX_OSFILE;
// Strip segments backed by mapped files, since they are not needed to
// generate a minidump.
std::vector<Phdr> stripped_program_headers;
StripSegments(program_headers, file_mappings, &stripped_program_headers);
// Calculate the core dump size limit.
const int64_t free_disk_space = GetFreeDiskSpace(coredump_path_);
if (free_disk_space < 0) {
PLOG_ERROR << "Failed to get free disk space";
return EX_OSERR;
}
const auto coredump_size_limit = std::min(kMaxAbsCoredumpSize,
static_cast<size_t>(free_disk_space * kMaxRelCoredumpSize));
// Calculate the output file size.
const auto &last = stripped_program_headers.back();
const auto expected_coredump_size = last.p_offset + last.p_filesz;
if (expected_coredump_size > coredump_size_limit) {
LOG_ERROR << "Exceeded maximum core dump size by "
<< expected_coredump_size - coredump_size_limit << " bytes";
return EX_CANTCREAT;
}
// Write /proc files.
error = WriteAuxv(note_buf);
if (error != EX_OK) {
LOG_ERROR << "Failed to write auxv";
return error;
}
error = WriteMaps(program_headers, file_mappings);
if (error != EX_OK) {
LOG_ERROR << "Failed to write maps";
return error;
}
// Write ELF header.
if (!WriteAllBlocking(dest, &elf_header, sizeof(elf_header))) {
PLOG_ERROR << "Failed to write ELF header";
return EX_IOERR;
}
// Write program headers.
for (size_t i = 0; i < stripped_program_headers.size(); ++i) {
const Phdr &program_header = stripped_program_headers[i];
const auto offset = sizeof(elf_header) + i * elf_header.e_phentsize;
if (!Seek(dest, offset) ||
!WriteAllBlocking(dest, &program_header, sizeof(program_header))) {
PLOG_ERROR << "Failed to write program header";
return EX_IOERR;
}
}
// Write PT_NOTE segment.
if (!Seek(dest, stripped_program_headers[0].p_offset) ||
!WriteAllBlocking(dest, note_buf.data(), note_buf.size())) {
PLOG_ERROR << "Failed to write PT_NOTE segment";
return EX_IOERR;
}
// Write segments that were not stripped.
for (size_t i = 1; i < stripped_program_headers.size(); ++i) {
const Phdr &program_header = stripped_program_headers[i];
if (program_header.p_filesz == 0)
continue;
const Phdr &program_header_original = program_headers[i];
if (!reader.Seek(program_header_original.p_offset)) {
PLOG_ERROR << "Failed to seek segment";
return EX_IOERR;
}
if (!Seek(dest, program_header.p_offset) ||
!reader.CopyTo(dest, program_header.p_filesz)) {
PLOG_ERROR << "Failed to write segment";
return EX_IOERR;
}
}
return EX_OK;
}
int CoredumpWriter::ReadUntilNote(Reader *reader,
Ehdr *elf_header,
std::vector<Phdr> *program_headers,
std::vector<char> *note_buf) {
// Read ELF header.
if (!reader->Read(elf_header, sizeof(*elf_header))) {
PLOG_ERROR << "Failed to read ELF header";
return EX_IOERR;
}
if (memcmp(elf_header->e_ident, ELFMAG, SELFMAG) != 0 ||
elf_header->e_ident[EI_CLASS] != ElfCoreDump::kClass ||
elf_header->e_version != EV_CURRENT ||
elf_header->e_type != ET_CORE ||
elf_header->e_ehsize != sizeof(Ehdr) ||
elf_header->e_phentsize != sizeof(Phdr)) {
LOG_ERROR << "Invalid ELF header";
return EX_OSFILE;
}
// Read program headers.
program_headers->resize(elf_header->e_phnum);
if (!reader->Seek(elf_header->e_phoff) ||
!reader->Read(program_headers->data(),
sizeof(Phdr) * program_headers->size())) {
PLOG_ERROR << "Failed to read program headers";
return EX_IOERR;
}
// The first segment should have type PT_NOTE. This assumption hinges on the
// kernel's fs/binfmt_elf.c implementation.
if (program_headers->empty() || (*program_headers)[0].p_type != PT_NOTE) {
LOG_ERROR << "Failed to locate PT_NOTE segment";
return EX_OSFILE;
}
const Phdr &note_program_header = (*program_headers)[0];
// Read PT_NOTE segment.
note_buf->resize(note_program_header.p_filesz);
if (!reader->Seek(note_program_header.p_offset) ||
!reader->Read(note_buf->data(), note_buf->size())) {
PLOG_ERROR << "Failed to read PT_NOTE segment";
return EX_IOERR;
}
return EX_OK;
}
bool CoredumpWriter::GetFileMappings(const std::vector<char> &note_buf,
FileMappings *file_mappings) {
// Locate NT_FILE note.
ElfCoreDump::Note note({ note_buf.data(), note_buf.size() });
while (note.IsValid() && note.GetType() != NT_FILE)
note = note.GetNextNote();
if (!note.IsValid()) {
LOG_ERROR << "Failed to locate NT_FILE note";
return false;
}
const auto note_desc = note.GetDescription();
// The NT_FILE note is generated by the kernel's fs/binfmt_elf.c as a
// sequence of long values, followed by a sequence of char values.
struct FileNote {
ElfW(Off) count;
ElfW(Off) page_size;
struct {
ElfW(Addr) start;
ElfW(Addr) end;
ElfW(Off) offset;
} files[1];
// The |files| array has |count| elements. The file paths are stored after
// the array, as |count| null-terminated strings.
};
const FileNote *file_note =
reinterpret_cast<const FileNote *>(note_desc.data());
const size_t num_files = file_note->count;
const char *path =
reinterpret_cast<const char *>(&file_note->files[num_files]);
// Populate file mappings.
for (size_t i = 0; i < num_files; ++i) {
const auto file = &file_note->files[i];
const auto it = file_mappings->insert({
FileRange(file->start, file->end),
{ file->offset * file_note->page_size, path }
});
// Skip past NUL to next path.
path += it.first->second.path.length() + 1;
}
// The last path should end the note.
if (path != reinterpret_cast<const char *>(file_note) + note_desc.length()) {
LOG_ERROR << "Invalid NT_FILE note";
return false;
}
return true;
}
void CoredumpWriter::StripSegments(
const std::vector<Phdr> &program_headers,
const FileMappings &file_mappings,
std::vector<Phdr> *stripped_program_headers) {
stripped_program_headers->resize(program_headers.size());
// The first segment has type PT_NOTE. Use the original data unchanged.
(*stripped_program_headers)[0] = program_headers[0];
for (size_t i = 1; i < program_headers.size(); ++i) {
Phdr &out = (*stripped_program_headers)[i];
out = program_headers[i];
// If the type is PT_LOAD and the range is found in the set, the segment is
// backed by a file, so it can be excluded as it doesn't contain stack data
// useful to generate a minidump.
const FileRange range(out.p_vaddr, out.p_vaddr + out.p_memsz);
if (out.p_type == PT_LOAD && file_mappings.count(range))
out.p_filesz = 0;
// Calculate offset.
const Phdr &prev_program_header = (*stripped_program_headers)[i - 1];
out.p_offset = prev_program_header.p_offset + prev_program_header.p_filesz;
// Offset alignment.
if (out.p_align != 0 && out.p_offset % out.p_align != 0)
out.p_offset += out.p_align - out.p_offset % out.p_align;
}
}
int CoredumpWriter::WriteAuxv(const std::vector<char> &note_buf) {
// Locate NT_AUXV note.
ElfCoreDump::Note note({ note_buf.data(), note_buf.size() });
while (note.IsValid() && note.GetType() != NT_AUXV)
note = note.GetNextNote();
if (!note.IsValid()) {
LOG_ERROR << "Failed to locate NT_AUXV note";
return EX_OSFILE;
}
const auto path = container_dir_ + std::string("/auxv");
const ScopedFd auxv(TEMP_FAILURE_RETRY(
open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)));
if (!auxv.IsValid()) {
PLOG_ERROR << "Failed to open '" << path.c_str() << "'";
return EX_CANTCREAT;
}
// The NT_AUXV note has the same format as /proc/[pid]/auxv.
const auto desc = note.GetDescription();
return WriteAllBlocking(auxv, desc.data(), desc.length()) ? EX_OK : EX_IOERR;
}
int CoredumpWriter::WriteMaps(const std::vector<Phdr> &program_headers,
const FileMappings &file_mappings) {
const auto path = container_dir_ + std::string("/maps");
const ScopedFd maps(TEMP_FAILURE_RETRY(
open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)));
if (!maps.IsValid()) {
PLOG_ERROR << "Failed to open '" << path.c_str() << "'";
return EX_CANTCREAT;
}
for (const auto &program_header : program_headers) {
if (program_header.p_type != PT_LOAD)
continue;
const FileRange range(program_header.p_vaddr,
program_header.p_vaddr + program_header.p_memsz);
// If a mapping is found for the range, the range is mapped to a file.
const auto it = file_mappings.find(range);
const ElfW(Off) offset = it != file_mappings.end() ? it->second.offset : 0;
const auto path = it != file_mappings.end() ? it->second.path.c_str() : "";
const int kBufSize = 1024;
char buf[kBufSize];
// See kernel's fs/proc/task_{no,}mmu.c for format string.
const int len = snprintf(
buf, kBufSize,
"%08" PRIxPTR
"-"
"%08" PRIxPTR
" %c%c%c%c"
" %08" PRIx64
" %02x:%02x %lu %s\n",
range.first, range.second,
program_header.p_flags & PF_R ? 'r' : '-',
program_header.p_flags & PF_W ? 'w' : '-',
program_header.p_flags & PF_X ? 'x' : '-',
'p', // Fake value: we can't know if the mapping is shared or private.
static_cast<uint64_t>(offset),
0, // Fake device (major) value.
0, // Fake device (minor) value.
0ul, // Fake inode value.
path);
if (len < 0 || len >= kBufSize || !WriteAllBlocking(maps, buf, len))
return EX_OSFILE;
}
return EX_OK;
}