mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-28 06:55:50 +00:00
Compare commits
1 Commits
ripple/sma
...
ripple/sma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518e08a537 |
@@ -671,11 +671,126 @@ struct Wasm_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWasmMemory()
|
||||
{
|
||||
testcase("Wasm additional memory limit tests");
|
||||
auto& engine = WasmEngine::instance();
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryPointerAtLimitHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
if (BEAST_EXPECT(re.has_value()))
|
||||
{
|
||||
BEAST_EXPECT(re->result == 1);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryPointerOverLimitHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryOffsetOverLimitHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryEndOfWordOverLimitHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryGrow0To1PageHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
if (BEAST_EXPECT(re.has_value()))
|
||||
{
|
||||
BEAST_EXPECT(re->result == 1);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryLastByteOf8MBHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
if (BEAST_EXPECT(re.has_value()))
|
||||
{
|
||||
BEAST_EXPECT(re->result == 1);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(memoryGrow1MoreThan8MBHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
if (BEAST_EXPECT(re.has_value()))
|
||||
{
|
||||
BEAST_EXPECT(re->result == -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWasmTable()
|
||||
{
|
||||
testcase("Wasm table limit tests");
|
||||
auto& engine = WasmEngine::instance();
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(table64ElementsHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
if (BEAST_EXPECT(re.has_value()))
|
||||
{
|
||||
BEAST_EXPECT(re->result == 1);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(table65ElementsHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(table2TablesHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWasmProposal()
|
||||
{
|
||||
testcase("Wasm disabled proposal tests");
|
||||
auto& engine = WasmEngine::instance();
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(proposalMutableGlobalHex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWasmTrap()
|
||||
{
|
||||
testcase("Wasm trap tests");
|
||||
auto& engine = WasmEngine::instance();
|
||||
{
|
||||
auto const ws = boost::algorithm::unhex(divideBy0Hex);
|
||||
Bytes const wasm(ws.begin(), ws.end());
|
||||
auto const re = engine.run(wasm, "finish");
|
||||
BEAST_EXPECT(!re.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
/*
|
||||
testGetDataHelperFunctions();
|
||||
testWasmLib();
|
||||
testBadWasm();
|
||||
@@ -697,6 +812,11 @@ struct Wasm_test : public beast::unit_test::suite
|
||||
|
||||
testCodecovWasm();
|
||||
testDisabledFloat();
|
||||
*/
|
||||
testWasmMemory();
|
||||
testWasmTable();
|
||||
testWasmProposal();
|
||||
testWasmTrap();
|
||||
|
||||
// perfTest();
|
||||
}
|
||||
|
||||
@@ -9578,3 +9578,56 @@ extern std::string const updateDataWasmHex =
|
||||
"3861373930636664623432626432343732302900490f7461726765745f6665617475726573"
|
||||
"042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e"
|
||||
"63652d74797065732b0a6d756c746976616c7565";
|
||||
|
||||
extern std::string const memoryPointerAtLimitHex =
|
||||
"0061736d010000000105016000017f030201000503010001070a010666696e69736800000a"
|
||||
"0e010c0041ffff032d00001a41010b";
|
||||
|
||||
extern std::string const memoryPointerOverLimitHex =
|
||||
"0061736d010000000105016000017f030201000503010001070a010666696e69736800000a"
|
||||
"0e010c00418080042d00001a41010b";
|
||||
|
||||
extern std::string const memoryOffsetOverLimitHex =
|
||||
"0061736d010000000105016000017f030201000503010001071302066d656d6f7279020006"
|
||||
"66696e69736800000a0e010c00410028028080041a41010b";
|
||||
|
||||
extern std::string const memoryEndOfWordOverLimitHex =
|
||||
"0061736d010000000105016000017f030201000503010001071302066d656d6f7279020006"
|
||||
"66696e69736800000a0e010c0041feff032802001a41010b";
|
||||
|
||||
extern std::string const memoryGrow0To1PageHex =
|
||||
"0061736d010000000105016000017f030201000503010000071302066d656d6f7279020006"
|
||||
"66696e69736800000a0b010900410140001a41010b";
|
||||
|
||||
extern std::string const memoryLastByteOf8MBHex =
|
||||
"0061736d010000000105016000017f030201000506010180018001071302066d656d6f7279"
|
||||
"02000666696e69736800000a0f010d0041ffffff032d00001a41010b";
|
||||
|
||||
extern std::string const memoryGrow1MoreThan8MBHex =
|
||||
"0061736d010000000105016000017f03020100050401008001071302066d656d6f72790200"
|
||||
"0666696e69736800000a1301110041014000417f460440417f0f0b41010b";
|
||||
|
||||
extern std::string const table64ElementsHex =
|
||||
"0061736d010000000108026000006000017f0303020001040401700040070a010666696e69"
|
||||
"736800010946010041000b4000000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"00000a090202000b040041010b";
|
||||
|
||||
extern std::string const table65ElementsHex =
|
||||
"0061736d010000000108026000006000017f0303020001040401700041070a010666696e69"
|
||||
"736800010947010041000b4100000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000a090202000b040041010b";
|
||||
|
||||
extern std::string const table2TablesHex =
|
||||
"0061736d010000000108026000006000017f03030200010409027001010170010101070a01"
|
||||
"0666696e6973680001090f020041000b0100020141000b0001000a090202000b040041010"
|
||||
"b";
|
||||
|
||||
extern std::string const proposalMutableGlobalHex =
|
||||
"0061736d010000000105016000017f030201000606017f0141000b07140207636f756e7465"
|
||||
"7203000666696e69736800000a0d010b00230041016a240041010b";
|
||||
|
||||
extern std::string const divideBy0Hex =
|
||||
"0061736d010000000105016000017f03020100070a010666696e69736800000a0c010a0041"
|
||||
"2a41006d1a41010b";
|
||||
|
||||
@@ -33,3 +33,19 @@ extern std::string const float0Hex;
|
||||
extern std::string const disabledFloatHex;
|
||||
|
||||
extern std::string const updateDataWasmHex;
|
||||
|
||||
extern std::string const memoryPointerAtLimitHex;
|
||||
extern std::string const memoryPointerOverLimitHex;
|
||||
extern std::string const memoryOffsetOverLimitHex;
|
||||
extern std::string const memoryEndOfWordOverLimitHex;
|
||||
extern std::string const memoryGrow0To1PageHex;
|
||||
extern std::string const memoryLastByteOf8MBHex;
|
||||
extern std::string const memoryGrow1MoreThan8MBHex;
|
||||
|
||||
extern std::string const table64ElementsHex;
|
||||
extern std::string const table65ElementsHex;
|
||||
extern std::string const table2TablesHex;
|
||||
|
||||
extern std::string const proposalMutableGlobalHex;
|
||||
|
||||
extern std::string const divideBy0Hex;
|
||||
15
src/test/app/wasm_fixtures/wat/divide_by_0.wat
Normal file
15
src/test/app/wasm_fixtures/wat/divide_by_0.wat
Normal file
@@ -0,0 +1,15 @@
|
||||
(module
|
||||
(func $finish (export "finish") (result i32)
|
||||
;; Setup for Requirement 2: Divide an i32 by 0
|
||||
i32.const 42 ;; Push numerator
|
||||
i32.const 0 ;; Push denominator (0)
|
||||
i32.div_s ;; Perform signed division (42 / 0)
|
||||
|
||||
;; --- NOTE: Execution usually traps (crashes) at the line above ---
|
||||
|
||||
;; Logic to satisfy Requirement 1: Return i32 = 1
|
||||
;; If execution continued, we would drop the division result and return 1
|
||||
drop ;; Clear the stack
|
||||
i32.const 1 ;; Push the return value
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
(module
|
||||
;; 1. Define Memory: 1 Page = 64KB = 65,536 bytes
|
||||
(memory 1)
|
||||
|
||||
;; Export memory so the host can inspect it if needed
|
||||
(export "memory" (memory 0))
|
||||
|
||||
(func $test_straddle (result i32)
|
||||
;; Push the address onto the stack.
|
||||
;; 65534 is valid, but it is only 2 bytes away from the end.
|
||||
i32.const 65534
|
||||
|
||||
;; Attempt to load an i32 (4 bytes) from that address.
|
||||
;; This requires bytes 65534, 65535, 65536, and 65537.
|
||||
;; Since 65536 is the first invalid byte, this MUST trap.
|
||||
i32.load
|
||||
|
||||
;; Clean up the stack.
|
||||
;; The load pushed a value, but we don't care what it is.
|
||||
drop
|
||||
|
||||
;; Return 1 to signal "I survived the memory access"
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
;; Export the function so you can call it from your host (JS, Python, etc.)
|
||||
(export "finish" (func $test_straddle))
|
||||
)
|
||||
26
src/test/app/wasm_fixtures/wat/memory_grow_0_to_1.wat
Normal file
26
src/test/app/wasm_fixtures/wat/memory_grow_0_to_1.wat
Normal file
@@ -0,0 +1,26 @@
|
||||
(module
|
||||
;; 1. Define Memory: Start with 0 pages
|
||||
(memory 0)
|
||||
|
||||
;; Export memory to host
|
||||
(export "memory" (memory 0))
|
||||
|
||||
(func $grow_from_zero (result i32)
|
||||
;; We have 0 pages. We want to add 1 page.
|
||||
;; Push delta (1) onto stack.
|
||||
i32.const 1
|
||||
|
||||
;; Grow the memory.
|
||||
;; If successful: memory becomes 64KB, returns old size (0).
|
||||
;; If failed: memory stays 0, returns -1.
|
||||
memory.grow
|
||||
|
||||
;; Drop the return value of memory.grow
|
||||
drop
|
||||
|
||||
;; Return 1 (as requested)
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
(export "finish" (func $grow_from_zero))
|
||||
)
|
||||
26
src/test/app/wasm_fixtures/wat/memory_last_byte_of_8MB.wat
Normal file
26
src/test/app/wasm_fixtures/wat/memory_last_byte_of_8MB.wat
Normal file
@@ -0,0 +1,26 @@
|
||||
(module
|
||||
;; Define memory: 128 pages (8MB) min, 128 pages max
|
||||
(memory 128 128)
|
||||
|
||||
;; Export memory so host can verify size
|
||||
(export "memory" (memory 0))
|
||||
|
||||
(func $access_last_byte (result i32)
|
||||
;; Math: 128 pages * 64,536 bytes/page = 8,388,608 bytes
|
||||
;; Valid indices: 0 to 8,388,607
|
||||
|
||||
;; Push the address of the LAST valid byte
|
||||
i32.const 8388607
|
||||
|
||||
;; Load byte from that address
|
||||
i32.load8_u
|
||||
|
||||
;; Drop the value (we don't care what it is, just that we could read it)
|
||||
drop
|
||||
|
||||
;; Return 1 to indicate success
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
(export "finish" (func $access_last_byte))
|
||||
)
|
||||
27
src/test/app/wasm_fixtures/wat/memory_offset_over_limit.wat
Normal file
27
src/test/app/wasm_fixtures/wat/memory_offset_over_limit.wat
Normal file
@@ -0,0 +1,27 @@
|
||||
(module
|
||||
;; 1. Define Memory: 1 Page = 64KB
|
||||
(memory 1)
|
||||
|
||||
(export "memory" (memory 0))
|
||||
|
||||
(func $test_offset_overflow (result i32)
|
||||
;; 1. Push the base address onto the stack.
|
||||
;; We use '0', which is the safest, most valid address possible.
|
||||
i32.const 0
|
||||
|
||||
;; 2. Attempt to load using a static offset.
|
||||
;; syntax: i32.load offset=N align=N
|
||||
;; We set the offset to 65536 (the size of the memory).
|
||||
;; The effective address becomes 0 + 65536 = 65536.
|
||||
i32.load offset=65536
|
||||
|
||||
;; Clean up the stack.
|
||||
;; The load pushed a value, but we don't care what it is.
|
||||
drop
|
||||
|
||||
;; Return 1 to signal "I survived the memory access"
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
(export "finish" (func $test_offset_overflow))
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
(module
|
||||
;; Start at your limit: 128 pages (8MB)
|
||||
(memory 128)
|
||||
(export "memory" (memory 0))
|
||||
|
||||
(func $try_grow_beyond_limit (result i32)
|
||||
;; Attempt to grow by 1 page
|
||||
i32.const 1
|
||||
memory.grow
|
||||
|
||||
;; memory.grow returns:
|
||||
;; -1 if the growth failed (Correct behavior for your limit)
|
||||
;; 128 (old size) if growth succeeded (Means limit was bypassed)
|
||||
|
||||
;; Check if result == -1
|
||||
i32.const -1
|
||||
i32.eq
|
||||
if
|
||||
;; Growth FAILED (Host blocked it). Return -1.
|
||||
i32.const -1
|
||||
return
|
||||
end
|
||||
|
||||
;; Growth SUCCEEDED (Host allowed it). Return 1.
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
(export "finish" (func $try_grow_beyond_limit))
|
||||
)
|
||||
22
src/test/app/wasm_fixtures/wat/memory_pointer_at_limit.wat
Normal file
22
src/test/app/wasm_fixtures/wat/memory_pointer_at_limit.wat
Normal file
@@ -0,0 +1,22 @@
|
||||
(module
|
||||
;; Define 1 page of memory (64KB = 65,536 bytes)
|
||||
(memory 1)
|
||||
|
||||
(func $read_edge (result i32)
|
||||
;; Push the index of the LAST valid byte
|
||||
i32.const 65535
|
||||
|
||||
;; Load 1 byte (unsigned)
|
||||
i32.load8_u
|
||||
|
||||
;; Clean up the stack.
|
||||
;; The load pushed a value, but we don't care what it is.
|
||||
drop
|
||||
|
||||
;; Return 1 to signal "I survived the memory access"
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
;; Export as "finish" as requested
|
||||
(export "finish" (func $read_edge))
|
||||
)
|
||||
23
src/test/app/wasm_fixtures/wat/memory_pointer_over_limit.wat
Normal file
23
src/test/app/wasm_fixtures/wat/memory_pointer_over_limit.wat
Normal file
@@ -0,0 +1,23 @@
|
||||
(module
|
||||
;; Define 1 page of memory (64KB = 65,536 bytes)
|
||||
(memory 1)
|
||||
|
||||
(func $read_overflow (result i32)
|
||||
;; Push the index of the FIRST invalid byte
|
||||
;; Memory is 0..65535, so 65536 is out of bounds.
|
||||
i32.const 65536
|
||||
|
||||
;; Load 1 byte (unsigned)
|
||||
i32.load8_u
|
||||
|
||||
;; Clean up the stack.
|
||||
;; The load pushed a value, but we don't care what it is.
|
||||
drop
|
||||
|
||||
;; Return 1 to signal "I survived the memory access"
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
;; Export as "finish" as requested
|
||||
(export "finish" (func $read_overflow))
|
||||
)
|
||||
25
src/test/app/wasm_fixtures/wat/proposal_mutable_global.wat
Normal file
25
src/test/app/wasm_fixtures/wat/proposal_mutable_global.wat
Normal file
@@ -0,0 +1,25 @@
|
||||
(module
|
||||
;; Define a mutable global initialized to 0
|
||||
(global $counter (mut i32) (i32.const 0))
|
||||
|
||||
;; EXPORTING a mutable global is the key feature of this proposal.
|
||||
;; In strict MVP, exported globals had to be immutable (const).
|
||||
(export "counter" (global $counter))
|
||||
|
||||
(func $finish (result i32)
|
||||
;; 1. Get current value
|
||||
global.get $counter
|
||||
|
||||
;; 2. Add 1
|
||||
i32.const 1
|
||||
i32.add
|
||||
|
||||
;; 3. Set new value (Mutation)
|
||||
global.set $counter
|
||||
|
||||
;; 4. Return 1 for success
|
||||
i32.const 1
|
||||
)
|
||||
|
||||
(export "finish" (func $finish))
|
||||
)
|
||||
1
src/test/app/wasm_fixtures/wat/proposal_stringref.wat
Normal file
1
src/test/app/wasm_fixtures/wat/proposal_stringref.wat
Normal file
@@ -0,0 +1 @@
|
||||
;;hard to generate
|
||||
24
src/test/app/wasm_fixtures/wat/table_2_tables.wat
Normal file
24
src/test/app/wasm_fixtures/wat/table_2_tables.wat
Normal file
@@ -0,0 +1,24 @@
|
||||
(module
|
||||
;; Define a dummy function to put in the tables
|
||||
(func $dummy)
|
||||
|
||||
;; TABLE 0: The default table (allowed in MVP)
|
||||
;; Size: 1 initial, 1 max
|
||||
(table $t0 1 1 funcref)
|
||||
|
||||
;; Initialize Table 0 at index 0
|
||||
(elem (table $t0) (i32.const 0) $dummy)
|
||||
|
||||
;; TABLE 1: The second table (Requires Reference Types proposal)
|
||||
;; If strict MVP is enforced, the parser should error here.
|
||||
(table $t1 1 1 funcref)
|
||||
|
||||
;; Initialize Table 1 at index 0
|
||||
(elem (table $t1) (i32.const 0) $dummy)
|
||||
|
||||
(func $finish (result i32)
|
||||
;; If we successfully loaded a module with 2 tables, return 1.
|
||||
i32.const 1
|
||||
)
|
||||
(export "finish" (func $finish))
|
||||
)
|
||||
25
src/test/app/wasm_fixtures/wat/table_64_elements.wat
Normal file
25
src/test/app/wasm_fixtures/wat/table_64_elements.wat
Normal file
@@ -0,0 +1,25 @@
|
||||
(module
|
||||
;; Define a table with exactly 64 entries
|
||||
(table 64 funcref)
|
||||
|
||||
;; A dummy function to reference
|
||||
(func $dummy)
|
||||
|
||||
;; Initialize the table at offset 0 with 64 references to $dummy
|
||||
(elem (i32.const 0)
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 8
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 16
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 24
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 32
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 40
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 48
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 56
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 64
|
||||
)
|
||||
|
||||
;; Standard finish function
|
||||
(func $finish (result i32)
|
||||
i32.const 1
|
||||
)
|
||||
(export "finish" (func $finish))
|
||||
)
|
||||
25
src/test/app/wasm_fixtures/wat/table_65_elements.wat
Normal file
25
src/test/app/wasm_fixtures/wat/table_65_elements.wat
Normal file
@@ -0,0 +1,25 @@
|
||||
(module
|
||||
;; Define a table with exactly 65 entries
|
||||
(table 65 funcref)
|
||||
|
||||
;; A dummy function to reference
|
||||
(func $dummy)
|
||||
|
||||
;; Initialize the table at offset 0 with 65 references to $dummy
|
||||
(elem (i32.const 0)
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 8
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 16
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 24
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 32
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 40
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 48
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 56
|
||||
$dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 64
|
||||
$dummy ;; 65 (The one that breaks the camel's back)
|
||||
)
|
||||
|
||||
(func $finish (result i32)
|
||||
i32.const 1
|
||||
)
|
||||
(export "finish" (func $finish))
|
||||
)
|
||||
Reference in New Issue
Block a user