diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/oc_common.hpp b/Source/Atmosphere/stratosphere/loader/source/oc/oc_common.hpp index 7b44b948..566cf14b 100644 --- a/Source/Atmosphere/stratosphere/loader/source/oc/oc_common.hpp +++ b/Source/Atmosphere/stratosphere/loader/source/oc/oc_common.hpp @@ -98,6 +98,7 @@ namespace ams::ldr { R_DEFINE_ERROR_RESULT(InvalidDvcoRateConfig, 1035); R_DEFINE_ERROR_RESULT(InvalidVoltCode, 1036); R_DEFINE_ERROR_RESULT(InvalidVoltLutIndex, 1037); + R_DEFINE_ERROR_RESULT(InvalidHookCave, 1038); } namespace ams::ldr::hoc { diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko.cpp b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko.cpp index 5bbda9a8..253e0707 100644 --- a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko.cpp +++ b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko.cpp @@ -29,6 +29,7 @@ #include "pcv_mariko_mtc.hpp" #include "pcv_mariko_soc.hpp" #include "pcv_mariko_log.hpp" +#include "pcv_mariko_cave.hpp" #include "calculate_timings_mariko.hpp" namespace ams::ldr::hoc::pcv::mariko { @@ -39,6 +40,8 @@ namespace ams::ldr::hoc::pcv::mariko { DEFINE_HOOK_PAYLOAD_PTR(HookPayloadData, m_HookPayloadData); Result InstallHooks() { + R_TRY(CaveInstall()); + R_TRY(Hooks().CheckEnabled()); R_TRY(Hooks().CopyPayload()); @@ -96,6 +99,7 @@ namespace ams::ldr::hoc::pcv::mariko { { "CPU Lut Max Asm2", &CpuLutMaxAsm2, 1, &CpuLutAsmPatternFn2 }, { "CPU Lut Writer", &LutWriterFind, 1, &LutWriterPatternFn }, { "CPU Dvco Rate Cfg", &CpuLutDvcoRateCfg, 1, &CpuLutDvcoRateCfgPatternFn }, + { "Hook Cave", &DeadDestructorCaveFind, 0, &CaveFlagRunPatternFn, 0, 0, true }, /* Debugging patches */ #if HOC_UART_LOG diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.cpp b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.cpp new file mode 100644 index 00000000..e468d64b --- /dev/null +++ b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "pcv_mariko_cave.hpp" + +namespace ams::ldr::hoc::pcv::mariko { + + namespace { + + struct { + u32 *site = nullptr; + u32 *end = nullptr; + } caveCache; + + bool HasDirectCaller(const u32 *target, const u32 *begin, const u32 *end) { + for (const u32 *ptr = begin; ptr < end; ++ptr) { + const u32 top6 = *ptr & 0xFC000000u; + if (top6 != _asm::op::B && top6 != _asm::op::Bl) { + continue; + } + + if (_asm::Target26(*ptr, reinterpret_cast(ptr)) == reinterpret_cast(target)) { + return true; + } + } + + return false; + } + + } + + Result DeadDestructorCaveFind(u32 *ptr) { + u32 *textEnd = reinterpret_cast(g_pcv_cave); + + u32 *site = _asm::FindFnPrologue(ptr, CavePrologueMargin, nsoStart); + R_UNLESS(site != nullptr, ldr::ResultInvalidHookCave()); + + u32 *end = _asm::FindFnEnd(site, textEnd); + R_UNLESS(end != nullptr, ldr::ResultInvalidHookCave()); + + /* Ensure this function is never called. */ + R_UNLESS(!HasDirectCaller(site, nsoStart, textEnd), ldr::ResultInvalidHookCave()); + + caveCache.site = site; + caveCache.end = end; + R_SUCCEED(); + } + + Result CaveInstall() { + if (Hooks().CaveFree() >= HookContext::RequiredCaveSize()) { + R_SUCCEED(); + } + + R_UNLESS(caveCache.site != nullptr, ldr::ResultInvalidHookCave()); + + const uintptr_t start = util::AlignUp(reinterpret_cast(caveCache.site + 1), sizeof(u64)); + const uintptr_t end = reinterpret_cast(caveCache.end); + R_UNLESS(start < end && end - start >= HookContext::RequiredCaveSize(), ldr::ResultHookPayloadTooLarge()); + + /* Insert a break in case this function gets called for whatever reason. */ + *caveCache.site = _asm::BrkIns; + + Hooks().SetCave(start, end - start); + R_SUCCEED(); + } + +} diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.hpp b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.hpp new file mode 100644 index 00000000..7d177b49 --- /dev/null +++ b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/mariko/pcv_mariko_cave.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "../pcv.hpp" +#include "../pcv_asm.hpp" +#include "../pcv_hook.hpp" + +namespace ams::ldr::hoc::pcv::mariko { + + extern u32 *nsoStart; + + constexpr u32 CaveFlagChunkCount = 4; + constexpr u32 CaveFlagChunkInsCount = 4; + constexpr u32 CaveFlagLdrbDelta = 0x30; + constexpr u32 CavePrologueMargin = 8; + + /* This function consists of multiple chunks. */ + /* Each chunk does a load, a comparison and a conditional branch. The next chunk's address setup (adrp) follows right after it. */ + inline bool IsFlagCheckChunk(const u32 *ptr) { + return _asm::IsOp(ptr[0], _asm::op::LdrbImm, _asm::field::Rt, _asm::field::Rn, _asm::field::Off1) + && _asm::Ignoring(ptr[1], _asm::CmpImm32(0, 1), _asm::field::Rn) + && _asm::Ignoring(ptr[2], _asm::Encode(_asm::op::BCond, {_asm::field::BCond, _asm::cond::Eq}), _asm::field::Imm19); + } + + inline bool IsChunkDeltaValid(const u32 *prev, const u32 *next) { + return IsFlagCheckChunk(next) && _asm::Get(*next, _asm::field::Off1) == _asm::Get(*prev, _asm::field::Off1) + CaveFlagLdrbDelta; + } + + inline bool ValidateChunkConsistency(const u32 *first) { + for (u32 i = 1; i < CaveFlagChunkCount; ++i) { + if (!IsChunkDeltaValid(first + (i - 1) * CaveFlagChunkInsCount, first + i * CaveFlagChunkInsCount)) { + return false; + } + } + + return true; + } + + inline bool CaveFlagRunPatternFn(u32 *ptr) { + if (Hooks().CaveFree() >= HookContext::RequiredCaveSize()) { + return false; + } + /* In case we don't have enough cave memory, we need to commit this warcrime. */ + + /* Ensure we don't dereference invalid memory. */ + u32 *textEnd = reinterpret_cast(g_pcv_cave); + if (ptr - CaveFlagChunkInsCount < nsoStart || ptr + CaveFlagChunkCount * CaveFlagChunkInsCount > textEnd) { + return false; + } + + /* Validate the current chunk and the previous chunk and ensure the delta of the ldrb instructions is consistent. */ + if (!IsFlagCheckChunk(ptr) || IsChunkDeltaValid(ptr - CaveFlagChunkInsCount, ptr)) { + return false; + } + + return ValidateChunkConsistency(ptr); + } + + Result DeadDestructorCaveFind(u32 *ptr); + Result CaveInstall(); + +} diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_asm.hpp b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_asm.hpp index 5f9171ee..5e901a52 100644 --- a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_asm.hpp +++ b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_asm.hpp @@ -26,6 +26,7 @@ namespace ams::ldr::hoc::pcv::_asm { constexpr u32 NopIns = 0xD503201F; constexpr u32 RetIns = 0xD65F03C0; + constexpr u32 BrkIns = 0xD4200000; constexpr u32 GetField(u32 ins, u8 lsb, u8 width) { return (ins >> lsb) & ((1u << width) - 1u); @@ -294,6 +295,41 @@ namespace ams::ldr::hoc::pcv::_asm { return IsFramePushPre(ins) || IsFramePushOffset(ins); } + constexpr bool IsCondBranch(u32 ins) { + return (ins & 0xFF000010u) == op::BCond; + } + + constexpr bool IsCompareBranch(u32 ins) { + return (ins & 0x7E000000u) == op::Cbz; + } + + constexpr bool IsTestBranch(u32 ins) { + return (ins & 0x7E000000u) == 0x36000000u; + } + + inline u32 *FindFnEnd(u32 *prologue, u32 *limit) { + u32 *furthest = prologue; + for (u32 *ptr = prologue; ptr < limit; ++ptr) { + const u32 ins = *ptr; + if (ins == RetIns && ptr >= furthest) { + return ptr + 1; + } + + s64 delta = 0; + if (IsCondBranch(ins) || IsCompareBranch(ins)) { + delta = SignExtend(static_cast(Get(ins, field::Imm19)) << 2, 21); + } else if (IsTestBranch(ins)) { + delta = SignExtend(static_cast(GetField(ins, 5, 14)) << 2, 16); + } + + if (ptr + (delta >> 2) > furthest) { + furthest = ptr + (delta >> 2); + } + } + + return nullptr; + } + inline u32 *FindFnPrologue(u32 *ptr, u32 margin, u32 *nsoStart) { for (u32 i = 0; i <= margin; ++i) { u32 *candidate = ptr - i; diff --git a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_hook.hpp b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_hook.hpp index d4a5eca5..ff66c42c 100644 --- a/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_hook.hpp +++ b/Source/Atmosphere/stratosphere/loader/source/oc/pcv/pcv_hook.hpp @@ -55,6 +55,7 @@ namespace ams::ldr::hoc::pcv { constexpr size_t HookPageSize = 0x1000; constexpr size_t PcvDataArenaSize = 0x1000; + constexpr size_t HookStubReserve = 0x80; inline s64 SignExtend(u64 value, int bits) { const int shift = 64 - bits; @@ -127,6 +128,15 @@ namespace ams::ldr::hoc::pcv { m_data_used = 0; } + void SetCave(uintptr_t cave, size_t cave_size) { + m_cave = cave; + m_cave_size = cave_size; + } + + static size_t RequiredCaveSize() { + return util::AlignUp(static_cast(__stop_hoc_hookpayload - __start_hoc_hookpayload), sizeof(u32)) + HookStubReserve; + } + bool IsEnabled() const { return m_cave != 0 && m_cave_size != 0; } Result CheckEnabled() const {