Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c711ed6253 | ||
|
|
356268eae5 | ||
|
|
2dd64b8a92 | ||
|
|
b5cddedb7d | ||
|
|
7afdc2b3b5 | ||
|
|
60c4956290 | ||
|
|
7331a919e4 | ||
|
|
33238cee54 | ||
|
|
384dd2ad81 | ||
|
|
74ac76ba84 | ||
|
|
253e031f83 | ||
|
|
ef1ecf859c | ||
|
|
a302ad3bea | ||
|
|
b87dadbb72 | ||
|
|
50a2424001 | ||
|
|
4f8c66b2b7 | ||
|
|
4d9cbc4e19 | ||
|
|
40369d44df | ||
|
|
ee3720f0b7 | ||
|
|
e5dcca1c2b |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,12 +1,12 @@
|
||||
out
|
||||
build
|
||||
loader/build
|
||||
screeninit/build
|
||||
injector/build
|
||||
exceptions/arm9/build
|
||||
exceptions/arm11/build
|
||||
*.bin
|
||||
*.3dsx
|
||||
*.smdh
|
||||
*.o
|
||||
*.d
|
||||
*.elf
|
||||
*.bat
|
||||
15
Makefile
15
Makefile
@@ -13,6 +13,7 @@ OC := arm-none-eabi-objcopy
|
||||
|
||||
name := Luma3DS
|
||||
revision := $(shell git describe --tags --match v[0-9]* --abbrev=8 | sed 's/-[0-9]*-g/-/i')
|
||||
commit := $(shell git rev-parse --short=8 HEAD)
|
||||
|
||||
dir_source := source
|
||||
dir_patches := patches
|
||||
@@ -32,7 +33,8 @@ objects = $(patsubst $(dir_source)/%.s, $(dir_build)/%.o, \
|
||||
$(patsubst $(dir_source)/%.c, $(dir_build)/%.o, \
|
||||
$(call rwildcard, $(dir_source), *.s *.c)))
|
||||
|
||||
bundled = $(dir_build)/rebootpatch.h $(dir_build)/emunandpatch.h $(dir_build)/injector.h $(dir_build)/loader.h
|
||||
bundled = $(dir_build)/rebootpatch.h $(dir_build)/emunandpatch.h $(dir_build)/svcGetCFWInfopatch.h $(dir_build)/twl_k11modulespatch.h \
|
||||
$(dir_build)/injector.h $(dir_build)/loader.h
|
||||
|
||||
.PHONY: all
|
||||
all: launcher a9lh ninjhax
|
||||
@@ -91,6 +93,16 @@ $(dir_build)/rebootpatch.h: $(dir_patches)/reboot.s
|
||||
@armips $<
|
||||
@bin2c -o $@ -n reboot $(@D)/reboot.bin
|
||||
|
||||
$(dir_build)/svcGetCFWInfopatch.h: $(dir_patches)/svcGetCFWInfo.s
|
||||
@mkdir -p "$(@D)"
|
||||
@armips $<
|
||||
@bin2c -o $@ -n svcGetCFWInfo $(@D)/svcGetCFWInfo.bin
|
||||
|
||||
$(dir_build)/twl_k11modulespatch.h: $(dir_patches)/twl_k11modules.s
|
||||
@mkdir -p "$(@D)"
|
||||
@armips $<
|
||||
@bin2c -o $@ -n twl_k11modules $(@D)/twl_k11modules.bin
|
||||
|
||||
$(dir_build)/injector.h: $(dir_injector)/Makefile
|
||||
@mkdir -p "$(@D)"
|
||||
@$(MAKE) -C $(dir_injector)
|
||||
@@ -102,6 +114,7 @@ $(dir_build)/loader.h: $(dir_loader)/Makefile
|
||||
|
||||
$(dir_build)/memory.o: CFLAGS += -O3
|
||||
$(dir_build)/config.o: CFLAGS += -DCONFIG_TITLE="\"$(name) $(revision) configuration\""
|
||||
$(dir_build)/patches.o: CFLAGS += -DREVISION=\"$(revision)\" -DCOMMIT_HASH="0x$(commit)"
|
||||
|
||||
$(dir_build)/%.o: $(dir_source)/%.c $(bundled)
|
||||
@mkdir -p "$(@D)"
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
#include "patcher.h"
|
||||
#include "ifile.h"
|
||||
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 255
|
||||
#define CONFIG(a) (((loadConfig() >> (a + 16)) & 1) != 0)
|
||||
#define MULTICONFIG(a) ((loadConfig() >> (a * 2 + 6)) & 3)
|
||||
#define BOOTCONFIG(a, b) ((loadConfig() >> a) & b)
|
||||
#endif
|
||||
static CFWInfo info = {0};
|
||||
|
||||
static int memcmp(const void *buf1, const void *buf2, u32 size)
|
||||
{
|
||||
@@ -90,6 +85,28 @@ static int fileOpen(IFile *file, FS_ArchiveID archiveId, const char *path, int f
|
||||
return IFile_Open(file, archiveId, archivePath, filePath, flags);
|
||||
}
|
||||
|
||||
int __attribute__((naked)) svcGetCFWInfo(CFWInfo __attribute__((unused)) *out)
|
||||
{
|
||||
__asm__ volatile("svc 0x2E; bx lr");
|
||||
}
|
||||
|
||||
static void loadCFWInfo(void)
|
||||
{
|
||||
static bool infoLoaded = false;
|
||||
|
||||
if(!infoLoaded)
|
||||
{
|
||||
svcGetCFWInfo(&info);
|
||||
IFile file;
|
||||
if(BOOTCONFIG(5, 1) && R_SUCCEEDED(fileOpen(&file, ARCHIVE_SDMC, "/", FS_OPEN_READ))) //Init SD card if SAFE_MODE is being booted
|
||||
{
|
||||
IFile_Close(&file);
|
||||
}
|
||||
|
||||
infoLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool secureInfoExists(void)
|
||||
{
|
||||
static bool exists = false;
|
||||
@@ -107,24 +124,6 @@ static bool secureInfoExists(void)
|
||||
return exists;
|
||||
}
|
||||
|
||||
static u32 loadConfig(void)
|
||||
{
|
||||
static u32 config = 0;
|
||||
|
||||
if(!config)
|
||||
{
|
||||
IFile file;
|
||||
if(R_SUCCEEDED(fileOpen(&file, ARCHIVE_SDMC, "/luma/config.bin", FS_OPEN_READ)))
|
||||
{
|
||||
u64 total;
|
||||
if(R_SUCCEEDED(IFile_Read(&file, &total, &config, 4))) config |= 1 << 4;
|
||||
IFile_Close(&file);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
static void progIdToStr(char *strEnd, u64 progId)
|
||||
{
|
||||
while(progId)
|
||||
@@ -319,6 +318,8 @@ static void patchCfgGetRegion(u8 *code, u32 size, u8 regionId, u32 CFGUHandleOff
|
||||
|
||||
void patchCode(u64 progId, u8 *code, u32 size)
|
||||
{
|
||||
loadCFWInfo();
|
||||
|
||||
switch(progId)
|
||||
{
|
||||
case 0x0004003000008F02LL: // USA Menu
|
||||
|
||||
@@ -2,4 +2,24 @@
|
||||
|
||||
#include <3ds/types.h>
|
||||
|
||||
#define PATH_MAX 255
|
||||
|
||||
#define CONFIG(a) (((info.config >> (a + 16)) & 1) != 0)
|
||||
#define MULTICONFIG(a) ((info.config >> (a * 2 + 6)) & 3)
|
||||
#define BOOTCONFIG(a, b) ((info.config >> a) & b)
|
||||
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
char magic[4];
|
||||
|
||||
u8 versionMajor;
|
||||
u8 versionMinor;
|
||||
u8 versionBuild;
|
||||
u8 flags; /* bit 0: dev branch; bit 1: is release */
|
||||
|
||||
u32 commitHash;
|
||||
|
||||
u32 config;
|
||||
} CFWInfo;
|
||||
|
||||
void patchCode(u64 progId, u8 *code, u32 size);
|
||||
@@ -23,11 +23,13 @@
|
||||
#include "memory.h"
|
||||
#include "cache.h"
|
||||
|
||||
extern u32 payloadSize; //defined in start.s
|
||||
|
||||
void main(void)
|
||||
{
|
||||
void *payloadAddress = (void *)0x23F00000;
|
||||
|
||||
memcpy(payloadAddress, (void*)0x24F00000, *(u32 *)0x24FFFF04);
|
||||
memcpy(payloadAddress, (void*)0x24F00000, payloadSize);
|
||||
|
||||
flushCaches();
|
||||
|
||||
|
||||
@@ -24,4 +24,6 @@
|
||||
_start:
|
||||
b main
|
||||
|
||||
.global payloadSize
|
||||
payloadSize:
|
||||
.word 0
|
||||
|
||||
48
patches/svcGetCFWInfo.s
Normal file
48
patches/svcGetCFWInfo.s
Normal file
@@ -0,0 +1,48 @@
|
||||
;
|
||||
; This file is part of Luma3DS
|
||||
; Copyright (C) 2016 Aurora Wright, TuxSH
|
||||
;
|
||||
; This program is free software: you can redistribute it and/or modify
|
||||
; it under the terms of the GNU General Public License as published by
|
||||
; the Free Software Foundation, either version 3 of the License, or
|
||||
; (at your option) any later version.
|
||||
;
|
||||
; This program is distributed in the hope that it will be useful,
|
||||
; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
; GNU General Public License for more details.
|
||||
;
|
||||
; You should have received a copy of the GNU General Public License
|
||||
; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
;
|
||||
; Additional Terms 7.b of GPLv3 applies to this file: Requiring preservation of specified
|
||||
; reasonable legal notices or author attributions in that material or in the Appropriate Legal
|
||||
; Notices displayed by works containing it.
|
||||
;
|
||||
|
||||
.arm.little
|
||||
|
||||
.create "build/svcGetCFWInfo.bin", 0
|
||||
|
||||
.arm
|
||||
|
||||
adr r1, infoStart
|
||||
add r2, r0, #(infoEnd - infoStart)
|
||||
|
||||
loop:
|
||||
ldrb r3, [r1], #1
|
||||
strbt r3, [r0], #1
|
||||
cmp r0, r2
|
||||
blo loop
|
||||
|
||||
mov r0, #0
|
||||
bx lr
|
||||
|
||||
.pool
|
||||
infoStart:
|
||||
.ascii "LUMA" ; magic
|
||||
.word 0 ; version
|
||||
.word 0 ; truncated commit hash
|
||||
.word 0 ; config
|
||||
infoEnd:
|
||||
.close
|
||||
144
patches/twl_k11modules.s
Normal file
144
patches/twl_k11modules.s
Normal file
@@ -0,0 +1,144 @@
|
||||
;
|
||||
; This file is part of Luma3DS
|
||||
; Copyright (C) 2016 Aurora Wright, TuxSH
|
||||
;
|
||||
; This program is free software: you can redistribute it and/or modify
|
||||
; it under the terms of the GNU General Public License as published by
|
||||
; the Free Software Foundation, either version 3 of the License, or
|
||||
; (at your option) any later version.
|
||||
;
|
||||
; This program is distributed in the hope that it will be useful,
|
||||
; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
; GNU General Public License for more details.
|
||||
;
|
||||
; You should have received a copy of the GNU General Public License
|
||||
; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
;
|
||||
; Additional Terms 7.b of GPLv3 applies to this file: Requiring preservation of specified
|
||||
; reasonable legal notices or author attributions in that material or in the Appropriate Legal
|
||||
; Notices displayed by works containing it.
|
||||
;
|
||||
|
||||
.arm.little
|
||||
|
||||
.create "build/twl_k11modules.bin", 0
|
||||
|
||||
.align 4
|
||||
.arm
|
||||
|
||||
patch:
|
||||
; r4: Pointer to a pointer to the exheader of the current NCCH
|
||||
; sp + 0xb0 - 0xa4: Pointer to the memory location where the NCCH text was loaded
|
||||
|
||||
add r3, sp, #(0xb0 - 0xa4)
|
||||
add r1, sp, #(0xb0 - 0xac)
|
||||
|
||||
push {r0-r11, lr}
|
||||
|
||||
ldr r9, [r3] ; load the address of the code section
|
||||
ldr r8, [r4] ; load the address of the exheader
|
||||
|
||||
ldr r7, [r8, #0x200] ; low titleID
|
||||
ldr r6, =#0x000001ff
|
||||
cmp r7, r6
|
||||
bne end
|
||||
|
||||
ldr r7, =#0xabcdabcd ; offset of the dev launcher (will be replaced later)
|
||||
add r7, r9
|
||||
|
||||
adr r5, patchesStart
|
||||
add r6, r5, #(patchesEnd - patchesStart)
|
||||
|
||||
patchLoop:
|
||||
ldrh r0, [r5, #4]
|
||||
cmp r0, #0
|
||||
moveq r4, r9
|
||||
movne r4, r7
|
||||
|
||||
ldrh r2, [r5, #6]
|
||||
add r1, r5, #8
|
||||
ldr r0, [r5]
|
||||
add r0, r4
|
||||
blx memcmp
|
||||
cmp r0, #0
|
||||
bne skipPatch
|
||||
|
||||
ldrh r2, [r5, #6]
|
||||
add r1, r5, #0x08
|
||||
add r1, r2
|
||||
ldr r0, [r5]
|
||||
add r0, r4
|
||||
blx memcpy
|
||||
|
||||
skipPatch:
|
||||
|
||||
ldrh r0, [r5, #6]
|
||||
add r5, r5, #0x08
|
||||
add r5, r0,lsl#1
|
||||
cmp r5, r6
|
||||
blo patchLoop
|
||||
|
||||
end:
|
||||
|
||||
pop {r0-r11, pc}
|
||||
|
||||
.align 2
|
||||
.thumb
|
||||
|
||||
memcmp:
|
||||
push {r4-r7, lr}
|
||||
mov r4, #0
|
||||
cmp_loop:
|
||||
cmp r4, r2
|
||||
bhs cmp_loop_end
|
||||
ldrb r6, [r0, r4]
|
||||
ldrb r7, [r1, r4]
|
||||
add r4, #1
|
||||
sub r6, r7
|
||||
cmp r6, #0
|
||||
beq cmp_loop
|
||||
|
||||
cmp_loop_end:
|
||||
mov r0, r6
|
||||
pop {r4-r7, pc}
|
||||
|
||||
memcpy:
|
||||
push {r4-r5, lr}
|
||||
mov r4, #0
|
||||
|
||||
copy_loop:
|
||||
cmp r4, r2
|
||||
bhs copy_loop_end
|
||||
ldrb r5, [r1, r4]
|
||||
strb r5, [r0, r4]
|
||||
add r4, #1
|
||||
b copy_loop
|
||||
|
||||
copy_loop_end:
|
||||
pop {r4-r5, pc}
|
||||
|
||||
.align 4
|
||||
|
||||
; Available space for patches: 152 bytes on N3DS, 666 on O3DS
|
||||
|
||||
patchesStart:
|
||||
; SCFG_EXT bit31 patches, based on https://github.com/ahezard/twl_firm_patcher (credits where they're due)
|
||||
|
||||
.word 0x07368 ; offset
|
||||
.halfword 1 ; type (0: relative to the start of TwlBg's code; 1: relative to the start of the dev SRL launcher)
|
||||
.halfword 4 ; size (must be a multiple of 4)
|
||||
.byte 0x94, 0x09, 0xfc, 0xed ; expected data (decrypted = 0x08, 0x60, 0x87, 0x05)
|
||||
.byte 0x24, 0x09, 0xbc, 0xe9 ; patched data (decrypted = 0xb8, 0x60, 0xc7, 0x01)
|
||||
|
||||
.word 0xa5888
|
||||
.halfword 1
|
||||
.halfword 8
|
||||
.byte 0x83, 0x30, 0x2e, 0xa4, 0xb0, 0xe2, 0xc2, 0xd6 ; (decrypted = 0x02, 0x01, 0x1a, 0xe3, 0x08, 0x60, 0x87, 0x05)
|
||||
.byte 0x83, 0x50, 0xf2, 0xa4, 0xb0, 0xe2, 0xc2, 0xd6 ; (decrypted = 0x02, 0x61, 0xc6, 0xe3, 0x08, 0x60, 0x87, 0xe5)
|
||||
|
||||
patchesEnd:
|
||||
|
||||
.pool
|
||||
|
||||
.close
|
||||
@@ -24,10 +24,9 @@
|
||||
#include "utils.h"
|
||||
#include "screen.h"
|
||||
#include "draw.h"
|
||||
#include "fs.h"
|
||||
#include "buttons.h"
|
||||
|
||||
void configureCFW(const char *configPath)
|
||||
void configureCFW(void)
|
||||
{
|
||||
initScreens();
|
||||
|
||||
@@ -44,7 +43,8 @@ void configureCFW(const char *configPath)
|
||||
"( ) Show current NAND in System Settings",
|
||||
"( ) Show GBA boot screen in patched AGB_FIRM",
|
||||
"( ) Display splash screen before payloads",
|
||||
"( ) Use a PIN" };
|
||||
"( ) Use a PIN",
|
||||
"( ) Enable experimental TwlBg patches" };
|
||||
|
||||
struct multiOption {
|
||||
int posXs[4];
|
||||
@@ -193,13 +193,6 @@ void configureCFW(const char *configPath)
|
||||
for(u32 i = 0; i < singleOptionsAmount; i++)
|
||||
config |= (singleOptions[i].enabled ? 1 : 0) << (i + 16);
|
||||
|
||||
if(!fileWrite(&config, configPath, 4))
|
||||
{
|
||||
createDirectory("luma");
|
||||
if(!fileWrite(&config, configPath, 4))
|
||||
error("Error writing the configuration file");
|
||||
}
|
||||
|
||||
//Wait for the pressed buttons to change
|
||||
while(HID_PAD == BUTTON_START);
|
||||
}
|
||||
@@ -30,4 +30,4 @@
|
||||
|
||||
extern u32 config;
|
||||
|
||||
void configureCFW(const char *configPath);
|
||||
void configureCFW(void);
|
||||
@@ -43,7 +43,7 @@ static inline int strlen(const char *string)
|
||||
bool loadSplash(void)
|
||||
{
|
||||
//Don't delay boot nor init the screens if no splash image is on the SD
|
||||
if(getFileSize("/luma/splash.bin") + getFileSize("/luma/splash.bin") == 0)
|
||||
if(getFileSize("/luma/splash.bin") + getFileSize("/luma/splashbottom.bin") == 0)
|
||||
return false;
|
||||
|
||||
initScreens();
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
void locateEmuNAND(u32 *off, u32 *head, FirmwareSource *emuNAND)
|
||||
{
|
||||
static u8 *const temp = (u8 *)0x24300000;
|
||||
static u8 temp[0x200];
|
||||
|
||||
const u32 nandSize = getMMCDevice(0)->total_size;
|
||||
u32 nandOffset = *emuNAND == FIRMWARE_EMUNAND ? 0 :
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include "screen.h"
|
||||
#include "buttons.h"
|
||||
#include "pin.h"
|
||||
#include "i2c.h"
|
||||
#include "../build/injector.h"
|
||||
|
||||
extern u16 launchedFirmTIDLow[8]; //defined in start.s
|
||||
@@ -44,18 +43,17 @@ static const firmSectionHeader *section;
|
||||
u32 config,
|
||||
emuOffset;
|
||||
|
||||
bool isN3DS, isDevUnit;
|
||||
bool isN3DS,
|
||||
isDevUnit,
|
||||
isFirmlaunch;
|
||||
|
||||
FirmwareSource firmSource;
|
||||
|
||||
PINData pin;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
bool isFirmlaunch,
|
||||
isA9lh;
|
||||
bool isA9lh;
|
||||
|
||||
u32 newConfig,
|
||||
u32 configTemp,
|
||||
emuHeader;
|
||||
|
||||
FirmwareType firmType;
|
||||
@@ -104,7 +102,8 @@ void main(void)
|
||||
//Determine if the user chose to use the SysNAND FIRM as default for a R boot
|
||||
bool useSysAsDefault = isA9lh ? CONFIG(1) : false;
|
||||
|
||||
newConfig = (u32)isA9lh << 3;
|
||||
//Save old options and begin saving the new boot configuration
|
||||
configTemp = (config & 0xFFFFFFC0) | ((u32)isA9lh << 3);
|
||||
|
||||
//If it's a MCU reboot, try to force boot options
|
||||
if(isA9lh && CFG_BOOTENV)
|
||||
@@ -117,7 +116,7 @@ void main(void)
|
||||
needConfig = DONT_CONFIGURE;
|
||||
|
||||
//Flag to prevent multiple boot options-forcing
|
||||
newConfig |= 1 << 4;
|
||||
configTemp |= 1 << 4;
|
||||
}
|
||||
|
||||
/* Else, force the last used boot options unless a button is pressed
|
||||
@@ -133,19 +132,21 @@ void main(void)
|
||||
//Boot options aren't being forced
|
||||
if(needConfig != DONT_CONFIGURE)
|
||||
{
|
||||
PINData pin;
|
||||
|
||||
bool pinExists = CONFIG(7) && readPin(&pin);
|
||||
|
||||
//If we get here we should check the PIN (if it exists) in all cases
|
||||
if(pinExists) verifyPin(&pin, true);
|
||||
if(pinExists) verifyPin(&pin);
|
||||
|
||||
//If no configuration file exists or SELECT is held, load configuration menu
|
||||
bool shouldLoadConfigurationMenu = needConfig == CREATE_CONFIGURATION || ((pressed & BUTTON_SELECT) && !(pressed & BUTTON_L1));
|
||||
|
||||
if(shouldLoadConfigurationMenu)
|
||||
{
|
||||
configureCFW(configPath);
|
||||
configureCFW();
|
||||
|
||||
if(!pinExists && CONFIG(7)) pin = newPin();
|
||||
if(!pinExists && CONFIG(7)) newPin();
|
||||
|
||||
chrono(2);
|
||||
|
||||
@@ -157,6 +158,9 @@ void main(void)
|
||||
{
|
||||
nandType = FIRMWARE_SYSNAND;
|
||||
firmSource = FIRMWARE_SYSNAND;
|
||||
|
||||
//Flag to tell loader to init SD
|
||||
configTemp |= 1 << 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -205,17 +209,21 @@ void main(void)
|
||||
|
||||
if(!isFirmlaunch)
|
||||
{
|
||||
newConfig |= (u32)nandType | ((u32)firmSource << 2);
|
||||
configTemp |= (u32)nandType | ((u32)firmSource << 2);
|
||||
|
||||
/* If the boot configuration is different from previously, overwrite it.
|
||||
/* If the configuration is different from previously, overwrite it.
|
||||
Just the no-forcing flag being set is not enough */
|
||||
if((newConfig & 0x2F) != (config & 0x3F))
|
||||
if((configTemp & 0xFFFFFFEF) != config)
|
||||
{
|
||||
//Preserve user settings (last 26 bits)
|
||||
newConfig |= config & 0xFFFFFFC0;
|
||||
//Merge the new options and new boot configuration
|
||||
config = (config & 0xFFFFFFC0) | (configTemp & 0x3F);
|
||||
|
||||
if(!fileWrite(&newConfig, configPath, 4))
|
||||
error("Error writing the configuration file");
|
||||
if(!fileWrite(&config, configPath, 4))
|
||||
{
|
||||
createDirectory("luma");
|
||||
if(!fileWrite(&config, configPath, 4))
|
||||
error("Error writing the configuration file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +243,7 @@ void main(void)
|
||||
break;
|
||||
}
|
||||
|
||||
launchFirm(firmType, isFirmlaunch);
|
||||
launchFirm(firmType);
|
||||
}
|
||||
|
||||
static inline u32 loadFirm(FirmwareType firmType)
|
||||
@@ -247,6 +255,11 @@ static inline u32 loadFirm(FirmwareType firmType)
|
||||
|
||||
if(!isN3DS && firmType == NATIVE_FIRM && firmVersion < 0x25)
|
||||
{
|
||||
//We can't boot < 3.x NANDs
|
||||
if(firmVersion < 0x18)
|
||||
error("An old unsupported NAND has been detected.\nLuma3DS is unable to boot it.");
|
||||
|
||||
//We can't boot a 4.x NATIVE_FIRM, load one from SD
|
||||
if(!fileRead(firm, "/luma/firmware.bin") || (((u32)section[2].address >> 8) & 0xFF) != 0x68)
|
||||
error("An old unsupported FIRM has been detected.\nCopy firmware.bin in /luma to boot");
|
||||
|
||||
@@ -261,6 +274,7 @@ static inline u32 loadFirm(FirmwareType firmType)
|
||||
static inline void patchNativeFirm(u32 firmVersion, FirmwareSource nandType, u32 emuHeader, bool isA9lh)
|
||||
{
|
||||
u8 *arm9Section = (u8 *)firm + section[2].offset;
|
||||
u8 *arm11Section1 = (u8 *)firm + section[1].offset;
|
||||
|
||||
if(isN3DS)
|
||||
{
|
||||
@@ -300,8 +314,10 @@ static inline void patchNativeFirm(u32 firmVersion, FirmwareSource nandType, u32
|
||||
patchTitleInstallMinVersionCheck(process9Offset, process9Size);
|
||||
|
||||
//Restore svcBackdoor
|
||||
reimplementSvcBackdoor((u8 *)firm + section[1].offset, section[1].size);
|
||||
reimplementSvcBackdoor(arm11Section1, section[1].size);
|
||||
}
|
||||
|
||||
implementSvcGetCFWInfo(arm11Section1, section[1].size);
|
||||
}
|
||||
|
||||
static inline void patchLegacyFirm(FirmwareType firmType)
|
||||
@@ -314,6 +330,9 @@ static inline void patchLegacyFirm(FirmwareType firmType)
|
||||
}
|
||||
|
||||
applyLegacyFirmPatches((u8 *)firm, firmType);
|
||||
|
||||
if(firmType == TWL_FIRM && CONFIG(8))
|
||||
patchTwlBg((u8 *)firm + section[1].offset);
|
||||
}
|
||||
|
||||
static inline void patchSafeFirm(void)
|
||||
@@ -331,25 +350,44 @@ static inline void patchSafeFirm(void)
|
||||
else patchFirmWriteSafe(arm9Section, section[2].size);
|
||||
}
|
||||
|
||||
static inline void copySection0AndInjectLoader(void)
|
||||
static inline void copySection0AndInjectSystemModules(void)
|
||||
{
|
||||
u8 *arm11Section0 = (u8 *)firm + section[0].offset;
|
||||
|
||||
u32 loaderSize;
|
||||
u32 loaderOffset = getLoader(arm11Section0, &loaderSize);
|
||||
struct
|
||||
{
|
||||
u32 size;
|
||||
const u8 *addr;
|
||||
} modules[5];
|
||||
|
||||
memcpy(section[0].address, arm11Section0, loaderOffset);
|
||||
memcpy(section[0].address + loaderOffset, injector, injector_size);
|
||||
memcpy(section[0].address + loaderOffset + injector_size, arm11Section0 + loaderOffset + loaderSize, section[0].size - (loaderOffset + loaderSize));
|
||||
u32 n = 0,
|
||||
loaderIndex;
|
||||
u8 *pos = arm11Section0;
|
||||
|
||||
for(u8 *end = pos + section[0].size; pos < end; pos += modules[n++].size)
|
||||
{
|
||||
modules[n].addr = pos;
|
||||
modules[n].size = *(u32 *)(pos + 0x104) * 0x200;
|
||||
|
||||
if(memcmp(modules[n].addr + 0x200, "loader", 7) == 0) loaderIndex = n;
|
||||
}
|
||||
|
||||
modules[loaderIndex].addr = injector;
|
||||
modules[loaderIndex].size = injector_size;
|
||||
|
||||
pos = section[0].address;
|
||||
|
||||
for(u32 i = 0; i < n; pos += modules[i++].size)
|
||||
memcpy(pos, modules[i].addr, modules[i].size);
|
||||
}
|
||||
|
||||
static inline void launchFirm(FirmwareType firmType, bool isFirmlaunch)
|
||||
static inline void launchFirm(FirmwareType firmType)
|
||||
{
|
||||
//If we're booting NATIVE_FIRM, section0 needs to be copied separately to inject 3ds_injector
|
||||
u32 sectionNum;
|
||||
if(firmType == NATIVE_FIRM)
|
||||
{
|
||||
copySection0AndInjectLoader();
|
||||
copySection0AndInjectSystemModules();
|
||||
sectionNum = 1;
|
||||
}
|
||||
else sectionNum = 0;
|
||||
|
||||
@@ -24,11 +24,6 @@
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#define PDN_MPCORE_CFG (*(vu32 *)0x10140FFC)
|
||||
#define PDN_SPI_CNT (*(vu32 *)0x101401C0)
|
||||
#define CFG_BOOTENV (*(vu32 *)0x10010000)
|
||||
#define CFG_UNITINFO (*(vu8 *)0x10010010)
|
||||
|
||||
//FIRM Header layout
|
||||
typedef struct firmSectionHeader {
|
||||
u32 offset;
|
||||
@@ -58,5 +53,5 @@ static inline u32 loadFirm(FirmwareType firmType);
|
||||
static inline void patchNativeFirm(u32 firmVersion, FirmwareSource nandType, u32 emuHeader, bool isA9lh);
|
||||
static inline void patchLegacyFirm(FirmwareType firmType);
|
||||
static inline void patchSafeFirm(void);
|
||||
static inline void copySection0AndInjectLoader(void);
|
||||
static inline void launchFirm(FirmwareType firmType, bool isFirmlaunch);
|
||||
static inline void copySection0AndInjectSystemModules(void);
|
||||
static inline void launchFirm(FirmwareType firmType);
|
||||
110
source/patches.c
110
source/patches.c
@@ -24,6 +24,49 @@
|
||||
#include "memory.h"
|
||||
#include "config.h"
|
||||
#include "../build/rebootpatch.h"
|
||||
#include "../build/svcGetCFWInfopatch.h"
|
||||
#include "../build/twl_k11modulespatch.h"
|
||||
|
||||
static u32 *arm11ExceptionsPage = NULL;
|
||||
static u32 *arm11SvcTable = NULL;
|
||||
static u32 *arm11SvcHandler = NULL;
|
||||
|
||||
static u8 *freeK11Space = NULL; //other than the one used for svcBackdoor
|
||||
|
||||
static void findArm11ExceptionsPageAndSvcHandlerAndTable(u8 *pos, u32 size)
|
||||
{
|
||||
const u8 arm11ExceptionsPagePattern[] = {0x00, 0xB0, 0x9C, 0xE5};
|
||||
|
||||
if(arm11ExceptionsPage == NULL) arm11ExceptionsPage = (u32 *)memsearch(pos, arm11ExceptionsPagePattern, size, 4) - 0xB;
|
||||
if((arm11SvcTable == NULL || arm11SvcHandler == NULL) && arm11ExceptionsPage != NULL)
|
||||
{
|
||||
u32 svcOffset = (-((arm11ExceptionsPage[2] & 0xFFFFFF) << 2) & (0xFFFFFF << 2)) - 8; //Branch offset + 8 for prefetch
|
||||
arm11SvcHandler = arm11SvcTable = (u32 *)(pos + *(u32 *)(pos + 0xFFFF0008 - svcOffset - 0xFFF00000 + 8) - 0xFFF00000); //SVC handler address
|
||||
while(*arm11SvcTable) arm11SvcTable++; //Look for SVC0 (NULL)
|
||||
}
|
||||
}
|
||||
|
||||
static void findFreeK11Space(u8 *pos, u32 size)
|
||||
{
|
||||
if(freeK11Space == NULL)
|
||||
{
|
||||
const u8 bogus_pattern[] = { 0x1E, 0xFF, 0x2F, 0xE1, 0x1E, 0xFF, 0x2F, 0xE1, 0x1E, 0xFF,
|
||||
0x2F, 0xE1, 0x00, 0x10, 0xA0, 0xE3, 0x00, 0x10, 0xC0, 0xE5,
|
||||
0x1E, 0xFF, 0x2F, 0xE1 };
|
||||
|
||||
u32 *someSpace = (u32 *)memsearch(pos, bogus_pattern, size, 24);
|
||||
|
||||
// We couldn't find the place where to begin our search of an empty block
|
||||
if (someSpace == NULL)
|
||||
return;
|
||||
|
||||
// Advance until we reach the padding area (filled with 0xFF)
|
||||
u32 *freeSpace;
|
||||
for(freeSpace = someSpace; *freeSpace != 0xFFFFFFFF; freeSpace++);
|
||||
|
||||
freeK11Space = (u8 *)freeSpace;
|
||||
}
|
||||
}
|
||||
|
||||
u8 *getProcess9(u8 *pos, u32 size, u32 *process9Size, u32 *process9MemAddr)
|
||||
{
|
||||
@@ -111,25 +154,49 @@ void reimplementSvcBackdoor(u8 *pos, u32 size)
|
||||
0x00, 0xD0, 0xA0, 0xE1, //mov sp, r0
|
||||
0x11, 0xFF, 0x2F, 0xE1}; //bx r1
|
||||
|
||||
const u8 pattern[] = {0x00, 0xB0, 0x9C, 0xE5}; //cpsid aif
|
||||
findArm11ExceptionsPageAndSvcHandlerAndTable(pos, size);
|
||||
|
||||
u32 *exceptionsPage = (u32 *)memsearch(pos, pattern, size, 4) - 0xB;
|
||||
|
||||
u32 svcOffset = (-((exceptionsPage[2] & 0xFFFFFF) << 2) & (0xFFFFFF << 2)) - 8; //Branch offset + 8 for prefetch
|
||||
u32 *svcTable = (u32 *)(pos + *(u32 *)(pos + 0xFFFF0008 - svcOffset - 0xFFF00000 + 8) - 0xFFF00000); //SVC handler address
|
||||
while(*svcTable) svcTable++; //Look for SVC0 (NULL)
|
||||
|
||||
if(svcTable[0x7B] == 0)
|
||||
if(!arm11SvcTable[0x7B])
|
||||
{
|
||||
u32 *freeSpace;
|
||||
for(freeSpace = exceptionsPage; *freeSpace != 0xFFFFFFFF; freeSpace++);
|
||||
for(freeSpace = arm11ExceptionsPage; *freeSpace != 0xFFFFFFFF; freeSpace++);
|
||||
|
||||
memcpy(freeSpace, svcBackdoor, 40);
|
||||
|
||||
svcTable[0x7B] = 0xFFFF0000 + ((u8 *)freeSpace - (u8 *)exceptionsPage);
|
||||
arm11SvcTable[0x7B] = 0xFFFF0000 + ((u8 *)freeSpace - (u8 *)arm11ExceptionsPage);
|
||||
}
|
||||
}
|
||||
|
||||
void implementSvcGetCFWInfo(u8 *pos, u32 size)
|
||||
{
|
||||
const char *rev = REVISION;
|
||||
bool isRelease;
|
||||
|
||||
findArm11ExceptionsPageAndSvcHandlerAndTable(pos, size);
|
||||
findFreeK11Space(pos, size);
|
||||
|
||||
memcpy(freeK11Space, svcGetCFWInfo, svcGetCFWInfo_size);
|
||||
|
||||
CFWInfo *info = (CFWInfo *)memsearch(freeK11Space, "LUMA", svcGetCFWInfo_size, 4);
|
||||
|
||||
info->commitHash = COMMIT_HASH;
|
||||
info->config = config;
|
||||
info->versionMajor = (u8)(rev[1] - '0');
|
||||
info->versionMinor = (u8)(rev[3] - '0');
|
||||
if(rev[4] == '.')
|
||||
{
|
||||
info->versionBuild = (u8)(rev[5] - '0');
|
||||
isRelease = rev[6] == 0;
|
||||
}
|
||||
else
|
||||
isRelease = rev[4] == 0;
|
||||
|
||||
info->flags = 0 /* master branch */ | (((isRelease) ? 1 : 0) << 1) /* is release */;
|
||||
|
||||
arm11SvcTable[0x2E] = 0xFFF00000 + freeK11Space - pos; //stubbed svc
|
||||
freeK11Space += svcGetCFWInfo_size;
|
||||
}
|
||||
|
||||
void patchTitleInstallMinVersionCheck(u8 *pos, u32 size)
|
||||
{
|
||||
const u8 pattern[] = {0x0A, 0x81, 0x42, 0x02};
|
||||
@@ -180,19 +247,20 @@ void applyLegacyFirmPatches(u8 *pos, FirmwareType firmType)
|
||||
}
|
||||
}
|
||||
|
||||
u32 getLoader(u8 *pos, u32 *loaderSize)
|
||||
void patchTwlBg(u8 *pos)
|
||||
{
|
||||
u8 *off = pos;
|
||||
u32 size;
|
||||
u8 *dst = pos + ((isN3DS) ? 0xFEA4 : 0xFCA0);
|
||||
u16 *src1 = (u16 *)(pos + ((isN3DS) ? 0xE38 : 0xE3C)), *src2 = (u16 *)(pos + ((isN3DS) ? 0xE54 : 0xE58));
|
||||
memcpy(dst, twl_k11modules, twl_k11modules_size); //install k11 hook
|
||||
|
||||
while(true)
|
||||
{
|
||||
size = *(u32 *)(off + 0x104) * 0x200;
|
||||
if(*(u32 *)(off + 0x200) == 0x64616F6C) break;
|
||||
off += size;
|
||||
}
|
||||
u32 *off;
|
||||
for(off = (u32 *)dst; *off != 0xABCDABCD; off++);
|
||||
*off = (isN3DS) ? 0xCDE88 : 0xCD5F8; //dev SRL launcher offset
|
||||
|
||||
*loaderSize = size;
|
||||
//Construct BLX instructions:
|
||||
src1[0] = 0xF000 | ((((u32)dst - (u32)src1 - 4) & (0xFFF << 11)) >> 12);
|
||||
src1[1] = 0xE800 | ((((u32)dst - (u32)src1 - 4) & 0xFFF) >> 1);
|
||||
|
||||
return (u32)(off - pos);
|
||||
src2[0] = 0xF000 | ((((u32)dst - (u32)src2 - 4) & (0xFFF << 11)) >> 12);
|
||||
src2[1] = 0xE800 | ((((u32)dst - (u32)src2 - 4) & 0xFFF) >> 1);
|
||||
}
|
||||
@@ -33,7 +33,22 @@ typedef struct patchData {
|
||||
u32 type;
|
||||
} patchData;
|
||||
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
char magic[4];
|
||||
|
||||
u8 versionMajor;
|
||||
u8 versionMinor;
|
||||
u8 versionBuild;
|
||||
u8 flags;
|
||||
|
||||
u32 commitHash;
|
||||
|
||||
u32 config;
|
||||
} CFWInfo;
|
||||
|
||||
extern bool isN3DS;
|
||||
extern u32 config;
|
||||
|
||||
u8 *getProcess9(u8 *pos, u32 size, u32 *process9Size, u32 *process9MemAddr);
|
||||
void patchSignatureChecks(u8 *pos, u32 size);
|
||||
@@ -42,5 +57,6 @@ void patchFirmlaunches(u8 *pos, u32 size, u32 process9MemAddr);
|
||||
void patchFirmWrites(u8 *pos, u32 size);
|
||||
void patchFirmWriteSafe(u8 *pos, u32 size);
|
||||
void reimplementSvcBackdoor(u8 *pos, u32 size);
|
||||
void implementSvcGetCFWInfo(u8 *pos, u32 size);
|
||||
void applyLegacyFirmPatches(u8 *pos, FirmwareType firmType);
|
||||
u32 getLoader(u8 *pos, u32 *loaderSize);
|
||||
void patchTwlBg(u8 *pos);
|
||||
57
source/pin.c
57
source/pin.c
@@ -31,7 +31,6 @@
|
||||
#include "memory.h"
|
||||
#include "buttons.h"
|
||||
#include "fs.h"
|
||||
#include "i2c.h"
|
||||
#include "pin.h"
|
||||
#include "crypto.h"
|
||||
|
||||
@@ -44,6 +43,7 @@ bool readPin(PINData *out)
|
||||
if(memcmp(out->magic, "PINF", 4) != 0) return false;
|
||||
|
||||
computePINHash(tmp, zeroes, 1);
|
||||
|
||||
return memcmp(out->testHash, tmp, 32) == 0; //test vector verification (SD card has (or hasn't) been used on another console)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ static inline char PINKeyToLetter(u32 pressed)
|
||||
return keys[31 - i];
|
||||
}
|
||||
|
||||
PINData newPin(void)
|
||||
void newPin(void)
|
||||
{
|
||||
clearScreens();
|
||||
|
||||
@@ -69,7 +69,7 @@ PINData newPin(void)
|
||||
u32 cnt = 0;
|
||||
int charDrawPos = 20 * SPACING_X;
|
||||
|
||||
while(true)
|
||||
while(cnt < PIN_LENGTH)
|
||||
{
|
||||
u32 pressed;
|
||||
do
|
||||
@@ -87,33 +87,28 @@ PINData newPin(void)
|
||||
// visualize character on screen.
|
||||
drawCharacter(key, 10 + charDrawPos, 10, COLOR_WHITE);
|
||||
charDrawPos += 2 * SPACING_X;
|
||||
|
||||
// we leave the rest of the array zeroed out.
|
||||
if(cnt >= PIN_LENGTH)
|
||||
{
|
||||
PINData pin = {0};
|
||||
u8 __attribute__((aligned(4))) tmp[32] = {0};
|
||||
u8 __attribute__((aligned(4))) zeroes[16] = {0};
|
||||
|
||||
memcpy(pin.magic, "PINF", 4);
|
||||
pin.formatVersionMajor = 1;
|
||||
pin.formatVersionMinor = 0;
|
||||
|
||||
computePINHash(tmp, zeroes, 1);
|
||||
memcpy(pin.testHash, tmp, 32);
|
||||
|
||||
computePINHash(tmp, enteredPassword, (PIN_LENGTH + 15) / 16);
|
||||
memcpy(pin.hash, tmp, 32);
|
||||
|
||||
fileWrite(&pin, "/luma/pin.bin", sizeof(PINData));
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
|
||||
PINData pin = {0};
|
||||
u8 __attribute__((aligned(4))) tmp[32] = {0};
|
||||
u8 __attribute__((aligned(4))) zeroes[16] = {0};
|
||||
|
||||
memcpy(pin.magic, "PINF", 4);
|
||||
pin.formatVersionMajor = 1;
|
||||
pin.formatVersionMinor = 0;
|
||||
|
||||
computePINHash(tmp, zeroes, 1);
|
||||
memcpy(pin.testHash, tmp, 32);
|
||||
|
||||
computePINHash(tmp, enteredPassword, (PIN_LENGTH + 15) / 16);
|
||||
memcpy(pin.hash, tmp, 32);
|
||||
|
||||
fileWrite(&pin, "/luma/pin.bin", sizeof(PINData));
|
||||
|
||||
while(HID_PAD & PIN_BUTTONS);
|
||||
}
|
||||
|
||||
void verifyPin(PINData *in, bool allowQuit)
|
||||
void verifyPin(PINData *in)
|
||||
{
|
||||
initScreens();
|
||||
|
||||
@@ -124,10 +119,10 @@ void verifyPin(PINData *in, bool allowQuit)
|
||||
u8 __attribute__((aligned(4))) enteredPassword[16 * ((PIN_LENGTH + 15) / 16)] = {0};
|
||||
|
||||
u32 cnt = 0;
|
||||
bool unlock;
|
||||
bool unlock = false;
|
||||
int charDrawPos = 5 * SPACING_X;
|
||||
|
||||
while(true)
|
||||
while(!unlock)
|
||||
{
|
||||
u32 pressed;
|
||||
do
|
||||
@@ -136,12 +131,11 @@ void verifyPin(PINData *in, bool allowQuit)
|
||||
}
|
||||
while(!(pressed & PIN_BUTTONS));
|
||||
|
||||
pressed &= PIN_BUTTONS;// & ~BUTTON_START;
|
||||
if(!allowQuit) pressed &= ~BUTTON_START;
|
||||
if(!pressed) continue;
|
||||
|
||||
if(pressed & BUTTON_START) mcuPowerOff();
|
||||
|
||||
pressed &= PIN_BUTTONS & ~BUTTON_START;
|
||||
if(!pressed) continue;
|
||||
|
||||
char key = PINKeyToLetter(pressed);
|
||||
enteredPassword[cnt++] = (u8)key; // add character to password.
|
||||
|
||||
@@ -167,7 +161,6 @@ void verifyPin(PINData *in, bool allowQuit)
|
||||
drawString("Pin: ", 10, 10 + 2 * SPACING_Y, COLOR_WHITE);
|
||||
drawString("Wrong pin! Try again!", 10, 10 + 3 * SPACING_Y, COLOR_RED);
|
||||
}
|
||||
else break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,7 @@
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#ifndef PIN_LENGTH
|
||||
#define PIN_LENGTH 4
|
||||
#endif
|
||||
#define PIN_LENGTH 4
|
||||
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
@@ -44,6 +42,5 @@ typedef struct __attribute__((packed))
|
||||
} PINData;
|
||||
|
||||
bool readPin(PINData* out);
|
||||
|
||||
PINData newPin(void);
|
||||
void verifyPin(PINData *in, bool allowQuit);
|
||||
void newPin(void);
|
||||
void verifyPin(PINData *in);
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#define PDN_GPU_CNT (*(vu8 *)0x10141200)
|
||||
#define ARM11_STUB_ADDRESS (0x25000000 - 0x30) //It's currently only 0x28 bytes large. We're putting 0x30 just to be sure here
|
||||
#define WAIT_FOR_ARM9() *arm11Entry = 0; while(!*arm11Entry); ((void (*)())*arm11Entry)();
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define CFG_BOOTENV (*(vu32 *)0x10010000)
|
||||
#define CFG_UNITINFO (*(vu8 *)0x10010010)
|
||||
|
||||
#define PDN_MPCORE_CFG (*(vu32 *)0x10140FFC)
|
||||
#define PDN_SPI_CNT (*(vu32 *)0x101401C0)
|
||||
#define PDN_GPU_CNT (*(vu8 *)0x10141200)
|
||||
|
||||
//Common data types
|
||||
typedef uint8_t u8;
|
||||
typedef uint16_t u16;
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include "draw.h"
|
||||
#include "cache.h"
|
||||
|
||||
extern bool isFirmlaunch;
|
||||
|
||||
u32 waitInput(void)
|
||||
{
|
||||
u32 pressedKey = 0,
|
||||
@@ -56,7 +58,7 @@ u32 waitInput(void)
|
||||
|
||||
void mcuReboot(void)
|
||||
{
|
||||
if(PDN_GPU_CNT != 1) clearScreens();
|
||||
if(!isFirmlaunch && PDN_GPU_CNT != 1) clearScreens();
|
||||
|
||||
flushEntireDCache(); //Ensure that all memory transfers have completed and that the data cache has been flushed
|
||||
|
||||
@@ -66,7 +68,7 @@ void mcuReboot(void)
|
||||
|
||||
void mcuPowerOff(void)
|
||||
{
|
||||
if(PDN_GPU_CNT != 1) clearScreens();
|
||||
if(!isFirmlaunch && PDN_GPU_CNT != 1) clearScreens();
|
||||
|
||||
flushEntireDCache(); //Ensure that all memory transfers have completed and that the data cache has been flushed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user