mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 02:47:02 +02:00
kernel: preserve placeholders during fixed memory replacement
This commit is contained in:
@@ -292,6 +292,8 @@ target_include_directories(resource_mutex_tests PRIVATE ${inc_headers})
|
||||
add_kyty_full_emulator_test(shader_recompiler_compute_tests ../tests/ShaderRecompilerComputeTests.cpp)
|
||||
|
||||
add_kyty_full_emulator_test(virtual_memory_allocation_tests ../tests/VirtualMemoryAllocationTests.cpp)
|
||||
target_compile_definitions(virtual_memory_allocation_tests PRIVATE
|
||||
KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS=1)
|
||||
|
||||
|
||||
add_executable(kyty_emulator main.cpp ${kyty_emulator_src})
|
||||
|
||||
+142
-27
@@ -713,6 +713,12 @@ private:
|
||||
Common::Mutex m_mutex;
|
||||
};
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
static uint32_t g_test_physical_memory_unmaps_before_failure = UINT32_MAX;
|
||||
static uint32_t g_test_host_reservation_pages_before_failure = UINT32_MAX;
|
||||
static bool g_test_fail_next_fixed_reserve_range_add = false;
|
||||
#endif
|
||||
|
||||
class PhysicalMemory {
|
||||
public:
|
||||
struct AllocatedBlock {
|
||||
@@ -1322,6 +1328,16 @@ bool PhysicalMemory::ReleasePoolExpansion(uint64_t phys_addr, size_t len) {
|
||||
|
||||
bool PhysicalMemory::Unmap(uint64_t vaddr, uint64_t size, GpuAccessMode* gpu_mode,
|
||||
uint64_t* host_vaddr_to_release) {
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_physical_memory_unmaps_before_failure == 0) {
|
||||
g_test_physical_memory_unmaps_before_failure = UINT32_MAX;
|
||||
return false;
|
||||
}
|
||||
if (g_test_physical_memory_unmaps_before_failure != UINT32_MAX) {
|
||||
g_test_physical_memory_unmaps_before_failure--;
|
||||
}
|
||||
#endif
|
||||
|
||||
EXIT_IF(gpu_mode == nullptr);
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
@@ -2943,17 +2959,35 @@ static bool ReserveFixedHostRange(uint64_t start, uint64_t size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
for (uint64_t addr = start; addr < start + size; addr += PAGE_SIZE) {
|
||||
if (g_test_host_reservation_pages_before_failure == 0) {
|
||||
g_test_host_reservation_pages_before_failure = UINT32_MAX;
|
||||
return false;
|
||||
}
|
||||
if (g_test_host_reservation_pages_before_failure != UINT32_MAX) {
|
||||
g_test_host_reservation_pages_before_failure--;
|
||||
}
|
||||
}
|
||||
g_test_host_reservation_pages_before_failure = UINT32_MAX;
|
||||
#endif
|
||||
|
||||
bool host_mutated = false;
|
||||
for (uint64_t addr = start; addr < start + size; addr += PAGE_SIZE) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MEMORY_BASIC_INFORMATION info {};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(addr), &info, sizeof(info)) != 0) {
|
||||
if (info.State == MEM_COMMIT) {
|
||||
if (!VirtualMemory::Decommit(addr, PAGE_SIZE)) {
|
||||
if (host_mutated) {
|
||||
EXIT("reserve-fixed partial host decommit cannot be rolled back safely\n");
|
||||
}
|
||||
LOGF_COLOR(Log::Color::Red,
|
||||
"\t reserve-fixed replace: decommit failed at 0x%016" PRIx64 "\n",
|
||||
addr);
|
||||
return false;
|
||||
}
|
||||
host_mutated = true;
|
||||
continue;
|
||||
}
|
||||
if (info.State == MEM_RESERVE) {
|
||||
@@ -2962,10 +2996,14 @@ static bool ReserveFixedHostRange(uint64_t start, uint64_t size) {
|
||||
}
|
||||
#endif
|
||||
if (!VirtualMemory::ReserveFixed(addr, PAGE_SIZE)) {
|
||||
if (host_mutated) {
|
||||
EXIT("reserve-fixed partial host reservation cannot be rolled back safely\n");
|
||||
}
|
||||
LOGF_COLOR(Log::Color::Red,
|
||||
"\t reserve-fixed replace: reserve failed at 0x%016" PRIx64 "\n", addr);
|
||||
return false;
|
||||
}
|
||||
host_mutated = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -2978,9 +3016,12 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
|
||||
struct ReplacedChunk {
|
||||
VirtualRanges::Range range {};
|
||||
VirtualMemory::Mode mode = VirtualMemory::Mode::NoAccess;
|
||||
GpuAccessMode gpu_mode = GpuAccessMode::NoAccess;
|
||||
bool shared_backing = false;
|
||||
VirtualMemory::Mode mode = VirtualMemory::Mode::NoAccess;
|
||||
GpuAccessMode gpu_mode = GpuAccessMode::NoAccess;
|
||||
bool shared_backing = false;
|
||||
bool placeholder_restored = false;
|
||||
bool host_unmapped = false;
|
||||
bool backend_unmapped = false;
|
||||
};
|
||||
|
||||
std::vector<ReplacedChunk> chunks;
|
||||
@@ -3026,22 +3067,39 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
bool backend_restored = true;
|
||||
|
||||
if (chunk.range.type == VirtualRangeType::Direct) {
|
||||
host_restored =
|
||||
(chunk.shared_backing
|
||||
? g_direct_memory_backing->MapFixed(chunk.range.start, chunk.range.size,
|
||||
chunk.range.offset, chunk.mode)
|
||||
: CommitFixedHostRange(chunk.range.start, chunk.range.size, chunk.mode));
|
||||
backend_restored =
|
||||
g_physical_memory->Map(chunk.range.start, chunk.range.offset, chunk.range.size,
|
||||
chunk.range.protection, chunk.mode, chunk.gpu_mode);
|
||||
if (chunk.host_unmapped && chunk.placeholder_restored) {
|
||||
const bool consumed =
|
||||
g_placeholder_address_space->Consume(chunk.range.start, chunk.range.size);
|
||||
host_restored =
|
||||
consumed &&
|
||||
g_direct_memory_backing->MapExistingPlaceholderFixed(
|
||||
chunk.range.start, chunk.range.size, chunk.range.offset, chunk.mode);
|
||||
if (consumed && !host_restored) {
|
||||
g_placeholder_address_space->AddFree(chunk.range.start, chunk.range.size);
|
||||
}
|
||||
} else if (chunk.host_unmapped || !chunk.shared_backing) {
|
||||
host_restored =
|
||||
(chunk.shared_backing ? g_direct_memory_backing->MapFixed(
|
||||
chunk.range.start, chunk.range.size,
|
||||
chunk.range.offset, chunk.mode)
|
||||
: CommitFixedHostRange(chunk.range.start,
|
||||
chunk.range.size, chunk.mode));
|
||||
}
|
||||
if (chunk.backend_unmapped) {
|
||||
backend_restored = g_physical_memory->Map(
|
||||
chunk.range.start, chunk.range.offset, chunk.range.size,
|
||||
chunk.range.protection, chunk.mode, chunk.gpu_mode);
|
||||
}
|
||||
} else if (chunk.range.type == VirtualRangeType::Flexible ||
|
||||
chunk.range.type == VirtualRangeType::Stack ||
|
||||
chunk.range.type == VirtualRangeType::Pooled) {
|
||||
host_restored =
|
||||
CommitFixedHostRange(chunk.range.start, chunk.range.size, chunk.mode);
|
||||
backend_restored = g_flexible_memory->Map(chunk.range.start, chunk.range.size,
|
||||
chunk.range.protection, chunk.mode,
|
||||
chunk.gpu_mode, chunk.range.name);
|
||||
if (chunk.backend_unmapped) {
|
||||
backend_restored = g_flexible_memory->Map(chunk.range.start, chunk.range.size,
|
||||
chunk.range.protection, chunk.mode,
|
||||
chunk.gpu_mode, chunk.range.name);
|
||||
}
|
||||
}
|
||||
|
||||
const bool range_restored = g_virtual_ranges->Add(
|
||||
@@ -3062,6 +3120,7 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
}
|
||||
const bool gpu_unmapped = std::any_of(chunks.begin(), chunks.end(), [](const auto& chunk) {
|
||||
return IsCommittedRangeType(chunk.range.type) &&
|
||||
chunk.gpu_mode != GpuAccessMode::NoAccess &&
|
||||
IsGpuAddressRange(chunk.range.start, chunk.range.size);
|
||||
});
|
||||
for (const auto& chunk: chunks) {
|
||||
@@ -3079,22 +3138,28 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
if (chunk.range.type == VirtualRangeType::Direct) {
|
||||
if (chunk.shared_backing) {
|
||||
bool chunk_placeholder_restored = false;
|
||||
if (!g_direct_memory_backing->Unmap(chunk.range.start, chunk.range.size, false,
|
||||
if (!g_direct_memory_backing->Unmap(chunk.range.start, chunk.range.size, true,
|
||||
&chunk_placeholder_restored)) {
|
||||
unmapped = false;
|
||||
} else if (chunk_placeholder_restored) {
|
||||
g_placeholder_address_space->AddFree(chunk.range.start, chunk.range.size);
|
||||
} else {
|
||||
chunk.host_unmapped = true;
|
||||
if (chunk_placeholder_restored) {
|
||||
g_placeholder_address_space->AddFree(chunk.range.start, chunk.range.size);
|
||||
chunk.placeholder_restored = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unmapped) {
|
||||
unmapped = g_physical_memory->Unmap(chunk.range.start, chunk.range.size, &gpu_mode);
|
||||
chunk.gpu_mode = gpu_mode;
|
||||
chunk.gpu_mode = gpu_mode;
|
||||
chunk.backend_unmapped = unmapped;
|
||||
}
|
||||
} else if (chunk.range.type == VirtualRangeType::Flexible ||
|
||||
chunk.range.type == VirtualRangeType::Stack ||
|
||||
chunk.range.type == VirtualRangeType::Pooled) {
|
||||
unmapped = g_flexible_memory->Unmap(chunk.range.start, chunk.range.size, &gpu_mode);
|
||||
chunk.gpu_mode = gpu_mode;
|
||||
chunk.gpu_mode = gpu_mode;
|
||||
chunk.backend_unmapped = unmapped;
|
||||
}
|
||||
|
||||
if (!unmapped) {
|
||||
@@ -3108,7 +3173,9 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
", size=0x%016" PRIx64 ", type=%s\n",
|
||||
chunk.range.start, chunk.range.size,
|
||||
Common::EnumName(chunk.range.type).c_str());
|
||||
restore_chunks();
|
||||
if (!restore_chunks()) {
|
||||
EXIT("reserve-fixed backend-unmap rollback failed\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3121,27 +3188,53 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size, bool* p
|
||||
" size=0x%016" PRIx64 "\n",
|
||||
start, size);
|
||||
}
|
||||
restore_chunks();
|
||||
if (!restore_chunks()) {
|
||||
EXIT("reserve-fixed host-reservation rollback failed\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!g_virtual_ranges->Add(start, size, 0, 0, 0, VirtualRangeType::Reserved, "anon", false,
|
||||
*placeholder_backed)) {
|
||||
bool range_added = false;
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_fail_next_fixed_reserve_range_add) {
|
||||
g_test_fail_next_fixed_reserve_range_add = false;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
range_added = g_virtual_ranges->Add(start, size, 0, 0, 0, VirtualRangeType::Reserved,
|
||||
"anon", false, *placeholder_backed);
|
||||
}
|
||||
if (!range_added) {
|
||||
if (gpu_unmapped) {
|
||||
EXIT("reserve-fixed range registration failed after GPU unmap: addr=0x%016" PRIx64
|
||||
" size=0x%016" PRIx64 "\n",
|
||||
start, size);
|
||||
}
|
||||
if (*placeholder_backed) {
|
||||
g_placeholder_address_space->ReleaseFree(start, size);
|
||||
} else {
|
||||
if (!*placeholder_backed) {
|
||||
VirtualMemory::Free(start);
|
||||
}
|
||||
LOGF_COLOR(Log::Color::Red,
|
||||
"\t reserve-fixed replace: range add failed at 0x%016" PRIx64
|
||||
", size=0x%016" PRIx64 "\n",
|
||||
start, size);
|
||||
restore_chunks();
|
||||
if (!restore_chunks()) {
|
||||
EXIT("reserve-fixed range-registration rollback failed\n");
|
||||
}
|
||||
if (*placeholder_backed) {
|
||||
auto free_start = start;
|
||||
for (const auto& chunk: chunks) {
|
||||
if (free_start < chunk.range.start &&
|
||||
!g_placeholder_address_space->ReleaseFree(free_start,
|
||||
chunk.range.start - free_start)) {
|
||||
EXIT("reserve-fixed range-registration gap cleanup failed\n");
|
||||
}
|
||||
free_start = chunk.range.start + chunk.range.size;
|
||||
}
|
||||
if (free_start < start + size &&
|
||||
!g_placeholder_address_space->ReleaseFree(free_start, start + size - free_start)) {
|
||||
EXIT("reserve-fixed range-registration tail cleanup failed\n");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3224,6 +3317,28 @@ int KYTY_SYSV_ABI KernelReserveVirtualRange(void** addr, size_t len, int flags,
|
||||
return OK;
|
||||
}
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestFailNextPhysicalMemoryUnmap() {
|
||||
TestFailPhysicalMemoryUnmapAfter(0);
|
||||
}
|
||||
|
||||
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps) {
|
||||
g_test_physical_memory_unmaps_before_failure = successful_unmaps;
|
||||
}
|
||||
|
||||
void TestFailHostReservationAfter(uint32_t successful_pages) {
|
||||
g_test_host_reservation_pages_before_failure = successful_pages;
|
||||
}
|
||||
|
||||
void TestFailNextFixedReserveRangeRegistration() {
|
||||
g_test_fail_next_fixed_reserve_range_add = true;
|
||||
}
|
||||
|
||||
bool TestPlaceholderRangeIsFree(uint64_t vaddr, uint64_t size) {
|
||||
return g_placeholder_address_space->TestContainsFree(vaddr, size);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool KernelHandleReservedRangeAccessViolation(uint64_t vaddr) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
|
||||
|
||||
@@ -159,6 +159,14 @@ void RegisterProgramMemory(uint64_t vaddr, uint64_t size, Common::VirtualMemory:
|
||||
void UpdateProgramMemoryProtection(uint64_t vaddr, uint64_t size, Common::VirtualMemory::Mode mode);
|
||||
void UnregisterProgramMemory(uint64_t vaddr, uint64_t size);
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestFailNextPhysicalMemoryUnmap();
|
||||
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps);
|
||||
void TestFailHostReservationAfter(uint32_t successful_pages);
|
||||
void TestFailNextFixedReserveRangeRegistration();
|
||||
bool TestPlaceholderRangeIsFree(uint64_t vaddr, uint64_t size);
|
||||
#endif
|
||||
|
||||
} // namespace Libs::LibKernel::Memory
|
||||
|
||||
#endif /* EMULATOR_INCLUDE_EMULATOR_KERNEL_MEMORY_H_ */
|
||||
|
||||
@@ -151,10 +151,10 @@ public:
|
||||
return false;
|
||||
}
|
||||
auto* destination = static_cast<uint8_t*>(data);
|
||||
return TryTransferBacking(vaddr, size,
|
||||
[destination](uint8_t* backing, uint64_t data_offset, size_t bytes) {
|
||||
std::memcpy(destination + data_offset, backing, bytes);
|
||||
});
|
||||
return TryTransferBacking(
|
||||
vaddr, size, [destination](uint8_t* backing, uint64_t data_offset, size_t bytes) {
|
||||
std::memcpy(destination + data_offset, backing, bytes);
|
||||
});
|
||||
}
|
||||
|
||||
uint64_t MapAligned(uint64_t search_addr, uint64_t size, uint64_t backing_offset,
|
||||
@@ -342,7 +342,7 @@ public:
|
||||
MapEntry old {};
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
auto next = m_maps.upper_bound(vaddr);
|
||||
auto next = m_maps.upper_bound(vaddr);
|
||||
if (next != m_maps.begin()) {
|
||||
auto entry = std::prev(next);
|
||||
if (vaddr + size <= entry->second.vaddr + entry->second.size) {
|
||||
@@ -497,8 +497,8 @@ public:
|
||||
|
||||
constexpr uint64_t test_value = 0x4b595459414c4941ull; // "KYTYALIA"
|
||||
EXIT_IF(!VirtualMemory::Protect(first, test_size, VirtualMemory::Mode::NoAccess));
|
||||
const bool ok = TryWriteBacking(first, &test_value, sizeof(test_value)) &&
|
||||
*second_ptr == test_value;
|
||||
const bool ok =
|
||||
TryWriteBacking(first, &test_value, sizeof(test_value)) && *second_ptr == test_value;
|
||||
const uint64_t clear = 0;
|
||||
EXIT_IF(!TryWriteBacking(first, &clear, sizeof(clear)));
|
||||
|
||||
@@ -518,8 +518,8 @@ private:
|
||||
}
|
||||
|
||||
std::lock_guard lock(m_mutex);
|
||||
const auto end = vaddr + size;
|
||||
auto current = vaddr;
|
||||
const auto end = vaddr + size;
|
||||
auto current = vaddr;
|
||||
while (current < end) {
|
||||
MapEntry entry {};
|
||||
if (!FindContainingUnlocked(current, 1, &entry)) {
|
||||
@@ -609,24 +609,24 @@ private:
|
||||
|
||||
[[nodiscard]] bool ShouldChunkMap(uint64_t size) const { return size > GetMapChunkSize(); }
|
||||
|
||||
uint64_t MapNewPlaceholderAligned(uint64_t search_addr, uint64_t size,
|
||||
uint64_t backing_offset, VirtualMemory::Mode mode,
|
||||
uint64_t alignment, FailureReason* failure_reason) {
|
||||
uint64_t MapNewPlaceholderAligned(uint64_t search_addr, uint64_t size, uint64_t backing_offset,
|
||||
VirtualMemory::Mode mode, uint64_t alignment,
|
||||
FailureReason* failure_reason) {
|
||||
MEM_ADDRESS_REQUIREMENTS requirements {};
|
||||
MEM_EXTENDED_PARAMETER parameter {};
|
||||
const auto aligned_search =
|
||||
(search_addr + alignment - 1u) & ~(alignment - 1u);
|
||||
requirements.LowestStartingAddress = reinterpret_cast<void*>(
|
||||
search_addr != 0 ? aligned_search : HOST_SYSTEM_MANAGED_MIN);
|
||||
const auto aligned_search = (search_addr + alignment - 1u) & ~(alignment - 1u);
|
||||
requirements.LowestStartingAddress =
|
||||
reinterpret_cast<void*>(search_addr != 0 ? aligned_search : HOST_SYSTEM_MANAGED_MIN);
|
||||
requirements.HighestEndingAddress =
|
||||
reinterpret_cast<void*>(search_addr != 0 ? HOST_USER_MAX : HOST_SYSTEM_MANAGED_MAX);
|
||||
requirements.Alignment = alignment;
|
||||
parameter.Type = MemExtendedParameterAddressRequirements;
|
||||
parameter.Pointer = &requirements;
|
||||
parameter.Type = MemExtendedParameterAddressRequirements;
|
||||
parameter.Pointer = &requirements;
|
||||
|
||||
auto reserve = [&]() {
|
||||
return VirtualAlloc2(GetCurrentProcess(), nullptr, size,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, ¶meter, 1);
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, ¶meter,
|
||||
1);
|
||||
};
|
||||
auto* placeholder = reserve();
|
||||
if (placeholder == nullptr && search_addr == 0) {
|
||||
@@ -1056,9 +1056,9 @@ private:
|
||||
int m_fd = -1;
|
||||
#endif
|
||||
|
||||
uint8_t* m_backing_base = nullptr;
|
||||
uint64_t m_size = 0;
|
||||
std::mutex m_mutex;
|
||||
uint8_t* m_backing_base = nullptr;
|
||||
uint64_t m_size = 0;
|
||||
std::mutex m_mutex;
|
||||
std::map<uint64_t, MapEntry> m_maps;
|
||||
};
|
||||
|
||||
@@ -1098,8 +1098,8 @@ public:
|
||||
if (ptr == nullptr && search_addr == 0) {
|
||||
req.LowestStartingAddress = reinterpret_cast<void*>(HOST_USER_MIN);
|
||||
req.HighestEndingAddress = reinterpret_cast<void*>(HOST_USER_MAX);
|
||||
ptr = VirtualAlloc2(GetCurrentProcess(), nullptr, size,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, ¶m, 1);
|
||||
ptr = VirtualAlloc2(GetCurrentProcess(), nullptr, size,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, ¶m, 1);
|
||||
}
|
||||
if (ptr == nullptr) {
|
||||
return 0;
|
||||
@@ -1161,6 +1161,17 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
bool TestContainsFree(uint64_t vaddr, uint64_t size) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::lock_guard lock(m_mutex);
|
||||
return FindContaining(vaddr, size) != m_free.end();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ReleaseFree(uint64_t vaddr, uint64_t size) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::lock_guard lock(m_mutex);
|
||||
|
||||
Reference in New Issue
Block a user