loader: implement PASLR (disabled by default)
Faithfully implement the PASLR algorithm official Loader uses (it's not very good). Physical address space layout randomization means that the layout of the pages in physical memory are randomized, but doens't randomize the virtual addresses. Also refactor some parts of our Loader impl a little more.
This commit is contained in:
parent
7074ac1166
commit
ffbd8554d5
@ -27,10 +27,13 @@
|
||||
#include <3ds.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "paslr.h"
|
||||
#include "util.h"
|
||||
#include "hbldr.h"
|
||||
#include "3dsx.h"
|
||||
|
||||
extern bool isN3DS;
|
||||
|
||||
static const char serviceList[32][8] =
|
||||
{
|
||||
"APT:U",
|
||||
@ -225,8 +228,6 @@ void hbldrPatchExHeaderInfo(ExHeader_Info *exhi)
|
||||
u64 lastdep = sizeof(dependencyListNativeFirm)/8;
|
||||
exhi->sci.dependencies[lastdep++] = 0x0004013000004002ULL; // nfc
|
||||
strncpy((char*)&localcaps0->service_access[0x20], "nfc:u", 8);
|
||||
s64 dummy = 0;
|
||||
bool isN3DS = svcGetSystemInfo(&dummy, 0x10001, 0) == 0;
|
||||
if (isN3DS)
|
||||
{
|
||||
exhi->sci.dependencies[lastdep++] = 0x0004013020004102ULL; // mvd
|
||||
@ -275,7 +276,7 @@ Result hbldrLoadProcess(Handle *outProcessHandle, const ExHeader_Info *exhi)
|
||||
|
||||
u32 tmp = 0;
|
||||
u32 addr = 0x10000000;
|
||||
res = svcControlMemory(&tmp, addr, 0, totalSize, MEMOP_ALLOC | region, MEMPERM_READ | MEMPERM_WRITE);
|
||||
res = allocateProgramMemory(exhi, addr, totalSize);
|
||||
if (R_FAILED(res))
|
||||
{
|
||||
IFile_Close(&file);
|
||||
|
@ -35,8 +35,10 @@ void hbldrHandleCommands(void *ctx);
|
||||
|
||||
static inline bool hbldrIs3dsxTitle(u64 tid)
|
||||
{
|
||||
return Luma_SharedConfig->use_hbldr && tid == Luma_SharedConfig->hbldr_3dsx_tid;
|
||||
}
|
||||
if (!Luma_SharedConfig->use_hbldr)
|
||||
return false;
|
||||
u64 hbldrTid = Luma_SharedConfig->hbldr_3dsx_tid;
|
||||
|
||||
void HBLDR_RestartHbApplication(void *p);
|
||||
void HBLDR_HandleCommands(void *ctx);
|
||||
// Just like p9 clears them, ignore platform/N3DS bits
|
||||
return ((tid ^ hbldrTid) & ~0xF0000000ull) == 0;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include <3ds.h>
|
||||
#include "memory.h"
|
||||
#include "patcher.h"
|
||||
#include "paslr.h"
|
||||
#include "ifile.h"
|
||||
#include "util.h"
|
||||
#include "hbldr.h"
|
||||
@ -9,8 +10,8 @@
|
||||
extern u32 config, multiConfig, bootConfig;
|
||||
extern bool isN3DS, isSdMode;
|
||||
|
||||
static u8 g_ret_buf[sizeof(ExHeader_Info)];
|
||||
static u64 g_cached_programHandle;
|
||||
static u64 g_cached_hbldr3dsxTid;
|
||||
static ExHeader_Info g_exheaderInfo;
|
||||
|
||||
typedef struct ContentPath {
|
||||
@ -20,7 +21,7 @@ typedef struct ContentPath {
|
||||
|
||||
static const ContentPath codeContentPath = {
|
||||
.contentType = 1, // ExeFS (with code)
|
||||
.fileName = ".code", // last 3 bytes have to be 0, but this is guaranteed here.s
|
||||
.fileName = ".code", // last 3 bytes have to be 0, but this is guaranteed here.
|
||||
};
|
||||
|
||||
typedef struct prog_addrs_t
|
||||
@ -100,18 +101,16 @@ static int lzss_decompress(u8 *end)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static Result allocateSharedMem(prog_addrs_t *shared, const prog_addrs_t *vaddr, int flags)
|
||||
static Result allocateProgramMemoryWrapper(prog_addrs_t *mapped, const ExHeader_Info *exhi, const prog_addrs_t *vaddr)
|
||||
{
|
||||
u32 dummy;
|
||||
|
||||
memcpy(shared, vaddr, sizeof(prog_addrs_t));
|
||||
shared->text_addr = 0x10000000;
|
||||
shared->ro_addr = shared->text_addr + (shared->text_size << 12);
|
||||
shared->data_addr = shared->ro_addr + (shared->ro_size << 12);
|
||||
return svcControlMemory(&dummy, shared->text_addr, 0, shared->total_size << 12, (flags & 0xF00) | MEMOP_ALLOC, MEMPERM_READ | MEMPERM_WRITE);
|
||||
memcpy(mapped, vaddr, sizeof(prog_addrs_t));
|
||||
mapped->text_addr = 0x10000000;
|
||||
mapped->ro_addr = mapped->text_addr + (mapped->text_size << 12);
|
||||
mapped->data_addr = mapped->ro_addr + (mapped->ro_size << 12);
|
||||
return allocateProgramMemory(exhi, mapped->text_addr, mapped->total_size << 12);
|
||||
}
|
||||
|
||||
static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_addrs_t *shared)
|
||||
static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_addrs_t *mapped)
|
||||
{
|
||||
IFile file;
|
||||
FS_Path archivePath;
|
||||
@ -123,7 +122,7 @@ static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_
|
||||
const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info;
|
||||
bool isCompressed = csi->flags.compress_exefs_code;
|
||||
|
||||
if(!CONFIG(PATCHGAMES) || !loadTitleCodeSection(titleId, (u8 *)shared->text_addr, (u64)shared->total_size << 12))
|
||||
if(!CONFIG(PATCHGAMES) || !loadTitleCodeSection(titleId, (u8 *)mapped->text_addr, (u64)mapped->total_size << 12))
|
||||
{
|
||||
archivePath.type = PATH_BINARY;
|
||||
archivePath.data = &programHandle;
|
||||
@ -132,30 +131,27 @@ static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_
|
||||
filePath.type = PATH_BINARY;
|
||||
filePath.data = &codeContentPath;
|
||||
filePath.size = sizeof(codeContentPath);
|
||||
Result res;
|
||||
if (R_FAILED(res = IFile_Open(&file, ARCHIVE_SAVEDATA_AND_CONTENT2, archivePath, filePath, FS_OPEN_READ)))
|
||||
*(u64 *)0x1238 = programHandle;//panic(programHandle);//svcBreak(USERBREAK_ASSERT);
|
||||
|
||||
// get file size
|
||||
assertSuccess(IFile_Open(&file, ARCHIVE_SAVEDATA_AND_CONTENT2, archivePath, filePath, FS_OPEN_READ));
|
||||
assertSuccess(IFile_GetSize(&file, &size));
|
||||
|
||||
// check size
|
||||
if (size > (u64)shared->total_size << 12)
|
||||
if (size > (u64)mapped->total_size << 12)
|
||||
{
|
||||
IFile_Close(&file);
|
||||
return 0xC900464F;
|
||||
}
|
||||
|
||||
// read code
|
||||
assertSuccess(IFile_Read(&file, &total, (void *)shared->text_addr, size));
|
||||
assertSuccess(IFile_Read(&file, &total, (void *)mapped->text_addr, size));
|
||||
IFile_Close(&file); // done reading
|
||||
|
||||
// decompress
|
||||
if (isCompressed)
|
||||
lzss_decompress((u8 *)shared->text_addr + size);
|
||||
lzss_decompress((u8 *)mapped->text_addr + size);
|
||||
}
|
||||
|
||||
patchCode(titleId, csi->flags.remaster_version, (u8 *)shared->text_addr, shared->total_size << 12, csi->text.size, csi->rodata.size, csi->data.size, csi->rodata.address, csi->data.address);
|
||||
patchCode(titleId, csi->flags.remaster_version, (u8 *)mapped->text_addr, mapped->total_size << 12, csi->text.size, csi->rodata.size, csi->data.size, csi->rodata.address, csi->data.address);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -171,7 +167,7 @@ static inline bool IsHioId(u64 id)
|
||||
return R_LEVEL(FSREG_CheckHostLoadId(id)) == RL_SUCCESS; // check if this is an alias to an HIO-loaded title
|
||||
}
|
||||
|
||||
static Result GetProgramInfo(ExHeader_Info *exheaderInfo, u64 programHandle)
|
||||
static Result GetProgramInfoImpl(ExHeader_Info *exheaderInfo, u64 programHandle)
|
||||
{
|
||||
Result res;
|
||||
TRY(IsHioId(programHandle) ? FSREG_GetProgramInfo(exheaderInfo, 1, programHandle) : PXIPM_GetProgramInfo(exheaderInfo, programHandle));
|
||||
@ -191,13 +187,31 @@ static Result GetProgramInfo(ExHeader_Info *exheaderInfo, u64 programHandle)
|
||||
return res;
|
||||
}
|
||||
|
||||
static Result GetProgramInfo(u64 programHandle)
|
||||
{
|
||||
Result res = 0;
|
||||
|
||||
u64 cachedTid = g_exheaderInfo.aci.local_caps.title_id;
|
||||
u64 hbldr3dsxTid = Luma_SharedConfig->hbldr_3dsx_tid;
|
||||
bool hbTitleChanged = hbldr3dsxTid != g_cached_hbldr3dsxTid;
|
||||
g_cached_hbldr3dsxTid = Luma_SharedConfig->hbldr_3dsx_tid;
|
||||
|
||||
if (programHandle != g_cached_programHandle || (hbldrIs3dsxTitle(cachedTid) && hbTitleChanged))
|
||||
{
|
||||
res = GetProgramInfoImpl(&g_exheaderInfo, programHandle);
|
||||
g_cached_programHandle = R_SUCCEEDED(res) ? programHandle : 0;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exhi, u64 programHandle)
|
||||
{
|
||||
const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info;
|
||||
|
||||
Result res = 0;
|
||||
u32 dummy;
|
||||
prog_addrs_t sharedAddr;
|
||||
prog_addrs_t mapped;
|
||||
prog_addrs_t vaddr;
|
||||
Handle codeset;
|
||||
CodeSetInfo codesetinfo;
|
||||
@ -223,11 +237,11 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
|
||||
vaddr.data_size = (csi->data.size + 4095) >> 12;
|
||||
dataMemSize = (csi->data.size + csi->bss_size + 4095) >> 12;
|
||||
vaddr.total_size = vaddr.text_size + vaddr.ro_size + vaddr.data_size;
|
||||
TRY(allocateSharedMem(&sharedAddr, &vaddr, region));
|
||||
TRY(allocateProgramMemoryWrapper(&mapped, exhi, &vaddr));
|
||||
|
||||
// load code
|
||||
u64 titleId = exhi->aci.local_caps.title_id;
|
||||
if (R_SUCCEEDED(res = loadCode(exhi, programHandle, &sharedAddr)))
|
||||
if (R_SUCCEEDED(res = loadCode(exhi, programHandle, &mapped)))
|
||||
{
|
||||
memcpy(&codesetinfo.name, csi->name, 8);
|
||||
codesetinfo.program_id = titleId;
|
||||
@ -240,7 +254,7 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
|
||||
codesetinfo.rw_addr = vaddr.data_addr;
|
||||
codesetinfo.rw_size = vaddr.data_size;
|
||||
codesetinfo.rw_size_total = dataMemSize;
|
||||
res = svcCreateCodeSet(&codeset, &codesetinfo, (void *)sharedAddr.text_addr, (void *)sharedAddr.ro_addr, (void *)sharedAddr.data_addr);
|
||||
res = svcCreateCodeSet(&codeset, &codesetinfo, (void *)mapped.text_addr, (void *)mapped.ro_addr, (void *)mapped.data_addr);
|
||||
if (R_SUCCEEDED(res))
|
||||
{
|
||||
// There are always 28 descriptors
|
||||
@ -250,25 +264,14 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
|
||||
}
|
||||
}
|
||||
|
||||
svcControlMemory(&dummy, sharedAddr.text_addr, 0, sharedAddr.total_size << 12, MEMOP_FREE, 0);
|
||||
svcControlMemory(&dummy, mapped.text_addr, 0, mapped.total_size << 12, MEMOP_FREE, 0);
|
||||
return res;
|
||||
}
|
||||
|
||||
static Result LoadProcess(Handle *process, u64 programHandle)
|
||||
{
|
||||
Result res;
|
||||
|
||||
// make sure the cached info corresponds to the current programHandle
|
||||
if (g_cached_programHandle != programHandle || hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id))
|
||||
{
|
||||
res = GetProgramInfo(&g_exheaderInfo, programHandle);
|
||||
g_cached_programHandle = programHandle;
|
||||
if (R_FAILED(res))
|
||||
{
|
||||
g_cached_programHandle = 0;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
Result res = 0;
|
||||
TRY(GetProgramInfo(programHandle));
|
||||
|
||||
if (hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id))
|
||||
return hbldrLoadProcess(process, &g_exheaderInfo);
|
||||
@ -348,16 +351,11 @@ void loaderHandleCommands(void *ctx)
|
||||
break;
|
||||
case 4: // GetProgramInfo
|
||||
memcpy(&programHandle, &cmdbuf[1], 8);
|
||||
if (programHandle != g_cached_programHandle || hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id))
|
||||
{
|
||||
res = GetProgramInfo(&g_exheaderInfo, programHandle);
|
||||
g_cached_programHandle = R_SUCCEEDED(res) ? programHandle : 0;
|
||||
}
|
||||
memcpy(&g_ret_buf, &g_exheaderInfo, sizeof(ExHeader_Info));
|
||||
res = GetProgramInfo(programHandle);
|
||||
cmdbuf[0] = IPC_MakeHeader(4, 1, 2);
|
||||
cmdbuf[1] = res;
|
||||
cmdbuf[2] = IPC_Desc_StaticBuffer(sizeof(ExHeader_Info), 0); //0x1000002;
|
||||
cmdbuf[3] = (u32)&g_ret_buf;
|
||||
cmdbuf[3] = (u32)&g_exheaderInfo; // official Loader makes a copy here, but this is isn't necessary
|
||||
break;
|
||||
default: // error
|
||||
cmdbuf[0] = IPC_MakeHeader(0, 1, 0);
|
||||
|
167
sysmodules/loader/source/paslr.c
Normal file
167
sysmodules/loader/source/paslr.c
Normal file
@ -0,0 +1,167 @@
|
||||
#include <3ds.h>
|
||||
#include "paslr.h"
|
||||
#include "util.h"
|
||||
#include "hbldr.h"
|
||||
|
||||
static const u32 titleUidListForPaslr[] = {
|
||||
// JPN/USA/EUR/KOR/TWN or GLOBAL
|
||||
0x0209, 0x0219, 0x0229, 0x0269, 0x0289, // Nintendo eShop
|
||||
0x0334, 0x0335, 0x0336, // The Legend of Zelda: Ocarina of Time 3D
|
||||
0x0343, 0x0465, 0x04B3, // Cubic Ninja
|
||||
0x0913, 0x09FA, 0x0933, // Freakyforms Deluxe
|
||||
0x07FD, 0x0961, // VVVVVV
|
||||
0x0A5D, 0x0A5E, 0x0A6F, 0x0C61, 0x0C81, // Paper Mario: Sticker Star
|
||||
0x0D7C, 0x0D7D, 0x0D7E, // Steel Diver: Sub Wars
|
||||
0x11C4, // Pokemon Omega Ruby (GLOBAL)
|
||||
0x11C5, // Pokemon Alpha Sapphire (GLOBAL)
|
||||
0x149B, 0x1746, 0x1744, // Pokemon Super Mystery Dungeon
|
||||
0x1773, 0x12C1, 0x136E, // Citizens of Earth
|
||||
0x17C1, // Pokemon Picross (GLOBAL)
|
||||
};
|
||||
|
||||
static u32 getRandomInt(u32 maxNum)
|
||||
{
|
||||
// As reverse-engineered from latest Loader
|
||||
static u64 state = 0;
|
||||
s64 tick = (s64)svcGetSystemTick();
|
||||
|
||||
u64 tmp = (state >> 7) - (tick >> 2);
|
||||
state = (tmp ^ state) & 0x49CC9BA7FC61CA67ull;
|
||||
|
||||
tmp = 0x5D588B656C078965ull * state + 0x29EC3;
|
||||
return (u32)(((tmp >> 32) * maxNum) >> 32);
|
||||
}
|
||||
|
||||
static bool needsPaslr(u32 *outRegion, const ExHeader_Info *exhi)
|
||||
{
|
||||
u32 region = 0;
|
||||
u16 minKernelVer = 0;
|
||||
for (u32 i = 0; i < 28; i++)
|
||||
{
|
||||
u32 desc = exhi->aci.kernel_caps.descriptors[i];
|
||||
if (desc >> 23 == 0x1FE)
|
||||
region = desc & 0xF00;
|
||||
else if (desc >> 25 == 0x7E)
|
||||
minKernelVer = (u16)desc;
|
||||
}
|
||||
|
||||
*outRegion = region;
|
||||
|
||||
#ifdef LOADER_ENABLE_PASLR
|
||||
// Only applications and system applets (HM, Internet Browser...) are eligible for PASLR
|
||||
if (region != MEMOP_REGION_APP && region != MEMOP_REGION_SYSTEM)
|
||||
return false;
|
||||
else if (region == MEMOP_REGION_SYSTEM && exhi->aci.local_caps.reslimit_category == RESLIMIT_CATEGORY_LIB_APPLET)
|
||||
return false;
|
||||
|
||||
// All titles requiring 11.2+ kernel get PASLR
|
||||
if (minKernelVer >= (SYSTEM_VERSION(2, 57, 0) >> 16))
|
||||
return true;
|
||||
|
||||
// Otherwise, only games with known exploits and eShop get it
|
||||
u64 titleId = exhi->aci.local_caps.title_id;
|
||||
|
||||
// Check if this is indeed a CTR title ID (high u32 = 00040xxx)
|
||||
if (titleId >> 46 != 0x10)
|
||||
return false;
|
||||
|
||||
for (u32 i = 0; i < sizeof(titleUidListForPaslr)/sizeof(titleUidListForPaslr[0]); i++)
|
||||
{
|
||||
if (((titleId >> 8) & 0xFFFFF) == titleUidListForPaslr[i])
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
(void)minKernelVer;
|
||||
(void)titleUidListForPaslr;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
Result allocateProgramMemory(const ExHeader_Info *exhi, u32 vaddr, u32 size)
|
||||
{
|
||||
Result res = 0;
|
||||
u32 region = 0;
|
||||
bool doPaslr = needsPaslr(®ion, exhi);
|
||||
u32 tmp = 0;
|
||||
|
||||
if (region == 0)
|
||||
return MAKERESULT(RL_PERMANENT, RS_INVALIDARG, 1, 2);
|
||||
|
||||
u32 allocFlags = MEMOP_ALLOC | region;
|
||||
if (!doPaslr)
|
||||
return svcControlMemory(&tmp, vaddr, 0, size, allocFlags, MEMPERM_READWRITE);
|
||||
|
||||
// Divide the virtual address space into up to 7 chunks, each a random multiple (between 1 and 16) of 64KB.
|
||||
// 64KB corresponds to a "large page" for the Armv6 MMU, hence the optimization choice.
|
||||
// An additional 8th chunk gets the rest (if anything).
|
||||
u32 totalNumPages = size >> 12;
|
||||
u32 curPage = 0;
|
||||
u32 numChunks;
|
||||
u32 chunkStartAddrs[8];
|
||||
u32 chunkSizes[8];
|
||||
for (numChunks = 0; numChunks < 7; numChunks++)
|
||||
{
|
||||
u32 maxUnits = (totalNumPages - curPage) / 16;
|
||||
if (maxUnits == 0)
|
||||
break;
|
||||
else if (maxUnits > 15)
|
||||
maxUnits = 15;
|
||||
|
||||
u32 numPages = 16 * (1 + getRandomInt(maxUnits));
|
||||
chunkStartAddrs[numChunks] = vaddr + (curPage << 12);
|
||||
chunkSizes[numChunks] = numPages << 12;
|
||||
curPage += numPages;
|
||||
}
|
||||
if (curPage < totalNumPages)
|
||||
{
|
||||
u32 numPages = totalNumPages - curPage;
|
||||
chunkStartAddrs[numChunks] = vaddr + (curPage << 12);
|
||||
chunkSizes[numChunks] = numPages << 12;
|
||||
curPage += numPages;
|
||||
++numChunks;
|
||||
}
|
||||
|
||||
// Randomize the order the VA chunks are allocated in physical memory, from last to first
|
||||
u32 chunkOrder[8] = {0, 1, 2, 3, 4, 5, 6, 7};
|
||||
u32 numChunksToRandomize = numChunks;
|
||||
if (totalNumPages % 16 != 0)
|
||||
{
|
||||
// For MMU optimization reasons, we only randomize chunks that are multiples of 64KB (large page)
|
||||
// in size. This means that if the process image (.text, .rodata, .data) is not a muliple of 64KB
|
||||
// in size, then its last few pages are not randomized at all.
|
||||
// In pratice, under normal circumstances, this means that the last few pages of applications are
|
||||
// guaranteed to be located in <APPLICATION memregion end> - (image_size & ~0xFFFF).
|
||||
--numChunksToRandomize;
|
||||
}
|
||||
|
||||
for (s32 i = numChunksToRandomize - 1; i >= 0; i--)
|
||||
{
|
||||
u32 j = getRandomInt(i + 1);
|
||||
tmp = chunkOrder[i];
|
||||
chunkOrder[i] = chunkOrder[j];
|
||||
chunkOrder[j] = tmp;
|
||||
}
|
||||
|
||||
// Allocate the memory
|
||||
u32 i;
|
||||
for (i = 0; i < numChunks; i++)
|
||||
{
|
||||
u32 idx = chunkOrder[i];
|
||||
res = svcControlMemory(&tmp, chunkStartAddrs[idx], 0, chunkSizes[idx], allocFlags, MEMPERM_READWRITE);
|
||||
if (R_FAILED(res))
|
||||
break;
|
||||
}
|
||||
|
||||
// Success
|
||||
if (i == numChunks)
|
||||
return res;
|
||||
|
||||
// Clean up on failure
|
||||
for (u32 j = 0; j < i; j++)
|
||||
{
|
||||
u32 idx = chunkOrder[i];
|
||||
svcControlMemory(&tmp, chunkStartAddrs[idx], 0, chunkSizes[idx], MEMOP_FREE, MEMPERM_DONTCARE);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
6
sysmodules/loader/source/paslr.h
Normal file
6
sysmodules/loader/source/paslr.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <3ds/types.h>
|
||||
#include <3ds/exheader.h>
|
||||
|
||||
Result allocateProgramMemory(const ExHeader_Info *exhi, u32 vaddr, u32 size);
|
Loading…
x
Reference in New Issue
Block a user