From 0c898a3293200ee116e6aea14da7a893736cc5f4 Mon Sep 17 00:00:00 2001 From: niklascfw Date: Tue, 31 Mar 2026 13:20:19 +0200 Subject: [PATCH] feat: Mariko warmboot extraction, hekate_ipl.ini update, docs - Add warmboot_extractor (Sthetix-derived) for PKG1 extraction and sd save - m_entry_fixMarikoWarmbootSleep: generate wb_xx.bin, INI warmboot= for [CFW-EmuMMC] or section chooser; blank-line handling for Hekate parsing - Menu: Mariko Sleep Fix under Bequemlichkeit; theme fix under MainAMS - README: Funktionen + Danksagungen for warmboot/Fuse-Mismatch workaround - Fix listdir memory corruption in Mac special-folder cleanup (apl.c) Made-with: Cursor --- README.md | 2 + source/apl/apl.c | 549 +++++++++++++++++++++++++++ source/apl/apl.h | 1 + source/tegraexplorer/mainmenu.c | 4 + source/warmboot/warmboot_extractor.c | 359 ++++++++++++++++++ source/warmboot/warmboot_extractor.h | 86 +++++ 6 files changed, 1001 insertions(+) create mode 100644 source/warmboot/warmboot_extractor.c create mode 100644 source/warmboot/warmboot_extractor.h diff --git a/README.md b/README.md index 4ff90b5..afe7f8a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Ohne verbundene Joy-Cons: - Loescht installierte Themes (kann Startprobleme nach Updates beheben) - Behebt Archiv-Bit (SD-Karte) - Entfernt Mac-Sonderordner +- Mariko Sleep Fix (Warmboot) bei Fuse-Mismatch (Standby/Sleep-Workaround) ## Support @@ -39,3 +40,4 @@ Fuer Hilfe: [unser Discord](https://discord.gg/VkaRjYN) - Dieses Projekt basiert stark auf dem [Original Common Problem Resolver](https://github.com/Team-Neptune/CommonProblemResolver) von [Team Neptune](https://github.com/Team-Neptune), sowie auf [TegraExplorer](https://github.com/suchmememanyskill/TegraExplorer) von suchmememanyskill, [Lockpick_RCM](https://github.com/shchmue/Lockpick_RCM) und [Hekate](https://github.com/CTCaer/hekate). +- Warmboot Extractor (Sthetix): extrahiert Warmboot-Caches bei Fuse-Mismatch (Standby/Sleep-Workaround auf Mariko) diff --git a/source/apl/apl.c b/source/apl/apl.c index a9488b0..fd09357 100644 --- a/source/apl/apl.c +++ b/source/apl/apl.c @@ -17,6 +17,8 @@ #include #include + +#include "../warmboot/warmboot_extractor.h" // #include // #include #include @@ -374,6 +376,553 @@ void m_entry_fixMacSpecialFolders(){ hidWait(); } +static u32 _hex_nibble_lower(char c) +{ + if (c >= '0' && c <= '9') + return (u32)(c - '0'); + if (c >= 'a' && c <= 'f') + return (u32)(c - 'a' + 10); + return 0xFFFFFFFFu; +} + +static bool _parse_wb_filename_lower(const char *fname, u32 *fuse_out) +{ + // Expected: wb_xx.bin where xx is lowercase hex (exactly 2 chars). + // Example: wb_16.bin + if (!fname || !fuse_out) + return false; + + // Length check: "wb_" + 2 + ".bin" = 3 + 2 + 4 = 9 + if (strlen(fname) != 9) + return false; + if (strncmp(fname, "wb_", 3) != 0) + return false; + if (fname[5] != '.') + return false; + if (strncmp(fname + 6, "bin", 3) != 0) + return false; + + u32 hi = _hex_nibble_lower(fname[3]); + u32 lo = _hex_nibble_lower(fname[4]); + if (hi == 0xFFFFFFFFu || lo == 0xFFFFFFFFu) + return false; + + *fuse_out = (hi << 4) | lo; + return true; +} + +static void _rstrip(char *s) +{ + if (!s) + return; + int n = (int)strlen(s); + while (n > 0) + { + char c = s[n - 1]; + if (c == '\n' || c == '\r' || c == ' ' || c == '\t') + s[--n] = 0; + else + break; + } +} + +static char *_ltrim(char *s) +{ + if (!s) + return s; + while (*s == ' ' || *s == '\t') + s++; + return s; +} + +static bool _read_line_no_nl(FIL *fp, char *out, u32 out_sz) +{ + if (!fp || !out || out_sz < 2) + return false; + + u32 idx = 0; + BYTE b; + UINT br = 0; + + // Return false on immediate EOF. + for (;;) + { + if (idx >= out_sz - 1) + break; + + FRESULT res = f_read(fp, &b, 1, &br); + if (res != FR_OK || br == 0) + break; + + if (b == '\n') + break; + if (b == '\r') + continue; + + out[idx++] = (char)b; + } + + out[idx] = 0; + return idx > 0 || br != 0; +} + +static bool _ini_section_exists(const char *ini_path, const char *section_name) +{ + FIL fp; + if (f_open(&fp, ini_path, FA_READ) != FR_OK) + return false; + + char line[256]; + bool found = false; + + while (_read_line_no_nl(&fp, line, sizeof(line))) + { + char *t = _ltrim(line); + if (*t == '[') + { + char *end = strchr(t, ']'); + if (end) + { + *end = 0; + if (!strcmp(t + 1, section_name)) + { + found = true; + break; + } + *end = ']'; + } + } + } + + f_close(&fp); + return found; +} + +static bool _ini_section_warmboot_is(const char *ini_path, const char *section_name, const char *warmboot_value_rel) +{ + FIL fp; + if (f_open(&fp, ini_path, FA_READ) != FR_OK) + return false; + + char line[256]; + bool in_section = false; + bool is_set = false; + + while (_read_line_no_nl(&fp, line, sizeof(line))) + { + char *t = _ltrim(line); + if (*t == '[') + { + char *end = strchr(t, ']'); + if (end) + { + *end = 0; + in_section = !strcmp(t + 1, section_name); + *end = ']'; + } + continue; + } + + if (!in_section) + continue; + + char *k = _ltrim(line); + if (strncmp(k, "warmboot", 8) != 0) + continue; + + // Ensure this is the "warmboot" key (not warmboot2/etc), allowing whitespace before '='. + char *p = k + 8; + while (*p == ' ' || *p == '\t') + p++; + if (*p != '=') + continue; + p++; + p = _ltrim(p); + _rstrip(p); + + if (!strcmp(p, warmboot_value_rel)) + { + is_set = true; + break; + } + } + + f_close(&fp); + return is_set; +} + +static bool _ini_update_section_warmboot(const char *ini_path, const char *section_name, const char *warmboot_value_rel) +{ + char tmp_path[128]; + u32 len = strlen(ini_path); + if (len + 4 >= sizeof(tmp_path)) + return false; + strcpy(tmp_path, ini_path); + strcat(tmp_path, ".tmp"); + + FIL in_fp; + FIL out_fp; + if (f_open(&in_fp, ini_path, FA_READ) != FR_OK) + return false; + if (f_open(&out_fp, tmp_path, FA_WRITE | FA_CREATE_ALWAYS) != FR_OK) + { + f_close(&in_fp); + return false; + } + + char line[256]; + bool in_section = false; + bool warmboot_seen = false; + bool wrote_any = false; + // Buffer trailing empty lines in the target section so the inserted/updated + // warmboot=... line stays directly after the last non-empty line. + #define MAX_PENDING_BLANKS 32 + char pending_blanks[MAX_PENDING_BLANKS][256]; + u32 pending_blank_count = 0; + + while (_read_line_no_nl(&in_fp, line, sizeof(line))) + { + char *t = _ltrim(line); + + // Section header? + if (*t == '[') + { + if (in_section && !warmboot_seen) + { + f_printf(&out_fp, "warmboot=%s\n", warmboot_value_rel); + warmboot_seen = true; + wrote_any = true; + } + + // Flush buffered empty lines after warmboot, so they don't split it. + for (u32 i = 0; i < pending_blank_count; i++) + { + f_printf(&out_fp, "%s\n", pending_blanks[i]); + } + pending_blank_count = 0; + + char *end = strchr(t, ']'); + if (end) + { + *end = 0; + in_section = !strcmp(t + 1, section_name); + *end = ']'; + } + + f_printf(&out_fp, "%s\n", line); + wrote_any = true; + continue; + } + + if (in_section) + { + char *k = _ltrim(line); + if (strncmp(k, "warmboot", 8) == 0) + { + // Replace existing warmboot=... line inside the section. + // Allow whitespace between key and '='. + char *p = k + 8; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '=') + { + // Keep the buffered empty lines where the original warmboot key appeared. + for (u32 i = 0; i < pending_blank_count; i++) + { + f_printf(&out_fp, "%s\n", pending_blanks[i]); + wrote_any = true; + } + pending_blank_count = 0; + + f_printf(&out_fp, "warmboot=%s\n", warmboot_value_rel); + warmboot_seen = true; + wrote_any = true; + continue; + } + } + } + + // If we're in the target section and warmboot isn't present yet, buffer empty lines + // so warmboot will not be separated from the last non-empty entry. + if (in_section && !warmboot_seen) + { + if (*t == 0) + { + if (pending_blank_count < MAX_PENDING_BLANKS) + { + strncpy(pending_blanks[pending_blank_count], line, 255); + pending_blanks[pending_blank_count][255] = 0; + pending_blank_count++; + } + else + { + for (u32 i = 0; i < pending_blank_count; i++) + { + f_printf(&out_fp, "%s\n", pending_blanks[i]); + wrote_any = true; + } + pending_blank_count = 0; + + f_printf(&out_fp, "%s\n", line); + wrote_any = true; + } + continue; + } + + // Non-empty line: flush buffered blanks first. + for (u32 i = 0; i < pending_blank_count; i++) + { + f_printf(&out_fp, "%s\n", pending_blanks[i]); + wrote_any = true; + } + pending_blank_count = 0; + } + + f_printf(&out_fp, "%s\n", line); + wrote_any = true; + } + + if (in_section && !warmboot_seen) + { + f_printf(&out_fp, "warmboot=%s\n", warmboot_value_rel); + wrote_any = true; + } + + // Flush trailing empty lines at EOF (after warmboot) if any. + for (u32 i = 0; i < pending_blank_count; i++) + { + f_printf(&out_fp, "%s\n", pending_blanks[i]); + wrote_any = true; + } + pending_blank_count = 0; + + f_close(&in_fp); + f_close(&out_fp); + + if (!wrote_any) + return false; + + // Replace original with temp. + f_unlink(ini_path); + f_rename(tmp_path, ini_path); + return true; +} + +void m_entry_fixMarikoWarmbootSleep(void) +{ + gfx_clearscreen(); + gfx_printf("\n\n-- Mariko Sleep Fix (Warmboot) --\n\n"); + + char warmboot_rel[64] = {0}; + bool warmboot_generated = false; + + // 1) Generate a fresh warmboot for the currently burnt fuses. + // Then we directly update the INI using that result. + gfx_printf("Erzeuge neuen Warmboot Cache...\n\n"); + + warmboot_info_t wb_info; + wb_extract_error_t wberr = extract_warmboot_from_pkg1_ex(&wb_info); + if (wberr == WB_SUCCESS) + { + char warmboot_sd_path[128]; + get_warmboot_path(warmboot_sd_path, sizeof(warmboot_sd_path), wb_info.burnt_fuses); + + if (save_warmboot_to_sd(&wb_info, warmboot_sd_path)) + { + s_printf(warmboot_rel, "warmboot_mariko/wb_%02x.bin", wb_info.burnt_fuses); + warmboot_generated = true; + gfx_printf("Neues warmboot: %s\n\n", warmboot_rel); + } + else + { + gfx_printf("Speichern auf SD fehlgeschlagen.\n\n"); + } + } + else + { + gfx_printf("Warmboot Extraktion fehlgeschlagen: %s\n\n", wb_error_to_string(wberr)); + } + + if (wb_info.data) + free(wb_info.data); + + // 2) Fallback: pick the newest cached wb_xx.bin from sd:/warmboot_mariko. + if (!warmboot_generated) + { + const char *wb_dir = "sd:/warmboot_mariko"; + + DIR dir; + static FILINFO fno; + if (f_opendir(&dir, wb_dir) != FR_OK) + { + gfx_printf("Ordner nicht gefunden: %s\n\n", wb_dir); + gfx_printf("Bitte erst Warmboot-Extractor ausfuehren:\n"); + gfx_printf("https://github.com/sthetix/Warmboot-Extractor\n\n"); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + u32 best_fuse = 0; + u32 best_ts = 0; + bool found_any = false; + + for (;;) + { + FRESULT res = f_readdir(&dir, &fno); + if (res != FR_OK || fno.fname[0] == 0) + break; + if (fno.fattrib & AM_DIR) + continue; + + u32 fuse = 0; + if (!_parse_wb_filename_lower(fno.fname, &fuse)) + continue; + + u32 ts = ((u32)fno.fdate << 16) | (u32)fno.ftime; + if (!found_any || fuse > best_fuse || (fuse == best_fuse && ts > best_ts)) + { + best_fuse = fuse; + best_ts = ts; + found_any = true; + } + } + + f_closedir(&dir); + + if (!found_any) + { + gfx_printf("Keine passenden Dateien gefunden in %s:\n", wb_dir); + gfx_printf("wb_xx.bin (xx = lowercase hex fuse)\n\n"); + gfx_printf("Bitte Warmboot-Extractor ausfuehren:\n"); + gfx_printf("https://github.com/sthetix/Warmboot-Extractor\n\n"); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + s_printf(warmboot_rel, "warmboot_mariko/wb_%02x.bin", best_fuse); + gfx_printf("Neuestes warmboot: %s\n\n", warmboot_rel); + } + + const char *ini_path = "sd:/bootloader/hekate_ipl.ini"; + const char *preferred_section = "CFW-EmuMMC"; + + if (!_ini_section_exists(ini_path, preferred_section)) + { + // Fallback: show a section chooser if CFW-EmuMMC doesn't exist. + gfx_printf("Section %s nicht gefunden.\n", preferred_section); + gfx_printf("Bitte waehle eine INI section.\n\n"); + + // Collect section names. + #define MAX_INI_SECTIONS 20 + char section_names[MAX_INI_SECTIONS][64]; + int section_count = 0; + + FIL ini_fp; + if (f_open(&ini_fp, ini_path, FA_READ) != FR_OK) + { + gfx_printf("Datei nicht gefunden: %s\n", ini_path); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + char line[256]; + while (_read_line_no_nl(&ini_fp, line, sizeof(line))) + { + char *t = _ltrim(line); + if (*t != '[') + continue; + char *end = strchr(t, ']'); + if (!end) + continue; + + *end = 0; + char *name = t + 1; + + name = _ltrim(name); + _rstrip(name); + + // Skip empty section headers like "[]" + if (!*name) + { + *end = ']'; + continue; + } + + if (strcmp(name, "config") && section_count < MAX_INI_SECTIONS) + { + strncpy(section_names[section_count], name, 63); + section_names[section_count][63] = 0; + section_count++; + } + *end = ']'; + } + f_close(&ini_fp); + + if (section_count <= 0) + { + gfx_printf("Keine INI sections gefunden in %s.\n", ini_path); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + MenuEntry_t entries[MAX_INI_SECTIONS + 1]; + memset(entries, 0, sizeof(entries)); + entries[0].optionUnion = COLORTORGB(COLOR_WHITE); + entries[0].name = "<- Zurueck"; + for (int i = 0; i < section_count; i++) + { + entries[i + 1].optionUnion = COLORTORGB(COLOR_YELLOW); + entries[i + 1].name = section_names[i]; + } + + Vector_t ent = vecFromArray(entries, section_count + 1, sizeof(MenuEntry_t)); + int sel = newMenu(&ent, 0, 79, 20, ENABLEB | ALWAYSREDRAW, section_count + 1); + // newMenu returns 0 for "<- Back" and also when the user presses B. + if (sel == 0) + return; + if (sel < 0 || sel > section_count) + return; + + const char *selected_section = section_names[sel - 1]; + // Continue below with chosen section. + // Clear and re-print banner so the header stays visible (success/fail). + gfx_clearscreen(); + gfx_printf("\n\n-- Mariko Sleep Fix (Warmboot) --\n\n"); + + if (_ini_section_warmboot_is(ini_path, selected_section, warmboot_rel)) + { + gfx_printf("\nwarmboot=%s existiert schon in [%s].\n", warmboot_rel, selected_section); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + _ini_update_section_warmboot(ini_path, selected_section, warmboot_rel); + gfx_printf("\nAktualisiert in [%s].\n", selected_section); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + // Preferred path: always update CFW-EmuMMC directly. + if (_ini_section_warmboot_is(ini_path, preferred_section, warmboot_rel)) + { + gfx_printf("\nwarmboot=%s existiert schon in [%s].\n", warmboot_rel, preferred_section); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); + return; + } + + _ini_update_section_warmboot(ini_path, preferred_section, warmboot_rel); + gfx_printf("\nAktualisiert in [%s].\n", preferred_section); + gfx_printf("Druecke eine Taste um zurueckzukehren"); + hidWait(); +} + void m_entry_stillNoBootInfo(){ gfx_clearscreen(); gfx_printf("\n\n-- Meine Switch startet immer noch nicht.\n\n"); diff --git a/source/apl/apl.h b/source/apl/apl.h index bee4948..0e41e07 100644 --- a/source/apl/apl.h +++ b/source/apl/apl.h @@ -11,3 +11,4 @@ void m_entry_ViewCredits(); void m_entry_fixAll(); void m_entry_stillNoBootInfo(); void m_entry_fixMacSpecialFolders(); +void m_entry_fixMarikoWarmbootSleep(void); diff --git a/source/tegraexplorer/mainmenu.c b/source/tegraexplorer/mainmenu.c index 240f5e5..9d8f2d0 100644 --- a/source/tegraexplorer/mainmenu.c +++ b/source/tegraexplorer/mainmenu.c @@ -41,6 +41,7 @@ enum { FixAMS_6900, FixAMS_BD00, MainConvenience, + FixMarikoWarmbootSleep, FixMacSpecialFolders, MainOther, MainViewStillNoBootInfo, @@ -66,6 +67,7 @@ MenuEntry_t mainMenuEntries[] = { [FixAMS_BD00] = {.optionUnion = COLORTORGB(COLOR_CYAN), .name = "Fix 010000000000BD00 (MissionControl)"}, [MainConvenience] = {.optionUnion = COLORTORGB(COLOR_WHITE) | SKIPBIT, .name = "\n-- Bequemlichkeit --"}, + [FixMarikoWarmbootSleep] = {.optionUnion = COLORTORGB(COLOR_ORANGE), .name = "Mariko Sleep Fix (Warmboot)"}, [FixMacSpecialFolders] = {.optionUnion = COLORTORGB(COLOR_ORANGE), .name = "Entferne spezielle MacOS Dateien"}, [MainOther] = {.optionUnion = COLORTORGB(COLOR_WHITE) | SKIPBIT, .name = "\n-- Sonstiges --"}, @@ -135,6 +137,7 @@ menuPaths mainMenuPaths[] = { [FixAMS_6900] = m_entry_fixAMSError_6900, [FixAMS_BD00] = m_entry_fixAMSError_BD00, [FixMacSpecialFolders] = m_entry_fixMacSpecialFolders, + [FixMarikoWarmbootSleep] = m_entry_fixMarikoWarmbootSleep, // [FixAll] = m_entry_fixAll, [MainViewStillNoBootInfo] = m_entry_stillNoBootInfo, [MainRebootHekate] = RebootToHekate, @@ -152,6 +155,7 @@ void EnterMainMenu(){ // // -- Exit -- mainMenuEntries[MainRebootHekate].hide = (!sd_get_card_mounted() || !FileExists("sd:/bootloader/update.bin")); mainMenuEntries[MainRebootRCM].hide = h_cfg.t210b01; + mainMenuEntries[FixMarikoWarmbootSleep].hide = !h_cfg.t210b01; gfx_clearscreen(); gfx_putc('\n'); diff --git a/source/warmboot/warmboot_extractor.c b/source/warmboot/warmboot_extractor.c new file mode 100644 index 0000000..1262968 --- /dev/null +++ b/source/warmboot/warmboot_extractor.c @@ -0,0 +1,359 @@ +/* + * Warmboot Extractor + * Based on Atmosphere fusee_setup_horizon.cpp + * + * Copyright (c) 2018-2025 Atmosphère-NX + */ + +#include "warmboot_extractor.h" + +#include +#include + +#include +#include + +#include + +#include "../storage/emummc.h" +#include "../storage/mountmanager.h" +#include + +#include +#include + +// Mariko keyslot for BEK (Boot Encryption Key) +#define KS_MARIKO_BEK 13 + +// Helper function to check if this is Mariko hardware +bool is_mariko(void) +{ + u32 odm4 = fuse_read_odm(4); + + // Extract hardware type from ODM4 bits (same logic as Atmosphere fuse_api.cpp) + u32 hw_type = ((odm4 >> 2) & 1) | (((odm4 >> 8) & 1) << 1) | (((odm4 >> 16) & 0xF) << 2); + + // Mariko hardware types: Iowa (0x04), Hoag (0x08), Aula (0x10) + if (hw_type == 0x04 || hw_type == 0x08 || hw_type == 0x10) + return true; + if (hw_type == 0x01) // Icosa + return false; + + // For 0x02 (Calcio/Copper), fall back to DRAM ID check + return (fuse_read_dramid(false) >= 4); +} + +// Read burnt fuse count (ODM6 + ODM7) +u8 get_burnt_fuses(void) +{ + u8 fuse_count = 0; + u32 fuse_odm6 = fuse_read_odm(6); + u32 fuse_odm7 = fuse_read_odm(7); + + // Count bits in ODM6 + for (u32 i = 0; i < 32; i++) + if ((fuse_odm6 >> i) & 1) + fuse_count++; + + // Count bits in ODM7 + for (u32 i = 0; i < 32; i++) + if ((fuse_odm7 >> i) & 1) + fuse_count++; + + return fuse_count; +} + +// Generate warmboot cache path based on fuse count +void get_warmboot_path(char *path, size_t path_size, u8 fuse_count) +{ + (void)path_size; + + // Format matches Atmosphère's convention: + // Mariko: sd:/warmboot_mariko/wb_xx.bin (lowercase hex) + // Erista: not used by Atmosphère, but kept for completeness. + if (is_mariko()) + s_printf(path, "sd:/warmboot_mariko/wb_%02x.bin", fuse_count); + else + s_printf(path, "sd:/warmboot_erista/wb_%02x.bin", fuse_count); +} + +// Get target firmware version from Package1 header (display only) +static u32 get_target_firmware_from_pkg1(const u8 *package1) +{ + switch (package1[0x1F]) + { + case 0x01: return 0x100; // 1.0.0 + case 0x02: return 0x200; // 2.0.0 + case 0x04: return 0x300; // 3.0.0 + case 0x07: return 0x400; // 4.0.0 + case 0x0B: return 0x500; // 5.0.0 + case 0x0E: + if (memcmp(package1 + 0x10, "20180802", 8) == 0) return 0x600; // 6.0.0 + if (memcmp(package1 + 0x10, "20181107", 8) == 0) return 0x620; // 6.2.0 + break; + case 0x0F: return 0x700; // 7.0.0 + case 0x10: + if (memcmp(package1 + 0x10, "20190314", 8) == 0) return 0x800; // 8.0.0 + if (memcmp(package1 + 0x10, "20190531", 8) == 0) return 0x810; // 8.1.0 + if (memcmp(package1 + 0x10, "20190809", 8) == 0) return 0x900; // 9.0.0 + if (memcmp(package1 + 0x10, "20191021", 8) == 0) return 0x910; // 9.1.0 + if (memcmp(package1 + 0x10, "20200303", 8) == 0) return 0xA00; // 10.0.0 + if (memcmp(package1 + 0x10, "20201030", 8) == 0) return 0xB00; // 11.0.0 + if (memcmp(package1 + 0x10, "20210129", 8) == 0) return 0xC00; // 12.0.0 + if (memcmp(package1 + 0x10, "20210422", 8) == 0) return 0xC02; // 12.0.2 + if (memcmp(package1 + 0x10, "20210607", 8) == 0) return 0xC10; // 12.1.0 + if (memcmp(package1 + 0x10, "20210805", 8) == 0) return 0xD00; // 13.0.0 + if (memcmp(package1 + 0x10, "20220105", 8) == 0) return 0xD21; // 13.2.1 + if (memcmp(package1 + 0x10, "20220209", 8) == 0) return 0xE00; // 14.0.0 + if (memcmp(package1 + 0x10, "20220801", 8) == 0) return 0xF00; // 15.0.0 + if (memcmp(package1 + 0x10, "20230111", 8) == 0) return 0x1000; // 16.0.0 + if (memcmp(package1 + 0x10, "20230906", 8) == 0) return 0x1100; // 17.0.0 + if (memcmp(package1 + 0x10, "20240207", 8) == 0) return 0x1200; // 18.0.0 + if (memcmp(package1 + 0x10, "20240808", 8) == 0) return 0x1300; // 19.0.0 + if (memcmp(package1 + 0x10, "20250206", 8) == 0) return 0x1400; // 20.0.0 + if (memcmp(package1 + 0x10, "20251009", 8) == 0) return 0x1500; // 21.0.0 + break; + default: + break; + } + + return 0; +} + +// Error code to string (display/debug) +const char *wb_error_to_string(wb_extract_error_t err) +{ + switch (err) + { + case WB_SUCCESS: return "Success"; + case WB_ERR_NULL_INFO: return "NULL wb_info pointer"; + case WB_ERR_ERISTA_NOT_SUPPORTED: return "Erista not supported (uses embedded warmboot)"; + case WB_ERR_MALLOC_PKG1: return "Failed to allocate Package1 buffer (256KB)"; + case WB_ERR_MMC_INIT: return "Failed to initialize eMMC"; + case WB_ERR_MMC_PARTITION: return "Failed to set BOOT0 partition"; + case WB_ERR_MMC_READ: return "Failed to read Package1 from BOOT0"; + case WB_ERR_DECRYPT_VERIFY: return "Package1 decryption failed (BEK missing or wrong)"; + case WB_ERR_PK11_MAGIC: return "PK11 magic not found (invalid Package1)"; + case WB_ERR_WB_SIZE_INVALID: return "Warmboot size invalid (not 0x800-0x1000)"; + case WB_ERR_MALLOC_WB: return "Failed to allocate warmboot buffer"; + default: return "Unknown error"; + } +} + +// Extended extraction with detailed error codes +wb_extract_error_t extract_warmboot_from_pkg1_ex(warmboot_info_t *wb_info) +{ + if (!wb_info) + return WB_ERR_NULL_INFO; + + // Initialize result + memset(wb_info, 0, sizeof(warmboot_info_t)); + wb_info->is_erista = !is_mariko(); + wb_info->fuse_count = get_burnt_fuses(); + + // Erista doesn't need warmboot extraction from Package1 + if (wb_info->is_erista) + return WB_ERR_ERISTA_NOT_SUPPORTED; + + // From here on, we're dealing with Mariko only + + // Allocate buffer for Package1 + u8 *pkg1_buffer = (u8 *)malloc(PKG1_SIZE); + if (!pkg1_buffer) + return WB_ERR_MALLOC_PKG1; + u8 *pkg1_buffer_orig = pkg1_buffer; + + // Connect eMMC (BOOT0 access) + if (connectMMC(MMC_CONN_EMMC)) + { + free(pkg1_buffer_orig); + return WB_ERR_MMC_INIT; + } + msleep(1); + + // Set to BOOT0 and read package1 + if (!emummc_storage_set_mmc_partition(&emmc_storage, EMMC_BOOT0)) + { + disconnectMMC(); + free(pkg1_buffer_orig); + return WB_ERR_MMC_PARTITION; + } + + if (!emummc_storage_read(&emmc_storage, PKG1_OFFSET / EMMC_BLOCKSIZE, PKG1_SIZE / EMMC_BLOCKSIZE, pkg1_buffer)) + { + disconnectMMC(); + free(pkg1_buffer_orig); + return WB_ERR_MMC_READ; + } + + disconnectMMC(); + + // On Mariko, Package1 is encrypted and needs decryption. + // Skip 0x170 byte header to get to the encrypted payload. + u8 *pkg1_mariko = pkg1_buffer + 0x170; + + // Set IV from package1 + 0x10 (16 bytes) + se_aes_iv_set(KS_MARIKO_BEK, pkg1_mariko + 0x10); + + // Decrypt using BEK at keyslot 13 + se_aes_crypt_cbc(KS_MARIKO_BEK, 0, /* DECRYPT */ + pkg1_mariko + 0x20, + 0x40000 - (0x20 + 0x170), + pkg1_mariko + 0x20, + 0x40000 - (0x20 + 0x170)); + + // Verify decryption (first 0x20 bytes should match decrypted header) + if (memcmp(pkg1_mariko, pkg1_mariko + 0x20, 0x20) != 0) + { + free(pkg1_buffer_orig); + return WB_ERR_DECRYPT_VERIFY; + } + + // Use pkg1_mariko as the working pointer + pkg1_buffer = pkg1_mariko; + + // Detect target firmware (display only) + u32 target_fw = get_target_firmware_from_pkg1(pkg1_buffer); + + // Store debug info: Package1 version byte and date string + wb_info->pkg1_version = pkg1_buffer[0x1F]; + memcpy(wb_info->pkg1_date, pkg1_buffer + 0x10, 8); + wb_info->pkg1_date[8] = '\0'; + + // Get burnt fuse count from device + u8 burnt_fuses = get_burnt_fuses(); + + // For warmboot naming, ALWAYS use burnt_fuses directly. + wb_info->fuse_count = burnt_fuses; + wb_info->burnt_fuses = burnt_fuses; + wb_info->target_firmware = target_fw; + + // Determine PK11 offset using firmware hint, then fall back. + u32 pk11_offset = 0x4000; + if (target_fw != 0 && target_fw >= 0x620) + pk11_offset = 0x7000; + + u8 *pk11_ptr_u8 = pkg1_buffer + pk11_offset; + bool pk11_ok = memcmp(pk11_ptr_u8, "PK11", 4) == 0; + + if (!pk11_ok) + { + pk11_offset = (pk11_offset == 0x4000) ? 0x7000 : 0x4000; + pk11_ptr_u8 = pkg1_buffer + pk11_offset; + pk11_ok = memcmp(pk11_ptr_u8, "PK11", 4) == 0; + } + + if (!pk11_ok) + { + free(pkg1_buffer_orig); + return WB_ERR_PK11_MAGIC; + } + + wb_info->pk11_offset = pk11_offset; + + u32 *pk11_ptr = (u32 *)pk11_ptr_u8; + + // Store debug header + for (int i = 0; i < 8; i++) + wb_info->pk11_header[i] = pk11_ptr[i]; + + // Navigate through PK11 container to find warmboot (Atmosphere logic) + u32 *pk11_data = pk11_ptr + (0x20 / sizeof(u32)); + + wb_info->sig_found[0] = 0; + wb_info->sig_found[1] = 0; + wb_info->sig_found[2] = 0; + + for (int i = 0; i < 3; i++) + { + u32 signature = *pk11_data; + wb_info->sig_found[i] = signature; + + switch (signature) + { + case SIG_NX_BOOTLOADER: + pk11_data += pk11_ptr[6] / sizeof(u32); + break; + case SIG_SECURE_MONITOR_1: + case SIG_SECURE_MONITOR_2: + pk11_data += pk11_ptr[4] / sizeof(u32); + break; + default: + i = 3; + break; + } + } + + wb_info->debug_ptr_offset = (u32)((u8 *)pk11_data - pk11_ptr_u8); + + u32 wb_size = *pk11_data; + + // Debug layout type + u32 warmboot_size_header = pk11_ptr[1]; + if (warmboot_size_header == wb_size && + warmboot_size_header >= WARMBOOT_MIN_SIZE && + warmboot_size_header < WARMBOOT_MAX_SIZE) + wb_info->debug_layout_type = 1; + else + wb_info->debug_layout_type = 2; + + if (wb_size < WARMBOOT_MIN_SIZE || wb_size >= WARMBOOT_MAX_SIZE) + { + wb_info->size = wb_size; + free(pkg1_buffer_orig); + return WB_ERR_WB_SIZE_INVALID; + } + + wb_info->size = wb_size; + + memcpy(wb_info->debug_warmboot_preview, (u8 *)pk11_data, 16); + + // Allocate warmboot buffer ([size_u32][warmboot_binary...]) + u8 *wb_data = (u8 *)malloc(wb_size); + if (!wb_data) + { + free(pkg1_buffer_orig); + return WB_ERR_MALLOC_WB; + } + + memcpy(wb_data, (u8 *)pk11_data, wb_size); + + wb_info->data = wb_data; + wb_info->size = wb_size; + + free(pkg1_buffer_orig); + return WB_SUCCESS; +} + +// Original wrapper for backward compatibility +bool extract_warmboot_from_pkg1(warmboot_info_t *wb_info) +{ + return extract_warmboot_from_pkg1_ex(wb_info) == WB_SUCCESS; +} + +// Save warmboot to SD card +bool save_warmboot_to_sd(const warmboot_info_t *wb_info, const char *path) +{ + if (!wb_info || !wb_info->data || !path) + return false; + + FIL fp; + UINT bytes_written = 0; + + const char *dir_path = wb_info->is_erista ? "sd:/warmboot_erista" : "sd:/warmboot_mariko"; + f_mkdir(dir_path); + + if (f_open(&fp, path, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) + return false; + + if (f_write(&fp, wb_info->data, wb_info->size, &bytes_written) != FR_OK) + { + f_close(&fp); + return false; + } + + f_close(&fp); + + return (bytes_written == wb_info->size); +} + diff --git a/source/warmboot/warmboot_extractor.h b/source/warmboot/warmboot_extractor.h new file mode 100644 index 0000000..7e5388b --- /dev/null +++ b/source/warmboot/warmboot_extractor.h @@ -0,0 +1,86 @@ +/* + * Warmboot Extractor + * Based on Atmosphere fusee_setup_horizon.cpp + * + * Copyright (c) 2018-2025 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as published + * by the Free Software Foundation. + */ + +#ifndef _WARMBOOT_EXTRACTOR_H_ +#define _WARMBOOT_EXTRACTOR_H_ + +#include +#include + +// Warmboot binary size constraints +#define WARMBOOT_MIN_SIZE 0x800 // 2048 bytes +#define WARMBOOT_MAX_SIZE 0x1000 // 4096 bytes + +// Package1 locations +#define PKG1_OFFSET 0x100000 // 1MB into BOOT0 +#define PKG1_SIZE 0x40000 // 256KB + +// PK11 magic +#define PK11_MAGIC 0x31314B50 // "PK11" in little endian + +// Known payload signatures to skip +#define SIG_NX_BOOTLOADER 0xD5034FDF +#define SIG_SECURE_MONITOR_1 0xE328F0C0 +#define SIG_SECURE_MONITOR_2 0xF0C0A7F0 + +// Warmboot metadata structure +typedef struct { + u32 magic; // "WBT0" (0x30544257) + u32 target_firmware; // Target firmware version + u32 reserved[2]; // Reserved fields +} warmboot_metadata_t; + +// Warmboot extraction result +typedef struct { + u8 *data; // Warmboot binary data + u32 size; // Size of warmboot binary + u8 fuse_count; // Burnt fuse count (used for naming: wb_XX.bin) + u8 burnt_fuses; // Actual burnt fuses on device (same as fuse_count) + u32 target_firmware; // Detected target firmware (0 if unknown, for display only) + bool is_erista; // True if Erista, false if Mariko + + // Debug fields for troubleshooting + u32 pk11_offset; + u32 pk11_header[8]; + u32 sig_found[3]; + u32 debug_ptr_offset; + u32 debug_layout_type; + u8 debug_warmboot_preview[16]; + u8 pkg1_version; // Package1 version byte at offset 0x1F + u8 pkg1_date[12]; // Package1 date string (8 chars + null) +} warmboot_info_t; + +// Extraction error codes +typedef enum { + WB_SUCCESS = 0, + WB_ERR_NULL_INFO, + WB_ERR_ERISTA_NOT_SUPPORTED, + WB_ERR_MALLOC_PKG1, + WB_ERR_MMC_INIT, + WB_ERR_MMC_PARTITION, + WB_ERR_MMC_READ, + WB_ERR_DECRYPT_VERIFY, + WB_ERR_PK11_MAGIC, + WB_ERR_WB_SIZE_INVALID, + WB_ERR_MALLOC_WB, +} wb_extract_error_t; + +// Function prototypes +wb_extract_error_t extract_warmboot_from_pkg1_ex(warmboot_info_t *wb_info); +bool extract_warmboot_from_pkg1(warmboot_info_t *wb_info); +bool save_warmboot_to_sd(const warmboot_info_t *wb_info, const char *path); +u8 get_burnt_fuses(void); +bool is_mariko(void); +void get_warmboot_path(char *path, size_t path_size, u8 fuse_count); +const char *wb_error_to_string(wb_extract_error_t err); + +#endif /* _WARMBOOT_EXTRACTOR_H_ */ +