APT/kernel: fix HOME button crash, region detection, GetSdmcCtrRootPath

svc.cpp / kernel:
- Add NullWaitObject: handle=0 in svcWaitSynchronizationN is treated as a
  permanently-unavailable slot (never fires) instead of returning
  ResultInvalidHandle.  Official Nintendo APT library builds pass aptSleepSync=0
  when sleeping for HOME Menu; real hardware silently ignores null slots and lets
  the other handles (APT:Parameter) drive the wakeup.
- Process now inherits WaitObject so svcWaitSynchronizationN on process handles
  works correctly.  LLE HOME Menu waits on a process handle during startup;
  previously Azahar returned ResultInvalidHandle because Process only extended
  Object, not WaitObject.  Process becomes available (ShouldWait=false) when it
  exits; TerminateProcess calls WakeupAllWaitingThreads.

applet_manager.cpp / apt.cpp:
- RequestForSysApplet auto-Response now attaches a pre-signaled event so the
  application can svcWaitSynchronizationN on the received handle without hanging.
- Wakeup parameters with no object now get a pre-signaled event (same fix).
- RegisterApplet initial Wakeup now sends a pre-signaled event.
- LeaveHomeMenu: supply a pre-signaled event when HOME Menu passes handle=0, so
  the application does not crash when it calls svcSignalEvent on the result.
- Removed the old logic that force-nulled objects on system→application sends
  (this was masking the real issue and breaking Wakeup handle delivery).

cfg.cpp:
- GetRegion defaults from_secure_info=true so CFGU_SecureInfoGetRegion reads
  SecureInfo_A (contains the hardware region) instead of falling back to
  auto-detect which returned JPN(0) for 3DSX homebrew with no preferred regions.

fs_user.cpp/h:
- Implement GetSdmcCtrRootPath (0x0848): returns /Nintendo 3DS/<ID0>/<ID1> as a
  UTF-16LE wide string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Masamune3210 2026-05-14 06:33:48 -05:00
parent ba9ebb58fe
commit b2819a7bbc
9 changed files with 177 additions and 61 deletions

View file

