|
| 1 | +#include <switch.h> |
| 2 | +#include <string.h> |
| 3 | +#include <fcntl.h> |
| 4 | +#include <unistd.h> |
| 5 | + |
| 6 | +#define DEFAULT_NRO "sdmc:/switch/AA-Reboot.nro" |
| 7 | + |
| 8 | +const char g_noticeText[] = |
| 9 | + "nx-hbloader but it starts aa reboot"; |
| 10 | + |
| 11 | +static char g_argv[2048]; |
| 12 | +static char g_nextArgv[2048]; |
| 13 | +static char g_nextNroPath[512]; |
| 14 | +u64 g_nroAddr = 0; |
| 15 | +static u64 g_nroSize = 0; |
| 16 | +static NroHeader g_nroHeader; |
| 17 | +static bool g_isApplication = 0; |
| 18 | + |
| 19 | +static NsApplicationControlData g_applicationControlData; |
| 20 | +static bool g_isAutomaticGameplayRecording = 0; |
| 21 | +static bool g_smCloseWorkaround = false; |
| 22 | + |
| 23 | +static u64 g_appletHeapSize = 0; |
| 24 | +static u64 g_appletHeapReservationSize = 0; |
| 25 | + |
| 26 | +static u128 g_userIdStorage; |
| 27 | + |
| 28 | +static u8 g_savedTls[0x100]; |
| 29 | + |
| 30 | +// Minimize fs resource usage |
| 31 | +u32 __nx_fs_num_sessions = 1; |
| 32 | +u32 __nx_fsdev_direntry_cache_size = 1; |
| 33 | +bool __nx_fsdev_support_cwd = false; |
| 34 | + |
| 35 | +// Used by trampoline.s |
| 36 | +Result g_lastRet = 0; |
| 37 | + |
| 38 | +extern void* __stack_top;//Defined in libnx. |
| 39 | +#define STACK_SIZE 0x100000 //Change this if main-thread stack size ever changes. |
| 40 | + |
| 41 | +void __libnx_initheap(void) |
| 42 | +{ |
| 43 | + static char g_innerheap[0x4000]; |
| 44 | + |
| 45 | + extern char* fake_heap_start; |
| 46 | + extern char* fake_heap_end; |
| 47 | + |
| 48 | + fake_heap_start = &g_innerheap[0]; |
| 49 | + fake_heap_end = &g_innerheap[sizeof g_innerheap]; |
| 50 | +} |
| 51 | + |
| 52 | +static Result readSetting(const char* key, void* buf, size_t size) |
| 53 | +{ |
| 54 | + Result rc; |
| 55 | + u64 actual_size; |
| 56 | + const char* const section_name = "hbloader"; |
| 57 | + rc = setsysGetSettingsItemValueSize(section_name, key, &actual_size); |
| 58 | + if (R_SUCCEEDED(rc) && actual_size != size) |
| 59 | + rc = MAKERESULT(Module_Libnx, LibnxError_BadInput); |
| 60 | + if (R_SUCCEEDED(rc)) |
| 61 | + rc = setsysGetSettingsItemValue(section_name, key, buf, size, &actual_size); |
| 62 | + if (R_SUCCEEDED(rc) && actual_size != size) |
| 63 | + rc = MAKERESULT(Module_Libnx, LibnxError_BadInput); |
| 64 | + if (R_FAILED(rc)) memset(buf, 0, size); |
| 65 | + return rc; |
| 66 | +} |
| 67 | + |
| 68 | +void __appInit(void) |
| 69 | +{ |
| 70 | + Result rc; |
| 71 | + |
| 72 | + rc = smInitialize(); |
| 73 | + if (R_FAILED(rc)) |
| 74 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 1)); |
| 75 | + |
| 76 | + rc = setsysInitialize(); |
| 77 | + if (R_SUCCEEDED(rc)) { |
| 78 | + SetSysFirmwareVersion fw; |
| 79 | + rc = setsysGetFirmwareVersion(&fw); |
| 80 | + if (R_SUCCEEDED(rc)) |
| 81 | + hosversionSet(MAKEHOSVERSION(fw.major, fw.minor, fw.micro)); |
| 82 | + readSetting("applet_heap_size", &g_appletHeapSize, sizeof(g_appletHeapSize)); |
| 83 | + readSetting("applet_heap_reservation_size", &g_appletHeapReservationSize, sizeof(g_appletHeapReservationSize)); |
| 84 | + setsysExit(); |
| 85 | + } |
| 86 | + |
| 87 | + rc = fsInitialize(); |
| 88 | + if (R_FAILED(rc)) |
| 89 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 2)); |
| 90 | +} |
| 91 | + |
| 92 | +void __wrap_exit(void) |
| 93 | +{ |
| 94 | + // exit() effectively never gets called, so let's stub it out. |
| 95 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 39)); |
| 96 | +} |
| 97 | + |
| 98 | +static void* g_heapAddr; |
| 99 | +static size_t g_heapSize; |
| 100 | + |
| 101 | +static u64 calculateMaxHeapSize(void) |
| 102 | +{ |
| 103 | + u64 size = 0; |
| 104 | + u64 mem_available = 0, mem_used = 0; |
| 105 | + |
| 106 | + svcGetInfo(&mem_available, InfoType_TotalMemorySize, CUR_PROCESS_HANDLE, 0); |
| 107 | + svcGetInfo(&mem_used, InfoType_UsedMemorySize, CUR_PROCESS_HANDLE, 0); |
| 108 | + |
| 109 | + if (mem_available > mem_used+0x200000) |
| 110 | + size = (mem_available - mem_used - 0x200000) & ~0x1FFFFF; |
| 111 | + if (size == 0) |
| 112 | + size = 0x2000000*16; |
| 113 | + if (size > 0x6000000 && g_isAutomaticGameplayRecording) |
| 114 | + size -= 0x6000000; |
| 115 | + |
| 116 | + return size; |
| 117 | +} |
| 118 | + |
| 119 | +static void setupHbHeap(void) |
| 120 | +{ |
| 121 | + void* addr = NULL; |
| 122 | + u64 size = calculateMaxHeapSize(); |
| 123 | + |
| 124 | + if (!g_isApplication) { |
| 125 | + if (g_appletHeapSize) { |
| 126 | + u64 requested_size = (g_appletHeapSize + 0x1FFFFF) &~ 0x1FFFFF; |
| 127 | + if (requested_size < size) |
| 128 | + size = requested_size; |
| 129 | + } |
| 130 | + else if (g_appletHeapReservationSize) { |
| 131 | + u64 reserved_size = (g_appletHeapReservationSize + 0x1FFFFF) &~ 0x1FFFFF; |
| 132 | + if (reserved_size < size) |
| 133 | + size -= reserved_size; |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + Result rc = svcSetHeapSize(&addr, size); |
| 138 | + |
| 139 | + if (R_FAILED(rc) || addr==NULL) |
| 140 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 9)); |
| 141 | + |
| 142 | + g_heapAddr = addr; |
| 143 | + g_heapSize = size; |
| 144 | +} |
| 145 | + |
| 146 | +static Handle g_procHandle; |
| 147 | + |
| 148 | +static void procHandleReceiveThread(void* arg) |
| 149 | +{ |
| 150 | + Handle session = (Handle)(uintptr_t)arg; |
| 151 | + Result rc; |
| 152 | + |
| 153 | + void* base = armGetTls(); |
| 154 | + hipcMakeRequestInline(base); |
| 155 | + |
| 156 | + s32 idx = 0; |
| 157 | + rc = svcReplyAndReceive(&idx, &session, 1, INVALID_HANDLE, UINT64_MAX); |
| 158 | + if (R_FAILED(rc)) |
| 159 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 15)); |
| 160 | + |
| 161 | + HipcParsedRequest r = hipcParseRequest(base); |
| 162 | + if (r.meta.num_copy_handles != 1) |
| 163 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 17)); |
| 164 | + |
| 165 | + g_procHandle = r.data.copy_handles[0]; |
| 166 | + svcCloseHandle(session); |
| 167 | +} |
| 168 | + |
| 169 | +//Gets the PID of the process with application_type==APPLICATION in the NPDM, then sets g_isApplication if it matches the current PID. |
| 170 | +static void getIsApplication(void) { |
| 171 | + Result rc=0; |
| 172 | + u64 cur_pid=0, app_pid=0; |
| 173 | + |
| 174 | + g_isApplication = 0; |
| 175 | + |
| 176 | + rc = svcGetProcessId(&cur_pid, CUR_PROCESS_HANDLE); |
| 177 | + if (R_FAILED(rc)) return; |
| 178 | + |
| 179 | + rc = pmshellInitialize(); |
| 180 | + |
| 181 | + if (R_SUCCEEDED(rc)) { |
| 182 | + rc = pmshellGetApplicationProcessIdForShell(&app_pid); |
| 183 | + pmshellExit(); |
| 184 | + } |
| 185 | + |
| 186 | + if (R_SUCCEEDED(rc) && cur_pid == app_pid) g_isApplication = 1; |
| 187 | +} |
| 188 | + |
| 189 | +//Gets the control.nacp for the current title id, and then sets g_isAutomaticGameplayRecording if less memory should be allocated. |
| 190 | +static void getIsAutomaticGameplayRecording(void) { |
| 191 | + if (hosversionAtLeast(5,0,0) && g_isApplication) { |
| 192 | + Result rc=0; |
| 193 | + u64 cur_tid=0; |
| 194 | + |
| 195 | + rc = svcGetInfo(&cur_tid, InfoType_ProgramId, CUR_PROCESS_HANDLE, 0); |
| 196 | + if (R_FAILED(rc)) return; |
| 197 | + |
| 198 | + g_isAutomaticGameplayRecording = 0; |
| 199 | + |
| 200 | + rc = nsInitialize(); |
| 201 | + |
| 202 | + if (R_SUCCEEDED(rc)) { |
| 203 | + size_t dummy; |
| 204 | + rc = nsGetApplicationControlData(0x1, cur_tid, &g_applicationControlData, sizeof(g_applicationControlData), &dummy); |
| 205 | + nsExit(); |
| 206 | + } |
| 207 | + |
| 208 | + if (R_SUCCEEDED(rc) && g_applicationControlData.nacp.video_capture == 2) g_isAutomaticGameplayRecording = 1; |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +static void getOwnProcessHandle(void) |
| 213 | +{ |
| 214 | + Result rc; |
| 215 | + |
| 216 | + Handle server_handle, client_handle; |
| 217 | + rc = svcCreateSession(&server_handle, &client_handle, 0, 0); |
| 218 | + if (R_FAILED(rc)) |
| 219 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 12)); |
| 220 | + |
| 221 | + Thread t; |
| 222 | + rc = threadCreate(&t, &procHandleReceiveThread, (void*)(uintptr_t)server_handle, NULL, 0x1000, 0x20, 0); |
| 223 | + if (R_FAILED(rc)) |
| 224 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 10)); |
| 225 | + |
| 226 | + rc = threadStart(&t); |
| 227 | + if (R_FAILED(rc)) |
| 228 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 13)); |
| 229 | + |
| 230 | + hipcMakeRequestInline(armGetTls(), |
| 231 | + .num_copy_handles = 1, |
| 232 | + ).copy_handles[0] = CUR_PROCESS_HANDLE; |
| 233 | + |
| 234 | + svcSendSyncRequest(client_handle); |
| 235 | + svcCloseHandle(client_handle); |
| 236 | + |
| 237 | + threadWaitForExit(&t); |
| 238 | + threadClose(&t); |
| 239 | +} |
| 240 | + |
| 241 | +void loadNro(void) |
| 242 | +{ |
| 243 | + NroHeader* header = NULL; |
| 244 | + size_t rw_size=0; |
| 245 | + Result rc=0; |
| 246 | + |
| 247 | + if (g_smCloseWorkaround) { |
| 248 | + // For old applications, wait for SM to handle closing the SM session from this process. |
| 249 | + // If we don't do this, smInitialize will fail once eventually used later. |
| 250 | + // This is caused by a bug in old versions of libnx that was fixed in commit 68a77ac950. |
| 251 | + g_smCloseWorkaround = false; |
| 252 | + svcSleepThread(1000000000); |
| 253 | + } |
| 254 | + |
| 255 | + memcpy((u8*)armGetTls() + 0x100, g_savedTls, 0x100); |
| 256 | + |
| 257 | + if (g_nroSize > 0) |
| 258 | + { |
| 259 | + // Unmap previous NRO. |
| 260 | + header = &g_nroHeader; |
| 261 | + rw_size = header->segments[2].size + header->bss_size; |
| 262 | + rw_size = (rw_size+0xFFF) & ~0xFFF; |
| 263 | + |
| 264 | + // .text |
| 265 | + rc = svcUnmapProcessCodeMemory( |
| 266 | + g_procHandle, g_nroAddr + header->segments[0].file_off, ((u64) g_heapAddr) + header->segments[0].file_off, header->segments[0].size); |
| 267 | + |
| 268 | + if (R_FAILED(rc)) |
| 269 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 24)); |
| 270 | + |
| 271 | + // .rodata |
| 272 | + rc = svcUnmapProcessCodeMemory( |
| 273 | + g_procHandle, g_nroAddr + header->segments[1].file_off, ((u64) g_heapAddr) + header->segments[1].file_off, header->segments[1].size); |
| 274 | + |
| 275 | + if (R_FAILED(rc)) |
| 276 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 25)); |
| 277 | + |
| 278 | + // .data + .bss |
| 279 | + rc = svcUnmapProcessCodeMemory( |
| 280 | + g_procHandle, g_nroAddr + header->segments[2].file_off, ((u64) g_heapAddr) + header->segments[2].file_off, rw_size); |
| 281 | + |
| 282 | + if (R_FAILED(rc)) |
| 283 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 26)); |
| 284 | + |
| 285 | + g_nroAddr = g_nroSize = 0; |
| 286 | + } |
| 287 | + |
| 288 | + if (g_nextNroPath[0] == '\0') |
| 289 | + { |
| 290 | + memcpy(g_nextNroPath, DEFAULT_NRO, sizeof(DEFAULT_NRO)); |
| 291 | + memcpy(g_nextArgv, DEFAULT_NRO, sizeof(DEFAULT_NRO)); |
| 292 | + } |
| 293 | + |
| 294 | + memcpy(g_argv, g_nextArgv, sizeof g_argv); |
| 295 | + |
| 296 | + uint8_t *nrobuf = (uint8_t*) g_heapAddr; |
| 297 | + |
| 298 | + NroStart* start = (NroStart*) (nrobuf + 0); |
| 299 | + header = (NroHeader*) (nrobuf + sizeof(NroStart)); |
| 300 | + uint8_t* rest = (uint8_t*) (nrobuf + sizeof(NroStart) + sizeof(NroHeader)); |
| 301 | + |
| 302 | + rc = fsdevMountSdmc(); |
| 303 | + if (R_FAILED(rc)) |
| 304 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 404)); |
| 305 | + |
| 306 | + int fd = open(g_nextNroPath, O_RDONLY); |
| 307 | + if (fd < 0) |
| 308 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 3)); |
| 309 | + |
| 310 | + // Reset NRO path to load hbmenu by default next time. |
| 311 | + g_nextNroPath[0] = '\0'; |
| 312 | + |
| 313 | + if (read(fd, start, sizeof(*start)) != sizeof(*start)) |
| 314 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 4)); |
| 315 | + |
| 316 | + if (read(fd, header, sizeof(*header)) != sizeof(*header)) |
| 317 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 4)); |
| 318 | + |
| 319 | + if (header->magic != NROHEADER_MAGIC) |
| 320 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 5)); |
| 321 | + |
| 322 | + size_t rest_size = header->size - (sizeof(NroStart) + sizeof(NroHeader)); |
| 323 | + if (read(fd, rest, rest_size) != rest_size) |
| 324 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 7)); |
| 325 | + |
| 326 | + close(fd); |
| 327 | + fsdevUnmountAll(); |
| 328 | + |
| 329 | + size_t total_size = header->size + header->bss_size; |
| 330 | + total_size = (total_size+0xFFF) & ~0xFFF; |
| 331 | + |
| 332 | + rw_size = header->segments[2].size + header->bss_size; |
| 333 | + rw_size = (rw_size+0xFFF) & ~0xFFF; |
| 334 | + |
| 335 | + bool has_mod0 = false; |
| 336 | + if (start->mod_offset > 0 && start->mod_offset <= (total_size-0x24)) // Validate MOD0 offset |
| 337 | + has_mod0 = *(uint32_t*)(nrobuf + start->mod_offset) == 0x30444F4D; // Validate MOD0 header |
| 338 | + |
| 339 | + int i; |
| 340 | + for (i=0; i<3; i++) |
| 341 | + { |
| 342 | + if (header->segments[i].file_off >= header->size || header->segments[i].size > header->size || |
| 343 | + (header->segments[i].file_off + header->segments[i].size) > header->size) |
| 344 | + { |
| 345 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 6)); |
| 346 | + } |
| 347 | + } |
| 348 | + |
| 349 | + // todo: Detect whether NRO fits into heap or not. |
| 350 | + |
| 351 | + // Copy header to elsewhere because we're going to unmap it next. |
| 352 | + memcpy(&g_nroHeader, header, sizeof(g_nroHeader)); |
| 353 | + header = &g_nroHeader; |
| 354 | + |
| 355 | + u64 map_addr; |
| 356 | + |
| 357 | + do { |
| 358 | + map_addr = randomGet64() & 0xFFFFFF000ull; |
| 359 | + rc = svcMapProcessCodeMemory(g_procHandle, map_addr, (u64)nrobuf, total_size); |
| 360 | + |
| 361 | + } while (rc == 0xDC01 || rc == 0xD401); |
| 362 | + |
| 363 | + if (R_FAILED(rc)) |
| 364 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 18)); |
| 365 | + |
| 366 | + // .text |
| 367 | + rc = svcSetProcessMemoryPermission( |
| 368 | + g_procHandle, map_addr + header->segments[0].file_off, header->segments[0].size, Perm_R | Perm_X); |
| 369 | + |
| 370 | + if (R_FAILED(rc)) |
| 371 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 19)); |
| 372 | + |
| 373 | + // .rodata |
| 374 | + rc = svcSetProcessMemoryPermission( |
| 375 | + g_procHandle, map_addr + header->segments[1].file_off, header->segments[1].size, Perm_R); |
| 376 | + |
| 377 | + if (R_FAILED(rc)) |
| 378 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 20)); |
| 379 | + |
| 380 | + // .data + .bss |
| 381 | + rc = svcSetProcessMemoryPermission( |
| 382 | + g_procHandle, map_addr + header->segments[2].file_off, rw_size, Perm_Rw); |
| 383 | + |
| 384 | + if (R_FAILED(rc)) |
| 385 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 21)); |
| 386 | + |
| 387 | + u64 nro_size = header->segments[2].file_off + rw_size; |
| 388 | + u64 nro_heap_start = ((u64) g_heapAddr) + nro_size; |
| 389 | + u64 nro_heap_size = g_heapSize + (u64) g_heapAddr - (u64) nro_heap_start; |
| 390 | + |
| 391 | + #define M EntryFlag_IsMandatory |
| 392 | + |
| 393 | + static ConfigEntry entries[] = { |
| 394 | + { EntryType_MainThreadHandle, 0, {0, 0} }, |
| 395 | + { EntryType_ProcessHandle, 0, {0, 0} }, |
| 396 | + { EntryType_AppletType, 0, {AppletType_LibraryApplet, 0} }, |
| 397 | + { EntryType_OverrideHeap, M, {0, 0} }, |
| 398 | + { EntryType_Argv, 0, {0, 0} }, |
| 399 | + { EntryType_NextLoadPath, 0, {0, 0} }, |
| 400 | + { EntryType_LastLoadResult, 0, {0, 0} }, |
| 401 | + { EntryType_SyscallAvailableHint, 0, {0xffffffffffffffff, 0x9fc1fff0007ffff} }, |
| 402 | + { EntryType_RandomSeed, 0, {0, 0} }, |
| 403 | + { EntryType_UserIdStorage, 0, {(u64)(uintptr_t)&g_userIdStorage, 0} }, |
| 404 | + { EntryType_HosVersion, 0, {0, 0} }, |
| 405 | + { EntryType_EndOfList, 0, {(u64)(uintptr_t)g_noticeText, sizeof(g_noticeText)} } |
| 406 | + }; |
| 407 | + |
| 408 | + ConfigEntry *entry_AppletType = &entries[2]; |
| 409 | + |
| 410 | + if (g_isApplication) { |
| 411 | + entry_AppletType->Value[0] = AppletType_SystemApplication; |
| 412 | + entry_AppletType->Value[1] = EnvAppletFlags_ApplicationOverride; |
| 413 | + } |
| 414 | + |
| 415 | + // MainThreadHandle |
| 416 | + entries[0].Value[0] = envGetMainThreadHandle(); |
| 417 | + // ProcessHandle |
| 418 | + entries[1].Value[0] = g_procHandle; |
| 419 | + // OverrideHeap |
| 420 | + entries[3].Value[0] = nro_heap_start; |
| 421 | + entries[3].Value[1] = nro_heap_size; |
| 422 | + // Argv |
| 423 | + entries[4].Value[1] = (u64) &g_argv[0]; |
| 424 | + // NextLoadPath |
| 425 | + entries[5].Value[0] = (u64) &g_nextNroPath[0]; |
| 426 | + entries[5].Value[1] = (u64) &g_nextArgv[0]; |
| 427 | + // LastLoadResult |
| 428 | + entries[6].Value[0] = g_lastRet; |
| 429 | + // RandomSeed |
| 430 | + entries[8].Value[0] = randomGet64(); |
| 431 | + entries[8].Value[1] = randomGet64(); |
| 432 | + // HosVersion |
| 433 | + entries[10].Value[0] = hosversionGet(); |
| 434 | + |
| 435 | + u64 entrypoint = map_addr; |
| 436 | + |
| 437 | + g_nroAddr = map_addr; |
| 438 | + g_nroSize = nro_size; |
| 439 | + |
| 440 | + memset(__stack_top - STACK_SIZE, 0, STACK_SIZE); |
| 441 | + |
| 442 | + if (!has_mod0) { |
| 443 | + // Apply sm-close workaround to NROs which do not contain a valid MOD0 header. |
| 444 | + // This heuristic is based on the fact that MOD0 support was added very shortly after |
| 445 | + // the fix for the sm-close bug (in fact, two commits later). |
| 446 | + g_smCloseWorkaround = true; |
| 447 | + } |
| 448 | + |
| 449 | + extern NORETURN void nroEntrypointTrampoline(u64 entries_ptr, u64 handle, u64 entrypoint); |
| 450 | + nroEntrypointTrampoline((u64) entries, -1, entrypoint); |
| 451 | +} |
| 452 | + |
| 453 | +int main(int argc, char **argv) |
| 454 | +{ |
| 455 | + memcpy(g_savedTls, (u8*)armGetTls() + 0x100, 0x100); |
| 456 | + |
| 457 | + getIsApplication(); |
| 458 | + getIsAutomaticGameplayRecording(); |
| 459 | + smExit(); // Close SM as we don't need it anymore. |
| 460 | + setupHbHeap(); |
| 461 | + getOwnProcessHandle(); |
| 462 | + loadNro(); |
| 463 | + |
| 464 | + fatalThrow(MAKERESULT(Module_HomebrewLoader, 8)); |
| 465 | + return 0; |
| 466 | +} |
0 commit comments