Browse Source

[core] Support libnx homebrew NextLoad handoff

Implement the homebrew NextLoad path used by libnx NROs to request another NRO from svcExitProcess.

Keep the existing process alive, rebuild the homebrew config and argv buffers, reset thread context, refresh process metadata, and add memory/address-space fallbacks needed for repeated in-place handoffs.

Reference: https://switchbrew.github.io/libnx/env_8h.html
xbzk 5 days ago
parent
commit
0de38866e8
  1. 32
      src/common/address_space.inc
  2. 5
      src/core/hle/kernel/k_page_table_base.h
  3. 10
      src/core/hle/kernel/k_process.cpp
  4. 30
      src/core/hle/kernel/k_process.h
  5. 9
      src/core/hle/kernel/svc.cpp
  6. 11
      src/core/hle/kernel/svc.h
  7. 221
      src/core/hle/kernel/svc/svc_memory.cpp
  8. 209
      src/core/hle/kernel/svc/svc_process.cpp
  9. 529
      src/core/loader/nro.cpp
  10. 8
      src/core/loader/nro.h

32
src/common/address_space.inc

@ -336,22 +336,22 @@ ALLOC_MEMBER(VaType)::Allocate(VaType size) {
current_linear_alloc_end = alloc_start + size;
} else { // If linear allocation overflows the AS then find a gap
if (this->blocks.size() <= 2) {
ASSERT_MSG(false, "Unexpected allocator state!");
}
auto search_predecessor{std::next(this->blocks.begin())};
auto search_successor{std::next(search_predecessor)};
alloc_start = virt_start;
} else {
auto search_predecessor{std::next(this->blocks.begin())};
auto search_successor{std::next(search_predecessor)};
while (search_successor != this->blocks.end() &&
(search_successor->virt - search_predecessor->virt < size ||
search_predecessor->Mapped())) {
search_predecessor = search_successor++;
}
while (search_successor != this->blocks.end() &&
(search_successor->virt - search_predecessor->virt < size ||
search_predecessor->Mapped())) {
search_predecessor = search_successor++;
}
if (search_successor != this->blocks.end()) {
alloc_start = search_predecessor->virt;
} else {
return {}; // AS is full
if (search_successor != this->blocks.end()) {
alloc_start = search_predecessor->virt;
} else {
return {}; // AS is full
}
}
}
@ -364,6 +364,10 @@ ALLOC_MEMBER(void)::AllocateFixed(VaType virt, VaType size) {
}
ALLOC_MEMBER(void)::Free(VaType virt, VaType size) {
const VaType virt_end = virt + size;
this->Unmap(virt, size);
if (virt_end >= virt && virt_end == current_linear_alloc_end) {
current_linear_alloc_end = virt < virt_start ? virt_start : virt;
}
}
} // namespace Common

5
src/core/hle/kernel/k_page_table_base.h

@ -670,6 +670,11 @@ public:
size_t GetHeapRegionSize() const {
return m_heap_region_end - m_heap_region_start;
}
size_t GetCurrentHeapSize() const {
KScopedLightLock lk(m_general_lock);
return m_current_heap_end - m_heap_region_start;
}
size_t GetAliasRegionSize() const {
return m_alias_region_end - m_alias_region_start;
}

10
src/core/hle/kernel/k_process.cpp

