mirror of
https://github.com/yuzu-emu/yuzu.git
synced 2024-11-27 11:54:18 +01:00
Merge pull request #1928 from lioncash/caps
kernel: Handle kernel capability descriptors
This commit is contained in:
commit
795335af0f
@ -44,6 +44,7 @@ add_library(common STATIC
|
||||
detached_tasks.cpp
|
||||
detached_tasks.h
|
||||
bit_field.h
|
||||
bit_util.h
|
||||
cityhash.cpp
|
||||
cityhash.h
|
||||
color.h
|
||||
|
61
src/common/bit_util.h
Normal file
61
src/common/bit_util.h
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// Gets the size of a specified type T in bits.
|
||||
template <typename T>
|
||||
constexpr std::size_t BitSize() {
|
||||
return sizeof(T) * CHAR_BIT;
|
||||
}
|
||||
|
||||
#ifdef _MSC_VER
|
||||
inline u32 CountLeadingZeroes32(u32 value) {
|
||||
unsigned long leading_zero = 0;
|
||||
|
||||
if (_BitScanReverse(&leading_zero, value) != 0) {
|
||||
return 31 - leading_zero;
|
||||
}
|
||||
|
||||
return 32;
|
||||
}
|
||||
|
||||
inline u64 CountLeadingZeroes64(u64 value) {
|
||||
unsigned long leading_zero = 0;
|
||||
|
||||
if (_BitScanReverse64(&leading_zero, value) != 0) {
|
||||
return 63 - leading_zero;
|
||||
}
|
||||
|
||||
return 64;
|
||||
}
|
||||
#else
|
||||
inline u32 CountLeadingZeroes32(u32 value) {
|
||||
if (value == 0) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
return __builtin_clz(value);
|
||||
}
|
||||
|
||||
inline u64 CountLeadingZeroes64(u64 value) {
|
||||
if (value == 0) {
|
||||
return 64;
|
||||
}
|
||||
|
||||
return __builtin_clzll(value);
|
||||
}
|
||||
#endif
|
||||
} // namespace Common
|
@ -115,6 +115,8 @@ add_library(core STATIC
|
||||
hle/kernel/object.h
|
||||
hle/kernel/process.cpp
|
||||
hle/kernel/process.h
|
||||
hle/kernel/process_capability.cpp
|
||||
hle/kernel/process_capability.h
|
||||
hle/kernel/readable_event.cpp
|
||||
hle/kernel/readable_event.h
|
||||
hle/kernel/resource_limit.cpp
|
||||
|
@ -40,6 +40,13 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
|
||||
if (sizeof(FileAccessHeader) != file->ReadObject(&aci_file_access, aci_header.fah_offset))
|
||||
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||
|
||||
aci_kernel_capabilities.resize(aci_header.kac_size / sizeof(u32));
|
||||
const u64 read_size = aci_header.kac_size;
|
||||
const u64 read_offset = npdm_header.aci_offset + aci_header.kac_offset;
|
||||
if (file->ReadBytes(aci_kernel_capabilities.data(), read_size, read_offset) != read_size) {
|
||||
return Loader::ResultStatus::ErrorBadKernelCapabilityDescriptors;
|
||||
}
|
||||
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
@ -71,6 +78,10 @@ u64 ProgramMetadata::GetFilesystemPermissions() const {
|
||||
return aci_file_access.permissions;
|
||||
}
|
||||
|
||||
const ProgramMetadata::KernelCapabilityDescriptors& ProgramMetadata::GetKernelCapabilities() const {
|
||||
return aci_kernel_capabilities;
|
||||
}
|
||||
|
||||
void ProgramMetadata::Print() const {
|
||||
LOG_DEBUG(Service_FS, "Magic: {:.4}", npdm_header.magic.data());
|
||||
LOG_DEBUG(Service_FS, "Main thread priority: 0x{:02X}", npdm_header.main_thread_priority);
|
||||
|
@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include "common/bit_field.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
@ -38,6 +39,8 @@ enum class ProgramFilePermission : u64 {
|
||||
*/
|
||||
class ProgramMetadata {
|
||||
public:
|
||||
using KernelCapabilityDescriptors = std::vector<u32>;
|
||||
|
||||
ProgramMetadata();
|
||||
~ProgramMetadata();
|
||||
|
||||
@ -50,6 +53,7 @@ public:
|
||||
u32 GetMainThreadStackSize() const;
|
||||
u64 GetTitleID() const;
|
||||
u64 GetFilesystemPermissions() const;
|
||||
const KernelCapabilityDescriptors& GetKernelCapabilities() const;
|
||||
|
||||
void Print() const;
|
||||
|
||||
@ -154,6 +158,8 @@ private:
|
||||
|
||||
FileAccessControl acid_file_access;
|
||||
FileAccessHeader aci_file_access;
|
||||
|
||||
KernelCapabilityDescriptors aci_kernel_capabilities;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
@ -11,6 +11,7 @@ namespace Kernel {
|
||||
// Confirmed Switch kernel error codes
|
||||
|
||||
constexpr ResultCode ERR_MAX_CONNECTIONS_REACHED{ErrorModule::Kernel, 7};
|
||||
constexpr ResultCode ERR_INVALID_CAPABILITY_DESCRIPTOR{ErrorModule::Kernel, 14};
|
||||
constexpr ResultCode ERR_INVALID_SIZE{ErrorModule::Kernel, 101};
|
||||
constexpr ResultCode ERR_INVALID_ADDRESS{ErrorModule::Kernel, 102};
|
||||
constexpr ResultCode ERR_HANDLE_TABLE_FULL{ErrorModule::Kernel, 105};
|
||||
@ -30,6 +31,7 @@ constexpr ResultCode ERR_NOT_FOUND{ErrorModule::Kernel, 121};
|
||||
constexpr ResultCode ERR_BUSY{ErrorModule::Kernel, 122};
|
||||
constexpr ResultCode ERR_SESSION_CLOSED_BY_REMOTE{ErrorModule::Kernel, 123};
|
||||
constexpr ResultCode ERR_INVALID_STATE{ErrorModule::Kernel, 125};
|
||||
constexpr ResultCode ERR_RESERVED_VALUE{ErrorModule::Kernel, 126};
|
||||
constexpr ResultCode ERR_RESOURCE_LIMIT_EXCEEDED{ErrorModule::Kernel, 132};
|
||||
|
||||
} // namespace Kernel
|
||||
|
@ -43,6 +43,9 @@ enum KernelHandle : Handle {
|
||||
*/
|
||||
class HandleTable final : NonCopyable {
|
||||
public:
|
||||
/// This is the maximum limit of handles allowed per process in Horizon
|
||||
static constexpr std::size_t MAX_COUNT = 1024;
|
||||
|
||||
HandleTable();
|
||||
~HandleTable();
|
||||
|
||||
@ -91,9 +94,6 @@ public:
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
/// This is the maximum limit of handles allowed per process in Horizon
|
||||
static constexpr std::size_t MAX_COUNT = 1024;
|
||||
|
||||
/// Stores the Object referenced by the handle or null if the slot is empty.
|
||||
std::array<SharedPtr<Object>, MAX_COUNT> objects;
|
||||
|
||||
|
@ -28,13 +28,11 @@ SharedPtr<Process> Process::Create(KernelCore& kernel, std::string&& name) {
|
||||
SharedPtr<Process> process(new Process(kernel));
|
||||
|
||||
process->name = std::move(name);
|
||||
process->flags.raw = 0;
|
||||
process->flags.memory_region.Assign(MemoryRegion::APPLICATION);
|
||||
process->resource_limit = kernel.GetSystemResourceLimit();
|
||||
process->status = ProcessStatus::Created;
|
||||
process->program_id = 0;
|
||||
process->process_id = kernel.CreateNewProcessID();
|
||||
process->svc_access_mask.set();
|
||||
process->capabilities.InitializeForMetadatalessProcess();
|
||||
|
||||
std::mt19937 rng(Settings::values.rng_seed.value_or(0));
|
||||
std::uniform_int_distribution<u64> distribution;
|
||||
@ -64,83 +62,15 @@ ResultCode Process::ClearSignalState() {
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
void Process::LoadFromMetadata(const FileSys::ProgramMetadata& metadata) {
|
||||
ResultCode Process::LoadFromMetadata(const FileSys::ProgramMetadata& metadata) {
|
||||
program_id = metadata.GetTitleID();
|
||||
ideal_processor = metadata.GetMainThreadCore();
|
||||
is_64bit_process = metadata.Is64BitProgram();
|
||||
|
||||
vm_manager.Reset(metadata.GetAddressSpaceType());
|
||||
}
|
||||
|
||||
void Process::ParseKernelCaps(const u32* kernel_caps, std::size_t len) {
|
||||
for (std::size_t i = 0; i < len; ++i) {
|
||||
u32 descriptor = kernel_caps[i];
|
||||
u32 type = descriptor >> 20;
|
||||
|
||||
if (descriptor == 0xFFFFFFFF) {
|
||||
// Unused descriptor entry
|
||||
continue;
|
||||
} else if ((type & 0xF00) == 0xE00) { // 0x0FFF
|
||||
// Allowed interrupts list
|
||||
LOG_WARNING(Loader, "ExHeader allowed interrupts list ignored");
|
||||
} else if ((type & 0xF80) == 0xF00) { // 0x07FF
|
||||
// Allowed syscalls mask
|
||||
unsigned int index = ((descriptor >> 24) & 7) * 24;
|
||||
u32 bits = descriptor & 0xFFFFFF;
|
||||
|
||||
while (bits && index < svc_access_mask.size()) {
|
||||
svc_access_mask.set(index, bits & 1);
|
||||
++index;
|
||||
bits >>= 1;
|
||||
}
|
||||
} else if ((type & 0xFF0) == 0xFE0) { // 0x00FF
|
||||
// Handle table size
|
||||
handle_table_size = descriptor & 0x3FF;
|
||||
} else if ((type & 0xFF8) == 0xFF0) { // 0x007F
|
||||
// Misc. flags
|
||||
flags.raw = descriptor & 0xFFFF;
|
||||
} else if ((type & 0xFFE) == 0xFF8) { // 0x001F
|
||||
// Mapped memory range
|
||||
if (i + 1 >= len || ((kernel_caps[i + 1] >> 20) & 0xFFE) != 0xFF8) {
|
||||
LOG_WARNING(Loader, "Incomplete exheader memory range descriptor ignored.");
|
||||
continue;
|
||||
}
|
||||
u32 end_desc = kernel_caps[i + 1];
|
||||
++i; // Skip over the second descriptor on the next iteration
|
||||
|
||||
AddressMapping mapping;
|
||||
mapping.address = descriptor << 12;
|
||||
VAddr end_address = end_desc << 12;
|
||||
|
||||
if (mapping.address < end_address) {
|
||||
mapping.size = end_address - mapping.address;
|
||||
} else {
|
||||
mapping.size = 0;
|
||||
}
|
||||
|
||||
mapping.read_only = (descriptor & (1 << 20)) != 0;
|
||||
mapping.unk_flag = (end_desc & (1 << 20)) != 0;
|
||||
|
||||
address_mappings.push_back(mapping);
|
||||
} else if ((type & 0xFFF) == 0xFFE) { // 0x000F
|
||||
// Mapped memory page
|
||||
AddressMapping mapping;
|
||||
mapping.address = descriptor << 12;
|
||||
mapping.size = Memory::PAGE_SIZE;
|
||||
mapping.read_only = false;
|
||||
mapping.unk_flag = false;
|
||||
|
||||
address_mappings.push_back(mapping);
|
||||
} else if ((type & 0xFE0) == 0xFC0) { // 0x01FF
|
||||
// Kernel version
|
||||
kernel_version = descriptor & 0xFFFF;
|
||||
|
||||
int minor = kernel_version & 0xFF;
|
||||
int major = (kernel_version >> 8) & 0xFF;
|
||||
LOG_INFO(Loader, "ExHeader kernel version: {}.{}", major, minor);
|
||||
} else {
|
||||
LOG_ERROR(Loader, "Unhandled kernel caps descriptor: 0x{:08X}", descriptor);
|
||||
}
|
||||
}
|
||||
const auto& caps = metadata.GetKernelCapabilities();
|
||||
return capabilities.InitializeForUserProcess(caps.data(), caps.size(), vm_manager);
|
||||
}
|
||||
|
||||
void Process::Run(VAddr entry_point, s32 main_thread_priority, u32 stack_size) {
|
||||
|
@ -11,9 +11,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
#include "common/bit_field.h"
|
||||
#include "common/common_types.h"
|
||||
#include "core/hle/kernel/handle_table.h"
|
||||
#include "core/hle/kernel/process_capability.h"
|
||||
#include "core/hle/kernel/thread.h"
|
||||
#include "core/hle/kernel/vm_manager.h"
|
||||
#include "core/hle/kernel/wait_object.h"
|
||||
@ -42,24 +42,6 @@ enum class MemoryRegion : u16 {
|
||||
BASE = 3,
|
||||
};
|
||||
|
||||
union ProcessFlags {
|
||||
u16 raw;
|
||||
|
||||
BitField<0, 1, u16>
|
||||
allow_debug; ///< Allows other processes to attach to and debug this process.
|
||||
BitField<1, 1, u16> force_debug; ///< Allows this process to attach to processes even if they
|
||||
/// don't have allow_debug set.
|
||||
BitField<2, 1, u16> allow_nonalphanum;
|
||||
BitField<3, 1, u16> shared_page_writable; ///< Shared page is mapped with write permissions.
|
||||
BitField<4, 1, u16> privileged_priority; ///< Can use priority levels higher than 24.
|
||||
BitField<5, 1, u16> allow_main_args;
|
||||
BitField<6, 1, u16> shared_device_mem;
|
||||
BitField<7, 1, u16> runnable_on_sleep;
|
||||
BitField<8, 4, MemoryRegion>
|
||||
memory_region; ///< Default region for memory allocations for this process
|
||||
BitField<12, 1, u16> loaded_high; ///< Application loaded high (not at 0x00100000).
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates the status of a Process instance.
|
||||
*
|
||||
@ -192,13 +174,13 @@ public:
|
||||
}
|
||||
|
||||
/// Gets the bitmask of allowed CPUs that this process' threads can run on.
|
||||
u32 GetAllowedProcessorMask() const {
|
||||
return allowed_processor_mask;
|
||||
u64 GetAllowedProcessorMask() const {
|
||||
return capabilities.GetCoreMask();
|
||||
}
|
||||
|
||||
/// Gets the bitmask of allowed thread priorities.
|
||||
u32 GetAllowedThreadPriorityMask() const {
|
||||
return allowed_thread_priority_mask;
|
||||
u64 GetAllowedThreadPriorityMask() const {
|
||||
return capabilities.GetPriorityMask();
|
||||
}
|
||||
|
||||
u32 IsVirtualMemoryEnabled() const {
|
||||
@ -239,15 +221,12 @@ public:
|
||||
* Loads process-specifics configuration info with metadata provided
|
||||
* by an executable.
|
||||
*
|
||||
* @param metadata The provided metadata to load process specific info.
|
||||
* @param metadata The provided metadata to load process specific info from.
|
||||
*
|
||||
* @returns RESULT_SUCCESS if all relevant metadata was able to be
|
||||
* loaded and parsed. Otherwise, an error code is returned.
|
||||
*/
|
||||
void LoadFromMetadata(const FileSys::ProgramMetadata& metadata);
|
||||
|
||||
/**
|
||||
* Parses a list of kernel capability descriptors (as found in the ExHeader) and applies them
|
||||
* to this process.
|
||||
*/
|
||||
void ParseKernelCaps(const u32* kernel_caps, std::size_t len);
|
||||
ResultCode LoadFromMetadata(const FileSys::ProgramMetadata& metadata);
|
||||
|
||||
/**
|
||||
* Applies address space changes and launches the process main thread.
|
||||
@ -308,22 +287,8 @@ private:
|
||||
/// Resource limit descriptor for this process
|
||||
SharedPtr<ResourceLimit> resource_limit;
|
||||
|
||||
/// The process may only call SVCs which have the corresponding bit set.
|
||||
std::bitset<0x80> svc_access_mask;
|
||||
/// Maximum size of the handle table for the process.
|
||||
u32 handle_table_size = 0x200;
|
||||
/// Special memory ranges mapped into this processes address space. This is used to give
|
||||
/// processes access to specific I/O regions and device memory.
|
||||
boost::container::static_vector<AddressMapping, 8> address_mappings;
|
||||
ProcessFlags flags;
|
||||
/// Kernel compatibility version for this process
|
||||
u16 kernel_version = 0;
|
||||
/// The default CPU for this process, threads are scheduled on this cpu by default.
|
||||
u8 ideal_processor = 0;
|
||||
/// Bitmask of allowed CPUs that this process' threads can run on. TODO(Subv): Actually parse
|
||||
/// this value from the process header.
|
||||
u32 allowed_processor_mask = THREADPROCESSORID_DEFAULT_MASK;
|
||||
u32 allowed_thread_priority_mask = 0xFFFFFFFF;
|
||||
u32 is_virtual_address_memory_enabled = 0;
|
||||
|
||||
/// The Thread Local Storage area is allocated as processes create threads,
|
||||
@ -333,6 +298,9 @@ private:
|
||||
/// This vector will grow as more pages are allocated for new threads.
|
||||
std::vector<std::bitset<8>> tls_slots;
|
||||
|
||||
/// Contains the parsed process capability descriptors.
|
||||
ProcessCapabilities capabilities;
|
||||
|
||||
/// Whether or not this process is AArch64, or AArch32.
|
||||
/// By default, we currently assume this is true, unless otherwise
|
||||
/// specified by metadata provided to the process during loading.
|
||||
|
355
src/core/hle/kernel/process_capability.cpp
Normal file
355
src/core/hle/kernel/process_capability.cpp
Normal file
@ -0,0 +1,355 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/bit_util.h"
|
||||
#include "core/hle/kernel/errors.h"
|
||||
#include "core/hle/kernel/handle_table.h"
|
||||
#include "core/hle/kernel/process_capability.h"
|
||||
#include "core/hle/kernel/vm_manager.h"
|
||||
|
||||
namespace Kernel {
|
||||
namespace {
|
||||
|
||||
// clang-format off
|
||||
|
||||
// Shift offsets for kernel capability types.
|
||||
enum : u32 {
|
||||
CapabilityOffset_PriorityAndCoreNum = 3,
|
||||
CapabilityOffset_Syscall = 4,
|
||||
CapabilityOffset_MapPhysical = 6,
|
||||
CapabilityOffset_MapIO = 7,
|
||||
CapabilityOffset_Interrupt = 11,
|
||||
CapabilityOffset_ProgramType = 13,
|
||||
CapabilityOffset_KernelVersion = 14,
|
||||
CapabilityOffset_HandleTableSize = 15,
|
||||
CapabilityOffset_Debug = 16,
|
||||
};
|
||||
|
||||
// Combined mask of all parameters that may be initialized only once.
|
||||
constexpr u32 InitializeOnceMask = (1U << CapabilityOffset_PriorityAndCoreNum) |
|
||||
(1U << CapabilityOffset_ProgramType) |
|
||||
(1U << CapabilityOffset_KernelVersion) |
|
||||
(1U << CapabilityOffset_HandleTableSize) |
|
||||
(1U << CapabilityOffset_Debug);
|
||||
|
||||
// Packed kernel version indicating 10.4.0
|
||||
constexpr u32 PackedKernelVersion = 0x520000;
|
||||
|
||||
// Indicates possible types of capabilities that can be specified.
|
||||
enum class CapabilityType : u32 {
|
||||
Unset = 0U,
|
||||
PriorityAndCoreNum = (1U << CapabilityOffset_PriorityAndCoreNum) - 1,
|
||||
Syscall = (1U << CapabilityOffset_Syscall) - 1,
|
||||
MapPhysical = (1U << CapabilityOffset_MapPhysical) - 1,
|
||||
MapIO = (1U << CapabilityOffset_MapIO) - 1,
|
||||
Interrupt = (1U << CapabilityOffset_Interrupt) - 1,
|
||||
ProgramType = (1U << CapabilityOffset_ProgramType) - 1,
|
||||
KernelVersion = (1U << CapabilityOffset_KernelVersion) - 1,
|
||||
HandleTableSize = (1U << CapabilityOffset_HandleTableSize) - 1,
|
||||
Debug = (1U << CapabilityOffset_Debug) - 1,
|
||||
Ignorable = 0xFFFFFFFFU,
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
|
||||
constexpr CapabilityType GetCapabilityType(u32 value) {
|
||||
return static_cast<CapabilityType>((~value & (value + 1)) - 1);
|
||||
}
|
||||
|
||||
u32 GetFlagBitOffset(CapabilityType type) {
|
||||
const auto value = static_cast<u32>(type);
|
||||
return static_cast<u32>(Common::BitSize<u32>() - Common::CountLeadingZeroes32(value));
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
ResultCode ProcessCapabilities::InitializeForKernelProcess(const u32* capabilities,
|
||||
std::size_t num_capabilities,
|
||||
VMManager& vm_manager) {
|
||||
Clear();
|
||||
|
||||
// Allow all cores and priorities.
|
||||
core_mask = 0xF;
|
||||
priority_mask = 0xFFFFFFFFFFFFFFFF;
|
||||
kernel_version = PackedKernelVersion;
|
||||
|
||||
return ParseCapabilities(capabilities, num_capabilities, vm_manager);
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::InitializeForUserProcess(const u32* capabilities,
|
||||
std::size_t num_capabilities,
|
||||
VMManager& vm_manager) {
|
||||
Clear();
|
||||
|
||||
return ParseCapabilities(capabilities, num_capabilities, vm_manager);
|
||||
}
|
||||
|
||||
void ProcessCapabilities::InitializeForMetadatalessProcess() {
|
||||
// Allow all cores and priorities
|
||||
core_mask = 0xF;
|
||||
priority_mask = 0xFFFFFFFFFFFFFFFF;
|
||||
kernel_version = PackedKernelVersion;
|
||||
|
||||
// Allow all system calls and interrupts.
|
||||
svc_capabilities.set();
|
||||
interrupt_capabilities.set();
|
||||
|
||||
// Allow using the maximum possible amount of handles
|
||||
handle_table_size = static_cast<u32>(HandleTable::MAX_COUNT);
|
||||
|
||||
// Allow all debugging capabilities.
|
||||
is_debuggable = true;
|
||||
can_force_debug = true;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::ParseCapabilities(const u32* capabilities,
|
||||
std::size_t num_capabilities,
|
||||
VMManager& vm_manager) {
|
||||
u32 set_flags = 0;
|
||||
u32 set_svc_bits = 0;
|
||||
|
||||
for (std::size_t i = 0; i < num_capabilities; ++i) {
|
||||
const u32 descriptor = capabilities[i];
|
||||
const auto type = GetCapabilityType(descriptor);
|
||||
|
||||
if (type == CapabilityType::MapPhysical) {
|
||||
i++;
|
||||
|
||||
// The MapPhysical type uses two descriptor flags for its parameters.
|
||||
// If there's only one, then there's a problem.
|
||||
if (i >= num_capabilities) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
|
||||
const auto size_flags = capabilities[i];
|
||||
if (GetCapabilityType(size_flags) != CapabilityType::MapPhysical) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
|
||||
const auto result = HandleMapPhysicalFlags(descriptor, size_flags, vm_manager);
|
||||
if (result.IsError()) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const auto result =
|
||||
ParseSingleFlagCapability(set_flags, set_svc_bits, descriptor, vm_manager);
|
||||
if (result.IsError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::ParseSingleFlagCapability(u32& set_flags, u32& set_svc_bits,
|
||||
u32 flag, VMManager& vm_manager) {
|
||||
const auto type = GetCapabilityType(flag);
|
||||
|
||||
if (type == CapabilityType::Unset) {
|
||||
return ERR_INVALID_CAPABILITY_DESCRIPTOR;
|
||||
}
|
||||
|
||||
// Bail early on ignorable entries, as one would expect,
|
||||
// ignorable descriptors can be ignored.
|
||||
if (type == CapabilityType::Ignorable) {
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
// Ensure that the give flag hasn't already been initialized before.
|
||||
// If it has been, then bail.
|
||||
const u32 flag_length = GetFlagBitOffset(type);
|
||||
const u32 set_flag = 1U << flag_length;
|
||||
if ((set_flag & set_flags & InitializeOnceMask) != 0) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
set_flags |= set_flag;
|
||||
|
||||
switch (type) {
|
||||
case CapabilityType::PriorityAndCoreNum:
|
||||
return HandlePriorityCoreNumFlags(flag);
|
||||
case CapabilityType::Syscall:
|
||||
return HandleSyscallFlags(set_svc_bits, flag);
|
||||
case CapabilityType::MapIO:
|
||||
return HandleMapIOFlags(flag, vm_manager);
|
||||
case CapabilityType::Interrupt:
|
||||
return HandleInterruptFlags(flag);
|
||||
case CapabilityType::ProgramType:
|
||||
return HandleProgramTypeFlags(flag);
|
||||
case CapabilityType::KernelVersion:
|
||||
return HandleKernelVersionFlags(flag);
|
||||
case CapabilityType::HandleTableSize:
|
||||
return HandleHandleTableFlags(flag);
|
||||
case CapabilityType::Debug:
|
||||
return HandleDebugFlags(flag);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ERR_INVALID_CAPABILITY_DESCRIPTOR;
|
||||
}
|
||||
|
||||
void ProcessCapabilities::Clear() {
|
||||
svc_capabilities.reset();
|
||||
interrupt_capabilities.reset();
|
||||
|
||||
core_mask = 0;
|
||||
priority_mask = 0;
|
||||
|
||||
handle_table_size = 0;
|
||||
kernel_version = 0;
|
||||
|
||||
program_type = ProgramType::SysModule;
|
||||
|
||||
is_debuggable = false;
|
||||
can_force_debug = false;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandlePriorityCoreNumFlags(u32 flags) {
|
||||
if (priority_mask != 0 || core_mask != 0) {
|
||||
return ERR_INVALID_CAPABILITY_DESCRIPTOR;
|
||||
}
|
||||
|
||||
const u32 core_num_min = (flags >> 16) & 0xFF;
|
||||
const u32 core_num_max = (flags >> 24) & 0xFF;
|
||||
if (core_num_min > core_num_max) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
|
||||
const u32 priority_min = (flags >> 10) & 0x3F;
|
||||
const u32 priority_max = (flags >> 4) & 0x3F;
|
||||
if (priority_min > priority_max) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
|
||||
// The switch only has 4 usable cores.
|
||||
if (core_num_max >= 4) {
|
||||
return ERR_INVALID_PROCESSOR_ID;
|
||||
}
|
||||
|
||||
const auto make_mask = [](u64 min, u64 max) {
|
||||
const u64 range = max - min + 1;
|
||||
const u64 mask = (1ULL << range) - 1;
|
||||
|
||||
return mask << min;
|
||||
};
|
||||
|
||||
core_mask = make_mask(core_num_min, core_num_max);
|
||||
priority_mask = make_mask(priority_min, priority_max);
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleSyscallFlags(u32& set_svc_bits, u32 flags) {
|
||||
const u32 index = flags >> 29;
|
||||
const u32 svc_bit = 1U << index;
|
||||
|
||||
// If we've already set this svc before, bail.
|
||||
if ((set_svc_bits & svc_bit) != 0) {
|
||||
return ERR_INVALID_COMBINATION;
|
||||
}
|
||||
set_svc_bits |= svc_bit;
|
||||
|
||||
const u32 svc_mask = (flags >> 5) & 0xFFFFFF;
|
||||
for (u32 i = 0; i < 24; ++i) {
|
||||
const u32 svc_number = index * 24 + i;
|
||||
|
||||
if ((svc_mask & (1U << i)) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (svc_number >= svc_capabilities.size()) {
|
||||
return ERR_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
svc_capabilities[svc_number] = true;
|
||||
}
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleMapPhysicalFlags(u32 flags, u32 size_flags,
|
||||
VMManager& vm_manager) {
|
||||
// TODO(Lioncache): Implement once the memory manager can handle this.
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleMapIOFlags(u32 flags, VMManager& vm_manager) {
|
||||
// TODO(Lioncache): Implement once the memory manager can handle this.
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleInterruptFlags(u32 flags) {
|
||||
constexpr u32 interrupt_ignore_value = 0x3FF;
|
||||
const u32 interrupt0 = (flags >> 12) & 0x3FF;
|
||||
const u32 interrupt1 = (flags >> 22) & 0x3FF;
|
||||
|
||||
for (u32 interrupt : {interrupt0, interrupt1}) {
|
||||
if (interrupt == interrupt_ignore_value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// This should be checking a generic interrupt controller value
|
||||
// as part of the calculation, however, given we don't currently
|
||||
// emulate that, it's sufficient to mark every interrupt as defined.
|
||||
|
||||
if (interrupt >= interrupt_capabilities.size()) {
|
||||
return ERR_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
interrupt_capabilities[interrupt] = true;
|
||||
}
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleProgramTypeFlags(u32 flags) {
|
||||
const u32 reserved = flags >> 17;
|
||||
if (reserved != 0) {
|
||||
return ERR_RESERVED_VALUE;
|
||||
}
|
||||
|
||||
program_type = static_cast<ProgramType>((flags >> 14) & 0b111);
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleKernelVersionFlags(u32 flags) {
|
||||
// Yes, the internal member variable is checked in the actual kernel here.
|
||||
// This might look odd for options that are only allowed to be initialized
|
||||
// just once, however the kernel has a separate initialization function for
|
||||
// kernel processes and userland processes. The kernel variant sets this
|
||||
// member variable ahead of time.
|
||||
|
||||
const u32 major_version = kernel_version >> 19;
|
||||
|
||||
if (major_version != 0 || flags < 0x80000) {
|
||||
return ERR_INVALID_CAPABILITY_DESCRIPTOR;
|
||||
}
|
||||
|
||||
kernel_version = flags;
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleHandleTableFlags(u32 flags) {
|
||||
const u32 reserved = flags >> 26;
|
||||
if (reserved != 0) {
|
||||
return ERR_RESERVED_VALUE;
|
||||
}
|
||||
|
||||
handle_table_size = (flags >> 16) & 0x3FF;
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
ResultCode ProcessCapabilities::HandleDebugFlags(u32 flags) {
|
||||
const u32 reserved = flags >> 19;
|
||||
if (reserved != 0) {
|
||||
return ERR_RESERVED_VALUE;
|
||||
}
|
||||
|
||||
is_debuggable = (flags & 0x20000) != 0;
|
||||
can_force_debug = (flags & 0x40000) != 0;
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace Kernel
|
264
src/core/hle/kernel/process_capability.h
Normal file
264
src/core/hle/kernel/process_capability.h
Normal file
@ -0,0 +1,264 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
union ResultCode;
|
||||
|
||||
namespace Kernel {
|
||||
|
||||
class VMManager;
|
||||
|
||||
/// The possible types of programs that may be indicated
|
||||
/// by the program type capability descriptor.
|
||||
enum class ProgramType {
|
||||
SysModule,
|
||||
Application,
|
||||
Applet,
|
||||
};
|
||||
|
||||
/// Handles kernel capability descriptors that are provided by
|
||||
/// application metadata. These descriptors provide information
|
||||
/// that alters certain parameters for kernel process instance
|
||||
/// that will run said application (or applet).
|
||||
///
|
||||
/// Capabilities are a sequence of flag descriptors, that indicate various
|
||||
/// configurations and constraints for a particular process.
|
||||
///
|
||||
/// Flag types are indicated by a sequence of set low bits. E.g. the
|
||||
/// types are indicated with the low bits as follows (where x indicates "don't care"):
|
||||
///
|
||||
/// - Priority and core mask : 0bxxxxxxxxxxxx0111
|
||||
/// - Allowed service call mask: 0bxxxxxxxxxxx01111
|
||||
/// - Map physical memory : 0bxxxxxxxxx0111111
|
||||
/// - Map IO memory : 0bxxxxxxxx01111111
|
||||
/// - Interrupts : 0bxxxx011111111111
|
||||
/// - Application type : 0bxx01111111111111
|
||||
/// - Kernel version : 0bx011111111111111
|
||||
/// - Handle table size : 0b0111111111111111
|
||||
/// - Debugger flags : 0b1111111111111111
|
||||
///
|
||||
/// These are essentially a bit offset subtracted by 1 to create a mask.
|
||||
/// e.g. The first entry in the above list is simply bit 3 (value 8 -> 0b1000)
|
||||
/// subtracted by one (7 -> 0b0111)
|
||||
///
|
||||
/// An example of a bit layout (using the map physical layout):
|
||||
/// <example>
|
||||
/// The MapPhysical type indicates a sequence entry pair of:
|
||||
///
|
||||
/// [initial, memory_flags], where:
|
||||
///
|
||||
/// initial:
|
||||
/// bits:
|
||||
/// 7-24: Starting page to map memory at.
|
||||
/// 25 : Indicates if the memory should be mapped as read only.
|
||||
///
|
||||
/// memory_flags:
|
||||
/// bits:
|
||||
/// 7-20 : Number of pages to map
|
||||
/// 21-25: Seems to be reserved (still checked against though)
|
||||
/// 26 : Whether or not the memory being mapped is IO memory, or physical memory
|
||||
/// </example>
|
||||
///
|
||||
class ProcessCapabilities {
|
||||
public:
|
||||
using InterruptCapabilities = std::bitset<1024>;
|
||||
using SyscallCapabilities = std::bitset<128>;
|
||||
|
||||
ProcessCapabilities() = default;
|
||||
ProcessCapabilities(const ProcessCapabilities&) = delete;
|
||||
ProcessCapabilities(ProcessCapabilities&&) = default;
|
||||
|
||||
ProcessCapabilities& operator=(const ProcessCapabilities&) = delete;
|
||||
ProcessCapabilities& operator=(ProcessCapabilities&&) = default;
|
||||
|
||||
/// Initializes this process capabilities instance for a kernel process.
|
||||
///
|
||||
/// @param capabilities The capabilities to parse
|
||||
/// @param num_capabilities The number of capabilities to parse.
|
||||
/// @param vm_manager The memory manager to use for handling any mapping-related
|
||||
/// operations (such as mapping IO memory, etc).
|
||||
///
|
||||
/// @returns RESULT_SUCCESS if this capabilities instance was able to be initialized,
|
||||
/// otherwise, an error code upon failure.
|
||||
///
|
||||
ResultCode InitializeForKernelProcess(const u32* capabilities, std::size_t num_capabilities,
|
||||
VMManager& vm_manager);
|
||||
|
||||
/// Initializes this process capabilities instance for a userland process.
|
||||
///
|
||||
/// @param capabilities The capabilities to parse.
|
||||
/// @param num_capabilities The total number of capabilities to parse.
|
||||
/// @param vm_manager The memory manager to use for handling any mapping-related
|
||||
/// operations (such as mapping IO memory, etc).
|
||||
///
|
||||
/// @returns RESULT_SUCCESS if this capabilities instance was able to be initialized,
|
||||
/// otherwise, an error code upon failure.
|
||||
///
|
||||
ResultCode InitializeForUserProcess(const u32* capabilities, std::size_t num_capabilities,
|
||||
VMManager& vm_manager);
|
||||
|
||||
/// Initializes this process capabilities instance for a process that does not
|
||||
/// have any metadata to parse.
|
||||
///
|
||||
/// This is necessary, as we allow running raw executables, and the internal
|
||||
/// kernel process capabilities also determine what CPU cores the process is
|
||||
/// allowed to run on, and what priorities are allowed for threads. It also
|
||||
/// determines the max handle table size, what the program type is, whether or
|
||||
/// not the process can be debugged, or whether it's possible for a process to
|
||||
/// forcibly debug another process.
|
||||
///
|
||||
/// Given the above, this essentially enables all capabilities across the board
|
||||
/// for the process. It allows the process to:
|
||||
///
|
||||
/// - Run on any core
|
||||
/// - Use any thread priority
|
||||
/// - Use the maximum amount of handles a process is allowed to.
|
||||
/// - Be debuggable
|
||||
/// - Forcibly debug other processes.
|
||||
///
|
||||
/// Note that this is not a behavior that the kernel allows a process to do via
|
||||
/// a single function like this. This is yuzu-specific behavior to handle
|
||||
/// executables with no capability descriptors whatsoever to derive behavior from.
|
||||
/// It being yuzu-specific is why this is also not the default behavior and not
|
||||
/// done by default in the constructor.
|
||||
///
|
||||
void InitializeForMetadatalessProcess();
|
||||
|
||||
/// Gets the allowable core mask
|
||||
u64 GetCoreMask() const {
|
||||
return core_mask;
|
||||
}
|
||||
|
||||
/// Gets the allowable priority mask
|
||||
u64 GetPriorityMask() const {
|
||||
return priority_mask;
|
||||
}
|
||||
|
||||
/// Gets the SVC access permission bits
|
||||
const SyscallCapabilities& GetServiceCapabilities() const {
|
||||
return svc_capabilities;
|
||||
}
|
||||
|
||||
/// Gets the valid interrupt bits.
|
||||
const InterruptCapabilities& GetInterruptCapabilities() const {
|
||||
return interrupt_capabilities;
|
||||
}
|
||||
|
||||
/// Gets the program type for this process.
|
||||
ProgramType GetProgramType() const {
|
||||
return program_type;
|
||||
}
|
||||
|
||||
/// Gets the number of total allowable handles for the process' handle table.
|
||||
u32 GetHandleTableSize() const {
|
||||
return handle_table_size;
|
||||
}
|
||||
|
||||
/// Gets the kernel version value.
|
||||
u32 GetKernelVersion() const {
|
||||
return kernel_version;
|
||||
}
|
||||
|
||||
/// Whether or not this process can be debugged.
|
||||
bool IsDebuggable() const {
|
||||
return is_debuggable;
|
||||
}
|
||||
|
||||
/// Whether or not this process can forcibly debug another
|
||||
/// process, even if that process is not considered debuggable.
|
||||
bool CanForceDebug() const {
|
||||
return can_force_debug;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Attempts to parse a given sequence of capability descriptors.
|
||||
///
|
||||
/// @param capabilities The sequence of capability descriptors to parse.
|
||||
/// @param num_capabilities The number of descriptors within the given sequence.
|
||||
/// @param vm_manager The memory manager that will perform any memory
|
||||
/// mapping if necessary.
|
||||
///
|
||||
/// @return RESULT_SUCCESS if no errors occur, otherwise an error code.
|
||||
///
|
||||
ResultCode ParseCapabilities(const u32* capabilities, std::size_t num_capabilities,
|
||||
VMManager& vm_manager);
|
||||
|
||||
/// Attempts to parse a capability descriptor that is only represented by a
|
||||
/// single flag set.
|
||||
///
|
||||
/// @param set_flags Running set of flags that are used to catch
|
||||
/// flags being initialized more than once when they shouldn't be.
|
||||
/// @param set_svc_bits Running set of bits representing the allowed supervisor calls mask.
|
||||
/// @param flag The flag to attempt to parse.
|
||||
/// @param vm_manager The memory manager that will perform any memory
|
||||
/// mapping if necessary.
|
||||
///
|
||||
/// @return RESULT_SUCCESS if no errors occurred, otherwise an error code.
|
||||
///
|
||||
ResultCode ParseSingleFlagCapability(u32& set_flags, u32& set_svc_bits, u32 flag,
|
||||
VMManager& vm_manager);
|
||||
|
||||
/// Clears the internal state of this process capability instance. Necessary,
|
||||
/// to have a sane starting point due to us allowing running executables without
|
||||
/// configuration metadata. We assume a process is not going to have metadata,
|
||||
/// and if it turns out that the process does, in fact, have metadata, then
|
||||
/// we attempt to parse it. Thus, we need this to reset data members back to
|
||||
/// a good state.
|
||||
///
|
||||
/// DO NOT ever make this a public member function. This isn't an invariant
|
||||
/// anything external should depend upon (and if anything comes to rely on it,
|
||||
/// you should immediately be questioning the design of that thing, not this
|
||||
/// class. If the kernel itself can run without depending on behavior like that,
|
||||
/// then so can yuzu).
|
||||
///
|
||||
void Clear();
|
||||
|
||||
/// Handles flags related to the priority and core number capability flags.
|
||||
ResultCode HandlePriorityCoreNumFlags(u32 flags);
|
||||
|
||||
/// Handles flags related to determining the allowable SVC mask.
|
||||
ResultCode HandleSyscallFlags(u32& set_svc_bits, u32 flags);
|
||||
|
||||
/// Handles flags related to mapping physical memory pages.
|
||||
ResultCode HandleMapPhysicalFlags(u32 flags, u32 size_flags, VMManager& vm_manager);
|
||||
|
||||
/// Handles flags related to mapping IO pages.
|
||||
ResultCode HandleMapIOFlags(u32 flags, VMManager& vm_manager);
|
||||
|
||||
/// Handles flags related to the interrupt capability flags.
|
||||
ResultCode HandleInterruptFlags(u32 flags);
|
||||
|
||||
/// Handles flags related to the program type.
|
||||
ResultCode HandleProgramTypeFlags(u32 flags);
|
||||
|
||||
/// Handles flags related to the handle table size.
|
||||
ResultCode HandleHandleTableFlags(u32 flags);
|
||||
|
||||
/// Handles flags related to the kernel version capability flags.
|
||||
ResultCode HandleKernelVersionFlags(u32 flags);
|
||||
|
||||
/// Handles flags related to debug-specific capabilities.
|
||||
ResultCode HandleDebugFlags(u32 flags);
|
||||
|
||||
SyscallCapabilities svc_capabilities;
|
||||
InterruptCapabilities interrupt_capabilities;
|
||||
|
||||
u64 core_mask = 0;
|
||||
u64 priority_mask = 0;
|
||||
|
||||
u32 handle_table_size = 0;
|
||||
u32 kernel_version = 0;
|
||||
|
||||
ProgramType program_type = ProgramType::SysModule;
|
||||
|
||||
bool is_debuggable = false;
|
||||
bool can_force_debug = false;
|
||||
};
|
||||
|
||||
} // namespace Kernel
|
@ -129,7 +129,10 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(Kernel::Process& process)
|
||||
return ResultStatus::Error32BitISA;
|
||||
}
|
||||
|
||||
process.LoadFromMetadata(metadata);
|
||||
if (process.LoadFromMetadata(metadata).IsError()) {
|
||||
return ResultStatus::ErrorUnableToParseKernelMetadata;
|
||||
}
|
||||
|
||||
const FileSys::PatchManager pm(metadata.GetTitleID());
|
||||
|
||||
// Load NSO modules
|
||||
|
@ -93,7 +93,7 @@ std::string GetFileTypeString(FileType type) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
constexpr std::array<const char*, 60> RESULT_MESSAGES{
|
||||
constexpr std::array<const char*, 62> RESULT_MESSAGES{
|
||||
"The operation completed successfully.",
|
||||
"The loader requested to load is already loaded.",
|
||||
"The operation is not implemented.",
|
||||
@ -103,6 +103,7 @@ constexpr std::array<const char*, 60> RESULT_MESSAGES{
|
||||
"The NPDM has a bad ACI header,",
|
||||
"The NPDM file has a bad file access control.",
|
||||
"The NPDM has a bad file access header.",
|
||||
"The NPDM has bad kernel capability descriptors.",
|
||||
"The PFS/HFS partition has a bad header.",
|
||||
"The PFS/HFS partition has incorrect size as determined by the header.",
|
||||
"The NCA file has a bad header.",
|
||||
@ -125,6 +126,7 @@ constexpr std::array<const char*, 60> RESULT_MESSAGES{
|
||||
"The file could not be found or does not exist.",
|
||||
"The game is missing a program metadata file (main.npdm).",
|
||||
"The game uses the currently-unimplemented 32-bit architecture.",
|
||||
"Unable to completely parse the kernel metadata when loading the emulated process",
|
||||
"The RomFS could not be found.",
|
||||
"The ELF file has incorrect size as determined by the header.",
|
||||
"There was a general error loading the NRO into emulated memory.",
|
||||
|
@ -71,6 +71,7 @@ enum class ResultStatus : u16 {
|
||||
ErrorBadACIHeader,
|
||||
ErrorBadFileAccessControl,
|
||||
ErrorBadFileAccessHeader,
|
||||
ErrorBadKernelCapabilityDescriptors,
|
||||
ErrorBadPFSHeader,
|
||||
ErrorIncorrectPFSFileSize,
|
||||
ErrorBadNCAHeader,
|
||||
@ -93,6 +94,7 @@ enum class ResultStatus : u16 {
|
||||
ErrorNullFile,
|
||||
ErrorMissingNPDM,
|
||||
Error32BitISA,
|
||||
ErrorUnableToParseKernelMetadata,
|
||||
ErrorNoRomFS,
|
||||
ErrorIncorrectELFFileSize,
|
||||
ErrorLoadingNRO,
|
||||
|
Loading…
Reference in New Issue
Block a user