blob: cf5e8f532d62377330c65cfe7c0df7c955ac44bf [file] [log] [blame]
// Copyright 2020 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 "src/media/audio/audio_core/pin_executable_memory.h"
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <lib/zx/clock.h>
#include <lib/zx/process.h>
#include <lib/zx/thread.h>
#include <zircon/status.h>
#include <iomanip>
namespace media::audio {
// Note: This is available in as PAGE_SIZE from <arch/defines.h>, but that header
// is not available outside of the kernel. We expect PAGE_SIZE to be no smaller than
// this value (it may be larger, e.g. 16k or 64k on some architectures).
static constexpr auto kPageSizeBytes = 4096;
// Memory is considered "unused" if it has not been touched for more than 30s.
// To keep all executable memory pinned, we must run at least once every 30s.
// To ensure we never miss a deadline, do this twice every 30s.
static constexpr auto kTimeBetweenPins = zx::sec(15);
PinExecutableMemory& PinExecutableMemory::Singleton() {
static PinExecutableMemory* p = new PinExecutableMemory;
return *p;
}
PinExecutableMemory::PinExecutableMemory() : loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {
loop_.StartThread("pin-executable-memory");
PeriodicPin();
}
void PinExecutableMemory::PeriodicPin() {
const auto next_task_time = zx::clock::get_monotonic() + kTimeBetweenPins;
Pin();
async::PostTaskForTime(
loop_.dispatcher(), [this]() { PeriodicPin(); }, next_task_time);
}
// Disable address sanitizer. While Pin() should never read from unmapped memory
// (i.e., it should never segfault), it might read an address that is not tracked
// by the address sanitizer.
__attribute__((no_sanitize_address)) void PinExecutableMemory::Pin() {
TRACE_DURATION("audio", "PinExecutableMemory::Pin");
{
// Reset so we only accumulate mappings that are discarded concurrently with
// this current Pin().
std::lock_guard<std::mutex> lock(mutex_);
discarded_mappings_.clear();
}
auto start_time = zx::clock::get_monotonic();
size_t total_bytes = 0;
size_t total_executable_bytes = 0;
for (auto& vmap : ListVMaps()) {
// All readable, non-writable pages are eligible for pinning.
if ((vmap.type != ZX_INFO_MAPS_TYPE_MAPPING) ||
(vmap.u.mapping.mmu_flags & ZX_VM_PERM_READ) == 0 ||
(vmap.u.mapping.mmu_flags & ZX_VM_PERM_WRITE) != 0) {
continue;
}
// We want to pin this RO mapping. We assume that executable mappings are not removed
// concurrently. If that were to happen, these accesses could crash. Currently, there is one
// case where we remove executable mappings: when the tuning manager loads a new effects
// pipeline. This can dlclose() a previously loaded shared object. Since the tuning manager
// is not being used at the moment, we don't bother supporting this case.
//
// Non-executable mappings might be removed concurrently with this method, between the
// above ListVMaps() call and here. For example, renderer payload buffers might use read-only
// shared VMOs and those mappings can come and go as renderers are created and destroyed.
// To handle this race, we use the below lock to make pinning atomic with VMO destruction.
// To minimize lock contention, we lock each mapping rather than locking the entire Pin().
std::unique_lock<std::mutex> lock(mutex_, std::defer_lock);
const bool executable = ((vmap.u.mapping.mmu_flags & ZX_VM_PERM_EXECUTE) != 0);
if (!executable) {
lock.lock();
}
// Read one byte from each page of this executable mapping.
// Using volatile ensures the memory access is not discarded: https://godbolt.org/z/YdzEPo
//
auto base = reinterpret_cast<volatile char*>(vmap.base);
for (auto ptr = base; ptr < base + vmap.size; ptr += kPageSizeBytes) {
if (!executable) {
AssertMutexHeld();
if (ShouldSkip(reinterpret_cast<size_t>(ptr))) {
continue;
}
}
(*ptr);
}
total_bytes += vmap.size;
if (executable) {
total_executable_bytes += vmap.size;
}
}
TRACE_INSTANT("audio", "Pinned bytes", TRACE_SCOPE_THREAD, total_bytes);
auto end_time = zx::clock::get_monotonic();
FX_LOGS(INFO) << "pinned " << total_bytes << " bytes (" << total_executable_bytes
<< " executable bytes) in " << (end_time - start_time).to_nsecs() << " ns";
}
std::vector<zx_info_maps_t> PinExecutableMemory::ListVMaps() {
auto proc = zx::process::self();
// Call first to get the number of mappings.
size_t actual;
size_t avail;
auto status = proc->get_info(ZX_INFO_PROCESS_MAPS, nullptr, 0, &actual, &avail);
if (status != ZX_OK) {
FX_PLOGS(WARNING, status) << "Couldn't get process vmaps";
return std::vector<zx_info_maps_t>();
}
// Call again to get the actual mappings. In theory avail can be larger if mappings
// are being added concurrently. In practice we don't expect that to happen, and in
// any case, we'll get those new mappings at the next pin, after kTimeBetweenPins.
std::vector<zx_info_maps_t> out(avail);
status =
proc->get_info(ZX_INFO_PROCESS_MAPS, &out[0], out.size() * sizeof(out[0]), &actual, &avail);
if (status != ZX_OK) {
FX_PLOGS(WARNING, status) << "Couldn't get process vmaps";
return std::vector<zx_info_maps_t>();
}
out.resize(actual);
return out;
}
bool PinExecutableMemory::ShouldSkip(size_t addr) const {
// Assuming this is usually empty, or at most has just a few mappings, hence O(n) is ok.
for (auto& m : discarded_mappings_) {
if (m.start <= addr && addr < m.end) {
return true;
}
}
return false;
}
} // namespace media::audio