@ -210,6 +210,10 @@ Result KProcess::Initialize(KernelCore& kernel, const Svc::CreateProcessParamete
m_arg_pointer = 0;
m_arg_return_address = 0;
m_main_thread_handle_addr = 0;
m_process_handle_addr = 0;
m_homebrew_next_load_path_addr = 0;
m_homebrew_next_load_argv_addr = 0;
m_is_homebrew_in_place_next_load = false;
m_code_size = params.code_num_pages * PageSize;
m_is_application = True(params.flags & Svc::CreateProcessFlag::IsApplication);
@ -947,6 +951,7 @@ Result KProcess::Run(KernelCore& kernel, s32 priority, size_t stack_size) {
stack_top = stack_bottom + stack_size;
m_main_thread_stack_size = stack_size;
m_main_thread_stack_top = stack_top;
}
// Ensure our stack is safe to clean up on exit.
@ -1005,6 +1010,11 @@ Result KProcess::Run(KernelCore& kernel, s32 priority, size_t stack_size) {
if (GetInteger(m_main_thread_handle_addr) != 0) {
this->GetMemory().Write32(m_main_thread_handle_addr, thread_handle);
}
if (GetInteger(m_process_handle_addr) != 0) {
Handle process_handle;
R_TRY(m_handle_table.Add(kernel, std::addressof(process_handle), this));
this->GetMemory().Write32(m_process_handle_addr, process_handle);
}
} else {
main_thread->GetContext().r[0] = 0;
main_thread->GetContext().r[1] = thread_handle;

30
src/core/hle/kernel/k_process.h

@ -87,6 +87,9 @@ private:
KProcessAddress m_arg_pointer{};
KProcessAddress m_arg_return_address{};
KProcessAddress m_main_thread_handle_addr{};
KProcessAddress m_process_handle_addr{};
KProcessAddress m_homebrew_next_load_path_addr{};
KProcessAddress m_homebrew_next_load_argv_addr{};
KHandleTable m_handle_table;
KProcessAddress m_plr_address{};
ThreadList m_thread_list{};
@ -112,6 +115,7 @@ private:
size_t m_code_size{};
size_t m_main_thread_stack_size{};
KProcessAddress m_main_thread_stack_top{};
size_t m_max_process_memory{};
size_t m_memory_release_hint{};
s64 m_schedule_count{};
@ -139,6 +143,7 @@ private:
bool m_is_suspended : 1 = false;
bool m_is_immortal : 1 = false;
bool m_is_handle_table_initialized : 1 = false;
bool m_is_homebrew_in_place_next_load : 1 = false;
private:
Result StartTermination(KernelCore& kernel);
@ -231,6 +236,31 @@ public:
void SetMainThreadHandleAddr(KProcessAddress addr) {
m_main_thread_handle_addr = addr;
}
void SetProcessHandleAddr(KProcessAddress addr) {
m_process_handle_addr = addr;
}
void SetHomebrewNextLoadBufferAddrs(KProcessAddress path_addr, KProcessAddress argv_addr) {
m_homebrew_next_load_path_addr = path_addr;
m_homebrew_next_load_argv_addr = argv_addr;
}
void SetHomebrewInPlaceNextLoad(bool enabled) {
m_is_homebrew_in_place_next_load = enabled;
}
bool IsHomebrewInPlaceNextLoad() const {
return m_is_homebrew_in_place_next_load;
}
KProcessAddress GetHomebrewNextLoadPathAddr() const {
return m_homebrew_next_load_path_addr;
}
KProcessAddress GetHomebrewNextLoadArgvAddr() const {
return m_homebrew_next_load_argv_addr;
}
size_t GetCodeSize() const {
return m_code_size;
}
KProcessAddress GetMainThreadStackTop() const {
return m_main_thread_stack_top;
}
size_t GetMainStackSize() const {
return m_main_thread_stack_size;

9
src/core/hle/kernel/svc.cpp

@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-late
// SPDX-License-Identifier: GPL-2.0-or-later
// This file is automatically generated using svc_generator.py.
// DO NOT MODIFY IT MANUALLY
@ -128,7 +129,7 @@ static void SvcWrap_QueryMemory64From32(Core::System& system, std::span<uint64_t
}
static void SvcWrap_ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess64From32(system);
ExitProcess64From32(system, args);
}
static void SvcWrap_CreateThread64From32(Core::System& system, std::span<uint64_t, 8> args) {
@ -1298,7 +1299,7 @@ static void SvcWrap_QueryMemory64(Core::System& system, std::span<uint64_t, 8> a
}
static void SvcWrap_ExitProcess64(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess64(system);
ExitProcess64(system, args);
}
static void SvcWrap_CreateThread64(Core::System& system, std::span<uint64_t, 8> args) {

11
src/core/hle/kernel/svc.h

@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-late
// SPDX-License-Identifier: GPL-2.0-or-later
// This file is automatically generated using svc_generator.py.
// DO NOT MODIFY IT MANUALLY
@ -25,7 +26,7 @@ Result SetMemoryAttribute(Core::System& system, uint64_t address, uint64_t size,
Result MapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result UnmapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result QueryMemory(Core::System& system, uint64_t out_memory_info, PageInfo* out_page_info, uint64_t address);
void ExitProcess(Core::System& system);
void ExitProcess(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread(Core::System& system, Handle* out_handle, uint64_t func, uint64_t arg, uint64_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread(Core::System& system, Handle thread_handle);
void ExitThread(Core::System& system);
@ -146,7 +147,7 @@ Result SetMemoryAttribute64From32(Core::System& system, uint32_t address, uint32
Result MapMemory64From32(Core::System& system, uint32_t dst_address, uint32_t src_address, uint32_t size);
Result UnmapMemory64From32(Core::System& system, uint32_t dst_address, uint32_t src_address, uint32_t size);
Result QueryMemory64From32(Core::System& system, uint32_t out_memory_info, PageInfo* out_page_info, uint32_t address);
void ExitProcess64From32(Core::System& system);
void ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread64From32(Core::System& system, Handle* out_handle, uint32_t func, uint32_t arg, uint32_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread64From32(Core::System& system, Handle thread_handle);
void ExitThread64From32(Core::System& system);
@ -267,7 +268,7 @@ Result SetMemoryAttribute64(Core::System& system, uint64_t address, uint64_t siz
Result MapMemory64(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result UnmapMemory64(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result QueryMemory64(Core::System& system, uint64_t out_memory_info, PageInfo* out_page_info, uint64_t address);
void ExitProcess64(Core::System& system);
void ExitProcess64(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread64(Core::System& system, Handle* out_handle, uint64_t func, uint64_t arg, uint64_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread64(Core::System& system, Handle thread_handle);
void ExitThread64(Core::System& system);

221
src/core/hle/kernel/svc/svc_memory.cpp

@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/svc.h"
@ -22,6 +25,179 @@ constexpr bool IsValidSetMemoryPermission(MemoryPermission perm) {
}
}
bool IsHomebrewInPlaceNextLoadCodeRange(const KProcess& process, u64 address, u64 size) {
if (!process.IsHomebrewInPlaceNextLoad()) {
return false;
}
const u64 code_start = GetInteger(process.GetEntryPoint());
const size_t code_size = process.GetCodeSize();
if (code_start == 0 || code_size == 0 || size > code_size || address < code_start) {
return false;
}
return address - code_start <= code_size - size;
}
struct HomebrewInPlaceMemoryBlock {
u64 address;
u64 size;
KMemoryState state;
KMemoryPermission permission;
KMemoryAttribute attribute;
bool use_process_permission;
};
Result SetHomebrewInPlaceMemoryPermissionByBlocks(KProcess& process, u64 address, u64 size,
MemoryPermission perm, Result original_result) {
auto& page_table = process.GetPageTable();
const u64 end = address + size;
const auto requested_permission = ConvertToKMemoryPermission(perm);
std::vector<HomebrewInPlaceMemoryBlock> blocks;
for (u64 cursor = address; cursor < end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result = page_table.QueryInfo(std::addressof(info),
std::addressof(page_info), cursor);
if (query_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission query failed "
"address=0x{:016X}, result={:#X}, original={:#X}",
cursor, query_result.raw, original_result.raw);
R_RETURN(original_result);
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), end);
if (block_end <= cursor) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission walk stalled "
"cursor=0x{:016X}, block=0x{:016X}/0x{:X}, original={:#X}",
cursor, info.GetAddress(), info.GetSize(), original_result.raw);
R_RETURN(original_result);
}
const bool can_reprotect = True(info.GetState() & KMemoryState::FlagCanReprotect);
const bool can_process_reprotect = True(info.GetState() & KMemoryState::FlagCode);
if (!can_reprotect && !can_process_reprotect) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission unsupported block "
"address=0x{:016X}, size=0x{:X}, state=0x{:08X}, svc_state={}, "
"perm=0x{:08X}, attr=0x{:08X}, original={:#X}",
cursor, block_end - cursor, static_cast<u32>(info.GetState()),
static_cast<u32>(info.GetSvcState()),
static_cast<u32>(info.GetPermission()),
static_cast<u32>(info.GetAttribute()), original_result.raw);
R_RETURN(original_result);
}
blocks.push_back({
.address = cursor,
.size = block_end - cursor,
.state = info.GetState(),
.permission = info.GetPermission(),
.attribute = info.GetAttribute(),
.use_process_permission = !can_reprotect && can_process_reprotect,
});
cursor = block_end;
}
for (const auto& block : blocks) {
if (block.permission == requested_permission) {
continue;
}
const auto block_result =
block.use_process_permission
? page_table.SetProcessMemoryPermission(block.address, block.size, perm)
: page_table.SetMemoryPermission(block.address, block.size, perm);
if (block_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission block failed "
"address=0x{:016X}, size=0x{:X}, state=0x{:08X}, perm=0x{:08X}, "
"attr=0x{:08X}, result={:#X}, original={:#X}",
block.address, block.size, static_cast<u32>(block.state),
static_cast<u32>(block.permission), static_cast<u32>(block.attribute),
block_result.raw, original_result.raw);
R_RETURN(block_result);
}
}
R_SUCCEED();
}
struct HomebrewInPlaceDeviceSharedBlock {
u64 address;
u64 size;
u16 device_use_count;
};
Result UnlockHomebrewInPlaceDeviceSharedSource(KProcess& process, u64 address, u64 size,
Result original_result) {
auto& page_table = process.GetPageTable();
const u64 end = address + size;
std::vector<HomebrewInPlaceDeviceSharedBlock> blocks;
for (u64 cursor = address; cursor < end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result = page_table.QueryInfo(std::addressof(info),
std::addressof(page_info), cursor);
if (query_result.IsError()) {
R_RETURN(original_result);
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), end);
if (block_end <= cursor) {
R_RETURN(original_result);
}
const bool can_device_map = True(info.GetState() & KMemoryState::FlagCanDeviceMap);
const bool is_clean_memory =
can_device_map && info.GetPermission() == KMemoryPermission::UserReadWrite &&
info.GetAttribute() == KMemoryAttribute::None && info.m_device_use_count == 0;
const bool is_stale_device_shared =
can_device_map && info.GetPermission() == KMemoryPermission::UserReadWrite &&
info.GetAttribute() == KMemoryAttribute::DeviceShared && info.m_device_use_count > 0;
if (is_clean_memory) {
cursor = block_end;
continue;
}
if (!is_stale_device_shared) {
R_RETURN(original_result);
}
blocks.push_back({
.address = cursor,
.size = block_end - cursor,
.device_use_count = info.m_device_use_count,
});
cursor = block_end;
}
if (blocks.empty()) {
R_RETURN(original_result);
}
for (const auto& block : blocks) {
for (u16 unlock = 0; unlock < block.device_use_count; unlock++) {
const auto unlock_result =
page_table.UnlockForDeviceAddressSpace(block.address, block.size);
if (unlock_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: device-shared source unlock failed "
"address=0x{:016X}, size=0x{:X}, remaining={}, result={:#X}, "
"original={:#X}",
block.address, block.size, block.device_use_count - unlock - 1,
unlock_result.raw, original_result.raw);
R_RETURN(original_result);
}
}
}
R_SUCCEED();
}
// Checks if address + size is greater than the given address
// This can return false if the size causes an overflow of a 64-bit type
// or if the given size is zero.
@ -92,11 +268,19 @@ Result SetMemoryPermission(Core::System& system, u64 address, u64 size, MemoryPe
R_UNLESS(IsValidSetMemoryPermission(perm), ResultInvalidNewMemoryPermission);
// Validate that the region is in range for the current process.
auto& page_table = GetCurrentProcess(system.Kernel()).GetPageTable();
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
R_UNLESS(page_table.Contains(address, size), ResultInvalidCurrentMemory);
// Set the memory attribute.
R_RETURN(page_table.SetMemoryPermission(address, size, perm));
const auto result = page_table.SetMemoryPermission(address, size, perm);
if (result.raw == ResultInvalidCurrentMemory.raw &&
IsHomebrewInPlaceNextLoadCodeRange(process, address, size)) {
R_RETURN(
SetHomebrewInPlaceMemoryPermissionByBlocks(process, address, size, perm, result));
}
R_RETURN(result);
}
Result SetMemoryAttribute(Core::System& system, u64 address, u64 size, u32 mask, u32 attr) {
@ -135,14 +319,30 @@ Result MapMemory(Core::System& system, u64 dst_addr, u64 src_addr, u64 size) {
LOG_TRACE(Kernel_SVC, "called, dst_addr={:#x}, src_addr={:#x}, size={:#x}", dst_addr,
src_addr, size);
auto& page_table{GetCurrentProcess(system.Kernel()).GetPageTable()};
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
if (const Result result{MapUnmapMemorySanityChecks(page_table, dst_addr, src_addr, size)};
result.IsError()) {
return result;
}
R_RETURN(page_table.MapMemory(dst_addr, src_addr, size));
const auto result = page_table.MapMemory(dst_addr, src_addr, size);
if (result.raw == ResultInvalidCurrentMemory.raw && process.IsHomebrewInPlaceNextLoad()) {
if (UnlockHomebrewInPlaceDeviceSharedSource(process, src_addr, size, result).IsSuccess()) {
const auto retry_result = page_table.MapMemory(dst_addr, src_addr, size);
if (retry_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: svcMapMemory retry failed after "
"DeviceShared cleanup dst=0x{:016X}, src=0x{:016X}, size=0x{:X}, "
"result={:#X}",
dst_addr, src_addr, size, retry_result.raw);
}
R_RETURN(retry_result);
}
}
R_RETURN(result);
}
/// Unmaps a region that was previously mapped with svcMapMemory
@ -150,14 +350,23 @@ Result UnmapMemory(Core::System& system, u64 dst_addr, u64 src_addr, u64 size) {
LOG_TRACE(Kernel_SVC, "called, dst_addr={:#x}, src_addr={:#x}, size={:#x}", dst_addr,
src_addr, size);
auto& page_table{GetCurrentProcess(system.Kernel()).GetPageTable()};
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
if (const Result result{MapUnmapMemorySanityChecks(page_table, dst_addr, src_addr, size)};
result.IsError()) {
return result;
}
R_RETURN(page_table.UnmapMemory(dst_addr, src_addr, size));
const auto result = page_table.UnmapMemory(dst_addr, src_addr, size);
if (result.raw == ResultInvalidCurrentMemory.raw && process.IsHomebrewInPlaceNextLoad()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: svcUnmapMemory failed dst=0x{:016X}, "
"src=0x{:016X}, size=0x{:X}, result={:#X}",
dst_addr, src_addr, size, result.raw);
}
R_RETURN(result);
}
Result SetMemoryPermission64(Core::System& system, uint64_t address, uint64_t size,

209
src/core/hle/kernel/svc/svc_process.cpp

@ -4,17 +4,216 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/input.h"
#include "core/core.h"
#include "core/file_sys/vfs/vfs_types.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/physical_core.h"
#include "core/hle/kernel/svc.h"
#include "core/hle/service/hid/hid_server.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
#include "core/hle/service/sm/sm.h"
#include "core/loader/nro.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/resource_manager.h"
namespace Kernel::Svc {
namespace {
constexpr size_t HomebrewNextLoadPathSize = 0x200;
constexpr size_t HomebrewNextLoadArgvSize = 0x800;
std::string ReadHomebrewString(Core::Memory::Memory& memory, KProcessAddress address,
size_t max_size) {
if (GetInteger(address) == 0) {
return {};
}
return memory.ReadCString(Common::ProcessAddress{GetInteger(address)}, max_size);
}
} // namespace
/// Exits the current process
void ExitProcess(Core::System& system) {
void ExitProcess(Core::System& system, std::span<uint64_t, 8> args) {
auto* current_process = GetCurrentProcessPointer(system.Kernel());
auto* current_thread = GetCurrentThreadPointer(system.Kernel());
LOG_INFO(Kernel_SVC, "Process {} exiting", current_process->GetProcessId());
const auto next_load_path_addr = current_process->GetHomebrewNextLoadPathAddr();
const auto next_load_argv_addr = current_process->GetHomebrewNextLoadArgvAddr();
if (GetInteger(next_load_path_addr) != 0) {
auto& memory = current_process->GetMemory();
const auto next_load_path =
ReadHomebrewString(memory, next_load_path_addr, HomebrewNextLoadPathSize);
const auto next_load_argv =
ReadHomebrewString(memory, next_load_argv_addr, HomebrewNextLoadArgvSize);
if (!next_load_path.empty()) {
auto guest_path = Common::FS::SanitizePath(next_load_path);
constexpr std::string_view SdmcPrefix{"sdmc:"};
FileSys::VirtualFile file{};
const bool is_sdmc_path = guest_path.rfind(SdmcPrefix, 0) == 0;
const bool is_absolute_guest_path = !guest_path.empty() && guest_path.front() == '/';
if (is_sdmc_path || is_absolute_guest_path) {
auto relative_path =
is_sdmc_path ? guest_path.substr(SdmcPrefix.size()) : guest_path;
while (!relative_path.empty() && relative_path.front() == '/') {
relative_path.erase(relative_path.begin());
}
const auto host_path = Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) /
std::filesystem::path{Common::FS::ToU8String(relative_path)};
const auto host_path_string = Common::FS::PathToUTF8String(host_path);
file = Core::GetGameFileFromPath(system.GetFilesystem(), host_path_string);
if (!file) {
LOG_WARNING(Kernel_SVC,
"NextLoad: failed to open guest_path='{}', host_path='{}'",
next_load_path, host_path_string);
}
} else {
file = Core::GetGameFileFromPath(system.GetFilesystem(), guest_path);
if (!file) {
LOG_WARNING(Kernel_SVC, "NextLoad: failed to open guest_path='{}'",
next_load_path);
}
}
if (file) {
const auto nvdrv =
system.ServiceManager().GetService<Service::Nvidia::NVDRV>("nvdrv:s");
if (!nvdrv) {
LOG_WARNING(Kernel_SVC, "NextLoad: NVDRV service unavailable for reset");
} else {
nvdrv->GetModule()->ResetForProcess(current_process);
}
auto& page_table = current_process->GetPageTable();
const u64 heap_start = GetInteger(page_table.GetHeapRegionStart());
const u64 heap_size = page_table.GetHeapRegionSize();
const u64 heap_end = heap_start + heap_size;
if (heap_start != 0 && heap_size != 0 && heap_end > heap_start) {
struct DeviceSharedBlock {
u64 address;
u64 size;
u16 device_use_count;
};
std::vector<DeviceSharedBlock> blocks;
for (u64 cursor = heap_start; cursor < heap_end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result =
page_table.QueryInfo(std::addressof(info), std::addressof(page_info),
cursor);
if (query_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup query failed "
"address=0x{:016X}, result={:#X}",
cursor, query_result.raw);
break;
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), heap_end);
if (block_end <= cursor) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup walk stalled "
"cursor=0x{:016X}, block=0x{:016X}/0x{:X}",
cursor, info.GetAddress(), info.GetSize());
break;
}
const bool is_device_shared =
True(info.GetState() & KMemoryState::FlagCanDeviceMap) &&
info.GetAttribute() == KMemoryAttribute::DeviceShared &&
info.m_device_use_count > 0;
if (is_device_shared) {
blocks.push_back(DeviceSharedBlock{
.address = cursor,
.size = block_end - cursor,
.device_use_count = info.m_device_use_count,
});
}
cursor = block_end;
}
for (const auto& block : blocks) {
for (u16 unlock = 0; unlock < block.device_use_count; unlock++) {
const auto unlock_result =
page_table.UnlockForDeviceAddressSpace(block.address, block.size);
if (unlock_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup unlock failed "
"address=0x{:016X}, size=0x{:X}, remaining={}, "
"result={:#X}",
block.address, block.size,
block.device_use_count - unlock - 1, unlock_result.raw);
break;
}
}
}
}
if (Loader::LoadNroInPlace(system, *current_process, *current_thread, file,
next_load_path, next_load_argv)) {
const auto aruid = current_process->GetProcessId();
if (const auto hid =
system.ServiceManager().GetService<Service::HID::IHidServer>("hid")) {
const auto resource_manager = hid->GetResourceManager();
resource_manager->UnregisterAppletResourceUserId(aruid);
const auto register_result =
resource_manager->RegisterAppletResourceUserId(aruid, true);
if (register_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: failed to register HID applet resource "
"aruid={}, result={:#X}",
aruid, register_result.raw);
}
} else {
LOG_WARNING(Kernel_SVC, "NextLoad: HID service unavailable for reset");
}
auto& hid_core = system.HIDCore();
hid_core.DisableAllControllerConfiguration();
hid_core.SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
hid_core.ReloadInputDevices();
const auto activate_controller = [&](Core::HID::NpadIdType npad_id) {
auto* controller = hid_core.GetEmulatedController(npad_id);
if (controller == nullptr) {
return;
}
(void)controller->SetPollingMode(Core::HID::EmulatedDeviceIndex::AllDevices,
Common::Input::PollingMode::Active);
};
activate_controller(Core::HID::NpadIdType::Player1);
activate_controller(Core::HID::NpadIdType::Handheld);
system.Kernel().CurrentPhysicalCore().LoadContext(current_thread);
const auto& context = current_thread->GetContext();
for (size_t i = 0; i < args.size(); i++) {
args[i] = context.r[i];
}
return;
}
}
}
}
ASSERT_MSG(current_process->GetState() == KProcess::State::Running,
"Process has already exited");
@ -132,8 +331,8 @@ Result TerminateProcess(Core::System& system, Handle process_handle) {
R_THROW(ResultNotImplemented);
}
void ExitProcess64(Core::System& system) {
ExitProcess(system);
void ExitProcess64(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess(system, args);
}
Result GetProcessId64(Core::System& system, uint64_t* out_process_id, Handle process_handle) {
@ -164,8 +363,8 @@ Result GetProcessInfo64(Core::System& system, int64_t* out_info, Handle process_
R_RETURN(GetProcessInfo(system, out_info, process_handle, info_type));
}
void ExitProcess64From32(Core::System& system) {
ExitProcess(system);
void ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess(system, args);
}
Result GetProcessId64From32(Core::System& system, uint64_t* out_process_id, Handle process_handle) {

529
src/core/loader/nro.cpp

@ -4,17 +4,20 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "common/alignment.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/settings.h"
#include "common/random.h"
@ -23,6 +26,8 @@
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/romfs_factory.h"
#include "core/file_sys/vfs/vfs_offset.h"
#include "core/hardware_properties.h"
#include "core/hle/api_version.h"
#include "core/hle/kernel/code_set.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
@ -152,8 +157,417 @@ static constexpr u32 PageAlignSize(u32 size) {
return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
}
static std::string MakeHomebrewSdmcPath(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
while (!path.empty() && path.front() == '/') {
path.erase(path.begin());
}
return "sdmc:/" + path;
}
static std::optional<std::string> TryMakeHomebrewSdmcPathFromHostPath(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
std::string sdmc_root =
Common::FS::PathToUTF8String(Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir));
std::replace(sdmc_root.begin(), sdmc_root.end(), '\\', '/');
while (!sdmc_root.empty() && sdmc_root.back() == '/') {
sdmc_root.pop_back();
}
if (!sdmc_root.empty() && path.size() > sdmc_root.size() &&
path.compare(0, sdmc_root.size(), sdmc_root) == 0 && path[sdmc_root.size()] == '/') {
return MakeHomebrewSdmcPath(path.substr(sdmc_root.size() + 1));
}
constexpr std::string_view SdmcMarker{"/sdmc/"};
if (const auto pos = path.rfind(SdmcMarker); pos != std::string::npos) {
return MakeHomebrewSdmcPath(path.substr(pos + SdmcMarker.size()));
}
return std::nullopt;
}
static std::string MakeHomebrewArgv0(std::string nro_path, std::string file_name) {
if (nro_path.empty()) {
nro_path = std::move(file_name);
}
std::replace(nro_path.begin(), nro_path.end(), '\\', '/');
if (nro_path.rfind("sdmc:/", 0) == 0) {
return nro_path;
}
if (auto sdmc_path = TryMakeHomebrewSdmcPathFromHostPath(nro_path)) {
return *sdmc_path;
}
#ifdef __ANDROID__
if (nro_path.find('%') != std::string::npos) {
const auto percent_decode = [](std::string value) {
const auto hex_value = [](char c) -> int {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
};
std::string decoded;
decoded.reserve(value.size());
for (size_t i = 0; i < value.size(); i++) {
if (value[i] == '%' && i + 2 < value.size()) {
const int high = hex_value(value[i + 1]);
const int low = hex_value(value[i + 2]);
if (high >= 0 && low >= 0) {
decoded.push_back(static_cast<char>((high << 4) | low));
i += 2;
continue;
}
}
decoded.push_back(value[i]);
}
return decoded;
};
if (auto sdmc_path = TryMakeHomebrewSdmcPathFromHostPath(percent_decode(nro_path))) {
return *sdmc_path;
}
}
#endif
if (!nro_path.empty() && nro_path.front() == '/') {
return "sdmc:" + nro_path;
}
return nro_path.empty() ? "homebrew" : nro_path;
}
static std::string QuoteHomebrewArgvComponent(const std::string& argument) {
if (argument.find_first_of(" \t\r\n") == std::string::npos) {
return argument;
}
std::string quoted{"\""};
quoted += argument;
quoted.push_back('"');
return quoted;
}
static std::string GetHomebrewInitialCwd(const std::string& argv0) {
constexpr std::string_view SdmcPrefix = "sdmc:";
if (argv0.substr(0, SdmcPrefix.size()) != SdmcPrefix) {
return {};
}
const auto last_slash = argv0.find_last_of('/');
if (last_slash == std::string_view::npos || last_slash < SdmcPrefix.size()) {
return {};
}
std::string cwd = argv0.substr(SdmcPrefix.size(), last_slash - SdmcPrefix.size());
if (cwd.empty()) {
cwd = "/";
}
while (cwd.size() > 1 && cwd.back() == '/') {
cwd.pop_back();
}
return cwd;
}
constexpr size_t HomebrewNextLoadPathSize = 0x200;
constexpr size_t HomebrewNextLoadArgvSize = 0x800;
constexpr u32 HomebrewSvcExitProcessInstruction = 0xD40000E1;
constexpr u32 HomebrewEntryEndOfList = 0;
constexpr u32 HomebrewEntryMainThreadHandle = 1;
constexpr u32 HomebrewEntryNextLoadPath = 2;
constexpr u32 HomebrewEntryOverrideHeap = 3;
constexpr u32 HomebrewEntryArgv = 5;
constexpr u32 HomebrewEntrySyscallAvailableHint = 6;
constexpr u32 HomebrewEntryAppletType = 7;
constexpr u32 HomebrewEntryProcessHandle = 10;
constexpr u32 HomebrewEntryRandomSeed = 14;
constexpr u32 HomebrewEntryHosVersion = 16;
constexpr u32 HomebrewEntrySyscallAvailableHint2 = 17;
constexpr u32 HomebrewAppletTypeApplication = 0;
constexpr u64 HomebrewAllSvcHints = ~u64{0};
constexpr u32 HomebrewHosVersion = (u32{HLE::ApiVersion::HOS_VERSION_MAJOR} << 16) |
(u32{HLE::ApiVersion::HOS_VERSION_MINOR} << 8) |
u32{HLE::ApiVersion::HOS_VERSION_MICRO};
struct HomebrewConfigEntry {
u32_le key;
u32_le flags;
u64_le value[2];
};
static_assert(sizeof(HomebrewConfigEntry) == 0x18);
constexpr size_t HomebrewBaseConfigEntryCount = 10;
constexpr size_t HomebrewInPlaceConfigEntryCount = 11;
constexpr size_t HomebrewBaseConfigTableSize =
HomebrewBaseConfigEntryCount * sizeof(HomebrewConfigEntry);
constexpr size_t HomebrewInPlaceConfigTableSize =
HomebrewInPlaceConfigEntryCount * sizeof(HomebrewConfigEntry);
struct HomebrewNroImage {
Kernel::CodeSet codeset;
size_t image_size{};
size_t args_offset{};
std::optional<size_t> exit_process_offset;
std::string argv_string;
};
static void SetHomebrewConfigPointers(Kernel::KProcess& process, u64 config_addr,
u64 next_load_path_addr, u64 next_load_argv_addr) {
constexpr size_t MainThreadHandleEntryIndex = 0;
constexpr size_t ProcessHandleEntryIndex = 1;
constexpr size_t EntryValueOffset = offsetof(HomebrewConfigEntry, value);
process.SetArgPointer(Kernel::KProcessAddress{config_addr});
process.SetMainThreadHandleAddr(Kernel::KProcessAddress{
config_addr + MainThreadHandleEntryIndex * sizeof(HomebrewConfigEntry) + EntryValueOffset});
process.SetProcessHandleAddr(Kernel::KProcessAddress{
config_addr + ProcessHandleEntryIndex * sizeof(HomebrewConfigEntry) + EntryValueOffset});
process.SetHomebrewNextLoadBufferAddrs(Kernel::KProcessAddress{next_load_path_addr},
Kernel::KProcessAddress{next_load_argv_addr});
}
static std::optional<HomebrewNroImage> BuildHomebrewNroImage(const std::vector<u8>& data,
std::string nro_path,
std::string file_name,
std::string launch_argv) {
if (data.size() < sizeof(NroHeader)) {
return std::nullopt;
}
NroHeader nro_header{};
std::memcpy(&nro_header, data.data(), sizeof(NroHeader));
if (nro_header.magic != Common::MakeMagic('N', 'R', 'O', '0')) {
return std::nullopt;
}
if (data.size() < nro_header.file_size ||
nro_header.module_header_offset + sizeof(ModHeader) > PageAlignSize(nro_header.file_size)) {
return std::nullopt;
}
std::vector<u8> program_image(PageAlignSize(nro_header.file_size));
std::memcpy(program_image.data(), data.data(), nro_header.file_size);
Kernel::CodeSet codeset;
for (std::size_t i = 0; i < nro_header.segments.size(); ++i) {
codeset.segments[i].addr = nro_header.segments[i].offset;
codeset.segments[i].offset = nro_header.segments[i].offset;
codeset.segments[i].size = PageAlignSize(nro_header.segments[i].size);
}
u32 bss_size{PageAlignSize(nro_header.bss_size)};
ModHeader mod_header{};
std::memcpy(&mod_header, program_image.data() + nro_header.module_header_offset,
sizeof(ModHeader));
if (mod_header.magic == Common::MakeMagic('M', 'O', 'D', '0')) {
bss_size = PageAlignSize(mod_header.bss_end_offset - mod_header.bss_start_offset);
}
codeset.DataSegment().size += bss_size;
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
HomebrewNroImage image{.codeset = std::move(codeset)};
const auto argv0 = MakeHomebrewArgv0(std::move(nro_path), std::move(file_name));
if (!launch_argv.empty()) {
image.argv_string = std::move(launch_argv);
} else {
image.argv_string = QuoteHomebrewArgvComponent(argv0);
}
if (image.argv_string.empty() || image.argv_string.back() != '\0') {
image.argv_string.push_back('\0');
}
const auto& code = image.codeset.CodeSegment();
const size_t code_end = (std::min)(program_image.size(), code.offset + code.size);
for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == HomebrewSvcExitProcessInstruction) {
image.exit_process_offset = offset;
break;
}
}
const size_t entries_and_buffers =
Common::AlignUp(HomebrewInPlaceConfigTableSize + HomebrewNextLoadPathSize +
HomebrewNextLoadArgvSize + image.argv_string.size(),
Core::Memory::YUZU_PAGESIZE);
image.args_offset = program_image.size();
image.codeset.DataSegment().size += static_cast<u32>(entries_and_buffers);
program_image.resize(image.args_offset + entries_and_buffers);
image.image_size = program_image.size();
image.codeset.memory = std::move(program_image);
return image;
}
bool LoadNroInPlace(Core::System& system, Kernel::KProcess& process, Kernel::KThread& thread,
const FileSys::VirtualFile& nro_file, const std::string& nro_path,
const std::string& launch_argv) {
if (!nro_file) {
return false;
}
#ifdef HAS_NCE
if (Settings::IsNceEnabled()) {
LOG_WARNING(Loader,
"Homebrew next-load: in-place handoff unavailable because NCE is enabled");
return false;
}
#endif
size_t live_threads = 0;
for (auto& candidate : process.GetThreadList()) {
if (candidate.GetState() != Kernel::ThreadState::Terminated) {
live_threads++;
}
}
if (live_threads != 1) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because live_threads={}",
live_threads);
return false;
}
const auto stack_top = process.GetMainThreadStackTop();
if (GetInteger(stack_top) == 0) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because stack_top=0");
return false;
}
auto image =
BuildHomebrewNroImage(nro_file->ReadAllBytes(), nro_path, nro_file->GetName(), launch_argv);
if (!image) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because '{}' is invalid",
nro_path);
return false;
}
const auto capacity = process.GetCodeSize();
if (image->image_size > capacity) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because image_size=0x{:X} exceeds "
"capacity=0x{:X}",
image->image_size, capacity);
return false;
}
const u64 base = GetInteger(process.GetEntryPoint());
const u64 heap_addr = GetInteger(process.GetPageTable().GetHeapRegionStart());
const size_t heap_size = process.GetPageTable().GetBasePageTable().GetCurrentHeapSize();
if (heap_addr == 0 || heap_size == 0) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because heap override is "
"unavailable (addr=0x{:016X}, size=0x{:X})",
heap_addr, heap_size);
return false;
}
const u64 config_addr = base + image->args_offset;
const u64 next_load_path_addr = config_addr + HomebrewInPlaceConfigTableSize;
const u64 next_load_argv_addr = next_load_path_addr + HomebrewNextLoadPathSize;
const u64 argv_addr = next_load_argv_addr + HomebrewNextLoadArgvSize;
const u64 argv_entry_addr = image->argv_string.empty() ? 0 : argv_addr;
const std::string argv0 = MakeHomebrewArgv0(nro_path, nro_file->GetName());
const std::string homebrew_initial_cwd = GetHomebrewInitialCwd(argv0);
u64 program_id{};
AppLoader_NRO loader{nro_file};
if (loader.ReadProgramId(program_id) != Loader::ResultStatus::Success) {
LOG_WARNING(Loader, "NextLoad: in-place handoff could not read NRO program id");
}
Kernel::Handle main_thread_handle{};
if (process.GetHandleTable().Add(system.Kernel(), std::addressof(main_thread_handle), &thread)
.IsError()) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because main thread handle failed");
return false;
}
Kernel::Handle process_handle{};
if (process.GetHandleTable().Add(system.Kernel(), std::addressof(process_handle), &process)
.IsError()) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because process handle failed");
return false;
}
if (!process.GetMemory().ZeroBlock(Common::ProcessAddress{heap_addr}, heap_size)) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because heap clear failed "
"(addr=0x{:016X}, size=0x{:X})",
heap_addr, heap_size);
return false;
}
process.LoadModule(system.Kernel(), std::move(image->codeset), process.GetEntryPoint());
const HomebrewConfigEntry entries[HomebrewInPlaceConfigEntryCount] = {
{HomebrewEntryMainThreadHandle, 0, {main_thread_handle, 0}},
{HomebrewEntryProcessHandle, 0, {process_handle, 0}},
{HomebrewEntryNextLoadPath, 0, {next_load_path_addr, next_load_argv_addr}},
{HomebrewEntryOverrideHeap, 0, {heap_addr, heap_size}},
{HomebrewEntryAppletType, 0, {HomebrewAppletTypeApplication, 0}},
{HomebrewEntryArgv, 0, {0, argv_entry_addr}},
{HomebrewEntrySyscallAvailableHint, 0, {HomebrewAllSvcHints, HomebrewAllSvcHints}},
{HomebrewEntryRandomSeed, 0,
{process.GetRandomEntropy(0), process.GetRandomEntropy(1)}},
{HomebrewEntryHosVersion, 0, {HomebrewHosVersion, 0}},
{HomebrewEntrySyscallAvailableHint2, 0, {HomebrewAllSvcHints, 0}},
{HomebrewEntryEndOfList, 0, {0, 0}},
};
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries, sizeof(entries));
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, image->argv_string.data(),
image->argv_string.size());
process.GetMemory().Write32(Common::ProcessAddress{GetInteger(thread.GetTlsAddress()) + 0x110},
main_thread_handle);
process.SetArgReturnAddress(Kernel::KProcessAddress{
image->exit_process_offset ? base + *image->exit_process_offset : 0});
SetHomebrewConfigPointers(process, config_addr, next_load_path_addr, next_load_argv_addr);
system.GetFileSystemController().RegisterProcess(
process.GetProcessId(), program_id,
std::make_unique<FileSys::RomFSFactory>(loader, system.GetContentProvider(),
system.GetFileSystemController()),
homebrew_initial_cwd);
process.SetHomebrewInPlaceNextLoad(true);
auto& context = thread.GetContext();
context = {};
context.r[0] = config_addr;
context.r[1] = UINT64_MAX;
context.r[18] = Common::Random::Random64(0) | 1;
context.lr = image->exit_process_offset ? base + *image->exit_process_offset : 0;
context.pc = base;
context.sp = GetInteger(stack_top);
context.fpcr = 0;
context.fpsr = 0;
for (std::size_t core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) {
if (auto* arm = process.GetArmInterface(core); arm != nullptr) {
arm->ClearInstructionCache();
}
}
return true;
}
static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
const std::vector<u8>& data) {
const std::vector<u8>& data, std::string nro_path,
std::string file_name) {
if (data.size() < sizeof(NroHeader)) {
return {};
}
@ -195,47 +609,44 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
codeset.DataSegment().size += bss_size;
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
struct ConfigEntry {
u32_le key;
u32_le flags;
u64_le value[2];
};
static_assert(sizeof(ConfigEntry) == 0x18);
// AArch64 encoding for svc #0x7 (ExitProcess).
constexpr u32 kSvcExitProcessInstruction = 0xD40000E1;
constexpr size_t kNumEntries = 4; // MainThreadHandle, AppletType, Argv, EndOfList
constexpr size_t kConfigTableSize = kNumEntries * sizeof(ConfigEntry);
std::string argv_string;
size_t args_offset_in_image = 0;
std::optional<size_t> exit_process_offset_in_image;
const auto& program_args = Settings::values.program_args.GetValue();
const std::string argv0 = MakeHomebrewArgv0(std::move(nro_path), std::move(file_name));
argv_string = QuoteHomebrewArgvComponent(argv0);
if (!program_args.empty()) {
argv_string = "homebrew ";
argv_string.push_back(' ');
argv_string += program_args;
}
if (argv_string.empty() || argv_string.back() != '\0') {
argv_string.push_back('\0');
}
const auto& code = codeset.CodeSegment();
const size_t code_end = (std::min)(program_image.size(), code.offset + code.size);
for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == kSvcExitProcessInstruction) {
exit_process_offset_in_image = offset;
break;
}
}
if (!exit_process_offset_in_image) {
LOG_WARNING(Loader,
"Unable to find svcExitProcess in NRO; returning from main may fault");
const auto& code_segment = codeset.CodeSegment();
const size_t code_end =
(std::min)(program_image.size(), code_segment.offset + code_segment.size);
for (size_t offset = code_segment.offset; offset + sizeof(u32) <= code_end;
offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == HomebrewSvcExitProcessInstruction) {
exit_process_offset_in_image = offset;
break;
}
}
if (!exit_process_offset_in_image) {
LOG_WARNING(Loader, "Unable to find svcExitProcess in NRO; returning from main may fault");
}
const size_t entries_and_argv =
Common::AlignUp(kConfigTableSize + argv_string.size(), Core::Memory::YUZU_PAGESIZE);
const size_t entries_and_buffers =
Common::AlignUp(HomebrewBaseConfigTableSize + HomebrewNextLoadPathSize +
HomebrewNextLoadArgvSize + argv_string.size(),
Core::Memory::YUZU_PAGESIZE);
args_offset_in_image = program_image.size();
codeset.DataSegment().size += static_cast<u32>(entries_and_argv);
program_image.resize(args_offset_in_image + entries_and_argv);
}
args_offset_in_image = program_image.size();
codeset.DataSegment().size += static_cast<u32>(entries_and_buffers);
program_image.resize(args_offset_in_image + entries_and_buffers);
size_t image_size = program_image.size();
#ifdef HAS_NCE
@ -263,6 +674,9 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
image_size += patch_segment.size;
}
#endif
// In-place NextLoad reuses the original code mapping; leave room for larger homebrew cores.
constexpr size_t HomebrewCodeArenaSize = 192 * 1024 * 1024;
image_size = (std::max)(image_size, HomebrewCodeArenaSize);
// Enable direct memory mapping in case of NCE.
const u64 fastmem_base = [&]() -> size_t {
@ -297,31 +711,38 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
// Load codeset for current process
codeset.memory = std::move(program_image);
process.LoadModule(system.Kernel(), std::move(codeset), process.GetEntryPoint());
if (!argv_string.empty()) {
constexpr u32 kEntryEndOfList = 0;
constexpr u32 kEntryMainThreadHandle = 1;
constexpr u32 kEntryArgv = 5;
constexpr u32 kEntryAppletType = 7;
constexpr u32 kAppletTypeApplication = 0;
process.SetHomebrewInPlaceNextLoad(false);
{
const u64 base = GetInteger(process.GetEntryPoint());
const u64 config_addr = base + args_offset_in_image;
const u64 argv_addr = config_addr + kConfigTableSize;
const ConfigEntry entries[kNumEntries] = {
{kEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run()
{kEntryAppletType, 0, {kAppletTypeApplication, 0}},
{kEntryArgv, 0, {0, argv_addr}},
{kEntryEndOfList, 0, {0, 0}},
const u64 next_load_path_addr = config_addr + HomebrewBaseConfigTableSize;
const u64 next_load_argv_addr = next_load_path_addr + HomebrewNextLoadPathSize;
const u64 argv_addr = next_load_argv_addr + HomebrewNextLoadArgvSize;
const u64 argv_entry_addr = argv_string.empty() ? 0 : argv_addr;
const HomebrewConfigEntry entries[HomebrewBaseConfigEntryCount] = {
{HomebrewEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run()
{HomebrewEntryProcessHandle, 0, {0, 0}}, // Value[0] patched in Run()
{HomebrewEntryNextLoadPath, 0, {next_load_path_addr, next_load_argv_addr}},
{HomebrewEntryAppletType, 0, {HomebrewAppletTypeApplication, 0}},
{HomebrewEntryArgv, 0, {0, argv_entry_addr}},
{HomebrewEntrySyscallAvailableHint, 0, {HomebrewAllSvcHints, HomebrewAllSvcHints}},
{HomebrewEntryRandomSeed, 0,
{process.GetRandomEntropy(0), process.GetRandomEntropy(1)}},
{HomebrewEntryHosVersion, 0, {HomebrewHosVersion, 0}},
{HomebrewEntrySyscallAvailableHint2, 0, {HomebrewAllSvcHints, 0}},
{HomebrewEntryEndOfList, 0, {0, 0}},
};
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries, sizeof(entries));
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, argv_string.data(), argv_string.size());
constexpr size_t kMainThreadHandleValueOffset = offsetof(ConfigEntry, value);
process.SetArgPointer(Kernel::KProcessAddress{config_addr});
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries,
sizeof(entries));
if (!argv_string.empty()) {
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, argv_string.data(),
argv_string.size());
}
if (exit_process_offset_in_image) {
process.SetArgReturnAddress(Kernel::KProcessAddress{base + *exit_process_offset_in_image});
}
process.SetMainThreadHandleAddr(Kernel::KProcessAddress{config_addr + kMainThreadHandleValueOffset});
SetHomebrewConfigPointers(process, config_addr, next_load_path_addr, next_load_argv_addr);
}
return true;
@ -329,7 +750,8 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
bool AppLoader_NRO::LoadNro(Core::System& system, Kernel::KProcess& process,
const FileSys::VfsFile& nro_file) {
return LoadNroImpl(system, process, nro_file.ReadAllBytes());
return LoadNroImpl(system, process, nro_file.ReadAllBytes(), nro_file.GetFullPath(),
nro_file.GetName());
}
AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::System& system) {
@ -343,10 +765,13 @@ AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::S
u64 program_id{};
ReadProgramId(program_id);
const std::string argv0 = MakeHomebrewArgv0(file->GetFullPath(), file->GetName());
const std::string homebrew_initial_cwd = GetHomebrewInitialCwd(argv0);
system.GetFileSystemController().RegisterProcess(
process.GetProcessId(), program_id,
std::make_unique<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
system.GetFileSystemController()));
system.GetFileSystemController()),
homebrew_initial_cwd);
is_loaded = true;
return {ResultStatus::Success, LoadParameters{Kernel::KThread::DefaultThreadPriority,

8
src/core/loader/nro.h

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -19,10 +22,15 @@ class NACP;
namespace Kernel {
class KProcess;
class KThread;
}
namespace Loader {
[[nodiscard]] bool LoadNroInPlace(Core::System& system, Kernel::KProcess& process,
Kernel::KThread& thread, const FileSys::VirtualFile& nro_file,
const std::string& nro_path, const std::string& launch_argv);
/// Loads an NRO file
class AppLoader_NRO final : public AppLoader {
public:

Loading…
Cancel
Save