sourcemod/sourcepawn/jit/smx-v1-image.cpp
David Anderson 04827466b0 Rewrite the .smx parser.
This removes one the last remnants of the SourceMod 1.0 VM implementation.

The new parser introduces a number of design changes in the VM. First, the VM now takes greater responsibility for validating and sanity checking the structure of the SMX container format. Previously, malformed SMX files could easily crash SourcePawn. The loader now rejects files that have out-of-bounds offsets or incomplete sections. Complex sections, like debug info or the code stream, are verified lazily.

Internally, the sp_plugin_t structure has been removed. It has been replaced by a new LegacyImage class, designed to be independent from the SPVM API. This potentially lets us load code streams from non-.smx containers. More importantly, it removes a lot of bookkeeping and pre-computed state from PluginRuntime. The LegacyImage class is now responsible for handling debug info as well.

PluginRuntime is now intended to hold only cached or immutable data, and PluginContext holds all VM state. As such PluginContext is now responsible for allocating a plugin's runtime memory, not PluginRuntime.

Finally, some aspects of the loading process have been cleaned up. The
decompression and image handoff logic should now be easier to
understand.
2015-02-25 22:28:10 -08:00

618 lines
17 KiB
C++

// vim: set sts=2 ts=8 sw=2 tw=99 et:
//
// Copyright (C) 2004-2015 AlliedModers LLC
//
// This file is part of SourcePawn. SourcePawn is licensed under the GNU
// General Public License, version 3.0 (GPL). If a copy of the GPL was not
// provided with this file, you can obtain it here:
// http://www.gnu.org/licenses/gpl.html
//
#include "smx-v1-image.h"
#include "zlib/zlib.h"
using namespace ke;
using namespace sp;
SmxV1Image::SmxV1Image(FILE *fp)
: FileReader(fp),
hdr_(nullptr),
header_strings_(nullptr),
names_section_(nullptr),
names_(nullptr),
debug_names_section_(nullptr),
debug_names_(nullptr),
debug_syms_(nullptr),
debug_syms_unpacked_(nullptr)
{
}
// Validating SMX v1 scripts is fairly expensive. We reserve real validation
// for v2.
bool
SmxV1Image::validate()
{
if (length_ < sizeof(sp_file_hdr_t))
return error("bad header");
hdr_ = (sp_file_hdr_t *)buffer();
if (hdr_->magic != SmxConsts::FILE_MAGIC)
return error("bad header");
switch (hdr_->version) {
case SmxConsts::SP1_VERSION_1_0:
case SmxConsts::SP1_VERSION_1_1:
case SmxConsts::SP1_VERSION_1_7:
break;
default:
return error("unsupported version");
}
switch (hdr_->compression) {
case SmxConsts::FILE_COMPRESSION_GZ:
{
// The start of the compression cannot be larger than the file.
if (hdr_->dataoffs > length_)
return error("illegal compressed region");
// The compressed region must start after the header.
if (hdr_->dataoffs < sizeof(sp_file_hdr_t))
return error("illegal compressed region");
// The full size of the image must be at least as large as the start
// of the compressed region.
if (hdr_->imagesize < hdr_->dataoffs)
return error("illegal image size");
// Allocate the uncompressed image buffer.
uint32_t compressedSize = hdr_->disksize - hdr_->dataoffs;
AutoArray<uint8_t> uncompressed(new uint8_t[hdr_->imagesize]);
if (!uncompressed)
return error("out of memory");
// Decompress.
const uint8_t *src = buffer() + hdr_->dataoffs;
uint8_t *dest = (uint8_t *)uncompressed + hdr_->dataoffs;
uLongf destlen = hdr_->imagesize - hdr_->dataoffs;
int rv = uncompress(
(Bytef *)dest,
&destlen,
src,
compressedSize);
if (rv != Z_OK)
return error("could not decode compressed region");
// Copy the initial uncompressed region back in.
memcpy((uint8_t *)uncompressed, buffer(), hdr_->dataoffs);
// Replace the original buffer.
length_ = hdr_->imagesize;
buffer_ = uncompressed.take();
hdr_ = (sp_file_hdr_t *)buffer();
break;
}
case SmxConsts::FILE_COMPRESSION_NONE:
break;
default:
return error("unknown compression type");
}
// Validate the string table.
if (hdr_->stringtab >= length_)
return error("invalid string table");
header_strings_ = reinterpret_cast<const char *>(buffer() + hdr_->stringtab);
// Validate sections header.
if ((sizeof(sp_file_hdr_t) + hdr_->sections * sizeof(sp_file_section_t)) > length_)
return error("invalid section table");
size_t last_header_string = 0;
const sp_file_section_t *sections =
reinterpret_cast<const sp_file_section_t *>(buffer() + sizeof(sp_file_hdr_t));
for (size_t i = 0; i < hdr_->sections; i++) {
if (sections[i].nameoffs >= (hdr_->dataoffs - hdr_->stringtab))
return error("invalid section name");
if (sections[i].nameoffs > last_header_string)
last_header_string = sections[i].nameoffs;
sections_.append(Section());
sections_.back().dataoffs = sections[i].dataoffs;
sections_.back().size = sections[i].size;
sections_.back().name = header_strings_ + sections[i].nameoffs;
}
// Validate sanity of section header strings.
bool found_terminator = false;
for (const uint8_t *iter = buffer() + last_header_string;
iter < buffer() + hdr_->dataoffs;
iter++)
{
if (*iter == '\0') {
found_terminator = true;
break;
}
}
if (!found_terminator)
return error("malformed section names header");
names_section_ = findSection(".names");
if (!names_section_)
return error("could not find .names section");
if (!validateSection(names_section_))
return error("invalid names section");
names_ = reinterpret_cast<const char *>(buffer() + names_section_->dataoffs);
// The names section must be 0-length or be null-terminated.
if (names_section_->size != 0 &&
*(names_ + names_section_->size - 1) != '\0')
{
return error("malformed names section");
}
if (!validateCode())
return false;
if (!validateData())
return false;
if (!validatePublics())
return false;
if (!validatePubvars())
return false;
if (!validateNatives())
return false;
if (!validateDebugInfo())
return false;
return true;
}
const SmxV1Image::Section *
SmxV1Image::findSection(const char *name)
{
for (size_t i = 0; i < sections_.length(); i++) {
if (strcmp(sections_[i].name, name) == 0)
return &sections_[i];
}
return nullptr;
}
bool
SmxV1Image::validateSection(const Section *section)
{
if (section->dataoffs >= length_)
return false;
if (section->size > length_ - section->dataoffs)
return false;
return true;
}
bool
SmxV1Image::validateData()
{
// .data is required.
const Section *section = findSection(".data");
if (!section)
return error("could not find data");
if (!validateSection(section))
return error("invalid data section");
const sp_file_data_t *data =
reinterpret_cast<const sp_file_data_t *>(buffer() + section->dataoffs);
if (data->data > section->size)
return error("invalid data blob");
if (data->datasize > (section->size - data->data))
return error("invalid data blob");
const uint8_t *blob =
reinterpret_cast<const uint8_t *>(data) + data->data;
data_ = Blob<sp_file_data_t>(section, data, blob, data->datasize);
return true;
}
bool
SmxV1Image::validateCode()
{
// .code is required.
const Section *section = findSection(".code");
if (!section)
return error("could not find code");
if (!validateSection(section))
return error("invalid code section");
const sp_file_code_t *code =
reinterpret_cast<const sp_file_code_t *>(buffer() + section->dataoffs);
if (code->codeversion < SmxConsts::CODE_VERSION_SP1_MIN)
return error("code version is too old, no longer supported");
if (code->codeversion > SmxConsts::CODE_VERSION_SP1_MAX)
return error("code version is too new, not supported");
if (code->cellsize != 4)
return error("unsupported cellsize");
if (code->flags & ~CODEFLAG_DEBUG)
return error("unsupported code settings");
if (code->code > section->size)
return error("invalid code blob");
if (code->codesize > (section->size - code->code))
return error("invalid code blob");
const uint8_t *blob =
reinterpret_cast<const uint8_t *>(code) + code->code;
code_ = Blob<sp_file_code_t>(section, code, blob, code->codesize);
return true;
}
bool
SmxV1Image::validatePublics()
{
const Section *section = findSection(".publics");
if (!section)
return true;
if (!validateSection(section))
return error("invalid .publics section");
if ((section->size % sizeof(sp_file_publics_t)) != 0)
return error("invalid .publics section");
const sp_file_publics_t *publics =
reinterpret_cast<const sp_file_publics_t *>(buffer() + section->dataoffs);
size_t length = section->size / sizeof(sp_file_publics_t);
for (size_t i = 0; i < length; i++) {
if (!validateName(publics[i].name))
return error("invalid public name");
}
publics_ = List<sp_file_publics_t>(publics, length);
return true;
}
bool
SmxV1Image::validatePubvars()
{
const Section *section = findSection(".pubvars");
if (!section)
return true;
if (!validateSection(section))
return error("invalid .pubvars section");
if ((section->size % sizeof(sp_file_pubvars_t)) != 0)
return error("invalid .pubvars section");
const sp_file_pubvars_t *pubvars =
reinterpret_cast<const sp_file_pubvars_t *>(buffer() + section->dataoffs);
size_t length = section->size / sizeof(sp_file_pubvars_t);
for (size_t i = 0; i < length; i++) {
if (!validateName(pubvars[i].name))
return error("invalid pubvar name");
}
pubvars_ = List<sp_file_pubvars_t>(pubvars, length);
return true;
}
bool
SmxV1Image::validateNatives()
{
const Section *section = findSection(".natives");
if (!section)
return true;
if (!validateSection(section))
return error("invalid .natives section");
if ((section->size % sizeof(sp_file_natives_t)) != 0)
return error("invalid .natives section");
const sp_file_natives_t *natives =
reinterpret_cast<const sp_file_natives_t *>(buffer() + section->dataoffs);
size_t length = section->size / sizeof(sp_file_natives_t);
for (size_t i = 0; i < length; i++) {
if (!validateName(natives[i].name))
return error("invalid pubvar name");
}
natives_ = List<sp_file_natives_t>(natives, length);
return true;
}
bool
SmxV1Image::validateName(size_t offset)
{
return offset < names_section_->size;
}
bool
SmxV1Image::validateDebugInfo()
{
const Section *dbginfo = findSection(".dbg.info");
if (!dbginfo)
return true;
if (!validateSection(dbginfo))
return error("invalid .dbg.info section");
debug_info_ =
reinterpret_cast<const sp_fdbg_info_t *>(buffer() + dbginfo->dataoffs);
debug_names_section_ = findSection(".dbg.strings");
if (!debug_names_section_)
return error("no debug string table");
if (!validateSection(debug_names_section_))
return error("invalid .dbg.strings section");
debug_names_ = reinterpret_cast<const char *>(buffer() + debug_names_section_->dataoffs);
// Name tables must be null-terminated.
if (debug_names_section_->size != 0 &&
*(debug_names_ + debug_names_section_->size - 1) != '\0')
{
return error("invalid .dbg.strings section");
}
const Section *files = findSection(".dbg.files");
if (!files)
return error("no debug file table");
if (!validateSection(files))
return error("invalid debug file table");
if (files->size < sizeof(sp_fdbg_file_t) * debug_info_->num_files)
return error("invalid debug file table");
debug_files_ = List<sp_fdbg_file_t>(
reinterpret_cast<const sp_fdbg_file_t *>(buffer() + files->dataoffs),
debug_info_->num_files);
const Section *lines = findSection(".dbg.lines");
if (!lines)
return error("no debug lines table");
if (!validateSection(lines))
return error("invalid debug lines table");
if (lines->size < sizeof(sp_fdbg_line_t) * debug_info_->num_lines)
return error("invalid debug lines table");
debug_lines_ = List<sp_fdbg_line_t>(
reinterpret_cast<const sp_fdbg_line_t *>(buffer() + lines->dataoffs),
debug_info_->num_lines);
debug_symbols_section_ = findSection(".dbg.symbols");
if (!debug_symbols_section_)
return error("no debug symbol table");
if (!validateSection(debug_symbols_section_))
return error("invalid debug symbol table");
// See the note about unpacked debug sections in smx-headers.h.
if (hdr_->version == SmxConsts::SP1_VERSION_1_0 &&
!findSection(".dbg.natives"))
{
debug_syms_unpacked_ =
reinterpret_cast<const sp_u_fdbg_symbol_t *>(buffer() + debug_symbols_section_->dataoffs);
} else {
debug_syms_ =
reinterpret_cast<const sp_fdbg_symbol_t *>(buffer() + debug_symbols_section_->dataoffs);
}
return true;
}
auto
SmxV1Image::DescribeCode() const -> Code
{
Code code;
code.bytes = code_.blob();
code.length = code_.length();
switch (code_->codeversion) {
case SmxConsts::CODE_VERSION_JIT_1_0:
code.version = CodeVersion::SP_1_0;
break;
case SmxConsts::CODE_VERSION_JIT_1_1:
code.version = CodeVersion::SP_1_1;
break;
default:
assert(false);
code.version = CodeVersion::Unknown;
break;
}
return code;
}
auto
SmxV1Image::DescribeData() const -> Data
{
Data data;
data.bytes = data_.blob();
data.length = data_.length();
return data;
}
size_t
SmxV1Image::NumNatives() const
{
return natives_.length();
}
const char *
SmxV1Image::GetNative(size_t index) const
{
assert(index < natives_.length());
return names_ + natives_[index].name;
}
bool
SmxV1Image::FindNative(const char *name, size_t *indexp) const
{
for (size_t i = 0; i < natives_.length(); i++) {
const char *candidate = names_ + natives_[i].name;
if (strcmp(candidate, name) == 0) {
if (indexp)
*indexp = i;
return true;
}
}
return false;
}
size_t
SmxV1Image::NumPublics() const
{
return publics_.length();
}
void
SmxV1Image::GetPublic(size_t index, uint32_t *offsetp, const char **namep) const
{
assert(index < publics_.length());
if (offsetp)
*offsetp = publics_[index].address;
if (namep)
*namep = names_ + publics_[index].name;
}
bool
SmxV1Image::FindPublic(const char *name, size_t *indexp) const
{
int high = publics_.length() - 1;
int low = 0;
while (low <= high) {
int mid = (low + high) / 2;
const char *candidate = names_ + publics_[mid].name;
int diff = strcmp(candidate, name);
if (diff == 0) {
if (indexp) {
*indexp = mid;
return true;
}
}
if (diff < 0)
low = mid + 1;
else
high = mid - 1;
}
return false;
}
size_t
SmxV1Image::NumPubvars() const
{
return pubvars_.length();
}
void
SmxV1Image::GetPubvar(size_t index, uint32_t *offsetp, const char **namep) const
{
assert(index < pubvars_.length());
if (offsetp)
*offsetp = pubvars_[index].address;
if (namep)
*namep = names_ + pubvars_[index].name;
}
bool
SmxV1Image::FindPubvar(const char *name, size_t *indexp) const
{
int high = pubvars_.length() - 1;
int low = 0;
while (low <= high) {
int mid = (low + high) / 2;
const char *candidate = names_ + pubvars_[mid].name;
int diff = strcmp(candidate, name);
if (diff == 0) {
if (indexp) {
*indexp = mid;
return true;
}
}
if (diff < 0)
low = mid + 1;
else
high = mid - 1;
}
return false;
}
size_t
SmxV1Image::HeapSize() const
{
return data_->memsize;
}
size_t
SmxV1Image::ImageSize() const
{
return length_;
}
const char *
SmxV1Image::LookupFile(uint32_t addr)
{
int high = debug_files_.length();
int low = -1;
while (high - low > 1) {
int mid = (low + high) / 2;
if (debug_files_[mid].addr <= addr)
low = mid;
else
high = mid;
}
if (low == -1)
return nullptr;
if (debug_files_[low].name >= debug_names_section_->size)
return nullptr;
return debug_names_ + debug_files_[low].name;
}
template <typename SymbolType, typename DimType>
const char *
SmxV1Image::lookupFunction(const SymbolType *syms, uint32_t addr)
{
const uint8_t *cursor = reinterpret_cast<const uint8_t *>(syms);
const uint8_t *cursor_end = cursor + debug_symbols_section_->size;
for (uint32_t i = 0; i < debug_info_->num_syms; i++) {
if (cursor + sizeof(SymbolType) > cursor_end)
break;
const SymbolType *sym = reinterpret_cast<const SymbolType *>(cursor);
if (sym->ident == sp::IDENT_FUNCTION &&
sym->codestart <= addr &&
sym->codeend > addr)
{
if (sym->name >= debug_names_section_->size)
return nullptr;
return debug_names_ + sym->name;
}
if (sym->dimcount > 0)
cursor += sizeof(DimType) * sym->dimcount;
cursor += sizeof(SymbolType);
}
return nullptr;
}
const char *
SmxV1Image::LookupFunction(uint32_t code_offset)
{
if (debug_syms_) {
return lookupFunction<sp_fdbg_symbol_t, sp_fdbg_arraydim_t>(
debug_syms_, code_offset);
}
return lookupFunction<sp_u_fdbg_symbol_t, sp_u_fdbg_arraydim_t>(
debug_syms_unpacked_, code_offset);
}
bool
SmxV1Image::LookupLine(uint32_t addr, uint32_t *line)
{
int high = debug_lines_.length();
int low = -1;
while (high - low > 1) {
int mid = (low + high) / 2;
if (debug_lines_[mid].addr <= addr)
low = mid;
else
high = mid;
}
if (low == -1)
return false;
// Since the CIP occurs BEFORE the line, we have to add one.
*line = debug_lines_[low].line + 1;
return true;
}