
This commit adds all the changes made to the 3GX plugin loader fork of Luma3DS. The most important features are: - Add 3GX plugin loader support. New service added to rosalina: plg:ldr - Add svcControlProcess, svcControlMemoryUnsafe and improve svcMapProcessMemoryEx (breaking change) - Allow applications to override certain configurations depending on their needs: - Disable core2 thread redirection - Disable game patching for the next app - Force New 3DS speedup - Force next application in a specific memory mode - Block the opening of the Rosalina menu - Add GDB commands to list all process handles and catch all SVC (latter is for IDA Pro as gdb client supports it) - Other changes necessary for plugins to work properly. Please check changed files in this PR for more details. --------- Co-authored-by: PabloMK7 <hackyglitch@gmail.com> Co-authored-by: Nanquitas <nath.doidi@gmail.com> Co-authored-by: TuxSH <1922548+TuxSH@users.noreply.github.com>
310 lines
9.2 KiB
C
310 lines
9.2 KiB
C
#include <3ds.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include "csvc.h"
|
|
#include "plugin.h"
|
|
#include "ifile.h"
|
|
#include "ifile.h"
|
|
#include "utils.h"
|
|
|
|
// Use a global to avoid stack overflow, those structs are quite heavy
|
|
static FS_DirectoryEntry g_entries[10];
|
|
|
|
static char g_path[256];
|
|
static const char *g_dirPath = "/luma/plugins/%016llX";
|
|
static const char *g_defaultPath = "/luma/plugins/default.3gx";
|
|
|
|
// pluginLoader.s
|
|
void gamePatchFunc(void);
|
|
|
|
static u32 strlen16(const u16 *str)
|
|
{
|
|
if (!str) return 0;
|
|
|
|
const u16 *strEnd = str;
|
|
|
|
while (*strEnd) ++strEnd;
|
|
|
|
return strEnd - str;
|
|
}
|
|
|
|
static Result FindPluginFile(u64 tid)
|
|
{
|
|
char filename[256];
|
|
u32 entriesNb = 0;
|
|
bool found = false;
|
|
Handle dir = 0;
|
|
Result res;
|
|
FS_Archive sdmcArchive = 0;
|
|
FS_DirectoryEntry * entries = g_entries;
|
|
|
|
memset(entries, 0, sizeof(g_entries));
|
|
sprintf(g_path, g_dirPath, tid);
|
|
|
|
if (R_FAILED((res = FSUSER_OpenArchive(&sdmcArchive, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, "")))))
|
|
goto exit;
|
|
|
|
if (R_FAILED((res = FSUSER_OpenDirectory(&dir, sdmcArchive, fsMakePath(PATH_ASCII, g_path)))))
|
|
goto exit;
|
|
|
|
strcat(g_path, "/");
|
|
while (!found && R_SUCCEEDED(FSDIR_Read(dir, &entriesNb, 10, entries)))
|
|
{
|
|
if (entriesNb == 0)
|
|
break;
|
|
|
|
static const u16 * validExtension = u"3gx";
|
|
|
|
for (u32 i = 0; i < entriesNb; ++i)
|
|
{
|
|
FS_DirectoryEntry *entry = &entries[i];
|
|
|
|
// If entry is a folder, skip it
|
|
if (entry->attributes & 1)
|
|
continue;
|
|
|
|
// Check extension
|
|
u32 size = strlen16(entry->name);
|
|
if (size <= 5)
|
|
continue;
|
|
|
|
u16 *fileExt = entry->name + size - 3;
|
|
|
|
if (memcmp(fileExt, validExtension, 3 * sizeof(u16)))
|
|
continue;
|
|
|
|
// Convert name from utf16 to utf8
|
|
int units = utf16_to_utf8((u8 *)filename, entry->name, 100);
|
|
if (units == -1)
|
|
continue;
|
|
filename[units] = 0;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
res = MAKERESULT(28, 4, 0, 1018);
|
|
else
|
|
{
|
|
u32 len = strlen(g_path);
|
|
filename[256 - len] = 0;
|
|
strcat(g_path, filename);
|
|
PluginLoaderCtx.pluginPath = g_path;
|
|
}
|
|
|
|
exit:
|
|
FSDIR_Close(dir);
|
|
FSUSER_CloseArchive(sdmcArchive);
|
|
|
|
return res;
|
|
}
|
|
|
|
static Result OpenFile(IFile *file, const char *path)
|
|
{
|
|
return IFile_Open(file, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""), fsMakePath(PATH_ASCII, path), FS_OPEN_READ);
|
|
}
|
|
|
|
static Result OpenPluginFile(u64 tid, IFile *plugin)
|
|
{
|
|
if (R_FAILED(FindPluginFile(tid)) || OpenFile(plugin, g_path))
|
|
{
|
|
// Try to open default plugin
|
|
if (OpenFile(plugin, g_defaultPath))
|
|
return -1;
|
|
|
|
PluginLoaderCtx.pluginPath = g_defaultPath;
|
|
PluginLoaderCtx.header.isDefaultPlugin = 1;
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static Result CheckPluginCompatibility(_3gx_Header *header, u32 processTitle)
|
|
{
|
|
static char errorBuf[0x100];
|
|
|
|
if (header->targets.count == 0)
|
|
return 0;
|
|
|
|
for (u32 i = 0; i < header->targets.count; ++i)
|
|
{
|
|
if (header->targets.titles[i] == processTitle)
|
|
return 0;
|
|
}
|
|
|
|
sprintf(errorBuf, "The plugin - %s -\nis not compatible with this game.\n" \
|
|
"Contact \"%s\" for more infos.", header->infos.titleMsg, header->infos.authorMsg);
|
|
|
|
PluginLoaderCtx.error.message = errorBuf;
|
|
|
|
return -1;
|
|
}
|
|
|
|
bool TryToLoadPlugin(Handle process)
|
|
{
|
|
u64 tid;
|
|
u64 fileSize;
|
|
IFile plugin;
|
|
Result res;
|
|
_3gx_Header fileHeader;
|
|
_3gx_Header *header = NULL;
|
|
_3gx_Executable *exeHdr = NULL;
|
|
PluginLoaderContext *ctx = &PluginLoaderCtx;
|
|
PluginHeader *pluginHeader = &ctx->header;
|
|
const u32 memRegionSizes[] =
|
|
{
|
|
5 * 1024 * 1024, // 5 MiB
|
|
2 * 1024 * 1024, // 2 MiB
|
|
10 * 1024 * 1024, // 10 MiB
|
|
5 * 1024 * 1024, // 5 MiB (Reserved)
|
|
};
|
|
|
|
// Get title id
|
|
svcGetProcessInfo((s64 *)&tid, process, 0x10001);
|
|
|
|
memset(pluginHeader, 0, sizeof(PluginHeader));
|
|
pluginHeader->magic = HeaderMagic;
|
|
|
|
// Try to open plugin file
|
|
if (ctx->useUserLoadParameters && (u32)tid == ctx->userLoadParameters.lowTitleId)
|
|
{
|
|
ctx->useUserLoadParameters = false;
|
|
ctx->pluginMemoryStrategy = ctx->userLoadParameters.pluginMemoryStrategy;
|
|
if (OpenFile(&plugin, ctx->userLoadParameters.path))
|
|
return false;
|
|
|
|
ctx->pluginPath = ctx->userLoadParameters.path;
|
|
|
|
memcpy(pluginHeader->config, ctx->userLoadParameters.config, 32 * sizeof(u32));
|
|
}
|
|
else
|
|
{
|
|
if (R_FAILED(OpenPluginFile(tid, &plugin)))
|
|
return false;
|
|
}
|
|
|
|
if (R_FAILED((res = IFile_GetSize(&plugin, &fileSize))))
|
|
ctx->error.message = "Couldn't get file size";
|
|
|
|
if (!res && R_FAILED(res = Check_3gx_Magic(&plugin)))
|
|
{
|
|
const char * errors[] =
|
|
{
|
|
"Couldn't read file.",
|
|
"Invalid plugin file\nNot a valid 3GX plugin format!",
|
|
"Outdated plugin file\nCheck for an updated plugin.",
|
|
"Outdated plugin loader\nCheck for Luma3DS updates."
|
|
};
|
|
|
|
ctx->error.message = errors[R_MODULE(res) == RM_LDR ? R_DESCRIPTION(res) : 0];
|
|
}
|
|
|
|
// Read header
|
|
if (!res && R_FAILED((res = Read_3gx_Header(&plugin, &fileHeader))))
|
|
ctx->error.message = "Couldn't read file";
|
|
|
|
// Set memory region size according to header
|
|
if (!res && R_FAILED((res = MemoryBlock__SetSize(memRegionSizes[fileHeader.infos.memoryRegionSize])))) {
|
|
ctx->error.message = "Couldn't set memblock size.";
|
|
}
|
|
|
|
// Ensure memory block is mounted
|
|
if (!res && R_FAILED((res = MemoryBlock__IsReady())))
|
|
ctx->error.message = "Failed to allocate memory.";
|
|
|
|
// Plugins will not exceed 5MB so this is fine
|
|
if (!res) {
|
|
header = (_3gx_Header *)(ctx->memblock.memblock + g_memBlockSize - (u32)fileSize);
|
|
memcpy(header, &fileHeader, sizeof(_3gx_Header));
|
|
}
|
|
|
|
// Parse rest of header
|
|
if (!res && R_FAILED((res = Read_3gx_ParseHeader(&plugin, header))))
|
|
ctx->error.message = "Couldn't read file";
|
|
|
|
// Read embedded save/load functions
|
|
if (!res && R_FAILED((res = Read_3gx_EmbeddedPayloads(&plugin, header))))
|
|
ctx->error.message = "Invalid save/load payloads.";
|
|
|
|
// Save exe checksum
|
|
if (!res)
|
|
ctx->exeLoadChecksum = header->infos.exeLoadChecksum;
|
|
|
|
// Check titles compatibility
|
|
if (!res) res = CheckPluginCompatibility(header, (u32)tid);
|
|
|
|
// Read code
|
|
if (!res && R_FAILED(res = Read_3gx_LoadSegments(&plugin, header, ctx->memblock.memblock + sizeof(PluginHeader)))) {
|
|
if (res == MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_NO_DATA)) ctx->error.message = "This plugin requires a loading function.";
|
|
else if (res == MAKERESULT(RL_PERMANENT, RS_INVALIDARG, RM_LDR, RD_INVALID_ADDRESS)) ctx->error.message = "This plugin file is corrupted.";
|
|
else ctx->error.message = "Couldn't read plugin's code";
|
|
}
|
|
|
|
if (R_FAILED(res))
|
|
{
|
|
ctx->error.code = res;
|
|
goto exitFail;
|
|
}
|
|
|
|
pluginHeader->version = header->version;
|
|
// Code size must be page aligned
|
|
exeHdr = &header->executable;
|
|
pluginHeader->exeSize = (sizeof(PluginHeader) + exeHdr->codeSize + exeHdr->rodataSize + exeHdr->dataSize + exeHdr->bssSize + 0x1000) & ~0xFFF;
|
|
pluginHeader->heapVA = 0x06000000;
|
|
pluginHeader->heapSize = g_memBlockSize - pluginHeader->exeSize;
|
|
pluginHeader->plgldrEvent = ctx->plgEventPA;
|
|
pluginHeader->plgldrReply = ctx->plgReplyPA;
|
|
|
|
// Clear old event data
|
|
PLG__NotifyEvent(PLG_OK, false);
|
|
|
|
// Copy header to memblock
|
|
memcpy(ctx->memblock.memblock, pluginHeader, sizeof(PluginHeader));
|
|
// Clear heap
|
|
memset(ctx->memblock.memblock + pluginHeader->exeSize, 0, pluginHeader->heapSize);
|
|
|
|
// Enforce RWX mmu mapping
|
|
svcControlProcess(process, PROCESSOP_SET_MMU_TO_RWX, 0, 0);
|
|
// Ask the kernel to signal when the process is about to be terminated
|
|
svcControlProcess(process, PROCESSOP_SIGNAL_ON_EXIT, 0, 0);
|
|
|
|
// Mount the plugin memory in the process
|
|
if (R_SUCCEEDED(MemoryBlock__MountInProcess()))
|
|
// Install hook
|
|
{
|
|
u32 procStart = 0x00100000;
|
|
u32 *game = (u32 *)procStart;
|
|
|
|
extern u32 g_savedGameInstr[2];
|
|
|
|
if (R_FAILED((res = svcMapProcessMemoryEx(CUR_PROCESS_HANDLE, procStart, process, procStart, 0x1000))))
|
|
{
|
|
ctx->error.message = "Couldn't map process";
|
|
ctx->error.code = res;
|
|
goto exitFail;
|
|
}
|
|
|
|
g_savedGameInstr[0] = game[0];
|
|
g_savedGameInstr[1] = game[1];
|
|
|
|
game[0] = 0xE51FF004; // ldr pc, [pc, #-4]
|
|
game[1] = (u32)PA_FROM_VA_PTR(gamePatchFunc);
|
|
svcFlushEntireDataCache();
|
|
svcUnmapProcessMemoryEx(CUR_PROCESS_HANDLE, procStart, 0x1000);
|
|
}
|
|
else
|
|
goto exitFail;
|
|
|
|
|
|
IFile_Close(&plugin);
|
|
return true;
|
|
|
|
exitFail:
|
|
IFile_Close(&plugin);
|
|
MemoryBlock__Free();
|
|
|
|
return false;
|
|
}
|