Files
ppsspp/Common/GhidraClient.cpp
T
Henrik Rydgård 98e8ffe7cd Check framebuffer copy sources, and fix two easy crashes
The three framebuffer upload paths took Memory::GetPointerUnchecked() on a
GE-supplied source address and then read height rows of it, without ever checking
that span was mapped. Only the destination was validated (and DoBlockTransfer's own
memcpy is carefully guarded, so the intent was clearly there). A copy whose source
starts near the end of RAM walks straight off the end of the view. Clamp the row
count to what's actually mapped, and warn when we do.

GhidraClient dereferenced getArray()->value for both "symbols" and "types" without a
null check, and the getTag() test underneath could never catch it - getArray() has
already filtered by tag, so it returns either a JSON_ARRAY node or nullptr. Any
HTTP 200 that parses as JSON but isn't the shape we expect - {}, a bare array, an
incompatible ghidra-rest-api, or the host/port pointed at some other JSON service -
crashed the worker thread. FetchTypes() runs first, so that's the one you'd hit.

RiscV and LoongArch CPU detection divided TotalLogicalCount() by ProcessorCount()
before checking it. ProcessorCount() returns 0 whenever /proc/cpuinfo can't be read
or doesn't parse, which is SIGFPE during static init of the cpu_info global - before
anything could handle it. The existing <= 0 guard sat after the division.

314 pspautotests pass; frametests show the same 3 pre-existing failures as master.
2026-08-31 12:15:50 +02:00

225 lines
6.9 KiB
C++

