UserspaceEmulator: Support dynamically loaded programs

When loading dynamic objects, the emulator loads the interpreter,
generates an auxiliary vector and starts executing the loader.

Additionally, this commits also makes the MallocTracer and backtrace
symbolication work for dynamically loaded programs.
This commit is contained in:
Itamar 2020-11-23 20:00:35 +02:00 committed by Andreas Kling
parent 28cda567c1
commit 72ca45e300
Notes: sideshowbarker 2024-07-19 00:50:07 +09:00
9 changed files with 247 additions and 64 deletions

View file

@ -31,7 +31,11 @@
#include "SoftCPU.h"
#include <AK/Format.h>
#include <AK/LexicalPath.h>
#include <AK/MappedFile.h>
#include <Kernel/API/Syscall.h>
#include <LibELF/AuxiliaryVector.h>
#include <LibELF/Image.h>
#include <LibELF/Validation.h>
#include <LibPthread/pthread.h>
#include <LibX86/ELFSymbolProvider.h>
#include <fcntl.h>
@ -55,7 +59,7 @@
# pragma GCC optimize("O3")
#endif
//#define DEBUG_SPAM
// #define DEBUG_SPAM
namespace UserspaceEmulator {
@ -70,20 +74,45 @@ Emulator& Emulator::the()
return *s_the;
}
Emulator::Emulator(const Vector<String>& arguments, const Vector<String>& environment, NonnullRefPtr<ELF::Loader> elf)
: m_elf(move(elf))
Emulator::Emulator(const String& executable_path, const Vector<String>& arguments, const Vector<String>& environment)
: m_executable_path(executable_path)
, m_arguments(arguments)
, m_environment(environment)
, m_mmu(*this)
, m_cpu(*this)
{
m_malloc_tracer = make<MallocTracer>(*this);
ASSERT(!s_the);
s_the = this;
setup_stack(arguments, environment);
// setup_stack(arguments, environment);
register_signal_handlers();
setup_signal_trampoline();
}
void Emulator::setup_stack(const Vector<String>& arguments, const Vector<String>& environment)
Vector<AuxiliaryValue> Emulator::generate_auxiliary_vector(FlatPtr load_base, FlatPtr entry_eip, String executable_path, int executable_fd) const
{
// FIXME: This is not fully compatible with the auxiliary vector the kernel generates, this is just the bare
// minimum to get the loader going.
Vector<AuxiliaryValue> auxv;
// PHDR/EXECFD
// PH*
auxv.append({ AuxiliaryValue::PageSize, PAGE_SIZE });
auxv.append({ AuxiliaryValue::BaseAddress, (void*)load_base });
auxv.append({ AuxiliaryValue::Entry, (void*)entry_eip });
// FIXME: Don't hard code this? We might support other platforms later.. (e.g. x86_64)
auxv.append({ AuxiliaryValue::Platform, "i386" });
auxv.append({ AuxiliaryValue::ExecFilename, executable_path });
auxv.append({ AuxiliaryValue::ExecFileDescriptor, executable_fd });
auxv.append({ AuxiliaryValue::Null, 0L });
return auxv;
}
void Emulator::setup_stack(Vector<AuxiliaryValue> aux_vector)
{
auto stack_region = make<SimpleRegion>(stack_location, stack_size);
stack_region->set_stack(true);
@ -92,18 +121,30 @@ void Emulator::setup_stack(const Vector<String>& arguments, const Vector<String>
Vector<u32> argv_entries;
for (auto& argument : arguments) {
for (auto& argument : m_arguments) {
m_cpu.push_string(argument.characters());
argv_entries.append(m_cpu.esp().value());
}
Vector<u32> env_entries;
for (auto& variable : environment) {
for (auto& variable : m_environment) {
m_cpu.push_string(variable.characters());
env_entries.append(m_cpu.esp().value());
}
for (auto& auxv : aux_vector) {
if (!auxv.optional_string.is_empty()) {
m_cpu.push_string(auxv.optional_string.characters());
auxv.auxv.a_un.a_ptr = (void*)m_cpu.esp().value();
}
}
for (ssize_t i = aux_vector.size() - 1; i >= 0; --i) {
auto& value = aux_vector[i].auxv;
m_cpu.push_buffer((const u8*)&value, sizeof(value));
}
m_cpu.push32(shadow_wrap_as_initialized<u32>(0)); // char** envp = { envv_entries..., nullptr }
for (ssize_t i = env_entries.size() - 1; i >= 0; --i)
m_cpu.push32(shadow_wrap_as_initialized(env_entries[i]));
@ -125,54 +166,70 @@ void Emulator::setup_stack(const Vector<String>& arguments, const Vector<String>
bool Emulator::load_elf()
{
m_elf->image().for_each_program_header([&](const ELF::Image::ProgramHeader& program_header) {
MappedFile mapped_executable(m_executable_path);
if (!mapped_executable.is_valid()) {
reportln("Unable to map {}", m_executable_path);
return false;
}
ELF::Image executable_elf((const u8*)mapped_executable.data(), mapped_executable.size());
if (!executable_elf.is_dynamic()) {
// FIXME: Support static objects
ASSERT_NOT_REACHED();
}
String interpreter_path;
if (!ELF::validate_program_headers(*(Elf32_Ehdr*)mapped_executable.data(), mapped_executable.size(), (u8*)mapped_executable.data(), mapped_executable.size(), &interpreter_path)) {
reportln("failed to validate ELF file");
return false;
}
ASSERT(!interpreter_path.is_null());
dbgln("interpreter: {}", interpreter_path);
auto interpreter_file = make<MappedFile>(interpreter_path);
ASSERT(interpreter_file->is_valid());
ELF::Image interpreter_image((const u8*)interpreter_file->data(), interpreter_file->size());
constexpr FlatPtr interpreter_load_offset = 0x08000000;
interpreter_image.for_each_program_header([&](const ELF::Image::ProgramHeader& program_header) {
// Loader is not allowed to have its own TLS regions
ASSERT(program_header.type() != PT_TLS);
if (program_header.type() == PT_LOAD) {
auto region = make<SimpleRegion>(program_header.vaddr().get(), program_header.size_in_memory());
auto region = make<SimpleRegion>(program_header.vaddr().offset(interpreter_load_offset).get(), program_header.size_in_memory());
if (program_header.is_executable() && !program_header.is_writable())
region->set_text(true);
memcpy(region->data(), program_header.raw_data(), program_header.size_in_image());
memset(region->shadow_data(), 0x01, program_header.size_in_memory());
if (program_header.is_executable()) {
m_loader_text_base = region->base();
m_loader_text_size = region->size();
}
mmu().add_region(move(region));
return;
}
if (program_header.type() == PT_TLS) {
auto tcb_region = make<SimpleRegion>(0x20000000, program_header.size_in_memory());
memcpy(tcb_region->data(), program_header.raw_data(), program_header.size_in_image());
memset(tcb_region->shadow_data(), 0x01, program_header.size_in_memory());
auto tls_region = make<SimpleRegion>(0, 4);
tls_region->write32(0, shadow_wrap_as_initialized(tcb_region->base() + program_header.size_in_memory()));
memset(tls_region->shadow_data(), 0x01, 4);
mmu().add_region(move(tcb_region));
mmu().set_tls_region(move(tls_region));
return;
}
});
m_cpu.set_eip(m_elf->image().entry().get());
auto entry_point = interpreter_image.entry().offset(interpreter_load_offset).get();
m_cpu.set_eip(entry_point);
auto malloc_symbol = m_elf->find_demangled_function("malloc");
auto free_symbol = m_elf->find_demangled_function("free");
auto realloc_symbol = m_elf->find_demangled_function("realloc");
auto malloc_size_symbol = m_elf->find_demangled_function("malloc_size");
// executable_fd will be used by the loader
int executable_fd = open(m_executable_path.characters(), O_RDONLY);
if (executable_fd < 0)
return false;
m_malloc_symbol_start = malloc_symbol.value().value();
m_malloc_symbol_end = m_malloc_symbol_start + malloc_symbol.value().size();
m_free_symbol_start = free_symbol.value().value();
m_free_symbol_end = m_free_symbol_start + free_symbol.value().size();
m_realloc_symbol_start = realloc_symbol.value().value();
m_realloc_symbol_end = m_realloc_symbol_start + realloc_symbol.value().size();
m_malloc_size_symbol_start = malloc_size_symbol.value().value();
m_malloc_size_symbol_end = m_malloc_size_symbol_start + malloc_size_symbol.value().size();
auto aux_vector = generate_auxiliary_vector(interpreter_load_offset, entry_point, m_executable_path, executable_fd);
setup_stack(move(aux_vector));
m_debug_info = make<Debug::DebugInfo>(m_elf);
return true;
}
int Emulator::exec()
{
X86::ELFSymbolProvider symbol_provider(*m_elf);
// X86::ELFSymbolProvider symbol_provider(*m_elf);
X86::ELFSymbolProvider* symbol_provider = nullptr;
bool trace = false;
@ -182,7 +239,7 @@ int Emulator::exec()
auto insn = X86::Instruction::from_stream(m_cpu, true, true);
if (trace)
outln("{:p} \033[33;1m{}\033[0m", m_cpu.base_eip(), insn.to_string(m_cpu.base_eip(), &symbol_provider));
outln("{:p} \033[33;1m{}\033[0m", m_cpu.base_eip(), insn.to_string(m_cpu.base_eip(), symbol_provider));
(m_cpu.*insn.handler())(insn);
@ -217,16 +274,58 @@ Vector<FlatPtr> Emulator::raw_backtrace()
return backtrace;
}
const MmapRegion* Emulator::find_text_region(FlatPtr address)
{
const MmapRegion* matching_region = nullptr;
mmu().for_each_region([&](auto& region) {
if (!region.is_mmap())
return IterationDecision::Continue;
const auto& mmap_region = static_cast<const MmapRegion&>(region);
if (!(mmap_region.is_executable() && address >= mmap_region.base() && address < mmap_region.base() + mmap_region.size()))
return IterationDecision::Continue;
matching_region = &mmap_region;
return IterationDecision::Break;
});
return matching_region;
}
String Emulator::create_backtrace_line(FlatPtr address)
{
String minimal = String::format("=={%d}== %p", getpid(), address);
const auto* region = find_text_region(address);
if (!region)
return minimal;
auto separator_index = region->name().index_of(":");
if (!separator_index.has_value())
return minimal;
String lib_name = region->name().substring(0, separator_index.value());
String lib_path = lib_name;
if (region->name().contains(".so"))
lib_path = String::format("/usr/lib/%s", lib_path.characters());
auto mapped_file = make<MappedFile>(lib_path);
if (!mapped_file->is_valid())
return minimal;
auto loader = ELF::Loader::create((const u8*)mapped_file->data(), mapped_file->size());
String symbol = loader->symbolicate(address - region->base());
auto line_without_source_info = String::format("=={%d}== %p [%s]: %s", getpid(), address, lib_name.characters(), symbol.characters());
auto debug_info = make<Debug::DebugInfo>(loader);
auto source_position = debug_info->get_source_position(address - region->base());
if (source_position.has_value())
return String::format("=={%d}== %p [%s]: %s (\033[34;1m%s\033[0m:%u)", getpid(), address, lib_name.characters(), symbol.characters(), LexicalPath(source_position.value().file_path).basename().characters(), source_position.value().line_number);
else {
return line_without_source_info;
}
}
void Emulator::dump_backtrace(const Vector<FlatPtr>& backtrace)
{
for (auto& address : backtrace) {
u32 offset = 0;
String symbol = m_elf->symbolicate(address, &offset);
auto source_position = m_debug_info->get_source_position(address);
if (source_position.has_value())
reportln("=={}== {:p} {} (\033[34;1m{}\033[0m:{})", getpid(), address, symbol, LexicalPath(source_position.value().file_path).basename(), source_position.value().line_number);
else
reportln("=={}== {:p} {} +{:x}", getpid(), address, symbol, offset);
reportln("{}", create_backtrace_line(address));
}
}
@ -406,6 +505,8 @@ u32 Emulator::virt_syscall(u32 function, u32 arg1, u32 arg2, u32 arg3)
return virt$clock_nanosleep(arg1);
case SC_readlink:
return virt$readlink(arg1);
case SC_allocate_tls:
return virt$allocate_tls(arg1);
default:
reportln("\n=={}== \033[31;1mUnimplemented syscall: {}\033[0m, {:p}", getpid(), Syscall::to_string((Syscall::Function)function), function);
dump_backtrace();
@ -870,8 +971,6 @@ u32 Emulator::virt$mmap(u32 params_addr)
Syscall::SC_mmap_params params;
mmu().copy_from_vm(&params, params_addr, sizeof(params));
ASSERT(params.addr == 0);
u32 final_size = round_up_to_power_of_two(params.size, PAGE_SIZE);
u32 final_address = allocate_vm(final_size, params.alignment);
if (params.addr != 0) {
@ -886,14 +985,18 @@ u32 Emulator::virt$mmap(u32 params_addr)
if (params.flags & MAP_ANONYMOUS)
mmu().add_region(MmapRegion::create_anonymous(final_address, final_size, params.prot));
else {
dbgln("chars: {:p}, len: {}", params.name.characters, params.name.length);
String name_str;
if (params.name.characters) {
auto name = ByteBuffer::create_uninitialized(params.name.length);
mmu().copy_from_vm(name.data(), (FlatPtr)params.name.characters, params.name.length);
name_str = { name.data(), name.size() };
}
mmu().add_region(MmapRegion::create_file_backed(final_address, final_size, params.prot, params.flags, params.fd, params.offset, name_str));
auto region = MmapRegion::create_file_backed(final_address, final_size, params.prot, params.flags, params.fd, params.offset, name_str);
if (region->name() == "libc.so: .text (Emulated)") {
bool rc = find_malloc_symbols(*region);
ASSERT(rc);
}
mmu().add_region(move(region));
}
return final_address;
@ -1567,4 +1670,46 @@ int Emulator::virt$readlink(FlatPtr params_addr)
return rc;
}
u32 Emulator::virt$allocate_tls(size_t size)
{
// TODO: Why is this needed? without this, the loader overflows the bounds of the TLS region.
constexpr size_t TLS_SIZE_HACK = 8;
auto tcb_region = make<SimpleRegion>(0x20000000, size + TLS_SIZE_HACK);
bzero(tcb_region->data(), size);
memset(tcb_region->shadow_data(), 0x01, size);
auto tls_region = make<SimpleRegion>(0, 4);
tls_region->write32(0, shadow_wrap_as_initialized(tcb_region->base() + (u32)size));
memset(tls_region->shadow_data(), 0x01, 4);
u32 tls_base = tcb_region->base();
mmu().add_region(move(tcb_region));
mmu().set_tls_region(move(tls_region));
return tls_base;
}
bool Emulator::find_malloc_symbols(const MmapRegion& libc_text)
{
auto mapped_file = make<MappedFile>("/usr/lib/libc.so");
if (!mapped_file->is_valid())
return {};
ELF::Image image((const u8*)mapped_file->data(), mapped_file->size());
auto malloc_symbol = image.find_demangled_function("malloc");
auto free_symbol = image.find_demangled_function("free");
auto realloc_symbol = image.find_demangled_function("realloc");
auto malloc_size_symbol = image.find_demangled_function("malloc_size");
if (!malloc_symbol.has_value() || !free_symbol.has_value() || !realloc_symbol.has_value() || !malloc_size_symbol.has_value())
return false;
m_malloc_symbol_start = malloc_symbol.value().value() + libc_text.base();
m_malloc_symbol_end = m_malloc_symbol_start + malloc_symbol.value().size();
m_free_symbol_start = free_symbol.value().value() + libc_text.base();
m_free_symbol_end = m_free_symbol_start + free_symbol.value().size();
m_realloc_symbol_start = realloc_symbol.value().value() + libc_text.base();
m_realloc_symbol_end = m_realloc_symbol_start + realloc_symbol.value().size();
m_malloc_size_symbol_start = malloc_size_symbol.value().value() + libc_text.base();
m_malloc_size_symbol_end = m_malloc_size_symbol_start + malloc_size_symbol.value().size();
return true;
}
}

View file

@ -32,6 +32,7 @@
#include "SoftMMU.h"
#include <AK/Types.h>
#include <LibDebug/DebugInfo.h>
#include <LibELF/AuxiliaryVector.h>
#include <LibELF/Loader.h>
#include <LibX86/Instruction.h>
#include <signal.h>
@ -45,7 +46,7 @@ class Emulator {
public:
static Emulator& the();
Emulator(const Vector<String>& arguments, const Vector<String>& environment, NonnullRefPtr<ELF::Loader>);
Emulator(const String& executable_path, const Vector<String>& arguments, const Vector<String>& environment);
bool load_elf();
void dump_backtrace();
@ -60,19 +61,22 @@ public:
MallocTracer* malloc_tracer() { return m_malloc_tracer; }
bool is_in_malloc_or_free() const;
bool is_in_loader_code() const;
void did_receive_signal(int signum) { m_pending_signals |= (1 << signum); }
private:
NonnullRefPtr<ELF::Loader> m_elf;
OwnPtr<Debug::DebugInfo> m_debug_info;
const String m_executable_path;
const Vector<String> m_arguments;
const Vector<String> m_environment;
SoftMMU m_mmu;
SoftCPU m_cpu;
OwnPtr<MallocTracer> m_malloc_tracer;
void setup_stack(const Vector<String>& arguments, const Vector<String>& environment);
void setup_stack(Vector<AuxiliaryValue>);
Vector<AuxiliaryValue> generate_auxiliary_vector(FlatPtr load_base, FlatPtr entry_eip, String executable_path, int executable_fd) const;
void register_signal_handlers();
void setup_signal_trampoline();
@ -158,10 +162,14 @@ private:
pid_t virt$setsid();
int virt$watch_file(FlatPtr, size_t);
int virt$readlink(FlatPtr);
u32 virt$allocate_tls(size_t);
FlatPtr allocate_vm(size_t size, size_t alignment);
bool find_malloc_symbols(const MmapRegion& libc_text);
void dispatch_one_pending_signal();
const MmapRegion* find_text_region(FlatPtr address);
String create_backtrace_line(FlatPtr address);
bool m_shutdown { false };
int m_exit_status { 0 };
@ -186,6 +194,8 @@ private:
SignalHandlerInfo m_signal_handler[NSIG];
FlatPtr m_signal_trampoline { 0 };
Optional<FlatPtr> m_loader_text_base;
Optional<size_t> m_loader_text_size;
};
ALWAYS_INLINE bool Emulator::is_in_malloc_or_free() const
@ -196,4 +206,11 @@ ALWAYS_INLINE bool Emulator::is_in_malloc_or_free() const
|| (m_cpu.base_eip() >= m_malloc_size_symbol_start && m_cpu.base_eip() < m_malloc_size_symbol_end);
}
ALWAYS_INLINE bool Emulator::is_in_loader_code() const
{
if (!m_loader_text_base.has_value() || !m_loader_text_size.has_value())
return false;
return (m_cpu.base_eip() >= m_loader_text_base.value() && m_cpu.base_eip() < m_loader_text_base.value() + m_loader_text_size.value());
}
}

View file

@ -58,6 +58,8 @@ inline void MallocTracer::for_each_mallocation(Callback callback) const
void MallocTracer::target_did_malloc(Badge<SoftCPU>, FlatPtr address, size_t size)
{
if (m_emulator.is_in_loader_code())
return;
auto* region = m_emulator.mmu().find_region({ 0x20, address });
ASSERT(region);
ASSERT(region->is_mmap());
@ -109,6 +111,7 @@ ALWAYS_INLINE size_t MallocRegionMetadata::chunk_index_for_address(FlatPtr addre
return 0;
}
auto chunk_offset = address - (this->address + sizeof(ChunkedBlock));
ASSERT(this->chunk_size);
return chunk_offset / this->chunk_size;
}
@ -116,6 +119,8 @@ void MallocTracer::target_did_free(Badge<SoftCPU>, FlatPtr address)
{
if (!address)
return;
if (m_emulator.is_in_loader_code())
return;
if (auto* mallocation = find_mallocation(address)) {
if (mallocation->freed) {
@ -136,6 +141,8 @@ void MallocTracer::target_did_free(Badge<SoftCPU>, FlatPtr address)
void MallocTracer::target_did_realloc(Badge<SoftCPU>, FlatPtr address, size_t size)
{
if (m_emulator.is_in_loader_code())
return;
auto* region = m_emulator.mmu().find_region({ 0x20, address });
ASSERT(region);
ASSERT(region->is_mmap());
@ -201,8 +208,13 @@ void MallocTracer::audit_read(const Region& region, FlatPtr address, size_t size
if (!m_auditing_enabled)
return;
if (m_emulator.is_in_malloc_or_free())
if (m_emulator.is_in_malloc_or_free()) {
return;
}
if (m_emulator.is_in_loader_code()) {
return;
}
auto* mallocation = find_mallocation(region, address);
@ -246,6 +258,10 @@ void MallocTracer::audit_write(const Region& region, FlatPtr address, size_t siz
if (m_emulator.is_in_malloc_or_free())
return;
if (m_emulator.is_in_loader_code()) {
return;
}
auto* mallocation = find_mallocation(region, address);
if (!mallocation) {
reportln("\n=={}== \033[31;1mHeap buffer overflow\033[0m, invalid {}-byte write at address {:p}", getpid(), size, address);
@ -315,6 +331,8 @@ bool MallocTracer::is_reachable(const Mallocation& mallocation) const
return IterationDecision::Continue;
if (region.is_text())
return IterationDecision::Continue;
if (!region.is_readable())
return IterationDecision::Continue;
// Skip malloc blocks
if (region.is_mmap() && static_cast<const MmapRegion&>(region).is_malloc_block())
return IterationDecision::Continue;

View file

@ -44,7 +44,6 @@ NonnullOwnPtr<MmapRegion> MmapRegion::create_file_backed(u32 base, u32 size, u32
auto region = adopt_own(*new MmapRegion(base, size, prot));
region->m_file_backed = true;
if (!name.is_empty()) {
dbgln("name is not empty");
name = String::format("%s (Emulated)", name.characters());
region->m_name = name;
}

View file

@ -61,6 +61,8 @@ public:
MallocRegionMetadata* malloc_metadata() { return m_malloc_metadata; }
void set_malloc_metadata(Badge<MallocTracer>, NonnullOwnPtr<MallocRegionMetadata> metadata) { m_malloc_metadata = move(metadata); }
const String& name() const { return m_name; }
private:
MmapRegion(u32 base, u32 size, int prot);
virtual bool is_mmap() const override { return true; }

View file

@ -229,6 +229,13 @@ void SoftCPU::push_string(const StringView& string)
m_emulator.mmu().write8({ 0x20, esp().value() + string.length() }, shadow_wrap_as_initialized((u8)'\0'));
}
void SoftCPU::push_buffer(const u8* data, size_t size)
{
set_esp({ esp().value() - size, esp().shadow() });
warn_if_uninitialized(esp(), "push_buffer");
m_emulator.mmu().copy_to_vm(esp().value(), data, size);
}
void SoftCPU::push32(ValueWithShadow<u32> value)
{
set_esp({ esp().value() - sizeof(u32), esp().shadow() });

View file

@ -93,6 +93,7 @@ public:
ValueWithShadow<u16> pop16();
void push_string(const StringView&);
void push_buffer(const u8* data, size_t);
u16 segment(X86::SegmentRegister seg) const { return m_segment[(int)seg]; }
u16& segment(X86::SegmentRegister seg) { return m_segment[(int)seg]; }

View file

@ -50,14 +50,6 @@ int main(int argc, char** argv, char** env)
auto executable_path = Core::find_executable_in_path(command[0]);
MappedFile mapped_file(executable_path);
if (!mapped_file.is_valid()) {
reportln("Unable to map {}", executable_path);
return 1;
}
auto elf = ELF::Loader::create((const u8*)mapped_file.data(), mapped_file.size());
Vector<String> arguments;
for (auto arg : command) {
arguments.append(arg);
@ -69,7 +61,7 @@ int main(int argc, char** argv, char** env)
}
// FIXME: It might be nice to tear down the emulator properly.
auto& emulator = *new UserspaceEmulator::Emulator(arguments, environment, move(elf));
auto& emulator = *new UserspaceEmulator::Emulator(executable_path, arguments, environment);
if (!emulator.load_elf())
return 1;

View file

@ -254,6 +254,8 @@ static FlatPtr loader_main(auxv_t* auxvp)
}
}
ASSERT(main_program_fd >= 0);
ASSERT(!main_program_name.is_null());
dbgln("loading: {}", main_program_name);
map_library(main_program_name, main_program_fd);
map_dependencies(main_program_name);