Merge pull request #248 from Splaser/main

fix: Handle unreachable NCE branch relocations with safe Dynarmic fallback
This commit is contained in:
cheezwiz7899
2026-07-03 11:13:08 +10:00
committed by GitHub
24 changed files with 693 additions and 73 deletions
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2026 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.citron.citron_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.citron.citron_emu.features.settings.model.AbstractStringSetting
class LogFilterSetting(
private val stringSetting: AbstractStringSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
private val customChoice: String
) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_LOG_FILTER
private val presetValues = arrayOf(
"*:Warning",
"*:Info",
"*:Debug",
"*:Trace",
"*:Warning Service.Audio:Debug HW.GPU:Debug"
)
val customIndex: Int
get() = presetValues.size
val choices: Array<String>
get() = presetValues + customChoice
val selectedValueIndex: Int
get() {
val index = presetValues.indexOf(getSelectedValue())
return if (index >= 0) index else customIndex
}
fun getValueAt(index: Int): String =
if (index >= 0 && index < presetValues.size) presetValues[index] else ""
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun getDisplayValue(): String {
val value = getSelectedValue()
return value.ifEmpty { customChoice }
}
fun setSelectedValue(value: String) = stringSetting.setString(value)
}
@@ -94,6 +94,7 @@ abstract class SettingsItem(
const val TYPE_INT_SINGLE_CHOICE = 9
const val TYPE_INPUT_PROFILE = 10
const val TYPE_STRING_INPUT = 11
const val TYPE_LOG_FILTER = 12
const val FASTMEM_COMBINED = "fastmem_combined"
@@ -53,7 +53,9 @@ class SettingsAdapter(
SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
SettingsItem.TYPE_SINGLE_CHOICE,
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
SettingsItem.TYPE_LOG_FILTER -> {
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
@@ -129,6 +131,15 @@ class SettingsAdapter(
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onLogFilterClick(item: LogFilterSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_LOG_FILTER,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
@@ -21,6 +21,7 @@ import org.citron.citron_emu.features.input.model.AnalogDirection
import org.citron.citron_emu.features.settings.model.view.AnalogInputSetting
import org.citron.citron_emu.features.settings.model.view.ButtonInputSetting
import org.citron.citron_emu.features.settings.model.view.IntSingleChoiceSetting
import org.citron.citron_emu.features.settings.model.view.LogFilterSetting
import org.citron.citron_emu.features.settings.model.view.SettingsItem
import org.citron.citron_emu.features.settings.model.view.SingleChoiceSetting
import org.citron.citron_emu.features.settings.model.view.SliderSetting
@@ -154,6 +155,26 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
.create()
}
SettingsItem.TYPE_LOG_FILTER -> {
val item = settingsViewModel.clickedItem as LogFilterSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
.create()
}
TYPE_LOG_FILTER_CUSTOM -> {
stringInputBinding = DialogEditTextBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as LogFilterSetting
stringInputBinding.editText.setText(item.getSelectedValue())
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(stringInputBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
@@ -174,6 +195,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
return when (type) {
SettingsItem.TYPE_SLIDER -> sliderBinding.root
SettingsItem.TYPE_STRING_INPUT -> stringInputBinding.root
TYPE_LOG_FILTER_CUSTOM -> stringInputBinding.root
else -> super.onCreateView(inflater, container, savedInstanceState)
}
}
@@ -206,6 +228,26 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
scSetting.setSelectedValue(value)
}
is LogFilterSetting -> {
val logFilterSetting = settingsViewModel.clickedItem as LogFilterSetting
if (type == TYPE_LOG_FILTER_CUSTOM) {
logFilterSetting.setSelectedValue(
(stringInputBinding.editText.text ?: "").toString()
)
} else if (which == logFilterSetting.customIndex) {
dismiss()
newInstance(
settingsViewModel,
logFilterSetting,
TYPE_LOG_FILTER_CUSTOM,
position
).show(parentFragmentManager, TAG)
return
} else {
logFilterSetting.setSelectedValue(logFilterSetting.getValueAt(which))
}
}
is IntSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
val value = scSetting.getValueAt(which)
@@ -265,6 +307,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
const val TAG = "SettingsDialogFragment"
const val TYPE_RESET_SETTING = -1
const val TYPE_LOG_FILTER_CUSTOM = -2
const val TITLE = "Title"
const val TYPE = "Type"
@@ -1003,10 +1003,11 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.logging))
add(
StringInputSetting(
LogFilterSetting(
StringSetting.LOG_FILTER,
titleId = R.string.log_filter,
descriptionId = R.string.log_filter_description
descriptionId = R.string.log_filter_description,
customChoice = context.getString(R.string.log_filter_custom)
)
)
}
@@ -6,6 +6,7 @@ package org.citron.citron_emu.features.settings.ui.viewholder
import android.view.View
import org.citron.citron_emu.databinding.ListItemSettingBinding
import org.citron.citron_emu.features.settings.model.view.IntSingleChoiceSetting
import org.citron.citron_emu.features.settings.model.view.LogFilterSetting
import org.citron.citron_emu.features.settings.model.view.SettingsItem
import org.citron.citron_emu.features.settings.model.view.SingleChoiceSetting
import org.citron.citron_emu.features.settings.model.view.StringSingleChoiceSetting
@@ -39,6 +40,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
binding.textSettingValue.text = item.getSelectedValue()
}
is LogFilterSetting -> {
binding.textSettingValue.text = item.getDisplayValue()
}
is IntSingleChoiceSetting -> {
binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
}
@@ -73,6 +78,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
)
}
is LogFilterSetting -> {
adapter.onLogFilterClick(setting as LogFilterSetting, bindingAdapterPosition)
}
is IntSingleChoiceSetting -> {
adapter.onIntSingleChoiceClick(
setting as IntSingleChoiceSetting,
@@ -208,12 +208,17 @@ jstring Java_org_citron_citron_1emu_utils_NativeConfig_getString(JNIEnv* env, jo
void Java_org_citron_citron_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
jstring value) {
const auto key = Common::Android::GetJString(env, jkey);
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetValue(Common::Android::GetJString(env, value));
auto setting_value = Common::Android::GetJString(env, value);
if (key == Settings::values.log_filter.GetLabel()) {
setting_value = Common::Log::CanonicalizeFilterString(setting_value);
}
setting->SetValue(setting_value);
}
jboolean Java_org_citron_citron_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
@@ -272,7 +272,8 @@
<string name="fastmem">Fastmem</string>
<string name="logging">Logging</string>
<string name="log_filter">Log Filter</string>
<string name="log_filter_description">Configure which log messages to display. Format: &lt;class&gt;:&lt;level&gt;. Example: *:Info Service:Debug</string>
<string name="log_filter_custom">Custom...</string>
<string name="log_filter_description">Configure which log messages to display. Format: &lt;class&gt;:&lt;level&gt;. Example: *:Warning Service:Debug</string>
<!-- Audio settings strings -->
<string name="audio_output_engine">Output engine</string>
+61 -6
View File
@@ -4,6 +4,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
#include <cctype>
#include <chrono>
#include <climits>
#include <cstdlib>
@@ -95,11 +96,23 @@ std::string FormatLogMessage(const Entry& entry) noexcept {
}
} // namespace
namespace {
template <typename It>
bool ComparePartialStringCaseInsensitive(It begin, It end, const char* other) noexcept {
for (; begin != end && *other != '\0'; ++begin, ++other) {
const auto lhs = static_cast<unsigned char>(*begin);
const auto rhs = static_cast<unsigned char>(*other);
if (std::tolower(lhs) != std::tolower(rhs)) {
return false;
}
}
return (begin == end) == (*other == '\0');
}
template <typename It>
Level GetLevelByName(const It begin, const It end) noexcept {
for (u32 i = 0; i < u32(Level::Count); ++i)
if (auto const name = GetLevelName(Level(i));
Common::ComparePartialString(begin, end, name))
ComparePartialStringCaseInsensitive(begin, end, name))
return Level(i);
return Level::Count;
}
@@ -107,7 +120,7 @@ template <typename It>
Class GetClassByName(const It begin, const It end) noexcept {
for (u32 i = 0; i < u32(Class::Count); ++i)
if (auto const name = GetLogClassName(Class(i));
Common::ComparePartialString(begin, end, name))
ComparePartialStringCaseInsensitive(begin, end, name))
return Class(i);
return Class::Count;
}
@@ -115,13 +128,13 @@ template <typename Iterator>
bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) noexcept {
auto level_separator = std::find(begin, end, ':');
if (level_separator == end) {
LOG_ERROR(Log, "Invalid log filter. Must specify a log level after `:`: {}",
std::string(begin, end));
LOG_WARNING(Log, "Invalid log filter. Must specify a log level after `:`: {}",
std::string(begin, end));
return false;
}
const Level level = GetLevelByName(level_separator + 1, end);
if (level == Level::Count) {
LOG_ERROR(Log, "Unknown log level in filter: {}", std::string(begin, end));
LOG_WARNING(Log, "Unknown log level in filter: {}", std::string(begin, end));
return false;
}
if (Common::ComparePartialString(begin, level_separator, "*")) {
@@ -130,7 +143,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) noexcept {
}
const Class log_class = GetClassByName(begin, level_separator);
if (log_class == Class::Count) {
LOG_ERROR(Log, "Unknown log class in filter: {}", std::string(begin, end));
LOG_WARNING(Log, "Unknown log class in filter: {}", std::string(begin, end));
return false;
}
instance.SetClassLevel(log_class, level);
@@ -159,6 +172,48 @@ void Filter::ParseFilterString(std::string_view filter_view) noexcept {
}
}
std::string CanonicalizeFilterString(std::string_view filter_view) {
std::string canonicalized;
auto it = filter_view.cbegin();
while (it != filter_view.cend()) {
auto end = std::find(it, filter_view.cend(), ' ');
if (end != it) {
if (!canonicalized.empty()) {
canonicalized.push_back(' ');
}
auto level_separator = std::find(it, end, ':');
if (level_separator == end) {
canonicalized.append(it, end);
} else {
if (ComparePartialStringCaseInsensitive(it, level_separator, "*")) {
canonicalized.push_back('*');
} else {
const Class log_class = GetClassByName(it, level_separator);
if (log_class == Class::Count) {
canonicalized.append(it, level_separator);
} else {
canonicalized.append(GetLogClassName(log_class));
}
}
canonicalized.push_back(':');
const Level level = GetLevelByName(level_separator + 1, end);
if (level == Level::Count) {
canonicalized.append(level_separator + 1, end);
} else {
canonicalized.append(GetLevelName(level));
}
}
}
if (end != filter_view.cend()) // Skip over the whitespace
++end;
it = end;
}
return canonicalized;
}
namespace {
/// @brief Trims up to and including the last of ../, ..\, src/, src\ in a string
/// do not be fooled this isn't generating new strings on .rodata :)
+4 -1
View File
@@ -5,8 +5,10 @@
#pragma once
#include <chrono>
#include <algorithm>
#include <chrono>
#include <string>
#include <string_view>
#include <type_traits>
#include <fmt/ranges.h>
#include "common/swap.h"
@@ -67,6 +69,7 @@ struct Filter {
}
std::array<Level, std::size_t(Class::Count)> class_levels;
};
[[nodiscard]] std::string CanonicalizeFilterString(std::string_view filter_view);
void Initialize() noexcept;
void Start() noexcept;
void Stop() noexcept;
+4
View File
@@ -199,6 +199,10 @@ bool IsNceEnabled() {
return is_nce_enabled;
}
void DisableNceForCurrentProcess() {
is_nce_enabled = false;
}
bool IsDockedMode() {
return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked;
}
+2 -1
View File
@@ -778,7 +778,7 @@ struct Values {
Setting<bool> perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging};
// Miscellaneous
Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Debugging};
Setting<std::string> log_filter{linkage, "*:Warning", "log_filter", Category::Debugging};
Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous};
// Network
@@ -840,6 +840,7 @@ bool IsGPULevelNormal();
bool IsFastmemEnabled();
bool IsCpuUltraLowAccuracy();
void SetNceEnabled(bool is_64bit);
void DisableNceForCurrentProcess();
bool IsNceEnabled();
bool IsDockedMode();
+107 -8
View File
@@ -2,8 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cinttypes>
#include <cstring>
#include <memory>
#include "common/assert.h"
#include "common/logging.h"
#include "common/signal_chain.h"
#include "core/arm/nce/arm_nce.h"
#include "core/arm/nce/interpreter_visitor.h"
@@ -24,6 +27,57 @@ namespace {
struct sigaction g_orig_bus_action;
struct sigaction g_orig_segv_action;
template <typename Callback>
void InvokeOriginalActionWithMask(int sig, const struct sigaction& original, Callback&& callback) {
sigset_t callback_mask = original.sa_mask;
sigset_t previous_mask;
if ((original.sa_flags & SA_NODEFER) == 0) {
sigaddset(&callback_mask, sig);
}
sigprocmask(SIG_BLOCK, &callback_mask, &previous_mask);
if ((original.sa_flags & SA_NODEFER) != 0) {
sigset_t nodefer_mask;
sigemptyset(&nodefer_mask);
sigaddset(&nodefer_mask, sig);
sigprocmask(SIG_UNBLOCK, &nodefer_mask, nullptr);
}
callback();
sigprocmask(SIG_SETMASK, &previous_mask, nullptr);
}
void ForwardSignalToOriginalAction(int sig, siginfo_t* info, void* raw_context,
const struct sigaction& original) {
if (original.sa_handler == SIG_IGN) {
return;
}
if (original.sa_handler == SIG_DFL) {
Common::SigAction(sig, &original, nullptr);
syscall(SYS_tkill, gettid(), sig);
return;
}
if ((original.sa_flags & SA_SIGINFO) != 0 && original.sa_sigaction != nullptr) {
InvokeOriginalActionWithMask(sig, original,
[&] { original.sa_sigaction(sig, info, raw_context); });
return;
}
if ((original.sa_flags & SA_SIGINFO) == 0 && original.sa_handler != nullptr) {
InvokeOriginalActionWithMask(sig, original, [&] { original.sa_handler(sig); });
return;
}
struct sigaction default_action {};
default_action.sa_handler = SIG_DFL;
Common::SigAction(sig, &default_action, nullptr);
syscall(SYS_tkill, gettid(), sig);
}
// Verify assembly offsets.
using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
static_assert(offsetof(NativeExecutionParameters, native_context) == TpidrEl0NativeContext);
@@ -31,11 +85,43 @@ static_assert(offsetof(NativeExecutionParameters, lock) == TpidrEl0Lock);
static_assert(offsetof(NativeExecutionParameters, magic) == TpidrEl0TlsMagic);
fpsimd_context* GetFloatingPointState(mcontext_t& host_ctx) {
_aarch64_ctx* header = reinterpret_cast<_aarch64_ctx*>(&host_ctx.__reserved);
while (header->magic != FPSIMD_MAGIC) {
header = reinterpret_cast<_aarch64_ctx*>(reinterpret_cast<char*>(header) + header->size);
auto* const begin = reinterpret_cast<char*>(&host_ctx.__reserved);
auto* const end = begin + sizeof(host_ctx.__reserved);
for (auto* ptr = begin; ptr + sizeof(_aarch64_ctx) <= end;) {
auto* header = reinterpret_cast<_aarch64_ctx*>(ptr);
if (header->magic == FPSIMD_MAGIC) {
if (header->size >= sizeof(fpsimd_context) &&
static_cast<size_t>(end - ptr) >= sizeof(fpsimd_context)) {
return reinterpret_cast<fpsimd_context*>(header);
}
LOG_CRITICAL(Core_ARM,
"Malformed NCE signal frame FPSIMD context: size={:#x}, "
"remaining={:#x}",
header->size, static_cast<size_t>(end - ptr));
return nullptr;
}
if (header->magic == 0 && header->size == 0) {
break;
}
if (header->size < sizeof(_aarch64_ctx) ||
header->size > static_cast<size_t>(end - ptr)) {
LOG_CRITICAL(Core_ARM,
"Malformed NCE signal frame context entry: magic={:#x}, size={:#x}, "
"remaining={:#x}",
header->magic, header->size, static_cast<size_t>(end - ptr));
return nullptr;
}
ptr += header->size;
}
return reinterpret_cast<fpsimd_context*>(header);
LOG_CRITICAL(Core_ARM, "NCE signal frame is missing FPSIMD context");
return nullptr;
}
using namespace Common::Literals;
@@ -53,6 +139,9 @@ void* ArmNce::RestoreGuestContext(void* raw_context) {
// Retrieve the host floating point state.
auto* fpctx = GetFloatingPointState(host_ctx);
if (fpctx == nullptr) {
UNREACHABLE_MSG("NCE cannot restore guest context without FPSIMD signal state");
}
// Save host callee-saved registers.
std::memcpy(guest_ctx->host_ctx.host_saved_vregs.data(), &fpctx->vregs[8],
@@ -82,6 +171,9 @@ void ArmNce::SaveGuestContext(GuestContext* guest_ctx, void* raw_context) {
// Retrieve the host floating point state.
auto* fpctx = GetFloatingPointState(host_ctx);
if (fpctx == nullptr) {
UNREACHABLE_MSG("NCE cannot save guest context without FPSIMD signal state");
}
// Save all guest registers except tpidr_el0.
std::memcpy(guest_ctx->cpu_registers.data(), host_ctx.regs, sizeof(host_ctx.regs));
@@ -116,13 +208,15 @@ bool ArmNce::HandleFailedGuestFault(GuestContext* guest_ctx, void* raw_info, voi
const bool is_prefetch_abort = host_ctx.pc == reinterpret_cast<u64>(info->si_addr);
// For data aborts, skip the instruction and return to guest code.
// This will allow games to continue in many scenarios where they would otherwise crash.
// This preserves the historical NCE behavior while branch relocation fallback is tested.
if (!is_prefetch_abort) {
host_ctx.pc += 4;
return true;
}
// This is a prefetch abort.
LOG_CRITICAL(Core_ARM, "Unhandled NCE prefetch abort: pc={:#x}, fault_addr={:#x}",
host_ctx.pc, reinterpret_cast<u64>(info->si_addr));
guest_ctx->esr_el1.fetch_or(static_cast<u64>(HaltReason::PrefetchAbort));
// Forcibly mark the context as locked. We are still running.
@@ -141,6 +235,9 @@ bool ArmNce::HandleFailedGuestFault(GuestContext* guest_ctx, void* raw_info, voi
bool ArmNce::HandleGuestAlignmentFault(GuestContext* guest_ctx, void* raw_info, void* raw_context) {
auto& host_ctx = static_cast<ucontext_t*>(raw_context)->uc_mcontext;
auto* fpctx = GetFloatingPointState(host_ctx);
if (fpctx == nullptr) {
UNREACHABLE_MSG("NCE cannot handle guest alignment fault without FPSIMD signal state");
}
auto& memory = guest_ctx->system->ApplicationMemory();
// Match and execute an instruction.
@@ -171,11 +268,13 @@ bool ArmNce::HandleGuestAccessFault(GuestContext* guest_ctx, void* raw_info, voi
}
void ArmNce::HandleHostAlignmentFault(int sig, void* raw_info, void* raw_context) {
return g_orig_bus_action.sa_sigaction(sig, static_cast<siginfo_t*>(raw_info), raw_context);
ForwardSignalToOriginalAction(sig, static_cast<siginfo_t*>(raw_info), raw_context,
g_orig_bus_action);
}
void ArmNce::HandleHostAccessFault(int sig, void* raw_info, void* raw_context) {
return g_orig_segv_action.sa_sigaction(sig, static_cast<siginfo_t*>(raw_info), raw_context);
ForwardSignalToOriginalAction(sig, static_cast<siginfo_t*>(raw_info), raw_context,
g_orig_segv_action);
}
void ArmNce::LockThread(Kernel::KThread* thread) {
@@ -302,7 +401,7 @@ void ArmNce::Initialize() {
alignment_fault_action.sa_sigaction =
reinterpret_cast<HandlerType>(&ArmNce::GuestAlignmentFaultSignalHandler);
alignment_fault_action.sa_mask = signal_mask;
Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, nullptr);
Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, &g_orig_bus_action);
struct sigaction access_fault_action {};
access_fault_action.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
+159 -11
View File
@@ -4,6 +4,7 @@
#include "common/arm64/native_clock.h"
#include "common/bit_cast.h"
#include "common/literals.h"
#include "common/logging.h"
#include "core/arm/nce/arm_nce.h"
#include "core/arm/nce/guest_context.h"
#include "core/arm/nce/instructions.h"
@@ -20,6 +21,9 @@ using namespace oaknut::util;
using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
constexpr size_t MaxRelativeBranch = 128_MiB;
constexpr ptrdiff_t MinRelativeBranchOffset = -static_cast<ptrdiff_t>(MaxRelativeBranch);
constexpr ptrdiff_t MaxRelativeBranchOffset =
static_cast<ptrdiff_t>(MaxRelativeBranch) - sizeof(u32);
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
Patcher::Patcher() : c(m_patch_instructions) {
@@ -38,6 +42,35 @@ Patcher::Patcher() : c(m_patch_instructions) {
Patcher::~Patcher() = default;
bool Patcher::IsValidBranchOffset(ptrdiff_t offset) noexcept {
return offset >= MinRelativeBranchOffset && offset <= MaxRelativeBranchOffset &&
offset % sizeof(u32) == 0;
}
ptrdiff_t Patcher::CalculateBranchToPatchOffset(PatchMode patch_mode, size_t patch_size,
size_t remaining_program_size,
const Relocation& rel) noexcept {
if (patch_mode == PatchMode::PreText) {
return rel.patch_offset - static_cast<ptrdiff_t>(patch_size) -
static_cast<ptrdiff_t>(rel.module_offset);
}
return static_cast<ptrdiff_t>(remaining_program_size) -
static_cast<ptrdiff_t>(rel.module_offset) + rel.patch_offset;
}
ptrdiff_t Patcher::CalculateBranchToModuleOffset(PatchMode patch_mode, size_t patch_size,
size_t remaining_program_size,
const Relocation& rel) noexcept {
if (patch_mode == PatchMode::PreText) {
return static_cast<ptrdiff_t>(patch_size) - rel.patch_offset +
static_cast<ptrdiff_t>(rel.module_offset);
}
return static_cast<ptrdiff_t>(rel.module_offset) -
static_cast<ptrdiff_t>(remaining_program_size) - rel.patch_offset;
}
bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
const Kernel::CodeSet::Segment& code) {
// If we have patched modules but cannot reach the new module, then it needs its own patcher.
@@ -49,6 +82,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// Add a new module patch to our list
modules.emplace_back();
curr_patch = &modules.back();
curr_patch->image_size = image_size;
// The first word of the patch section is always a branch to the first instruction of the
// module.
@@ -116,6 +150,10 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// Determine patching mode for the final relocation step
total_program_size += image_size;
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
if (mode == PatchMode::PreText) {
ASSERT(modules.size() == 1);
ASSERT(total_program_size == image_size);
}
return true;
}
@@ -131,22 +169,43 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
const auto text_words =
std::span<u32>{reinterpret_cast<u32*>(text.data()), text.size() / sizeof(u32)};
const auto LogBranchRelocation = [&](const char* kind, ptrdiff_t branch_offset,
const Relocation& rel) {
if (IsValidBranchOffset(branch_offset)) {
return;
}
LOG_CRITICAL(Core_ARM,
"NCE branch relocation is outside AArch64 B range: kind={}, mode={}, "
"relocate_module={}, modules={}, branch_offset={:#x}, valid_range=[{:#x}, "
"{:#x}], patch_offset={:#x}, module_offset={:#x}, patch_size={:#x}, "
"image_size={:#x}, total_program_size={:#x}, load_base={:#x}",
kind, mode, m_relocate_module_index, modules.size(), branch_offset,
MinRelativeBranchOffset, MaxRelativeBranchOffset, rel.patch_offset,
rel.module_offset, patch_size, image_size, total_program_size,
GetInteger(load_base));
};
const auto ApplyBranchToPatchRelocation = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
if (mode == PatchMode::PreText) {
rc.B(rel.patch_offset - patch_size - rel.module_offset);
} else {
rc.B(total_program_size - rel.module_offset + rel.patch_offset);
const ptrdiff_t branch_offset =
CalculateBranchToPatchOffset(mode, patch_size, total_program_size, rel);
LogBranchRelocation("module_to_patch", branch_offset, rel);
if (!IsValidBranchOffset(branch_offset)) {
UNREACHABLE_MSG("NCE branch relocation must be validated before RelocateAndCopy");
}
rc.B(branch_offset);
};
const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
if (mode == PatchMode::PreText) {
rc.B(patch_size - rel.patch_offset + rel.module_offset);
} else {
rc.B(rel.module_offset - total_program_size - rel.patch_offset);
const ptrdiff_t branch_offset =
CalculateBranchToModuleOffset(mode, patch_size, total_program_size, rel);
LogBranchRelocation("patch_to_module", branch_offset, rel);
if (!IsValidBranchOffset(branch_offset)) {
UNREACHABLE_MSG("NCE branch relocation must be validated before RelocateAndCopy");
}
rc.B(branch_offset);
};
const auto RebasePatch = [&](ptrdiff_t patch_offset) {
@@ -166,7 +225,10 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
};
// We are now ready to relocate!
auto& patch = modules[m_relocate_module_index++];
ASSERT(m_relocate_module_index < modules.size());
const size_t module_index = m_relocate_module_index++;
auto& patch = modules[module_index];
const size_t module_image_size = patch.image_size;
for (const Relocation& rel : patch.m_branch_to_patch_relocations) {
ApplyBranchToPatchRelocation(text_words.data() + rel.module_offset / sizeof(u32), rel);
}
@@ -193,12 +255,24 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
// Remove the patched module size from the total. This is done so total_program_size
// always represents the distance from the currently patched module to the patch section.
total_program_size -= image_size;
total_program_size -= module_image_size;
// Only copy to the program image of the last module
if (m_relocate_module_index == modules.size()) {
if (this->mode == PatchMode::PreText) {
ASSERT(image_size == total_program_size);
const size_t expected_image_size = module_image_size + patch_size;
if (image_size != expected_image_size || total_program_size != 0) {
LOG_CRITICAL(Core_ARM,
"NCE PreText final copy state mismatch: image_size={:#x}, "
"expected_image_size={:#x}, module_image_size={:#x}, "
"total_program_size={:#x}, patch_size={:#x}, relocate_module={}, "
"modules={}, load_base={:#x}",
image_size, expected_image_size, module_image_size,
total_program_size, patch_size, m_relocate_module_index,
modules.size(), GetInteger(load_base));
}
ASSERT(image_size == expected_image_size);
ASSERT(total_program_size == 0);
std::memcpy(program_image.data(), m_patch_instructions.data(),
m_patch_instructions.size() * sizeof(u32));
} else {
@@ -216,6 +290,80 @@ size_t Patcher::GetSectionSize() const noexcept {
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::CITRON_PAGESIZE);
}
bool Patcher::CanRelocateBranches() const noexcept {
if (modules.empty() || mode == PatchMode::None) {
LOG_CRITICAL(Core_ARM,
"NCE branch relocation preflight called without patched modules: mode={}, "
"modules={}, total_program_size={:#x}",
mode, modules.size(), total_program_size);
return false;
}
const size_t patch_size = GetSectionSize();
const auto LogInvalidBranch = [&](const char* kind, size_t module_index,
size_t remaining_program_size,
const Relocation& rel, ptrdiff_t branch_offset) {
LOG_CRITICAL(Core_ARM,
"NCE branch relocation preflight failed: kind={}, mode={}, "
"relocate_module={}, modules={}, branch_offset={:#x}, valid_range=[{:#x}, "
"{:#x}], patch_offset={:#x}, module_offset={:#x}, patch_size={:#x}, "
"image_size={:#x}, total_program_size={:#x}",
kind, mode, module_index, modules.size(), branch_offset,
MinRelativeBranchOffset, MaxRelativeBranchOffset, rel.patch_offset,
rel.module_offset, patch_size, modules[module_index].image_size,
remaining_program_size);
};
size_t remaining_program_size = total_program_size;
for (size_t module_index = 0; module_index < modules.size(); module_index++) {
const auto& patch = modules[module_index];
if (patch.image_size > remaining_program_size) {
LOG_CRITICAL(Core_ARM,
"NCE branch relocation preflight size state mismatch: mode={}, "
"relocate_module={}, modules={}, image_size={:#x}, "
"remaining_program_size={:#x}, total_program_size={:#x}",
mode, module_index, modules.size(), patch.image_size,
remaining_program_size, total_program_size);
return false;
}
for (const Relocation& rel : patch.m_branch_to_patch_relocations) {
const ptrdiff_t branch_offset =
CalculateBranchToPatchOffset(mode, patch_size, remaining_program_size, rel);
if (!IsValidBranchOffset(branch_offset)) {
LogInvalidBranch("module_to_patch", module_index, remaining_program_size, rel,
branch_offset);
return false;
}
}
for (const Relocation& rel : patch.m_branch_to_module_relocations) {
const ptrdiff_t branch_offset =
CalculateBranchToModuleOffset(mode, patch_size, remaining_program_size, rel);
if (!IsValidBranchOffset(branch_offset)) {
LogInvalidBranch("patch_to_module", module_index, remaining_program_size, rel,
branch_offset);
return false;
}
}
remaining_program_size -= patch.image_size;
}
if (remaining_program_size != 0) {
LOG_CRITICAL(Core_ARM,
"NCE branch relocation preflight size state mismatch after scan: mode={}, "
"modules={}, remaining_program_size={:#x}, total_program_size={:#x}",
mode, modules.size(), remaining_program_size, total_program_size);
return false;
}
return true;
}
void Patcher::WriteLoadContext() {
// This function was called, which modifies X30, so use that as a scratch register.
// SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes
+12
View File
@@ -36,6 +36,7 @@ public:
bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code,
Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines);
size_t GetSectionSize() const noexcept;
bool CanRelocateBranches() const noexcept;
[[nodiscard]] PatchMode GetPatchMode() const noexcept {
return mode;
@@ -84,7 +85,18 @@ private:
uintptr_t module_offset; ///< Offset in bytes from the start of the text section.
};
[[nodiscard]] static bool IsValidBranchOffset(ptrdiff_t offset) noexcept;
[[nodiscard]] static ptrdiff_t CalculateBranchToPatchOffset(PatchMode patch_mode,
size_t patch_size,
size_t remaining_program_size,
const Relocation& rel) noexcept;
[[nodiscard]] static ptrdiff_t CalculateBranchToModuleOffset(PatchMode patch_mode,
size_t patch_size,
size_t remaining_program_size,
const Relocation& rel) noexcept;
struct ModulePatch {
size_t image_size{};
std::vector<Trampoline> m_trampolines;
std::vector<Relocation> m_branch_to_patch_relocations{};
std::vector<Relocation> m_branch_to_module_relocations{};
+4
View File
@@ -1118,6 +1118,10 @@ void System::Exit() {
}
void System::ApplySettings() {
Common::Log::Filter filter;
filter.ParseFilterString(Settings::values.log_filter.GetValue());
Common::Log::SetGlobalFilter(filter);
impl->RefreshTime(*this);
if (IsPoweredOn()) {
+5 -12
View File
@@ -129,18 +129,11 @@ void PhysicalCore::RunThread(Kernel::KThread* thread) {
// Notify the debugger and go to sleep on data abort.
if (data_abort) {
if (system.DebuggerEnabled()) {
system.GetDebugger().NotifyThreadWatchpoint(thread, *interface->HaltedWatchpoint());
} else {
// Enhanced data abort handling for Nintendo SDK crashes
LOG_WARNING(Core_ARM, "Data abort detected - checking if recoverable...");
// For Nintendo SDK crashes, try to continue execution
// Many data aborts in Nintendo SDK are recoverable
LOG_INFO(Core_ARM, "Attempting to continue execution after data abort");
// Don't suspend the thread, let it continue
}
if (system.DebuggerEnabled()) {
if (const auto* watchpoint = interface->HaltedWatchpoint(); watchpoint != nullptr) {
system.GetDebugger().NotifyThreadWatchpoint(thread, *watchpoint);
} else {
system.GetDebugger().NotifyThreadStopped(thread);
}
thread->RequestSuspend(SuspendType::Debug);
return;
}
+71 -18
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <optional>
#include "common/logging.h"
#include "common/settings.h"
#include "core/core.h"
@@ -29,7 +30,12 @@ namespace Loader {
struct PatchCollection {
explicit PatchCollection(bool is_application_) : is_application{is_application_} {
Reset();
}
void Reset() {
module_patcher_indices.fill(-1);
patchers.clear();
patchers.emplace_back();
}
@@ -41,6 +47,10 @@ struct PatchCollection {
}
size_t GetTotalPatchSize() const {
if (!is_application || !Settings::IsNceEnabled()) {
return 0;
}
size_t total_size{};
#ifdef HAS_NCE
for (auto& patcher : patchers) {
@@ -62,6 +72,21 @@ struct PatchCollection {
return static_cast<s32>(patchers.size()) - 1;
}
bool CanRelocateBranches() const {
#ifdef HAS_NCE
if (!is_application || !Settings::IsNceEnabled()) {
return true;
}
for (const auto& patcher : patchers) {
if (!patcher.CanRelocateBranches()) {
return false;
}
}
#endif
return true;
}
bool is_application;
std::vector<Core::NCE::Patcher> patchers;
std::array<s32, 13> module_patcher_indices{};
@@ -175,6 +200,9 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
metadata.Print();
const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(),
system.GetContentProvider()};
// Enable NCE only for applications with 39-bit address space.
const bool is_39bit =
metadata.GetAddressSpaceType() == FileSys::ProgramAddressSpaceType::Is39Bit;
@@ -185,30 +213,57 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7",
"subsdk8", "subsdk9", "sdk"};
std::size_t code_size{};
// Define an nce patch context for each potential module.
PatchCollection patch_ctx{is_application};
// Use the NSO module loader to figure out the code layout
for (size_t i = 0; i < static_modules.size(); i++) {
const auto& module = static_modules[i];
const FileSys::VirtualFile module_file{dir->GetFile(module)};
if (!module_file) {
continue;
const auto CalculateCodeLayout = [&]() -> std::optional<std::size_t> {
std::size_t next_code_size{};
patch_ctx.Reset();
// Use the NSO module loader to figure out the code layout.
for (size_t i = 0; i < static_modules.size(); i++) {
const auto& module = static_modules[i];
const FileSys::VirtualFile module_file{dir->GetFile(module)};
if (!module_file) {
continue;
}
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
const auto tentative_next_load_addr = AppLoader_NSO::LoadModule(
process, system, *module_file, next_code_size, should_pass_arguments, false, pm,
patch_ctx.GetPatchers(), patch_ctx.GetLastIndex());
if (!tentative_next_load_addr) {
return std::nullopt;
}
patch_ctx.SaveIndex(i);
next_code_size = *tentative_next_load_addr;
}
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
const auto tentative_next_load_addr = AppLoader_NSO::LoadModule(
process, system, *module_file, code_size, should_pass_arguments, false, {},
patch_ctx.GetPatchers(), patch_ctx.GetLastIndex());
if (!tentative_next_load_addr) {
return next_code_size;
};
auto calculated_code_size = CalculateCodeLayout();
if (!calculated_code_size) {
return {ResultStatus::ErrorLoadingNSO, {}};
}
#ifdef HAS_NCE
if (!patch_ctx.CanRelocateBranches()) {
LOG_WARNING(Loader,
"NCE patch layout contains an out-of-range branch relocation for "
"program_id={:016X}; falling back to Dynarmic for this boot",
metadata.GetTitleID());
Settings::DisableNceForCurrentProcess();
calculated_code_size = CalculateCodeLayout();
if (!calculated_code_size) {
return {ResultStatus::ErrorLoadingNSO, {}};
}
patch_ctx.SaveIndex(i);
code_size = *tentative_next_load_addr;
}
#endif
std::size_t code_size = *calculated_code_size;
// Enable direct memory mapping in case of NCE.
const u64 fastmem_base = [&]() -> size_t {
@@ -232,8 +287,6 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
modules.clear();
const VAddr base_address{GetInteger(process.GetEntryPoint())};
VAddr next_load_addr{base_address};
const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(),
system.GetContentProvider()};
for (size_t i = 0; i < static_modules.size(); i++) {
const auto& module = static_modules[i];
const FileSys::VirtualFile module_file{dir->GetFile(module)};
+50 -2
View File
@@ -3,6 +3,7 @@
#include <cinttypes>
#include <cstring>
#include <exception>
#include <vector>
#include "common/common_funcs.h"
@@ -148,7 +149,10 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
// Apply patches if necessary
const auto name = nso_file.GetName();
if (pm && (pm->HasNSOPatch(nso_header.build_id, name) || Settings::values.dump_nso)) {
const bool has_nso_patch = pm && pm->HasNSOPatch(nso_header.build_id, name);
const bool should_patch_nso =
has_nso_patch || (load_into_process && Settings::values.dump_nso);
if (pm && should_patch_nso) {
std::span<u8> patchable_section(program_image.data() + module_start,
program_image.size() - module_start);
std::vector<u8> pi_header(sizeof(NSOHeader) + patchable_section.size());
@@ -158,6 +162,21 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
pi_header = pm->PatchNSO(pi_header, name);
if (pi_header.size() < sizeof(NSOHeader)) {
LOG_ERROR(Loader, "Patched NSO is too small: module={}, patched_size={:#x}", name,
pi_header.size());
return std::nullopt;
}
const auto patched_size = pi_header.size() - sizeof(NSOHeader);
if (patched_size != patchable_section.size()) {
LOG_ERROR(Loader,
"Patched NSO size mismatch: module={}, original_size={:#x}, "
"patched_size={:#x}",
name, patchable_section.size(), patched_size);
return std::nullopt;
}
std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data());
}
@@ -172,7 +191,36 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
}
} else if (patch) {
// Relocate code patch and copy to the program_image.
if (patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers())) {
const auto build_id = Common::HexToString(nso_header.build_id);
LOG_DEBUG(Loader,
"NCE relocating NSO module: name={}, build_id={}, patch_index={}, mode={}, "
"load_base={:#x}, image_size={:#x}, code_offset={:#x}, code_size={:#x}, "
"patch_section_size={:#x}",
name, build_id, patch_index, patch->GetPatchMode(), load_base,
program_image.size(), code.offset, code.size, patch->GetSectionSize());
bool copied_patch_section;
try {
copied_patch_section =
patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers());
} catch (const std::exception& ex) {
LOG_CRITICAL(Loader,
"NCE failed while relocating NSO module: name={}, build_id={}, "
"patch_index={}, mode={}, load_base={:#x}, image_size={:#x}, "
"code_offset={:#x}, code_size={:#x}, patch_section_size={:#x}, "
"exception={}",
name, build_id, patch_index, patch->GetPatchMode(), load_base,
program_image.size(), code.offset, code.size, patch->GetSectionSize(),
ex.what());
throw;
}
LOG_DEBUG(Loader,
"NCE relocated NSO module: name={}, build_id={}, patch_index={}, "
"copied_patch_section={}, final_image_size={:#x}",
name, build_id, patch_index, copied_patch_section, program_image.size());
if (copied_patch_section) {
// Update patch section.
auto& patch_segment = codeset.PatchSegment();
patch_segment.addr =
+1 -1
View File
@@ -1065,7 +1065,7 @@ bool Memory::InvalidateNCE(Common::ProcessAddress vaddr, size_t size) {
u8* const ptr = impl->GetPointerImpl(
GetInteger(vaddr),
[&] {
LOG_ERROR(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size,
LOG_DEBUG(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size,
GetInteger(vaddr));
mapped = false;
},

Some files were not shown because too many files have changed in this diff Show More