mirror of
https://github.com/mii443/nel_os.git
synced 2025-08-22 16:15:38 +00:00
I/O Emulation
This commit is contained in:
@ -1,5 +1,3 @@
|
|||||||
use core::fmt::Write;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use spin::Mutex;
|
use spin::Mutex;
|
||||||
use uart_16550::SerialPort;
|
use uart_16550::SerialPort;
|
||||||
@ -25,19 +23,6 @@ pub fn _print(args: ::core::fmt::Arguments) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn write_byte(byte: u8) {
|
|
||||||
use x86_64::instructions::interrupts;
|
|
||||||
|
|
||||||
if interrupts::are_enabled() {
|
|
||||||
interrupts::without_interrupts(|| {
|
|
||||||
SERIAL1.lock().send(byte);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
SERIAL1.lock().send(byte);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! serial_print {
|
macro_rules! serial_print {
|
||||||
($($arg:tt)*) => {
|
($($arg:tt)*) => {
|
||||||
|
@ -4,7 +4,6 @@ use lazy_static::lazy_static;
|
|||||||
use spin::Mutex;
|
use spin::Mutex;
|
||||||
use volatile::Volatile;
|
use volatile::Volatile;
|
||||||
|
|
||||||
use crate::serial::SERIAL1;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
|
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
|
||||||
@ -37,15 +36,11 @@ macro_rules! error {
|
|||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn _print(args: fmt::Arguments) {
|
pub fn _print(args: fmt::Arguments) {
|
||||||
use core::fmt::Write;
|
|
||||||
use x86_64::instructions::interrupts;
|
use x86_64::instructions::interrupts;
|
||||||
|
|
||||||
interrupts::without_interrupts(|| {
|
interrupts::without_interrupts(|| {
|
||||||
//WRITER.lock().write_fmt(args).unwrap();
|
//WRITER.lock().write_fmt(args).unwrap();
|
||||||
SERIAL1
|
crate::serial::_print(args);
|
||||||
.lock()
|
|
||||||
.write_fmt(args)
|
|
||||||
.expect("Printing to serial failed");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ pub fn handle_cpuid_exit(vcpu: &mut VCpu) {
|
|||||||
pae: true,
|
pae: true,
|
||||||
mce: false,
|
mce: false,
|
||||||
cx8: true,
|
cx8: true,
|
||||||
apic: true,
|
apic: false,
|
||||||
_reserved_0: false,
|
_reserved_0: false,
|
||||||
sep: true,
|
sep: true,
|
||||||
mtrr: false,
|
mtrr: false,
|
||||||
|
138
src/vmm/io.rs
138
src/vmm/io.rs
@ -1,9 +1,4 @@
|
|||||||
use x86::io::inb;
|
use crate::vmm::{qual::QualIo, vcpu::VCpu};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
serial,
|
|
||||||
vmm::{qual::QualIo, vcpu::VCpu},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Serial {
|
pub struct Serial {
|
||||||
@ -11,6 +6,36 @@ pub struct Serial {
|
|||||||
pub mcr: u8,
|
pub mcr: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum InitPhase {
|
||||||
|
Uninitialized,
|
||||||
|
Phase1,
|
||||||
|
Phase2,
|
||||||
|
Phase3,
|
||||||
|
Initialized,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PIC {
|
||||||
|
pub primary_mask: u8,
|
||||||
|
pub secondary_mask: u8,
|
||||||
|
pub primary_phase: InitPhase,
|
||||||
|
pub secondary_phase: InitPhase,
|
||||||
|
pub primary_base: u8,
|
||||||
|
pub secondary_base: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PIC {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
primary_mask: 0xFF,
|
||||||
|
secondary_mask: 0xFF,
|
||||||
|
primary_phase: InitPhase::Uninitialized,
|
||||||
|
secondary_phase: InitPhase::Uninitialized,
|
||||||
|
primary_base: 0,
|
||||||
|
secondary_base: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_io(vcpu: &mut VCpu, qual: QualIo) {
|
pub fn handle_io(vcpu: &mut VCpu, qual: QualIo) {
|
||||||
match qual.direction() {
|
match qual.direction() {
|
||||||
0 => {
|
0 => {
|
||||||
@ -26,59 +51,82 @@ pub fn handle_io(vcpu: &mut VCpu, qual: QualIo) {
|
|||||||
pub fn handle_io_in(vcpu: &mut VCpu, qual: QualIo) {
|
pub fn handle_io_in(vcpu: &mut VCpu, qual: QualIo) {
|
||||||
let regs = &mut vcpu.guest_registers;
|
let regs = &mut vcpu.guest_registers;
|
||||||
match qual.port() {
|
match qual.port() {
|
||||||
0x0CF8..0x0CFF => {
|
0x0CF8..=0x0CFF => regs.rax = 0,
|
||||||
regs.rax = 0;
|
0xC000..=0xCFFF => {} //ignore
|
||||||
}
|
0x20..=0x21 => handle_pic_in(vcpu, qual),
|
||||||
0xC000..0xCFFF => {} //ignore
|
0xA0..=0xA1 => handle_pic_in(vcpu, qual),
|
||||||
|
0x0070..=0x0071 => regs.rax = 0,
|
||||||
0x03F..0x03FF => handle_serial_in(vcpu, qual),
|
_ => regs.rax = 0,
|
||||||
_ => {
|
|
||||||
panic!("IO in: invalid port: {:#x}", qual.port());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_io_out(vcpu: &mut VCpu, qual: QualIo) {
|
pub fn handle_io_out(vcpu: &mut VCpu, qual: QualIo) {
|
||||||
let regs = &vcpu.guest_registers;
|
|
||||||
match qual.port() {
|
match qual.port() {
|
||||||
0x0CF8..0x0CFF => {} //ignore
|
0x0CF8..=0x0CFF => {} //ignore
|
||||||
0xC000..0xCFFF => {} //ignore
|
0xC000..=0xCFFF => {} //ignore
|
||||||
0x03F8..0x03FF => handle_serial_out(vcpu, qual),
|
0x20..=0x21 => handle_pic_out(vcpu, qual),
|
||||||
_ => {
|
0xA0..=0xA1 => handle_pic_out(vcpu, qual),
|
||||||
panic!("IO out: invalid port: {:#x}", qual.port());
|
0x0070..=0x0071 => {} //ignore
|
||||||
}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_serial_in(vcpu: &mut VCpu, qual: QualIo) {
|
pub fn handle_pic_in(vcpu: &mut VCpu, qual: QualIo) {
|
||||||
let regs = &mut vcpu.guest_registers;
|
let regs = &mut vcpu.guest_registers;
|
||||||
match qual.port() {
|
match qual.port() {
|
||||||
0x3F8 => regs.rax = unsafe { inb(qual.port()).into() },
|
0x21 => match vcpu.pic.primary_phase {
|
||||||
0x3F9 => regs.rax = vcpu.serial.ier as u64,
|
InitPhase::Uninitialized | InitPhase::Initialized => {
|
||||||
0x3FA => regs.rax = unsafe { inb(qual.port()).into() },
|
regs.rax = vcpu.pic.primary_mask as u64;
|
||||||
0x3FB => regs.rax = 0,
|
}
|
||||||
0x3FC => regs.rax = vcpu.serial.mcr as u64,
|
_ => {}
|
||||||
0x3FD => regs.rax = unsafe { inb(qual.port()).into() },
|
},
|
||||||
0x3FE => regs.rax = unsafe { inb(qual.port()).into() },
|
0xA1 => match vcpu.pic.secondary_phase {
|
||||||
0x3FF => regs.rax = 0,
|
InitPhase::Uninitialized | InitPhase::Initialized => {
|
||||||
_ => {
|
regs.rax = vcpu.pic.secondary_mask as u64;
|
||||||
panic!("Serial in: invalid port: {:#x}", qual.port());
|
}
|
||||||
}
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_serial_out(vcpu: &mut VCpu, qual: QualIo) {
|
pub fn handle_pic_out(vcpu: &mut VCpu, qual: QualIo) {
|
||||||
let regs = &mut vcpu.guest_registers;
|
let regs = &mut vcpu.guest_registers;
|
||||||
|
let pic = &mut vcpu.pic;
|
||||||
|
let dx = regs.rax as u8;
|
||||||
match qual.port() {
|
match qual.port() {
|
||||||
0x3F8 => serial::write_byte(regs.rax as u8),
|
0x20 => match dx {
|
||||||
0x3F9 => vcpu.serial.ier = regs.rax as u8,
|
0x11 => pic.primary_phase = InitPhase::Phase1,
|
||||||
0x3FA => {}
|
0x60..=0x67 => {}
|
||||||
0x3FB => {}
|
_ => panic!("Primary PIC command: {:#x}", dx),
|
||||||
0x3FC => vcpu.serial.mcr = regs.rax as u8,
|
},
|
||||||
0x3FD => {}
|
0x21 => match pic.primary_phase {
|
||||||
0x3FF => {}
|
InitPhase::Uninitialized | InitPhase::Initialized => pic.primary_mask = dx,
|
||||||
_ => {
|
InitPhase::Phase1 => {
|
||||||
panic!("Serial out: invalid port: {:#x}", qual.port());
|
pic.primary_base = dx;
|
||||||
}
|
pic.primary_phase = InitPhase::Phase2;
|
||||||
|
}
|
||||||
|
InitPhase::Phase2 => {
|
||||||
|
pic.primary_phase = InitPhase::Phase3;
|
||||||
|
}
|
||||||
|
InitPhase::Phase3 => pic.primary_phase = InitPhase::Initialized,
|
||||||
|
},
|
||||||
|
0xA0 => match dx {
|
||||||
|
0x11 => pic.secondary_phase = InitPhase::Phase1,
|
||||||
|
0x60..=0x67 => {}
|
||||||
|
_ => panic!("Secondary PIC command: {:#x}", dx),
|
||||||
|
},
|
||||||
|
0xA1 => match pic.secondary_phase {
|
||||||
|
InitPhase::Uninitialized | InitPhase::Initialized => pic.secondary_mask = dx,
|
||||||
|
InitPhase::Phase1 => {
|
||||||
|
pic.secondary_base = dx;
|
||||||
|
pic.secondary_phase = InitPhase::Phase2;
|
||||||
|
}
|
||||||
|
InitPhase::Phase2 => {
|
||||||
|
pic.secondary_phase = InitPhase::Phase3;
|
||||||
|
}
|
||||||
|
InitPhase::Phase3 => pic.secondary_phase = InitPhase::Initialized,
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
src/vmm/vcpu.rs
100
src/vmm/vcpu.rs
@ -1,6 +1,7 @@
|
|||||||
use core::{
|
use core::{
|
||||||
arch::x86_64::{_xgetbv, _xsetbv},
|
arch::x86_64::{_xgetbv, _xsetbv},
|
||||||
u64,
|
convert::TryInto,
|
||||||
|
u64, u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
use x86::{
|
use x86::{
|
||||||
@ -10,20 +11,24 @@ use x86::{
|
|||||||
msr::{rdmsr, IA32_EFER, IA32_FS_BASE},
|
msr::{rdmsr, IA32_EFER, IA32_FS_BASE},
|
||||||
vmx::{vmcs, VmFail},
|
vmx::{vmcs, VmFail},
|
||||||
};
|
};
|
||||||
use x86_64::{registers::control::Cr4Flags, structures::paging::OffsetPageTable, VirtAddr};
|
use x86_64::{
|
||||||
|
registers::control::Cr4Flags,
|
||||||
|
structures::paging::{FrameAllocator, OffsetPageTable},
|
||||||
|
VirtAddr,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
info,
|
info,
|
||||||
memory::BootInfoFrameAllocator,
|
memory::BootInfoFrameAllocator,
|
||||||
vmm::{
|
vmm::{
|
||||||
cpuid, cr, fpu,
|
cpuid, cr, fpu,
|
||||||
io::{self, Serial},
|
io::{self, Serial, PIC},
|
||||||
msr,
|
msr,
|
||||||
qual::{QualCr, QualIo},
|
qual::{QualCr, QualIo},
|
||||||
vmcs::{
|
vmcs::{
|
||||||
DescriptorType, EntryControls, Granularity, PrimaryExitControls,
|
DescriptorType, EntryControls, Granularity, PrimaryExitControls,
|
||||||
PrimaryProcessorBasedVmExecutionControls, SecondaryProcessorBasedVmExecutionControls,
|
PrimaryProcessorBasedVmExecutionControls, SecondaryProcessorBasedVmExecutionControls,
|
||||||
SegmentRights, VmxExitInfo, VmxExitReason,
|
SegmentRights, VmxExitReason,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -53,6 +58,9 @@ pub struct VCpu {
|
|||||||
pub xcr0: XCR0,
|
pub xcr0: XCR0,
|
||||||
pub host_xcr0: u64,
|
pub host_xcr0: u64,
|
||||||
pub serial: Serial,
|
pub serial: Serial,
|
||||||
|
pub io_bitmap_a: x86_64::structures::paging::PhysFrame,
|
||||||
|
pub io_bitmap_b: x86_64::structures::paging::PhysFrame,
|
||||||
|
pub pic: PIC,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMP_STACK_SIZE: usize = 4096;
|
const TEMP_STACK_SIZE: usize = 4096;
|
||||||
@ -66,6 +74,10 @@ impl VCpu {
|
|||||||
let ept = EPT::new(frame_allocator);
|
let ept = EPT::new(frame_allocator);
|
||||||
let eptp = EPTP::new(&ept.root_table);
|
let eptp = EPTP::new(&ept.root_table);
|
||||||
|
|
||||||
|
// Allocate I/O bitmaps (4KB each)
|
||||||
|
let io_bitmap_a = frame_allocator.allocate_frame().unwrap();
|
||||||
|
let io_bitmap_b = frame_allocator.allocate_frame().unwrap();
|
||||||
|
|
||||||
VCpu {
|
VCpu {
|
||||||
vmxon,
|
vmxon,
|
||||||
vmcs,
|
vmcs,
|
||||||
@ -80,6 +92,9 @@ impl VCpu {
|
|||||||
xcr0: XCR0(3),
|
xcr0: XCR0(3),
|
||||||
host_xcr0: 0,
|
host_xcr0: 0,
|
||||||
serial: Serial::default(),
|
serial: Serial::default(),
|
||||||
|
io_bitmap_a,
|
||||||
|
io_bitmap_b,
|
||||||
|
pic: PIC::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +114,7 @@ impl VCpu {
|
|||||||
self.setup_exit_ctrls().unwrap();
|
self.setup_exit_ctrls().unwrap();
|
||||||
self.setup_host_state().unwrap();
|
self.setup_host_state().unwrap();
|
||||||
self.setup_guest_state().unwrap();
|
self.setup_guest_state().unwrap();
|
||||||
|
self.setup_io_bitmaps();
|
||||||
self.setup_guest_memory(frame_allocator);
|
self.setup_guest_memory(frame_allocator);
|
||||||
self.register_msrs(&mapper);
|
self.register_msrs(&mapper);
|
||||||
}
|
}
|
||||||
@ -304,11 +320,12 @@ impl VCpu {
|
|||||||
|
|
||||||
primary_exec_ctrl.0 |= (reserved_bits & 0xFFFFFFFF) as u32;
|
primary_exec_ctrl.0 |= (reserved_bits & 0xFFFFFFFF) as u32;
|
||||||
primary_exec_ctrl.0 &= (reserved_bits >> 32) as u32;
|
primary_exec_ctrl.0 &= (reserved_bits >> 32) as u32;
|
||||||
primary_exec_ctrl.set_hlt(false);
|
primary_exec_ctrl.set_hlt(true);
|
||||||
primary_exec_ctrl.set_activate_secondary_controls(true);
|
primary_exec_ctrl.set_activate_secondary_controls(true);
|
||||||
primary_exec_ctrl.set_use_tpr_shadow(true);
|
primary_exec_ctrl.set_use_tpr_shadow(true);
|
||||||
primary_exec_ctrl.set_use_msr_bitmap(false);
|
primary_exec_ctrl.set_use_msr_bitmap(false);
|
||||||
primary_exec_ctrl.set_unconditional_io(true);
|
primary_exec_ctrl.set_unconditional_io(false);
|
||||||
|
primary_exec_ctrl.set_use_io_bitmap(true);
|
||||||
|
|
||||||
primary_exec_ctrl.write();
|
primary_exec_ctrl.write();
|
||||||
|
|
||||||
@ -395,6 +412,59 @@ impl VCpu {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setup_io_bitmaps(&mut self) {
|
||||||
|
info!("Setting up I/O bitmaps");
|
||||||
|
|
||||||
|
let bitmap_a_vaddr = self.io_bitmap_a.start_address().as_u64() + self.phys_mem_offset;
|
||||||
|
let bitmap_b_vaddr = self.io_bitmap_b.start_address().as_u64() + self.phys_mem_offset;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
core::ptr::write_bytes(bitmap_a_vaddr as *mut u8, u8::MAX, 4096);
|
||||||
|
core::ptr::write_bytes(bitmap_b_vaddr as *mut u8, u8::MAX, 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitmap_a = unsafe { core::slice::from_raw_parts_mut(bitmap_a_vaddr as *mut u8, 4096) };
|
||||||
|
let bitmap_b = unsafe { core::slice::from_raw_parts_mut(bitmap_b_vaddr as *mut u8, 4096) };
|
||||||
|
|
||||||
|
self.set_io_ports(bitmap_a, bitmap_b, 0x02F8..=0x03FF);
|
||||||
|
self.set_io_ports(bitmap_a, bitmap_b, 0x0040..=0x0047);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
vmwrite(
|
||||||
|
vmcs::control::IO_BITMAP_A_ADDR_FULL,
|
||||||
|
self.io_bitmap_a.start_address().as_u64(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
vmwrite(
|
||||||
|
vmcs::control::IO_BITMAP_B_ADDR_FULL,
|
||||||
|
self.io_bitmap_b.start_address().as_u64(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("I/O bitmaps configured - PCI ports 0xC000-0xCFFF will trigger VM exits");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_io_ports(
|
||||||
|
&self,
|
||||||
|
bitmap_a: &mut [u8],
|
||||||
|
bitmap_b: &mut [u8],
|
||||||
|
ports: core::ops::RangeInclusive<u16>,
|
||||||
|
) {
|
||||||
|
for port in ports {
|
||||||
|
if port <= 0x7FFF {
|
||||||
|
let byte_index = port as usize / 8;
|
||||||
|
let bit_index = port as usize % 8;
|
||||||
|
bitmap_a[byte_index] &= !(1 << bit_index);
|
||||||
|
} else {
|
||||||
|
let adjusted_port = port - 0x8000;
|
||||||
|
let byte_index = adjusted_port as usize / 8;
|
||||||
|
let bit_index = adjusted_port as usize % 8;
|
||||||
|
bitmap_b[byte_index] &= !(1 << bit_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setup_host_state(&mut self) -> Result<(), VmFail> {
|
pub fn setup_host_state(&mut self) -> Result<(), VmFail> {
|
||||||
info!("Setting up host state");
|
info!("Setting up host state");
|
||||||
unsafe {
|
unsafe {
|
||||||
@ -681,10 +751,11 @@ impl VCpu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn vmexit_handler(&mut self) {
|
fn vmexit_handler(&mut self) {
|
||||||
let info = VmxExitInfo::read();
|
let exit_reason_raw = unsafe { vmread(vmcs::ro::EXIT_REASON).unwrap() as u32 };
|
||||||
|
|
||||||
if info.entry_failure() {
|
if (exit_reason_raw & (1 << 31)) != 0 {
|
||||||
let reason = info.0 & 0xFF;
|
// VM-entry failure
|
||||||
|
let reason = exit_reason_raw & 0xFF;
|
||||||
match reason {
|
match reason {
|
||||||
33 => {
|
33 => {
|
||||||
info!(" Reason: VM-entry failure due to invalid guest state");
|
info!(" Reason: VM-entry failure due to invalid guest state");
|
||||||
@ -698,7 +769,9 @@ impl VCpu {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match info.get_reason() {
|
let basic_reason = (exit_reason_raw & 0xFFFF) as u16;
|
||||||
|
let exit_reason: VmxExitReason = basic_reason.try_into().unwrap();
|
||||||
|
match exit_reason {
|
||||||
VmxExitReason::HLT => {
|
VmxExitReason::HLT => {
|
||||||
info!("HLT instruction executed");
|
info!("HLT instruction executed");
|
||||||
}
|
}
|
||||||
@ -734,12 +807,13 @@ impl VCpu {
|
|||||||
}
|
}
|
||||||
VmxExitReason::IO_INSTRUCTION => {
|
VmxExitReason::IO_INSTRUCTION => {
|
||||||
let qual = unsafe { vmread(vmcs::ro::EXIT_QUALIFICATION).unwrap() };
|
let qual = unsafe { vmread(vmcs::ro::EXIT_QUALIFICATION).unwrap() };
|
||||||
let qual = QualIo(qual);
|
let qual_io = QualIo(qual);
|
||||||
io::handle_io(self, qual);
|
|
||||||
|
io::handle_io(self, qual_io);
|
||||||
self.step_next_inst().unwrap();
|
self.step_next_inst().unwrap();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("VMExit reason: {:?}", info.get_reason());
|
panic!("VMExit reason: {:?}", exit_reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user