GameDB: Fix infinite loops caused by UB when YAML parsing fails

This commit is contained in:
chaoticgd 2025-10-28 01:21:03 +00:00 committed by Ty
parent 07bc2fa452
commit 0ce312c1c3
7 changed files with 116 additions and 42 deletions

View File

@ -7,7 +7,6 @@ endif(NOT TOP_CMAKE_WAS_SOURCED)
add_library(common)
# x86emitter sources
target_sources(common PRIVATE
AlignedMalloc.cpp
Assertions.cpp
@ -34,9 +33,9 @@ target_sources(common PRIVATE
Timer.cpp
WAVWriter.cpp
WindowInfo.cpp
YAML.cpp
)
# x86emitter headers
target_sources(common PRIVATE
AlignedMalloc.h
Assertions.h
@ -81,6 +80,7 @@ target_sources(common PRIVATE
WAVWriter.h
WindowInfo.h
WrappedMemCopy.h
YAML.h
)
if(_M_X86)
@ -208,6 +208,7 @@ target_link_libraries(common PRIVATE
target_link_libraries(common PUBLIC
fmt::fmt
fast_float
rapidyaml::rapidyaml
)
fixup_file_properties(common)

43
common/YAML.cpp Normal file
View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "YAML.h"
#include <csetjmp>
#include <cstdlib>
struct RapidYAMLContext
{
std::jmp_buf env;
Error* error = nullptr;
};
std::optional<ryml::Tree> ParseYAMLFromString(ryml::csubstr yaml, ryml::csubstr file_name, Error* error)
{
RapidYAMLContext context;
context.error = error;
ryml::Callbacks callbacks;
callbacks.m_user_data = static_cast<void*>(&context);
callbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location location, void* user_data) {
RapidYAMLContext* context = static_cast<RapidYAMLContext*>(user_data);
Error::SetString(context->error, std::string(msg, msg_len));
std::longjmp(context->env, 1);
};
ryml::EventHandlerTree event_handler(callbacks);
ryml::Parser parser(&event_handler);
ryml::Tree tree;
// The only options RapidYAML provides for recovering from errors are
// throwing an exception or using setjmp/longjmp. Since we have exceptions
// disabled we have to use the latter option.
if (setjmp(context.env))
return std::nullopt;
ryml::parse_in_arena(&parser, file_name, yaml, &tree);
return tree;
}

18
common/YAML.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Error.h"
#include "ryml_std.hpp"
#include "ryml.hpp"
#include "ryml.hpp"
#include <optional>
/// Parse a YAML file with RapidYAML, and use setjmp/longjmp to recover from
/// parsing errors (as is recommended by the documentation for cases where
/// exceptions are disabled). The file_name parameter is only used for error
/// messages, which are returned via the error parameter.
std::optional<ryml::Tree> ParseYAMLFromString(ryml::csubstr yaml, ryml::csubstr file_name, Error* error);

View File

@ -36,10 +36,12 @@
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fast_float\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fmt\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\jpgd</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\include</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<ForcedIncludeFiles>PrecompiledHeader.h</ForcedIncludeFiles>
<PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
<ObjectFileName>$(IntDir)%(RelativeDir)</ObjectFileName>
<PreprocessorDefinitions>C4_NO_DEBUG_BREAK;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
@ -71,6 +73,7 @@
<ClCompile Include="Timer.cpp" />
<ClCompile Include="WAVWriter.cpp" />
<ClCompile Include="WindowInfo.cpp" />
<ClCompile Include="YAML.cpp" />
<ClCompile Include="Perf.cpp" />
<ClCompile Include="PrecompiledHeader.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
@ -157,6 +160,7 @@
<ClInclude Include="Timer.h" />
<ClInclude Include="WAVWriter.h" />
<ClInclude Include="WindowInfo.h" />
<ClInclude Include="YAML.h" />
<ClInclude Include="Threading.h" />
<ClInclude Include="emitter\implement\avx.h" />
<ClInclude Include="emitter\implement\bmi.h" />

View File

@ -127,6 +127,9 @@
<ClCompile Include="SmallString.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="YAML.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AlignedMalloc.h">
@ -335,6 +338,9 @@
</ClInclude>
<ClInclude Include="SingleRegisterTypes.h" />
<ClInclude Include="FPControl.h" />
<ClInclude Include="YAML.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
@ -349,4 +355,4 @@
<Filter>Source Files</Filter>
</MASM>
</ItemGroup>
</Project>
</Project>

View File

@ -1140,7 +1140,6 @@ target_link_libraries(PCSX2_FLAGS INTERFACE
common
imgui
fmt::fmt
rapidyaml::rapidyaml
libchdr
libzip::zip
cpuinfo

View File

@ -14,6 +14,7 @@
#include "common/Path.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "common/YAML.h"
#include <sstream>
#include "ryml_std.hpp"
@ -357,8 +358,7 @@ static const char* s_round_modes[static_cast<u32>(FPRoundMode::MaxCount)] = {
"Nearest",
"NegativeInfinity",
"PositiveInfinity",
"Chop"
};
"Chop"};
static const char* s_gs_hw_fix_names[] = {
"autoFlush",
@ -930,7 +930,7 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions&
Host::AddKeyedOSDMessage("HWFixesWarning",
fmt::format(ICON_FA_WAND_MAGIC_SPARKLES " {}\n{}",
TRANSLATE_SV("GameDatabase", "Manual GS hardware renderer fixes are enabled, automatic fixes were not applied:"),
disabled_fixes),
disabled_fixes),
Host::OSD_ERROR_DURATION);
}
else
@ -941,25 +941,28 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions&
void GameDatabase::initDatabase()
{
ryml::Callbacks rymlCallbacks = ryml::get_callbacks();
rymlCallbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void* userdata) {
Console.Error(fmt::format("[GameDB YAML] Parsing error at {}:{} (bufpos={}): {}",
loc.line, loc.col, loc.offset, std::string_view(msg, msg_len)));
};
ryml::set_callbacks(rymlCallbacks);
ryml::set_error_callback([](const char* msg, size_t msg_size) {
Console.Error(fmt::format("[GameDB YAML] Internal Parsing error: {}", std::string_view(msg, msg_size)));
});
const std::string path(Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME));
const std::string name(GAMEDB_YAML_FILE_NAME);
auto buf = FileSystem::ReadFileToString(Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME).c_str());
if (!buf.has_value())
const std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
Console.Error("GameDB: Unable to open GameDB file, file does not exist.");
return;
}
ryml::Tree tree = ryml::parse_in_arena(ryml::to_csubstr(buf.value()));
ryml::NodeRef root = tree.rootref();
const ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error);
if (!tree.has_value())
{
Console.ErrorFmt("GameDB: Failed to parse game database file {}:", path);
Console.Error(error.GetDescription());
return;
}
ryml::NodeRef root = tree->rootref();
for (const ryml::NodeRef& n : root.children())
{
@ -971,7 +974,7 @@ void GameDatabase::initDatabase()
// However, YAML's keys are as expected case-sensitive, so we have to explicitly do our own duplicate checking
if (s_game_db.count(serial) == 1)
{
Console.Error(fmt::format("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial));
Console.ErrorFmt("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial);
continue;
}
@ -980,8 +983,6 @@ void GameDatabase::initDatabase()
parseAndInsert(serial, n);
}
}
ryml::reset_callbacks();
}
void GameDatabase::ensureLoaded()
@ -1069,7 +1070,7 @@ static bool parseHashDatabaseEntry(const ryml::NodeRef& node)
{
if (!n.is_map() || !n.has_child("size") || !n.has_child("md5"))
{
Console.Error(fmt::format("[HashDatabase] Incomplete hash definition in {}", entry.name));
Console.ErrorFmt("[HashDatabase] Incomplete hash definition in {}", entry.name);
return false;
}
@ -1080,12 +1081,12 @@ static bool parseHashDatabaseEntry(const ryml::NodeRef& node)
if (!th.parseHash(md5))
{
Console.Error(fmt::format("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5));
Console.ErrorFmt("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5);
return false;
}
if (entry.tracks.empty() && s_track_hash_to_entry_map.find(th) != s_track_hash_to_entry_map.end())
Console.Warning(fmt::format("[HashDatabase] Duplicate first track hash in {}", entry.name));
Console.WarningFmt("[HashDatabase] Duplicate first track hash in {}", entry.name);
entry.tracks.push_back(th);
s_track_hash_to_entry_map.emplace(th, index);
@ -1100,27 +1101,30 @@ bool GameDatabase::loadHashDatabase()
if (!s_hash_database.empty())
return true;
ryml::Callbacks rymlCallbacks = ryml::get_callbacks();
rymlCallbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void*) {
Console.Error(fmt::format(
"[HashDatabase YAML] Parsing error at {}:{} (bufpos={}): {}", loc.line, loc.col, loc.offset, msg));
};
ryml::set_callbacks(rymlCallbacks);
ryml::set_error_callback([](const char* msg, size_t msg_size) {
Console.Error(fmt::format("[HashDatabase YAML] Internal Parsing error: {}", std::string_view(msg, msg_size)));
});
Common::Timer load_timer;
auto buf = FileSystem::ReadFileToString(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME).c_str());
if (!buf.has_value())
const std::string path(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME));
const std::string name(HASHDB_YAML_FILE_NAME);
std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
Console.Error("GameDB: Unable to open hash database file, file does not exist.");
Console.Error("[HashDatabase] Unable to open hash database file, file does not exist.");
return false;
}
ryml::Tree tree = ryml::parse_in_arena(ryml::to_csubstr(buf.value()));
ryml::NodeRef root = tree.rootref();
ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error);
if (!tree.has_value())
{
Console.ErrorFmt("[HashDatabase] Failed to parse hash database file {}:", path);
Console.Error(error.GetDescription());
return false;
}
ryml::NodeRef root = tree->rootref();
bool okay = true;
for (const ryml::NodeRef& n : root.children())
@ -1132,7 +1136,6 @@ bool GameDatabase::loadHashDatabase()
}
}
ryml::reset_callbacks();
if (!okay)
{
s_track_hash_to_entry_map.clear();