diff --git a/defects/decaf-0001/patch/decaf-0001.patch b/defects/decaf-0001/patch/decaf-0001.patch new file mode 100644 index 000000000..b56b4bd29 --- /dev/null +++ b/defects/decaf-0001/patch/decaf-0001.patch @@ -0,0 +1,57 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/libdecaf/src/ios/mcp/ios_mcp_mcp_device.cpp ++++ b/src/libdecaf/src/ios/mcp/ios_mcp_mcp_device.cpp +@@ -1,5 +1,6 @@ + #include "ios_mcp_config.h" + #include "ios_mcp_enum.h" + #include "ios_mcp_mcp_device.h" + #include "ios_mcp_mcp_types.h" + #include "ios_mcp_mcp_request.h" + #include "ios_mcp_mcp_response.h" + #include "ios_mcp_mcp_thread.h" + #include "ios_mcp_title.h" + + #include "cafe/libraries/cafe_hle.h" + #include "decaf_config.h" + #include "ios/ios.h" + #include "ios/ios_stackobject.h" + #include "ios/fs/ios_fs_fsa_ipc.h" + #include "ios/kernel/ios_kernel_process.h" + #include "ios/kernel/ios_kernel_resourcemanager.h" + #include "vfs/vfs_host_device.h" + #include "vfs/vfs_virtual_device.h" + + #include ++#include + + namespace ios::mcp::internal + { +@@ -89,9 +90,9 @@ MCPError + mcpGetFileLength(phys_ptr request) + { + auto path = std::string { }; + auto name = std::string_view { phys_addrof(request->name).get() }; + + if (request->fileType == MCPFileType::CafeOS) { +- if (std::find(decaf::config()->system.lle_modules.begin(), +- decaf::config()->system.lle_modules.end(), +- name) == decaf::config()->system.lle_modules.end()) { ++ const auto &lleVec = decaf::config()->system.lle_modules; ++ static const auto sLleModuleSet = std::unordered_set(lleVec.begin(), lleVec.end()); ++ if (sLleModuleSet.find(std::string(name)) == sLleModuleSet.end()) { + auto library = cafe::hle::getLibrary(name); + if (library) { + auto &rpl = library->getGeneratedRpl(); +@@ -155,9 +158,9 @@ mcpLoadFile(phys_ptr request, + auto name = std::string_view { phys_addrof(request->name).get() }; + + if (request->fileType == MCPFileType::CafeOS) { +- if (std::find(decaf::config()->system.lle_modules.begin(), +- decaf::config()->system.lle_modules.end(), +- name) == decaf::config()->system.lle_modules.end()) { ++ const auto &lleVec = decaf::config()->system.lle_modules; ++ static const auto sLleModuleSet = std::unordered_set(lleVec.begin(), lleVec.end()); ++ if (sLleModuleSet.find(std::string(name)) == sLleModuleSet.end()) { + auto library = cafe::hle::getLibrary(name); + if (library) { + auto &rpl = library->getGeneratedRpl(); diff --git a/defects/decaf-0001/test/test_decaf_0001.cpp b/defects/decaf-0001/test/test_decaf_0001.cpp new file mode 100644 index 000000000..239d0038b --- /dev/null +++ b/defects/decaf-0001/test/test_decaf_0001.cpp @@ -0,0 +1,102 @@ +// UNDF: UNDF-2026-XXXXXXXXX +// Unit test for decaf-0001: lle_modules O(M) linear scan in mcpGetFileLength / mcpLoadFile +// +// Defect: mcpGetFileLength() and mcpLoadFile() in ios_mcp_mcp_device.cpp each call +// std::find(lle_modules.begin(), lle_modules.end(), name) +// This is O(M) per RPL load where M = lle_modules.size(). +// With R RPL files loaded at title startup the total cost is O(R * M). +// A Wii U game loads 40-60 RPL modules; with all 40 native modules in lle_modules +// that is 60 * 2 * 40 = 4800 string comparisons at title load time. +// +// Fix: replace std::find on the vector with a lookup on a std::unordered_set built once +// from lle_modules at first call. O(1) amortised per lookup. +// +// Build: +// g++ -std=c++17 -O2 -o test_decaf_0001 test_decaf_0001.cpp && ./test_decaf_0001 + +#include +#include +#include +#include +#include +#include +#include +#include + +static bool defective_lookup(const std::vector &v, const std::string &name) +{ + // Original pattern: O(M) linear scan on every call. + return std::find(v.begin(), v.end(), name) != v.end(); +} + +static bool patched_lookup(const std::unordered_set &s, const std::string &name) +{ + // Patched pattern: O(1) hash lookup, set built once from the vector at startup. + return s.count(name) > 0; +} + +int main() +{ + // Realistic scenario: M lle_modules entries, R RPL queries per title load. + // Half the queries are present-at-end (worst case scan), half are absent (full scan). + const int M = 40; // lle_modules count (all native modules enabled as LLE) + const int R = 60; // RPL files a typical Wii U game loads + + std::vector lleVec; + for (int i = 0; i < M; ++i) + lleVec.push_back("lle_module_" + std::to_string(i) + ".rpl"); + + auto lleSet = std::unordered_set(lleVec.begin(), lleVec.end()); + + // Build queries: half at end of list (worst-case position), half absent. + std::vector queries; + for (int i = 0; i < R / 2; ++i) + queries.push_back(lleVec[M - 1 - (i % M)]); // last entries -- near-full scan + for (int i = 0; i < R - R / 2; ++i) + queries.push_back("hle_lib_" + std::to_string(i) + ".rpl"); // absent -- full scan + + // Correctness ----------------------------------------------------------- + for (const auto &q : queries) + assert(defective_lookup(lleVec, q) == patched_lookup(lleSet, q)); + assert(defective_lookup(lleVec, "lle_module_0.rpl") == true); + assert(patched_lookup(lleSet, "lle_module_0.rpl") == true); + assert(defective_lookup(lleVec, "absent.rpl") == false); + assert(patched_lookup(lleSet, "absent.rpl") == false); + std::printf("Correctness: PASS\n"); + + // Op-count model -------------------------------------------------------- + // Defective: each query scans up to M entries (worst case full scan for absent/end entries). + // Patched: each query costs O(1). + // With 2 IPC calls per RPL (mcpGetFileLength + mcpLoadFile): + long long opDef = (long long)M * R * 2; + long long opPatch = R * 2; + double opRatio = (double)opDef / opPatch; + std::printf("Op-count defective: %lld patched: %lld ratio: %.1fx\n", opDef, opPatch, opRatio); + assert(opRatio >= (double)M); + std::printf("Op-count ratio >= M=%d: PASS\n", M); + + // Timing ---------------------------------------------------------------- + const int iters = 200000; + volatile int sink = 0; + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iters; ++i) + for (const auto &q : queries) + sink += defective_lookup(lleVec, q) ? 1 : 0; + auto t1 = std::chrono::high_resolution_clock::now(); + + auto t2 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iters; ++i) + for (const auto &q : queries) + sink += patched_lookup(lleSet, q) ? 1 : 0; + auto t3 = std::chrono::high_resolution_clock::now(); + + double msDef = std::chrono::duration(t1 - t0).count(); + double msPatch = std::chrono::duration(t3 - t2).count(); + double ratio = msDef / msPatch; + std::printf("Defective: %.2f ms Patched: %.2f ms ratio: %.1fx\n", msDef, msPatch, ratio); + assert(ratio >= 1.5); + std::printf("Performance: %.1fx speedup, PASS (sink=%d)\n", ratio, (int)sink); + + return 0; +}