#include "Common/Data/Format/JSONReader.h"
#include "Common/Net/HTTPClient.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/GhidraClient.h"
using namespace json;
static GhidraTypeKind ResolveTypeKind(const std::string &kind) {
if (kind == "ENUM") return ENUM;
if (kind == "TYPEDEF") return TYPEDEF;
if (kind == "POINTER") return POINTER;
if (kind == "ARRAY") return ARRAY;
if (kind == "STRUCTURE") return STRUCTURE;
if (kind == "UNION") return UNION;
if (kind == "FUNCTION_DEFINITION") return FUNCTION_DEFINITION;
if (kind == "BUILT_IN") return BUILT_IN;
return UNKNOWN;
}
static bool IsBitfieldEnum(const std::vector<GhidraEnumMember> &enumMembers) {
u64 previousValues = 0;
for (const auto &member : enumMembers) {
if (previousValues & member.value) {
return false;
}
previousValues |= member.value;
}
return true;
}
GhidraClient::~GhidraClient() {
if (thread_.joinable()) {
thread_.join();
}
}
void GhidraClient::FetchAll(const std::string &host, const int port) {
std::lock_guard<std::mutex> lock(mutex_);
if (status_ != Status::Idle) {
return;
}
status_ = Status::Pending;
thread_ = std::thread([this, host, port] {
SetCurrentThreadName("GhidraClient");
FetchAllDo(host, port);
});
}
bool GhidraClient::FetchAllDo(const std::string &host, const int port) {
std::lock_guard<std::mutex> lock(mutex_);
host_ = host;
port_ = port;
const bool result = FetchTypes() && FetchSymbols();
status_ = Status::Ready;
return result;
}
void GhidraClient::UpdateResult() {
std::lock_guard<std::mutex> lock(mutex_);
if (status_ != Status::Ready) {
return;
}
if (thread_.joinable()) {
thread_.join();
}
result = std::move(pendingResult_);
pendingResult_ = Result();
status_ = Status::Idle;
}
bool GhidraClient::FetchSymbols() {
std::string json;
if (!FetchResource("/v1/symbols", json)) {
return false;
}
JsonReader reader(json.c_str(), json.size());
if (!reader.ok()) {
pendingResult_.error = "symbols parsing error";
return false;
}
const JsonNode *entriesNode = reader.root().getArray("symbols");
if (!entriesNode) {
// Null for a missing key, a non-array value, or a root that isn't an object at all -
// so any JSON that parses but isn't what we expect. The getTag() check below it could
// never catch that, since getArray() already filtered by tag.
pendingResult_.error = "symbols is not an array";
return false;
}
const JsonValue entries = entriesNode->value;
for (const auto pEntry : entries) {
JsonGet entry = pEntry->value;
GhidraSymbol symbol;
symbol.address = entry.getInt("address", 0);
symbol.name = entry.getStringOr("name", "");
symbol.label = strcmp(entry.getStringOr("type", ""), "Label") == 0;
symbol.userDefined = strcmp(entry.getStringOr("source", ""), "USER_DEFINED") == 0;
symbol.dataTypePathName = entry.getStringOr("dataTypePathName", "");
pendingResult_.symbols.emplace_back(symbol);
}
return true;
}
bool GhidraClient::FetchTypes() {
std::string json;
if (!FetchResource("/v1/types", json)) {
return false;
}
JsonReader reader(json.c_str(), json.size());
if (!reader.ok()) {
pendingResult_.error = "types parsing error";
return false;
}
const JsonNode *entriesNode = reader.root().getArray("types");
if (!entriesNode) {
// Null for a missing key, a non-array value, or a root that isn't an object at all -
// so any JSON that parses but isn't what we expect. The getTag() check below it could
// never catch that, since getArray() already filtered by tag.
pendingResult_.error = "types is not an array";
return false;
}
const JsonValue entries = entriesNode->value;
for (const auto pEntry : entries) {
const JsonGet entry = pEntry->value;
GhidraType type;
type.displayName = entry.getStringOr("displayName", "");
type.pathName = entry.getStringOr("pathName", "");
type.length = entry.getInt("length", 0);
type.alignedLength = entry.getInt("alignedLength", 0);
type.zeroLength = entry.getBoolOr("zeroLength", false);
type.description = entry.getStringOr("description", "");
type.kind = ResolveTypeKind(entry.getStringOr("kind", ""));
switch (type.kind) {
case ENUM: {
const JsonNode *enumEntries = entry.getArray("members");
if (!enumEntries) {
pendingResult_.error = "missing enum members";
return false;
}
for (const JsonNode *pEnumEntry : enumEntries->value) {
JsonGet enumEntry = pEnumEntry->value;
GhidraEnumMember member;
member.name = enumEntry.getStringOr("name", "");
member.value = enumEntry.getInt("value", 0);
member.comment = enumEntry.getStringOr("comment", "");
type.enumMembers.push_back(member);
}
type.enumBitfield = IsBitfieldEnum(type.enumMembers);
break;
}
case TYPEDEF:
type.typedefTypePathName = entry.getStringOr("typePathName", "");
type.typedefBaseTypePathName = entry.getStringOr("baseTypePathName", "");
break;
case POINTER:
type.pointerTypePathName = entry.getStringOr("typePathName", "");
break;
case ARRAY:
type.arrayTypePathName = entry.getStringOr("typePathName", "");
type.arrayElementLength = entry.getInt("elementLength", 0);
type.arrayElementCount = entry.getInt("elementCount", 0);
break;
case STRUCTURE:
case UNION: {
const JsonNode *compositeEntries = entry.getArray("members");
if (!compositeEntries) {
pendingResult_.error = "missing composite members";
return false;
}
for (const JsonNode *pCompositeEntry : compositeEntries->value) {
JsonGet compositeEntry = pCompositeEntry->value;
GhidraCompositeMember member;
member.fieldName = compositeEntry.getStringOr("fieldName", "");
member.ordinal = compositeEntry.getInt("ordinal", 0);
member.offset = compositeEntry.getInt("offset", 0);
member.length = compositeEntry.getInt("length", 0);
member.typePathName = compositeEntry.getStringOr("typePathName", "");
member.comment = compositeEntry.getStringOr("comment", "");
type.compositeMembers.push_back(member);
}
break;
}
case FUNCTION_DEFINITION:
type.functionPrototypeString = entry.getStringOr("prototypeString", "");
break;
case BUILT_IN:
type.builtInGroup = entry.getStringOr("group", "");
break;
default:
continue;
}
pendingResult_.types.emplace(type.pathName, type);
}
return true;
}
bool GhidraClient::FetchResource(const std::string &path, std::string &outResult) {
http::Client http(nullptr);
if (!http.Resolve(host_.c_str(), port_)) {
pendingResult_.error = "can't resolve host";
return false;
}
bool cancelled = false;
if (!http.Connect(1, 5.0, &cancelled)) {
pendingResult_.error = "can't connect to host";
return false;
}
net::RequestProgress progress(&cancelled);
Buffer result;
const int code = http.GET(http::RequestParams(path.c_str()), &result, &progress);
http.Disconnect();
if (code != 200) {
pendingResult_.error = "unsuccessful response code from API endpoint";
return false;
}
result.TakeAll(&outResult);
return true;
}