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
This commit is contained in:
2026-03-31 13:20:19 +02:00
parent c6b6b42eda
commit 0c898a3293
6 changed files with 1001 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ Ohne verbundene Joy-Cons:
- Loescht installierte Themes (kann Startprobleme nach Updates beheben) - Loescht installierte Themes (kann Startprobleme nach Updates beheben)
- Behebt Archiv-Bit (SD-Karte) - Behebt Archiv-Bit (SD-Karte)
- Entfernt Mac-Sonderordner - Entfernt Mac-Sonderordner
- Mariko Sleep Fix (Warmboot) bei Fuse-Mismatch (Standby/Sleep-Workaround)
## Support ## 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), - 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, 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). [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)

View File

@@ -17,6 +17,8 @@
#include <unistd.h> #include <unistd.h>
#include <sys/types.h> #include <sys/types.h>
#include "../warmboot/warmboot_extractor.h"
// #include <dirent.h> // #include <dirent.h>
// #include <stdio.h> // #include <stdio.h>
#include <string.h> #include <string.h>
@@ -374,6 +376,553 @@ void m_entry_fixMacSpecialFolders(){
hidWait(); 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(){ void m_entry_stillNoBootInfo(){
gfx_clearscreen(); gfx_clearscreen();
gfx_printf("\n\n-- Meine Switch startet immer noch nicht.\n\n"); gfx_printf("\n\n-- Meine Switch startet immer noch nicht.\n\n");

View File

@@ -11,3 +11,4 @@ void m_entry_ViewCredits();
void m_entry_fixAll(); void m_entry_fixAll();
void m_entry_stillNoBootInfo(); void m_entry_stillNoBootInfo();
void m_entry_fixMacSpecialFolders(); void m_entry_fixMacSpecialFolders();
void m_entry_fixMarikoWarmbootSleep(void);

View File

@@ -41,6 +41,7 @@ enum {
FixAMS_6900, FixAMS_6900,
FixAMS_BD00, FixAMS_BD00,
MainConvenience, MainConvenience,
FixMarikoWarmbootSleep,
FixMacSpecialFolders, FixMacSpecialFolders,
MainOther, MainOther,
MainViewStillNoBootInfo, MainViewStillNoBootInfo,
@@ -66,6 +67,7 @@ MenuEntry_t mainMenuEntries[] = {
[FixAMS_BD00] = {.optionUnion = COLORTORGB(COLOR_CYAN), .name = "Fix 010000000000BD00 (MissionControl)"}, [FixAMS_BD00] = {.optionUnion = COLORTORGB(COLOR_CYAN), .name = "Fix 010000000000BD00 (MissionControl)"},
[MainConvenience] = {.optionUnion = COLORTORGB(COLOR_WHITE) | SKIPBIT, .name = "\n-- Bequemlichkeit --"}, [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"}, [FixMacSpecialFolders] = {.optionUnion = COLORTORGB(COLOR_ORANGE), .name = "Entferne spezielle MacOS Dateien"},
[MainOther] = {.optionUnion = COLORTORGB(COLOR_WHITE) | SKIPBIT, .name = "\n-- Sonstiges --"}, [MainOther] = {.optionUnion = COLORTORGB(COLOR_WHITE) | SKIPBIT, .name = "\n-- Sonstiges --"},
@@ -135,6 +137,7 @@ menuPaths mainMenuPaths[] = {
[FixAMS_6900] = m_entry_fixAMSError_6900, [FixAMS_6900] = m_entry_fixAMSError_6900,
[FixAMS_BD00] = m_entry_fixAMSError_BD00, [FixAMS_BD00] = m_entry_fixAMSError_BD00,
[FixMacSpecialFolders] = m_entry_fixMacSpecialFolders, [FixMacSpecialFolders] = m_entry_fixMacSpecialFolders,
[FixMarikoWarmbootSleep] = m_entry_fixMarikoWarmbootSleep,
// [FixAll] = m_entry_fixAll, // [FixAll] = m_entry_fixAll,
[MainViewStillNoBootInfo] = m_entry_stillNoBootInfo, [MainViewStillNoBootInfo] = m_entry_stillNoBootInfo,
[MainRebootHekate] = RebootToHekate, [MainRebootHekate] = RebootToHekate,
@@ -152,6 +155,7 @@ void EnterMainMenu(){
// // -- Exit -- // // -- Exit --
mainMenuEntries[MainRebootHekate].hide = (!sd_get_card_mounted() || !FileExists("sd:/bootloader/update.bin")); mainMenuEntries[MainRebootHekate].hide = (!sd_get_card_mounted() || !FileExists("sd:/bootloader/update.bin"));
mainMenuEntries[MainRebootRCM].hide = h_cfg.t210b01; mainMenuEntries[MainRebootRCM].hide = h_cfg.t210b01;
mainMenuEntries[FixMarikoWarmbootSleep].hide = !h_cfg.t210b01;
gfx_clearscreen(); gfx_clearscreen();
gfx_putc('\n'); gfx_putc('\n');

View File

@@ -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 <string.h>
#include <mem/heap.h>
#include <soc/fuse.h>
#include <soc/timer.h>
#include <sec/se.h>
#include "../storage/emummc.h"
#include "../storage/mountmanager.h"
#include <storage/emmc.h>
#include <libs/fatfs/ff.h>
#include <utils/sprintf.h>
// 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);
}

View File

@@ -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 <stddef.h>
#include <utils/types.h>
// 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_ */