@ -25,11 +25,11 @@ bool Object::IsWaitable() const {
case HandleType::Timer:
case HandleType::ServerPort:
case HandleType::ServerSession:
case HandleType::Process:
return true;
case HandleType::Unknown:
case HandleType::SharedMemory:
case HandleType::Process:
case HandleType::AddressArbiter:
case HandleType::ResourceLimit:
case HandleType::CodeSet:

View file

@ -47,7 +47,7 @@ SERIALIZE_IMPL(AddressMapping)
template <class Archive>
void Process::serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<Object>(*this);
ar& boost::serialization::base_object<WaitObject>(*this);
ar & handle_table;
ar & codeset; // TODO: Replace with apploader reference
ar & resource_limit;
@ -122,6 +122,9 @@ void KernelSystem::TerminateProcess(std::shared_ptr<Process> process) {
ASSERT_MSG(process->status == ProcessStatus::Running, "Process has already exited");
process->status = ProcessStatus::Exited;
// Wake up any threads waiting for this process to exit.
process->WakeupAllWaitingThreads();
// Stop all process threads.
for (u32 core = 0; core < Core::GetNumCores(); core++) {
GetThreadManager(core).TerminateProcessThreads(process);
@ -277,6 +280,14 @@ void Process::Run(s32 main_thread_priority, u32 stack_size) {
}
}
bool Process::ShouldWait(const Thread* thread) const {
return status != ProcessStatus::Exited;
}
void Process::Acquire(Thread* thread) {
ASSERT_MSG(!ShouldWait(thread), "object unavailable!");
}
void Process::Exit() {
#ifdef ENABLE_GDBSTUB
GDBStub::OnProcessExit(process_id);
@ -684,7 +695,7 @@ void Process::FreeAllMemory() {
}
Kernel::Process::Process(KernelSystem& kernel)
: Object(kernel), handle_table(kernel), vm_manager(kernel.memory, *this), kernel(kernel) {
: WaitObject(kernel), handle_table(kernel), vm_manager(kernel.memory, *this), kernel(kernel) {
kernel.memory.RegisterPageTable(vm_manager.page_table);
}
Kernel::Process::~Process() {

View file

@ -17,6 +17,7 @@
#include "core/hle/kernel/handle_table.h"
#include "core/hle/kernel/object.h"
#include "core/hle/kernel/vm_manager.h"
#include "core/hle/kernel/wait_object.h"
namespace Kernel {
@ -124,7 +125,7 @@ private:
void serialize(Archive& ar, const unsigned int);
};
class Process final : public Object {
class Process final : public WaitObject {
public:
explicit Process(Kernel::KernelSystem& kernel);
~Process() override;
@ -141,6 +142,10 @@ public:
return HANDLE_TYPE;
}
/// WaitObject interface: a process can be waited on; it becomes available when it exits.
bool ShouldWait(const Thread* thread) const override;
void Acquire(Thread* thread) override;
HandleTable handle_table;
std::shared_ptr<CodeSet> codeset;

View file

@ -46,6 +46,33 @@
namespace Kernel {
/// A WaitObject that is permanently unavailable (ShouldWait always returns true).
/// Used as a placeholder for null handles (handle == 0) in svcWaitSynchronizationN so that
/// null-handle slots are silently ignored instead of causing a ResultInvalidHandle crash.
/// The real 3DS kernel appears to allow handle 0 in WaitSynchronizationN arrays; it effectively
/// acts as a slot that never fires, letting the other handles drive the wakeup logic.
class NullWaitObject final : public WaitObject {
public:
explicit NullWaitObject(KernelSystem& kernel) : WaitObject(kernel) {}
std::string GetTypeName() const override {
return "NullWaitObject";
}
std::string GetName() const override {
return "NullWaitObject (null-handle placeholder)";
}
HandleType GetHandleType() const override {
return HandleType::Unknown;
}
/// Never becomes available — null-handle slot never wins a WaitSynchronizationN race.
bool ShouldWait(const Thread*) const override {
return true;
}
/// No-op: null-handle slot has nothing to acquire.
void Acquire(Thread*) override {}
};
enum ControlMemoryOperation {
MEMOP_FREE = 1,
MEMOP_RESERVE = 2, // This operation seems to be unsupported in the kernel
@ -849,7 +876,18 @@ Result SVC::WaitSynchronizationN(s32* out, VAddr handles_address, s32 handle_cou
for (int i = 0; i < handle_count; ++i) {
Handle handle = memory.Read32(handles_address + i * sizeof(Handle));
auto object = kernel.GetCurrentProcess()->handle_table.Get<WaitObject>(handle);
R_UNLESS(object, ResultInvalidHandle);
if (!object) {
// Handle 0 (null) appears in some official Nintendo APT library builds where
// aptSleepSync is 0 because NotifyToWait has not yet delivered the sleep-sync event.
// On real hardware the 3DS kernel treats a null handle in WaitSynchronizationN as a
// slot that is permanently unavailable (never fires), letting the other valid handles
// determine when the thread wakes. Emulate this with a NullWaitObject.
if (handle == 0) {
objects[i] = std::make_shared<NullWaitObject>(kernel);
continue;
}
return ResultInvalidHandle;
}
objects[i] = object;
}
@ -1563,7 +1601,7 @@ Result SVC::DuplicateHandle(Handle* out, Handle handle) {
Result SVC::SignalEvent(Handle handle) {
LOG_TRACE(Kernel_SVC, "called event=0x{:08X}", handle);
std::shared_ptr<Event> evt = kernel.GetCurrentProcess()->handle_table.Get<Event>(handle);
auto evt = kernel.GetCurrentProcess()->handle_table.Get<Event>(handle);
R_UNLESS(evt, ResultInvalidHandle);
evt->Signal();

View file

@ -153,13 +153,6 @@ static u64 ConvertTitleID(Core::System& system, u64 base_title_id) {
return base_title_id;
}
static bool IsSystemAppletId(AppletId applet_id) {
return (static_cast<u32>(applet_id) & static_cast<u32>(AppletId::AnySystemApplet)) != 0;
}
static bool IsApplicationAppletId(AppletId applet_id) {
return (static_cast<u32>(applet_id) & static_cast<u32>(AppletId::Application)) != 0;
}
AppletManager::AppletSlot AppletManager::GetAppletSlotFromId(AppletId id) {
if (id == AppletId::Application) {
@ -254,10 +247,6 @@ AppletManager::AppletSlot AppletManager::GetAppletSlotFromPos(AppletPos pos) {
}
void AppletManager::CancelAndSendParameter(const MessageParameter& parameter) {
LOG_DEBUG(
Service_APT, "Sending parameter from {:03X} to {:03X} with signal {:08X} and size {:08X}",
parameter.sender_id, parameter.destination_id, parameter.signal, parameter.buffer.size());
// If the applet is being HLEd, send directly to the applet.
const auto applet = hle_applets[parameter.destination_id];
if (applet != nullptr) {
@ -267,7 +256,7 @@ void AppletManager::CancelAndSendParameter(const MessageParameter& parameter) {
next_parameter = parameter;
if (parameter.signal == SignalType::RequestForSysApplet) {
// APT handles RequestForSysApplet messages itself.
// APT handles RequestForSysApplet messages itself by sending back a Response.
LOG_DEBUG(Service_APT, "Replying to RequestForSysApplet from {:03X}",
parameter.sender_id);
@ -280,12 +269,24 @@ void AppletManager::CancelAndSendParameter(const MessageParameter& parameter) {
next_parameter->destination_id = parameter.sender_id;
next_parameter->signal = SignalType::Response;
next_parameter->buffer.clear();
next_parameter->object = nullptr;
} else if (IsSystemAppletId(parameter.sender_id) &&
IsApplicationAppletId(parameter.destination_id) && parameter.object) {
// When a message is sent from a system applet to an application, APT
// replaces its object with the zero handle.
next_parameter->object = nullptr;
// Provide a pre-signaled event so the app can svcWaitSynchronizationN on it without
// hanging or crashing with handle=0. The real NS firmware does the same.
auto event = system.Kernel().CreateEvent(Kernel::ResetType::OneShot,
"APT:RequestForSysApplet response");
event->Signal();
next_parameter->object = std::move(event);
}
// If a Wakeup is sent with no event object (e.g. HOME Menu waking up an application
// after returning from a system applet), the application will call svcSignalEvent or
// svcWaitSynchronizationN on the received handle. If that handle is 0 the syscall
// returns ResultInvalidHandle and the app crashes. Provide a pre-signaled event so
// that any such handle is always valid and resolves immediately.
if (parameter.signal == SignalType::Wakeup && !next_parameter->object) {
auto event = system.Kernel().CreateEvent(Kernel::ResetType::OneShot,
"APT:Wakeup event");
event->Signal();
next_parameter->object = std::move(event);
}
// Signal the event to let the receiver know that a new parameter is ready to be read
@ -411,11 +412,18 @@ ResultVal<AppletManager::InitializeResult> AppletManager::Initialize(AppletId ap
// APT automatically calls enable on the first registered applet.
Enable(attributes);
// Wake up the applet.
// Wake up the applet. Provide a pre-signaled event so that downstream code
// (e.g. HOME Menu) can safely pass it along when waking up an application; an app
// that calls svcWaitSynchronizationN or svcSignalEvent on the received handle will
// not crash even if it does not first check for handle=0.
auto wakeup_event = system.Kernel().CreateEvent(Kernel::ResetType::OneShot,
"APT:RegisterApplet wakeup");
wakeup_event->Signal();
SendParameter({
.sender_id = AppletId::None,
.destination_id = app_id,
.signal = SignalType::Wakeup,
.object = std::move(wakeup_event),
});
}
@ -1037,6 +1045,15 @@ Result AppletManager::LeaveHomeMenu(std::shared_ptr<Kernel::Object> object,
const std::vector<u8>& buffer) {
active_slot = AppletSlot::Application;
// If no event was provided (HOME Menu passed handle=0), supply a pre-signaled event so the
// application can safely call svcSignalEvent on the received handle without crashing.
if (!object) {
auto event = system.Kernel().CreateEvent(Kernel::ResetType::OneShot,
"APT:LeaveHomeMenu wakeup confirm");
event->Signal();
object = std::move(event);
}
SendParameter({
.sender_id = AppletId::HomeMenu,
.destination_id = AppletId::Application,

View file

@ -357,9 +357,9 @@ void Module::APTInterface::NotifyToWait(Kernel::HLERequestContext& ctx) {
const auto app_id = rp.Pop<u32>();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess); // No error
rb.Push(ResultSuccess);
LOG_WARNING(Service_APT, "(STUBBED) app_id={}", app_id);
LOG_DEBUG(Service_APT, "(STUBBED) app_id={}", app_id);
}
void Module::APTInterface::GetLockHandle(Kernel::HLERequestContext& ctx) {
@ -489,10 +489,11 @@ void Module::APTInterface::SendParameter(Kernel::HLERequestContext& ctx) {
const auto object = rp.PopGenericObject();
const auto buffer = rp.PopStaticBuffer();
LOG_DEBUG(Service_APT,
"called src_app_id={:#010X}, dst_app_id={:#010X}, signal_type={:#010X},"
"buffer_size={:#010X}",
src_app_id, dst_app_id, signal_type, buffer_size);
LOG_WARNING(Service_APT,
"called src_app_id={:#010X}, dst_app_id={:#010X}, signal_type={:#010X},"
"buffer_size={:#010X}, object={}",
src_app_id, dst_app_id, signal_type, buffer_size,
object ? object->GetName() : "(null)");
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(apt->applet_manager->SendParameter({
@ -509,25 +510,25 @@ void Module::APTInterface::ReceiveParameter(Kernel::HLERequestContext& ctx) {
const auto app_id = rp.PopEnum<AppletId>();
const auto buffer_size = rp.Pop<u32>();
LOG_DEBUG(Service_APT, "called app_id={:#010X}, buffer_size={:#010X}", app_id, buffer_size);
LOG_DEBUG(Service_APT, "called app_id={:#06X}, buffer_size={:#010X}", app_id, buffer_size);
auto next_parameter = apt->applet_manager->ReceiveParameter(app_id);
if (next_parameter.Failed()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(next_parameter.Code());
} else {
const auto size = std::min(static_cast<u32>(next_parameter->buffer.size()), buffer_size);
next_parameter->buffer.resize(
buffer_size); // APT always push a buffer with the maximum size
IPC::RequestBuilder rb = rp.MakeBuilder(4, 4);
rb.Push(ResultSuccess); // No error
rb.PushEnum(next_parameter->sender_id);
rb.PushEnum(next_parameter->signal); // Signal type
rb.Push(size); // Parameter buffer size
rb.PushMoveObjects(next_parameter->object);
rb.PushStaticBuffer(std::move(next_parameter->buffer), 0);
return;
}
const auto size = std::min(static_cast<u32>(next_parameter->buffer.size()), buffer_size);
next_parameter->buffer.resize(buffer_size); // APT always push a buffer with the maximum size
IPC::RequestBuilder rb = rp.MakeBuilder(4, 4);
rb.Push(ResultSuccess); // No error
rb.PushEnum(next_parameter->sender_id);
rb.PushEnum(next_parameter->signal); // Signal type
rb.Push(size); // Parameter buffer size
rb.PushMoveObjects(next_parameter->object);
rb.PushStaticBuffer(std::move(next_parameter->buffer), 0);
}
void Module::APTInterface::GlanceParameter(Kernel::HLERequestContext& ctx) {
@ -535,25 +536,25 @@ void Module::APTInterface::GlanceParameter(Kernel::HLERequestContext& ctx) {
const auto app_id = rp.PopEnum<AppletId>();
const u32 buffer_size = rp.Pop<u32>();
LOG_DEBUG(Service_APT, "called app_id={:#010X}, buffer_size={:#010X}", app_id, buffer_size);
LOG_DEBUG(Service_APT, "called app_id={:#06X}, buffer_size={:#010X}", app_id, buffer_size);
auto next_parameter = apt->applet_manager->GlanceParameter(app_id);
if (next_parameter.Failed()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(next_parameter.Code());
} else {
const auto size = std::min(static_cast<u32>(next_parameter->buffer.size()), buffer_size);
next_parameter->buffer.resize(
buffer_size); // APT always push a buffer with the maximum size
IPC::RequestBuilder rb = rp.MakeBuilder(4, 4);
rb.Push(ResultSuccess); // No error
rb.PushEnum(next_parameter->sender_id);
rb.PushEnum(next_parameter->signal); // Signal type
rb.Push(size); // Parameter buffer size
rb.PushMoveObjects(next_parameter->object);
rb.PushStaticBuffer(std::move(next_parameter->buffer), 0);
return;
}
const auto size = std::min(static_cast<u32>(next_parameter->buffer.size()), buffer_size);
next_parameter->buffer.resize(buffer_size); // APT always push a buffer with the maximum size
IPC::RequestBuilder rb = rp.MakeBuilder(4, 4);
rb.Push(ResultSuccess); // No error
rb.PushEnum(next_parameter->sender_id);
rb.PushEnum(next_parameter->signal); // Signal type
rb.Push(size); // Parameter buffer size
rb.PushCopyObjects(next_parameter->object);
rb.PushStaticBuffer(std::move(next_parameter->buffer), 0);
}
void Module::APTInterface::CancelParameter(Kernel::HLERequestContext& ctx) {
@ -1010,7 +1011,8 @@ void Module::APTInterface::JumpToHomeMenu(Kernel::HLERequestContext& ctx) {
const auto object = rp.PopGenericObject();
const auto buffer = rp.PopStaticBuffer();
LOG_DEBUG(Service_APT, "called size={}", parameter_size);
LOG_WARNING(Service_APT, "called size={}, object={}", parameter_size,
object ? object->GetName() : "(null)");
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(apt->applet_manager->JumpToHomeMenu(object, buffer));
@ -1031,7 +1033,8 @@ void Module::APTInterface::LeaveHomeMenu(Kernel::HLERequestContext& ctx) {
const auto object = rp.PopGenericObject();
const auto buffer = rp.PopStaticBuffer();
LOG_DEBUG(Service_APT, "called size={}", parameter_size);
LOG_WARNING(Service_APT, "called size={}, object={}", parameter_size,
object ? object->GetName() : "(null)");
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(apt->applet_manager->LeaveHomeMenu(object, buffer));

View file

@ -445,8 +445,11 @@ void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
u64 caller_tid = ctx.ClientThread()->owner_process.lock()->codeset->program_id;
// Default to reading from SecureInfo_A so that CFGU_SecureInfoGetRegion returns
// the correct hardware region when a dump is present. Falls back to auto-detect
// (preferred region / settings) when SecureInfo_A is absent or invalid.
bool from_secure_info = Common::Hacks::hack_manager.OverrideBooleanSetting(
Common::Hacks::HackType::REGION_FROM_SECURE, caller_tid, false);
Common::Hacks::HackType::REGION_FROM_SECURE, caller_tid, true);
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);

View file

@ -1339,6 +1339,31 @@ void FS_USER::GetFormatInfo(Kernel::HLERequestContext& ctx) {
rb.Push<bool>(format_info->duplicate_data != 0);
}
void FS_USER::GetSdmcCtrRootPath(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const u32 buffer_size = rp.Pop<u32>(); // in wide characters, including null terminator
auto& output_buffer = rp.PopMappedBuffer();
// Path is /Nintendo 3DS/<ID0>/<ID1>; both IDs are all-zero in the emulator
const std::string path = fmt::format("/Nintendo 3DS/{}/{}", SYSTEM_ID, SDCARD_ID);
LOG_DEBUG(Service_FS, "buffer_size={} path={}", buffer_size, path);
// Write as UTF-16LE; all characters in this path are ASCII so each code unit is just the byte
const u32 chars_to_write = std::min(static_cast<u32>(path.size()), buffer_size - 1);
for (u32 i = 0; i < chars_to_write; ++i) {
const u16 wc = static_cast<u16>(static_cast<unsigned char>(path[i]));
output_buffer.Write(&wc, i * sizeof(u16), sizeof(u16));
}
if (buffer_size > 0) {
const u16 null_char = 0;
output_buffer.Write(&null_char, chars_to_write * sizeof(u16), sizeof(u16));
}
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
}
void FS_USER::GetProductInfo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@ -1997,7 +2022,7 @@ FS_USER::FS_USER(Core::System& system)
{0x0845, &FS_USER::GetFormatInfo, "GetFormatInfo"},
{0x0846, nullptr, "GetLegacyRomHeader2"},
{0x0847, nullptr, "FormatCtrCardUserSaveData"},
{0x0848, nullptr, "GetSdmcCtrRootPath"},
{0x0848, &FS_USER::GetSdmcCtrRootPath, "GetSdmcCtrRootPath"},
{0x0849, &FS_USER::GetArchiveResource, "GetArchiveResource"},
{0x084A, &FS_USER::ExportIntegrityVerificationSeed, "ExportIntegrityVerificationSeed"},
{0x084B, nullptr, "ImportIntegrityVerificationSeed"},

View file

@ -621,6 +621,20 @@ private:
*/
void GetFormatInfo(Kernel::HLERequestContext& ctx);
/**
* FS_User::GetSdmcCtrRootPath service function (0x08480042).
* Retrieves the /Nintendo 3DS/<ID0>/<ID1> path as a UTF-16LE wide string.
* Inputs:
* 0 : 0x08480042
* 1 : Buffer size in wide characters (including null terminator)
* 2 : (bytesize << 4) | 0xC (output buffer descriptor)
* 3 : Output buffer pointer
* Outputs:
* 0 : Header code
* 1 : Result code
*/
void GetSdmcCtrRootPath(Kernel::HLERequestContext& ctx);
/**
* FS_User::GetProductInfo service function.
* Inputs: