#include "pxi.hpp" #include "cryptopp/aes.h" #include "cryptopp/modes.h" #include "os.hpp" #include #include #include "platform/crypto.hpp" #include "platform/file_formats/ncch.hpp" #include "platform/pxi.hpp" #include "pxi_fs.hpp" #include "framework/meta_tools.hpp" #include #include #include #include #include #include #include #include std::vector* nand_titles = nullptr; namespace std { static std::ostream& operator<<(std::ostream& os, const Platform::PXI::PM::ProgramInfo& info) { os << fmt::format("(title id {:#x}, media type {})", info.program_id, info.media_type == 0 ? "NAND" : info.media_type == 1 ? "SD" : info.media_type == 2 ? "Game card" : "Unknown"); return os; } } // namespace std namespace HLE { // namespace OS { namespace PXI { using Result = OS::Result; using Thread = OS::Thread; using FakeThread = OS::FakeThread; using WrappedFakeThread = OS::WrappedFakeThread; using ThreadPrinter = OS::ThreadPrinter; using OSImpl = OS::OS; static const auto RESULT_OK = OS::RESULT_OK; struct HLETitle { const char* name; // module name specified in the original NCCH std::vector dependencies = {}; }; const std::unordered_map hle_titles = { { 0x4003000008a02, { "ErrDisp" } }, { 0x4013000001002, { "sm" } }, { 0x4013000001102, { "fs" } }, { 0x4013000001402, { "pxi" } }, { 0x4013000001502, { "am" } }, { 0x4013000001602, { "cam" } }, { 0x4013000001702, { "cfg", { 0x4013000003102 } } }, { 0x4013000001802, { "cdc", { 0x4013000001f02, 0x4013000002102 } } }, { 0x4013000001a02, { "dsp" } }, { 0x4013000001b02, { "gpio" } }, // { 0x4013000001c02, { "gsp", { 0x4013000003502 } } }, // NEWS Hack { 0x4013000001d02, { "hid" } }, { 0x4013000001e02, { "i2c" } }, { 0x4013000001f02, { "mcu", { 0x4013000001e02 } } }, { 0x4013000002002, { "mic" } }, { 0x4013000002102, { "pdn" } }, { 0x4013000002202, { "ptm", { 0x4013000001e02, 0x4013000001f02, 0x4013000002102 } } }, { 0x4013000002402, { "ac", { 0x4013000002e02 } } }, { 0x4013000002602, { "cecd", { 0x4013000003202 } } }, { 0x4013000002702, { "csnd" } }, { 0x4013000002802, { "dlp" } }, { 0x4013000002902, { "http", { 0x4013000002e02 } } }, { 0x4013000002a02, { "mp" } }, { 0x4013000002b02, { "ndm", { 0x4013000002402, 0x4013000003202, 0x4013000003402 } } }, { 0x4013000002c02, { "nim", { 0x4013000002402 } } }, { 0x4013000002d02, { "nwm", { 0x4013000001f02 } } }, { 0x4013000002e02, { "socket" } }, { 0x4013000002f02, { "ssl", { 0x4013000002e02 } } }, { 0x4013000003102, { "ps", { 0x4013000001f02 } } }, { 0x4013000003202, { "friends", { 0x4013000002402, 0x4013000002e02 } } }, { 0x4013000003302, { "ir", { 0x4013000001e02 } } }, { 0x4013000003402, { "boss", { 0x4013000002402, 0x4013000003202 } } }, { 0x4013000003502, { "news", { 0x4013000003202, 0x4013000003402 } } }, { 0x4013000003702, { "ro" } }, { 0x4013000003802, { "act" } }, { 0x4013000004002, { "nfc" } }, { 0x4013000008002, { "ns" } }, }; std::optional GetHLEModuleName(uint64_t title_id) { auto it = hle_titles.find(title_id); if (it == hle_titles.end()) { return std::nullopt; } return it->second.name; } FakePXI::FakePXI(FakeThread& thread) : os(thread.GetOS()), logger(*thread.GetLogger()) { thread.name = "PxiFS0Thread"; Context context; // Search for installed NAND titles // TODO: Move titles to ./data/title { std::filesystem::path base_path = "./data/"; auto parse_title_id_part = [](const std::string& filename) -> std::optional { // Expect an 8-digit zero-padded hexadecimal number if (filename.size() != 8) { return std::nullopt; } uint32_t title_id_word = 0; auto result = std::from_chars(filename.data(), filename.data() + 8, title_id_word, 16); if (result.ptr != filename.data() + 8) { return std::nullopt; } return title_id_word; }; for (const auto& dir : std::filesystem::directory_iterator(base_path)) { if (auto title_id_high = parse_title_id_part(dir.path().filename().string())) { for (const auto& subdir : std::filesystem::directory_iterator(dir)) { if (auto title_id_low = parse_title_id_part(subdir.path().filename().string())) { context.nand_titles.push_back((uint64_t { *title_id_high } << 32) | *title_id_low); } } } } nand_titles = &context.nand_titles; } { auto pxifs1_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return FSThread(thread, context, "PxiFS1"); }); pxifs1_thread->name = "PxiFS1Thread"; thread.GetParentProcess().AttachThread(pxifs1_thread); } { auto pxifsb_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return FSThread(thread, context, "PxiFSB"); }); pxifsb_thread->name = "PxiFSBThread"; thread.GetParentProcess().AttachThread(pxifsb_thread); } { auto pxifsr_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return FSThread(thread, context, "PxiFSR"); }); pxifsr_thread->name = "PxiFSRThread"; thread.GetParentProcess().AttachThread(pxifsr_thread); } { auto pxipm_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return PMThread(thread, context); }); pxipm_thread->name = "PxiPMThread"; thread.GetParentProcess().AttachThread(pxipm_thread); } { auto pxips_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return PSThread(thread, context); }); pxips_thread->name = "PxiPSThread"; thread.GetParentProcess().AttachThread(pxips_thread); } { auto new_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return MCThread(thread, context); }); new_thread->name = "PxiMCThread"; thread.GetParentProcess().AttachThread(new_thread); } { auto new_thread = std::make_shared(thread.GetParentProcess(), [this,&context](FakeThread& thread) { return AMThread(thread, context); }); new_thread->name = "PxiAMThread"; thread.GetParentProcess().AttachThread(new_thread); } FSThread(thread, context, "PxiFS0"); } namespace PM = Platform::PXI::PM; FileFormat::ExHeader GetExtendedHeader(FS::FileContext& file_context, const KeyDatabase& keydb, HLE::PXI::FS::File& ncch_file) { ncch_file.OpenReadOnly(file_context); std::array ncch_raw; auto [result, bytes_read] = ncch_file.Read(file_context, 0, sizeof(ncch_raw), FS::FileBufferInHostMemory(ncch_raw.data(), sizeof(ncch_raw))); assert(result == RESULT_OK && bytes_read == sizeof(ncch_raw)); std::array exheader_raw; std::tie(result, bytes_read) = ncch_file.Read(file_context, FileFormat::NCCHHeader::Tags::expected_serialized_size, sizeof(exheader_raw), FS::FileBufferInHostMemory(exheader_raw.data(), sizeof(exheader_raw))); assert(result == RESULT_OK && bytes_read == sizeof(exheader_raw)); auto ncch_data_stream = FileFormat::MakeStreamInFromContainer(ncch_raw.begin(), ncch_raw.end()); auto ncch_header = FileFormat::Load(ncch_data_stream); auto exheader_data_stream = FileFormat::MakeStreamInFromContainer(exheader_raw.begin(), exheader_raw.end()); auto exheader = FileFormat::Load(exheader_data_stream); bool is_encrypted = !(ncch_header.flags & 4); if (is_encrypted && exheader.aci.program_id == ncch_header.program_id) { // ExHeader is not actually encrypted => override encryption state is_encrypted = false; } if (is_encrypted) { fprintf(stderr, "DECRYPTING EXHEADER: %c%c%c%c\n", ncch_header.magic[0], ncch_header.magic[1], ncch_header.magic[2], ncch_header.magic[3]); std::array GenerateAESKey(const std::array& key_x, const std::array& key_y); // 0x2c key slot const std::array& key_x = keydb.aes_slots[0x2c].x.value(); std::array key_y; memcpy(key_y.data(), &ncch_header, sizeof(key_y)); auto key = GenerateAESKey(key_x, key_y); std::array iv {}; // First 8 bytes are the partition id interpreted as big-endian memcpy(iv.data(), &ncch_header.partition_id, sizeof(ncch_header.partition_id)); std::reverse(iv.begin(), iv.begin() + 8); iv[8] = 1; CryptoPP::CTR_Mode::Decryption dec; dec.SetKeyWithIV(key.data(), sizeof(key), iv.data()); dec.ProcessData(reinterpret_cast(&exheader), reinterpret_cast(&exheader), sizeof(exheader)); } return exheader; } FileFormat::ExHeader GetExtendedHeader(Thread& thread, const Platform::FS::ProgramInfo& title_info) { const auto& keydb = thread.GetParentProcess().interpreter_setup.keydb; if (title_info.media_type == 0) { for (auto& [title_id, module_info] : hle_titles) { // If this module is HLE-ed, return a dummy exheader if (title_id != title_info.program_id || !thread.GetOS().ShouldHLEProcess(module_info.name)) { continue; } // Return a dummy exheader with minimal code segments. A virtual ExeFS is provided along with this. FileFormat::ExHeader ret {}; strcpy((char*)ret.application_title.data(), module_info.name); ret.section_text = { 0x100000, 1, 0x1000 }; ret.section_ro = { 0x101000, 1, 0x1000 }; ret.section_data = { 0x102000, 1, 0x1000 }; ret.aci.program_id = title_id; ret.aci.version = 2; // constant for all titles ranges::copy(module_info.dependencies, ret.dependencies.begin()); using KernelFlags = FileFormat::ExHeader::ARM11KernelCapabilityDescriptor::KernelFlags; ret.aci.arm11_kernel_capabilities[0] = KernelFlags::Make().memory_type()(2); ret.flags = ret.flags.compress_exefs_code()(false); return ret; } // TODO: The following should perhaps be done in the PXIFS subsystem instead // TODO: The content ID is hardcoded currently. uint32_t content_id = 0; const std::string filename = Meta::invoke([&] { std::stringstream filename; filename << "data/"; filename << std::hex << std::setw(8) << std::setfill('0') << (title_info.program_id >> 32); filename << "/"; filename << std::hex << std::setw(8) << std::setfill('0') << (title_info.program_id & 0xFFFFFFFF); filename << "/content/"; filename << std::hex << std::setw(8) << std::setfill('0') << content_id; filename << ".cxi"; return filename.str(); }); thread.GetLogger()->info("{}Opening \"{}\" to get the extended header", ThreadPrinter{thread}, filename); std::ifstream input_file; input_file.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit); try { input_file.open(filename, std::ios::binary); } catch (const std::ios_base::failure&) { if ((title_info.program_id & 0xf0000000) == 0x20000000) { // // Trying to launch N3DS dependency on O3DS => non-critical error // // Some games (e.g. Retro City Rampage DX) run into this throw IPC::IPCError { 0, 0xc8804465 }; // TODO: Which error code to return? } throw std::runtime_error(fmt::format( "Tried to launch non-existing title {:#x} from emulated NAND.\n\nPlease dump the title from your 3DS and install it manually to this path:\n{}", title_info.program_id, filename)); } auto ncch_begin = static_cast(input_file.tellg()); auto exheader_begin = ncch_begin + FileFormat::NCCHHeader::Tags::expected_serialized_size; input_file.seekg(exheader_begin); auto exheader = FileFormat::Load(reinterpret_cast(input_file)); input_file.seekg(ncch_begin + 0x188 + 7); uint8_t encryption_flags; input_file.read(reinterpret_cast(&encryption_flags), sizeof(encryption_flags)); bool is_encrypted = !(encryption_flags & 4); // TODO: read full exheader //if (is_encrypted && exheader.aci.program_id == ncch_header.program_id) { // // ExHeader is not actually encrypted => override encryption state // is_encrypted = false; //} if (is_encrypted) { fprintf(stderr, "DECRYPTING EXHEADER\n"); std::array GenerateAESKey(const std::array& key_x, const std::array& key_y); // 0x2c key slot const std::array& key_x = keydb.aes_slots[0x2c].x.value(); std::array key_y; input_file.seekg(ncch_begin); input_file.read(reinterpret_cast(key_y.data()), sizeof(key_y)); auto key = GenerateAESKey(key_x, key_y); std::array iv {}; // First 8 bytes are the partition id interpreted as big-endian input_file.seekg(ncch_begin + 0x108); input_file.read(reinterpret_cast(iv.data()), 8); std::reverse(iv.begin(), iv.begin() + 8); iv[8] = 1; CryptoPP::CTR_Mode::Decryption dec; dec.SetKeyWithIV(key.data(), sizeof(key), iv.data()); dec.ProcessData(reinterpret_cast(&exheader), reinterpret_cast(&exheader), sizeof(exheader)); } return exheader; } else if (title_info.media_type == 2) { auto&& ncch = thread.GetOS().setup.gamecard->GetPartitionFromId(Loader::NCSDPartitionId::Executable); assert(ncch); FS::FileContext file_context { *thread.GetLogger() }; return GetExtendedHeader(file_context, keydb, **ncch); } else { // TODO: Implement support for SD titles (mediatype 1) if (title_info.program_id >> 32 == 0x4000e) { // This is a patch title. NS::LaunchApplication will attempt to load these before loading the application itself. throw IPC::IPCError { 0, 0xc8804471 }; // TODO: Which error code to return? } else { throw Mikage::Exceptions::NotImplemented( "{}Trying to load a title with media info {:#x}, which is currently not supported", ThreadPrinter{thread}, title_info.media_type); } } } static std::tuple PMGetExtendedHeader(FakeThread& thread, Context& context, PMProgramHandle program_handle, const PXI::PXIBuffer& exheader_buffer) { thread.GetLogger()->info("{}received GetExtendedHeader with internal program handle {:#x}", ThreadPrinter{thread}, program_handle.value); auto program_infos = context.programs.find(program_handle); if (program_infos == context.programs.end()) { thread.GetLogger()->error("{}Unknown program handle", ThreadPrinter{thread}); thread.CallSVC(&OSImpl::SVCBreak, OSImpl::BreakReason::Panic); } PM::ProgramInfo title_info, update_info; std::tie(title_info, update_info) = program_infos->second; thread.GetLogger()->info("{}Getting extended header for title id {:#x} (media type {:#x})", ThreadPrinter{thread}, title_info.program_id, title_info.media_type); auto exheader = GetExtendedHeader(thread, title_info); // TODO: Serialize only the SCI and ACI, but for that we need to regroup the members in the ExHeader definition... uint32_t buffer_offset = 0; thread.GetLogger()->info("{}Writing ExHeader data...", ThreadPrinter{thread}); auto write_buffer_data = [&](char* data, size_t size) { for (auto data_ptr = data; data_ptr - data < size; ++data_ptr) { // TODO: Don't hardcode this size... but either way, we shouldn't // need to outright abort here, either, but we currently do // this because our serialization code currently operates on // the entire exheader rather than only on the sci and aci. if (buffer_offset >= 0x400) return; if (buffer_offset + size > 0x400) { throw std::runtime_error("Data corruption here :("); } exheader_buffer.Write(thread, buffer_offset++, *data_ptr); } }; FileFormat::SerializationInterface::Save(exheader, write_buffer_data); thread.GetLogger()->info("{}... done", ThreadPrinter{thread}); return std::make_tuple(RESULT_OK); } static std::tuple PMRegisterProgram(FakeThread& thread, Context& context, PM::ProgramInfo title_info, PM::ProgramInfo update_info) { thread.GetLogger()->info("{}received RegisterProgram with title_info={}, update_info={}; attempting to assign internal program handle {:#x}", ThreadPrinter{thread}, title_info, update_info, context.next_program_handle.value); for (auto& entries : context.programs) { if (entries.second.first.program_id == title_info.program_id) { throw Mikage::Exceptions::Invalid("Tried to re-register title {:#x} with PxiPM", title_info.program_id); } } auto map_entry = context.programs.emplace(std::make_pair(context.next_program_handle, std::make_pair(title_info, update_info))); if (!map_entry.second) thread.CallSVC(&OSImpl::SVCBreak, OSImpl::BreakReason::Panic); context.next_program_handle.value++; return std::make_tuple(RESULT_OK, map_entry.first->first); } static std::tuple PMUnregisterProgram(FakeThread& thread, Context& context, PMProgramHandle program_handle) { thread.GetLogger()->info("{}received UnregisterProgram with program_handle={}", ThreadPrinter{thread}, program_handle.value); auto program_it = context.programs.find(program_handle); if (program_it == context.programs.end()) { throw Mikage::Exceptions::Invalid("Attempted to unregister unknown program handle"); } context.programs.erase(program_it); return std::make_tuple(RESULT_OK); } static void PXIPMCommandHandler(FakeThread& thread, Context& context, const IPC::CommandHeader& header) try { switch (header.command_id) { case PM::GetExtendedHeader::id: return IPC::HandleIPCCommand(PMGetExtendedHeader, thread, thread, context); case PM::RegisterProgram::id: return IPC::HandleIPCCommand(PMRegisterProgram, thread, thread, context); case PM::UnregisterProgram::id: return IPC::HandleIPCCommand(PMUnregisterProgram, thread, thread, context); default: throw std::runtime_error(fmt::format("Unknown PxiPM service command with header {:#010x}", header.raw)); } } catch (const IPC::IPCError& exc) { thread.WriteTLS(0x80, IPC::CommandHeader::Make(header.command_id, 1, 0).raw); thread.WriteTLS(0x84, exc.result); } void FakePXI::PMThread(FakeThread& thread, Context& context) { // NOTE: Actually, there should be four of these buffers! for (unsigned buffer_index = 0; buffer_index < 1; ++buffer_index) { const auto buffer_size = 0x1000; thread.WriteTLS(0x180 + 8 * buffer_index, IPC::TranslationDescriptor::MakeStaticBuffer(0, buffer_size).raw); Result result; uint32_t buffer_addr; std::tie(result, buffer_addr) = thread.CallSVC(&OSImpl::SVCControlMemory, 0, 0, 0x1000, 3 /* COMMIT */, 3 /* RW */); thread.WriteTLS(0x184 + 8 * buffer_index, buffer_addr); } OS::ServiceUtil service(thread, "PxiPM", 1); OS::Handle last_signalled = OS::HANDLE_INVALID; for (;;) { Result result; int32_t index; std::tie(result,index) = service.ReplyAndReceive(thread, last_signalled); last_signalled = OS::HANDLE_INVALID; if (result != RESULT_OK) os.SVCBreak(thread, OSImpl::BreakReason::Panic); if (index == 0) { // ServerPort: Incoming client connection int32_t session_index; std::tie(result,session_index) = service.AcceptSession(thread, index); if (result != RESULT_OK) { auto session = service.GetObject(session_index); if (!session) { logger.error("{}Failed to accept session.", ThreadPrinter{thread}); os.SVCBreak(thread, OSImpl::BreakReason::Panic); } auto session_handle = service.GetHandle(session_index); logger.warn("{}Failed to accept session. Maximal number of sessions exhausted? Closing session handle {}", ThreadPrinter{thread}, OS::HandlePrinter{thread,session_handle}); os.SVCCloseHandle(thread, session_handle); } } else { // server_session: Incoming IPC command from the indexed client logger.info("{}received IPC request", ThreadPrinter{thread}); logger.info("{}received IPC request, {:#08x}, {:#08x}, {:#08x}, {:#08x}", ThreadPrinter{thread}, thread.ReadTLS(0x80), thread.ReadTLS(0x84), thread.ReadTLS(0x88), thread.ReadTLS(0x8c)); Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) }; auto signalled_handle = service.GetHandle(index); PXIPMCommandHandler(thread, context, header); last_signalled = signalled_handle; } } } static std::tuple PSVerifyRsaSha256(FakeThread& thread, Context& context, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, const PXI::PXIBuffer&, const PXI::PXIBuffer&) { thread.GetLogger()->info("{}received VerifyRsaSha256 (stub)", ThreadPrinter{thread}); return std::make_tuple(RESULT_OK); } static void PXIPSCommandHandler(FakeThread& thread, Context& context, const IPC::CommandHeader& header) try { namespace PXIPS = Platform::PXI::PS; switch (header.command_id) { case PXIPS::VerifyRsaSha256::id: return IPC::HandleIPCCommand(PSVerifyRsaSha256, thread, thread, context); default: throw IPC::IPCError{header.raw, 0xdeadbef1}; } } catch (const IPC::IPCError& err) { throw std::runtime_error(fmt::format("Unknown PxiPM service command with header {:#010x}", err.header)); } void FakePXI::PSThread(FakeThread& thread, Context& context) { // NOTE: Actually, there should be four of these buffers! for (unsigned buffer_index = 0; buffer_index < 2; ++buffer_index) { const auto buffer_size = 0x1000; thread.WriteTLS(0x180 + 8 * buffer_index, IPC::TranslationDescriptor::MakeStaticBuffer(0, buffer_size).raw); Result result; uint32_t buffer_addr; std::tie(result, buffer_addr) = thread.CallSVC(&OSImpl::SVCControlMemory, 0, 0, 0x1000, 3 /* COMMIT */, 3 /* RW */); thread.WriteTLS(0x184 + 8 * buffer_index, buffer_addr); } OS::ServiceUtil service(thread, "pxi:ps9", 1); OS::Handle last_signalled = OS::HANDLE_INVALID; for (;;) { Result result; int32_t index; std::tie(result,index) = service.ReplyAndReceive(thread, last_signalled); last_signalled = OS::HANDLE_INVALID; if (result != RESULT_OK) os.SVCBreak(thread, OSImpl::BreakReason::Panic); if (index == 0) { // ServerPort: Incoming client connection int32_t session_index; std::tie(result,session_index) = service.AcceptSession(thread, index); if (result != RESULT_OK) { auto session = service.GetObject(session_index); if (!session) { logger.error("{}Failed to accept session.", ThreadPrinter{thread}); os.SVCBreak(thread, OSImpl::BreakReason::Panic); } auto session_handle = service.GetHandle(session_index); logger.warn("{}Failed to accept session. Maximal number of sessions exhausted? Closing session handle {}", ThreadPrinter{thread}, OS::HandlePrinter{thread,session_handle}); os.SVCCloseHandle(thread, session_handle); } } else { // server_session: Incoming IPC command from the indexed client logger.info("{}received IPC request, {:#08x}, {:#08x}, {:#08x}, {:#08x}", ThreadPrinter{thread}, thread.ReadTLS(0x80), thread.ReadTLS(0x84), thread.ReadTLS(0x88), thread.ReadTLS(0x8c)); Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) }; auto signalled_handle = service.GetHandle(index); PXIPSCommandHandler(thread, context, header); last_signalled = signalled_handle; } } } static std::tuple MCHandleUnknown0x1(FakeThread& thread, Context& context, uint32_t arg1) { thread.GetLogger()->info("{}received stubbed command 0x1 with argument {:#x}", ThreadPrinter{thread}, arg1); return std::make_tuple(RESULT_OK); } static std::tuple MCHandleUnknown0xa(FakeThread& thread, Context& context, uint32_t arg1, uint32_t arg2, uint32_t arg3) { thread.GetLogger()->info("{}received stubbed command 0xa with arguments {:#x}, {:#x}, and {:#x}", ThreadPrinter{thread}, arg1, arg2, arg3); return std::make_tuple(RESULT_OK); } static void PXIMCCommandHandler(FakeThread& thread, Context& context, const IPC::CommandHeader& header) try { namespace PXIMC = Platform::PXI::MC; switch (header.command_id) { // case PXIMC::Unknown0x1::id: // return IPC::HandleIPCCommand(MCHandleUnknown0x1, thread, thread, context); // // case PXIMC::Unknown0x2::id: // return IPC::HandleIPCCommand(MCHandleUnknown0x2, thread, thread, context); // // case PXIMC::Unknown0xa::id: // return IPC::HandleIPCCommand(MCHandleUnknown0xa, thread, thread, context); default: throw IPC::IPCError{header.raw, 0xdeadbef2}; } } catch (const IPC::IPCError& err) { throw std::runtime_error(fmt::format("Unknown pxi:mc service command with header {:#010x}", err.header)); } void FakePXI::MCThread(FakeThread& thread, Context& context) { OS::ServiceUtil service(thread, "pxi:mc", 1); OS::Handle last_signalled = OS::HANDLE_INVALID; for (;;) { Result result; int32_t index; std::tie(result,index) = service.ReplyAndReceive(thread, last_signalled); last_signalled = OS::HANDLE_INVALID; if (result != RESULT_OK) os.SVCBreak(thread, OSImpl::BreakReason::Panic); if (index == 0) { // ServerPort: Incoming client connection int32_t session_index; std::tie(result,session_index) = service.AcceptSession(thread, index); if (result != RESULT_OK) { auto session = service.GetObject(session_index); if (!session) { logger.error("{}Failed to accept session.", ThreadPrinter{thread}); os.SVCBreak(thread, OSImpl::BreakReason::Panic); } auto session_handle = service.GetHandle(session_index); logger.warn("{}Failed to accept session. Maximal number of sessions exhausted? Closing session handle {}", ThreadPrinter{thread}, OS::HandlePrinter{thread,session_handle}); os.SVCCloseHandle(thread, session_handle); } } else { // server_session: Incoming IPC command from the indexed client logger.info("{}received IPC request, {:#08x}, {:#08x}, {:#08x}, {:#08x}", ThreadPrinter{thread}, thread.ReadTLS(0x80), thread.ReadTLS(0x84), thread.ReadTLS(0x88), thread.ReadTLS(0x8c)); Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) }; auto signalled_handle = service.GetHandle(index); PXIMCCommandHandler(thread, context, header); last_signalled = signalled_handle; } } } static std::tuple AMHandleGetTitleCount(FakeThread& thread, Context& context, uint32_t media_type) { thread.GetLogger()->info("{}received stubbed GetTitleCount with media_type {:#x}", ThreadPrinter{thread}, media_type); // Only the lowest byte of the media type is used media_type &= 0xff; if (media_type == 0) { return std::make_tuple(RESULT_OK, context.nand_titles.size()); } else if (media_type == 2) { return std::make_tuple(RESULT_OK, uint32_t { thread.GetOS().setup.gamecard != nullptr }); } else { return std::make_tuple(RESULT_OK, uint32_t{0}); // throw Mikage::Exceptions::NotImplemented("Unsupported media type"); } } static std::tuple AMHandleGetTitleList(FakeThread& thread, Context& context, uint32_t num_titles, uint32_t media_type, const PXI::PXIBuffer& output) { thread.GetLogger()->info("{}received stubbed GetTitleList for {:#x} titles with media_type {:#x}", ThreadPrinter{thread}, num_titles, media_type); // Only the lowest byte of the media type and of the title count are used media_type &= 0xff; // TODO: Validate buffer size if (media_type == 0) { // NOTE: In recent system versions (>= 9.0.0), HOME Menu always uses num_titles = 0x1c00 if (num_titles > context.nand_titles.size()) { // throw Mikage::Exceptions::Invalid("Invalid number of titles queried"); // thread.GetOS().SVCBreak(thread, OSImpl::BreakReason::Panic); num_titles = context.nand_titles.size(); } for (uint32_t title_index = 0; title_index < num_titles; ++title_index) { output.Write(thread, title_index * sizeof(uint64_t), context.nand_titles[title_index]); } return std::make_tuple(RESULT_OK, num_titles); } else if (media_type == 2) { if (num_titles > (thread.GetOS().setup.gamecard ? 1 : 0)) thread.GetOS().SVCBreak(thread, OSImpl::BreakReason::Panic); if (thread.GetOS().setup.gamecard) { // TODO: Do we need to return any actual data for this? output.Write(thread, 0, 0); } return std::make_tuple(RESULT_OK, uint32_t { thread.GetOS().setup.gamecard != nullptr }); } else if (media_type == 1) { // No titles installed to the SD card return std::make_tuple(RESULT_OK, uint32_t { 0 }); } else { throw Mikage::Exceptions::NotImplemented("Unsupported media type"); } } static std::tuple AMHandleGetTitleInfos(FakeThread& thread, Context& context, uint32_t media_type, uint32_t num_titles, const PXI::PXIBuffer& title_ids, const PXI::PXIBuffer& infos) { thread.GetLogger()->info("{}received stubbed GetTitleInfos for {:#x} titles with media_type {:#x}", ThreadPrinter{thread}, num_titles, media_type); // Only the lowest byte of the media type is used media_type &= 0xff; // TODO: Validate buffer size if (media_type == 0) { if (num_titles > context.nand_titles.size()) thread.GetOS().SVCBreak(thread, OSImpl::BreakReason::Panic); for (uint32_t title_index = 0; title_index < num_titles; ++title_index) { auto title_id = title_ids.Read(thread, title_index * sizeof(uint64_t)); auto title_it = ranges::find(context.nand_titles, title_id); if (title_it == context.nand_titles.end()) { // throw Mikage::Exceptions::Invalid("Queried title info for unknown NAND title id {:#x}", title_id); // Selecting the "Game Notes" icon in HOME Menu makes the menu query for possibly non-existing title IDs to check for New3DS-specific manuals infos.Write(thread, title_index * 24, 0); // TODO: What data should be returned here? infos.Write(thread, title_index * 24 + 0x8, 0); infos.Write(thread, title_index * 24 + 0x10, 0); infos.Write(thread, title_index * 24 + 0x14, 0); continue; } infos.Write(thread, title_index * 24, *title_it); // TODO: What data should be returned here? infos.Write(thread, title_index * 24 + 0x8, 0x447000); infos.Write(thread, title_index * 24 + 0x10, 2055 ); infos.Write(thread, title_index * 24 + 0x14, 0x1); } return std::make_tuple(RESULT_OK); } else if (media_type == 2) { if (num_titles > (thread.GetOS().setup.gamecard ? 1 : 0)) thread.GetOS().SVCBreak(thread, OSImpl::BreakReason::Panic); if (thread.GetOS().setup.gamecard) { // TODO: Do we need to return any actual data for this? infos.Write(thread, 0x0, 0); infos.Write(thread, 0x8, 0); infos.Write(thread, 0x10, 0); } return std::make_tuple(RESULT_OK); } else { throw Mikage::Exceptions::NotImplemented("Unsupported media type"); } return std::make_tuple(RESULT_OK); } static auto PXIAMCommandHandler(FakeThread& thread, Context& context, const IPC::CommandHeader& header) try { namespace PXIAM = Platform::PXI::AM; switch (header.command_id) { case PXIAM::GetTitleCount::id: IPC::HandleIPCCommand(AMHandleGetTitleCount, thread, thread, context); break; case PXIAM::GetTitleList::id: IPC::HandleIPCCommand(AMHandleGetTitleList, thread, thread, context); break; case PXIAM::GetTitleInfos::id: IPC::HandleIPCCommand(AMHandleGetTitleInfos, thread, thread, context); break; // Takes a single input word (presumably a media_type) case 0x3f: // NOTE: This function is called by AM command 0x13 ("NeedsCleanup"), // which returns a boolean, so my current guess is it just // forwards that from this function. This needs to be verified, // though! thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw); thread.WriteTLS(0x84, RESULT_OK); thread.WriteTLS(0x88, 0); // Does not need cleanup break; default: throw IPC::IPCError{header.raw, 0xdeadbef3}; } return OS::ServiceHelper::SendReply; } catch (const IPC::IPCError& err) { throw std::runtime_error(fmt::format("Unknown pxi:am9 service command with header {:#010x}", err.header)); } void FakePXI::AMThread(FakeThread& thread, Context& context) { // NOTE: Actually, there should be four of these buffers! for (unsigned buffer_index = 0; buffer_index < 2; ++buffer_index) { const auto buffer_size = 0x1000; thread.WriteTLS(0x180 + 8 * buffer_index, IPC::TranslationDescriptor::MakeStaticBuffer(0, buffer_size).raw); Result result; uint32_t buffer_addr; std::tie(result, buffer_addr) = thread.CallSVC(&OSImpl::SVCControlMemory, 0, 0, 0x1000, 3 /* COMMIT */, 3 /* RW */); thread.WriteTLS(0x184 + 8 * buffer_index, buffer_addr); } OS::ServiceHelper service; service.Append(OS::ServiceUtil::SetupService(thread, "pxi:am9", 1)); auto InvokeCommandHandler = [&context](FakeThread& thread, uint32_t /* index of signalled handle */) { Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) }; return PXIAMCommandHandler(thread, context, header); }; service.Run(thread, std::move(InvokeCommandHandler)); } } // namespace PXI // } // namespace OS } // namespace HLE