Commit 716bc803 authored by titzer's avatar titzer Committed by Commit bot

[wasm] Fix misaligned accesses and endianness issues in decoders.

R=ahaas@chromium.org,bradnelson@chromium.org
BUG=

Review URL: https://codereview.chromium.org/1644023002

Cr-Commit-Position: refs/heads/master@{#33595}
parent 6399fce5
......@@ -112,28 +112,28 @@ class WasmDecoder : public Decoder {
function_env_ = function_env;
}
// Load an operand at [pc + 1].
template <typename V>
V Operand(const byte* pc) {
if ((limit_ - pc) < static_cast<int>(1 + sizeof(V))) {
const char* msg = "Expected operand following opcode";
switch (sizeof(V)) {
case 1:
msg = "Expected 1-byte operand following opcode";
break;
case 2:
msg = "Expected 2-byte operand following opcode";
break;
case 4:
msg = "Expected 4-byte operand following opcode";
break;
default:
break;
}
byte ByteOperand(const byte* pc, const char* msg = "missing 1-byte operand") {
if ((pc + sizeof(byte)) >= limit_) {
error(pc, msg);
return -1;
return 0;
}
return pc[1];
}
uint32_t Uint32Operand(const byte* pc) {
if ((pc + sizeof(uint32_t)) >= limit_) {
error(pc, "missing 4-byte operand");
return 0;
}
return read_u32(pc + 1);
}
uint64_t Uint64Operand(const byte* pc) {
if ((pc + sizeof(uint64_t)) >= limit_) {
error(pc, "missing 8-byte operand");
return 0;
}
return *reinterpret_cast<const V*>(pc + 1);
return read_u64(pc + 1);
}
LocalType LocalOperand(const byte* pc, uint32_t* index, int* length) {
......@@ -185,7 +185,7 @@ class WasmDecoder : public Decoder {
}
void MemoryAccessOperand(const byte* pc, int* length, uint32_t* offset) {
byte bitfield = Operand<uint8_t>(pc);
byte bitfield = ByteOperand(pc, "missing memory access operand");
if (MemoryAccess::OffsetField::decode(bitfield)) {
*offset = UnsignedLEB128Operand(pc + 1, length);
(*length)++; // to account for the memory access byte
......@@ -431,7 +431,7 @@ class LR_WasmDecoder : public WasmDecoder {
Leaf(kAstStmt);
break;
case kExprBlock: {
int length = Operand<uint8_t>(pc_);
int length = ByteOperand(pc_);
if (length < 1) {
Leaf(kAstStmt);
} else {
......@@ -445,7 +445,7 @@ class LR_WasmDecoder : public WasmDecoder {
break;
}
case kExprLoop: {
int length = Operand<uint8_t>(pc_);
int length = ByteOperand(pc_);
if (length < 1) {
Leaf(kAstStmt);
} else {
......@@ -474,7 +474,7 @@ class LR_WasmDecoder : public WasmDecoder {
Shift(kAstStmt, 3); // Result type is typeof(x) in {c ? x : y}.
break;
case kExprBr: {
uint32_t depth = Operand<uint8_t>(pc_);
uint32_t depth = ByteOperand(pc_);
Shift(kAstEnd, 1);
if (depth >= blocks_.size()) {
error("improperly nested branch");
......@@ -483,7 +483,7 @@ class LR_WasmDecoder : public WasmDecoder {
break;
}
case kExprBrIf: {
uint32_t depth = Operand<uint8_t>(pc_);
uint32_t depth = ByteOperand(pc_);
Shift(kAstStmt, 2);
if (depth >= blocks_.size()) {
error("improperly nested conditional branch");
......@@ -496,8 +496,8 @@ class LR_WasmDecoder : public WasmDecoder {
error("expected #tableswitch <cases> <table>, fell off end");
break;
}
uint16_t case_count = *reinterpret_cast<const uint16_t*>(pc_ + 1);
uint16_t table_count = *reinterpret_cast<const uint16_t*>(pc_ + 3);
uint16_t case_count = read_u16(pc_ + 1);
uint16_t table_count = read_u16(pc_ + 3);
len = 5 + table_count * 2;
if (table_count == 0) {
......@@ -514,8 +514,7 @@ class LR_WasmDecoder : public WasmDecoder {
// Verify table.
for (int i = 0; i < table_count; i++) {
uint16_t target =
*reinterpret_cast<const uint16_t*>(pc_ + 5 + i * 2);
uint16_t target = read_u16(pc_ + 5 + i * 2);
if (target >= 0x8000) {
size_t depth = target - 0x8000;
if (depth > blocks_.size()) {
......@@ -547,31 +546,31 @@ class LR_WasmDecoder : public WasmDecoder {
break;
}
case kExprI8Const: {
int32_t value = Operand<int8_t>(pc_);
int32_t value = bit_cast<int8_t>(ByteOperand(pc_));
Leaf(kAstI32, BUILD(Int32Constant, value));
len = 2;
break;
}
case kExprI32Const: {
int32_t value = Operand<int32_t>(pc_);
uint32_t value = Uint32Operand(pc_);
Leaf(kAstI32, BUILD(Int32Constant, value));
len = 5;
break;
}
case kExprI64Const: {
int64_t value = Operand<int64_t>(pc_);
uint64_t value = Uint64Operand(pc_);
Leaf(kAstI64, BUILD(Int64Constant, value));
len = 9;
break;
}
case kExprF32Const: {
float value = Operand<float>(pc_);
float value = bit_cast<float>(Uint32Operand(pc_));
Leaf(kAstF32, BUILD(Float32Constant, value));
len = 5;
break;
}
case kExprF64Const: {
double value = Operand<double>(pc_);
double value = bit_cast<double>(Uint64Operand(pc_));
Leaf(kAstF64, BUILD(Float64Constant, value));
len = 9;
break;
......@@ -877,7 +876,7 @@ class LR_WasmDecoder : public WasmDecoder {
break;
}
case kExprBr: {
uint32_t depth = Operand<uint8_t>(p->pc());
uint32_t depth = ByteOperand(p->pc());
if (depth >= blocks_.size()) {
error("improperly nested branch");
break;
......@@ -890,7 +889,7 @@ class LR_WasmDecoder : public WasmDecoder {
if (p->index == 1) {
TypeCheckLast(p, kAstI32);
} else if (p->done()) {
uint32_t depth = Operand<uint8_t>(p->pc());
uint32_t depth = ByteOperand(p->pc());
if (depth >= blocks_.size()) {
error("improperly nested branch");
break;
......@@ -911,8 +910,7 @@ class LR_WasmDecoder : public WasmDecoder {
// Switch key finished.
TypeCheckLast(p, kAstI32);
uint16_t table_count =
*reinterpret_cast<const uint16_t*>(p->pc() + 3);
uint16_t table_count = read_u16(p->pc() + 3);
// Build the switch only if it has more than just a default target.
bool build_switch = table_count > 1;
......@@ -920,7 +918,7 @@ class LR_WasmDecoder : public WasmDecoder {
if (build_switch) sw = BUILD(Switch, table_count, p->last()->node);
// Allocate environments for each case.
uint16_t case_count = *reinterpret_cast<const uint16_t*>(p->pc() + 1);
uint16_t case_count = read_u16(p->pc() + 1);
SsaEnv** case_envs = zone_->NewArray<SsaEnv*>(case_count);
for (int i = 0; i < case_count; i++) case_envs[i] = UnreachableEnv();
......@@ -931,10 +929,8 @@ class LR_WasmDecoder : public WasmDecoder {
ssa_env_ = copy;
// Build the environments for each case based on the table.
const uint16_t* table =
reinterpret_cast<const uint16_t*>(p->pc() + 5);
for (int i = 0; i < table_count; i++) {
uint16_t target = table[i];
uint16_t target = read_u16(p->pc() + 5 + i * 2);
SsaEnv* env = copy;
if (build_switch) {
env = Split(env);
......
......@@ -24,6 +24,12 @@ namespace wasm {
#define TRACE(...)
#endif
#if !(V8_TARGET_ARCH_MIPS || V8_TARGET_ARCH_MIPS64 || V8_TARGET_ARCH_ARM)
#define UNALIGNED_ACCESS_OK 1
#else
#define UNALIGNED_ACCESS_OK 0
#endif
// A helper utility to decode bytes, integers, fields, varints, etc, from
// a buffer of bytes.
class Decoder {
......@@ -32,107 +38,126 @@ class Decoder {
: start_(start),
pc_(start),
limit_(end),
end_(end),
error_pc_(nullptr),
error_pt_(nullptr) {}
virtual ~Decoder() {}
// Reads a single 16-bit unsigned integer (little endian).
inline uint16_t read_u16(const byte* ptr) {
DCHECK(ptr >= start_ && (ptr + 2) <= end_);
#if V8_TARGET_LITTLE_ENDIAN && UNALIGNED_ACCESS_OK
return *reinterpret_cast<const uint16_t*>(ptr);
#else
uint16_t b0 = ptr[0];
uint16_t b1 = ptr[1];
return (b1 << 8) | b0;
#endif
}
// Reads a single 32-bit unsigned integer (little endian).
inline uint32_t read_u32(const byte* ptr) {
DCHECK(ptr >= start_ && (ptr + 4) <= end_);
#if V8_TARGET_LITTLE_ENDIAN && UNALIGNED_ACCESS_OK
return *reinterpret_cast<const uint32_t*>(ptr);
#else
uint32_t b0 = ptr[0];
uint32_t b1 = ptr[1];
uint32_t b2 = ptr[2];
uint32_t b3 = ptr[3];
return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
#endif
}
// Reads a single 64-bit unsigned integer (little endian).
inline uint64_t read_u64(const byte* ptr) {
DCHECK(ptr >= start_ && (ptr + 8) <= end_);
#if V8_TARGET_LITTLE_ENDIAN && UNALIGNED_ACCESS_OK
return *reinterpret_cast<const uint64_t*>(ptr);
#else
uint32_t b0 = ptr[0];
uint32_t b1 = ptr[1];
uint32_t b2 = ptr[2];
uint32_t b3 = ptr[3];
uint32_t low = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
uint32_t b4 = ptr[4];
uint32_t b5 = ptr[5];
uint32_t b6 = ptr[6];
uint32_t b7 = ptr[7];
uint64_t high = (b7 << 24) | (b6 << 16) | (b5 << 8) | b4;
return (high << 32) | low;
#endif
}
// Reads a 8-bit unsigned integer (byte) and advances {pc_}.
uint8_t u8(const char* name = nullptr) {
uint8_t consume_u8(const char* name = nullptr) {
TRACE(" +%d %-20s: ", static_cast<int>(pc_ - start_),
name ? name : "uint8_t");
if (checkAvailable(1)) {
byte val = *(pc_++);
TRACE("%02x = %d\n", val, val);
return val;
} else {
error("expected 1 byte, but fell off end");
return traceOffEnd<uint8_t>();
}
return traceOffEnd<uint8_t>();
}
// Reads a 16-bit unsigned integer (little endian) and advances {pc_}.
uint16_t u16(const char* name = nullptr) {
uint16_t consume_u16(const char* name = nullptr) {
TRACE(" +%d %-20s: ", static_cast<int>(pc_ - start_),
name ? name : "uint16_t");
if (checkAvailable(2)) {
#ifdef V8_TARGET_LITTLE_ENDIAN
byte b0 = pc_[0];
byte b1 = pc_[1];
#else
byte b1 = pc_[0];
byte b0 = pc_[1];
#endif
uint16_t val = static_cast<uint16_t>(b1 << 8) | b0;
uint16_t val = read_u16(pc_);
TRACE("%02x %02x = %d\n", pc_[0], pc_[1], val);
pc_ += 2;
return val;
} else {
error("expected 2 bytes, but fell off end");
return traceOffEnd<uint16_t>();
}
return traceOffEnd<uint16_t>();
}
// Reads a single 32-bit unsigned integer (little endian) and advances {pc_}.
uint32_t u32(const char* name = nullptr) {
uint32_t consume_u32(const char* name = nullptr) {
TRACE(" +%d %-20s: ", static_cast<int>(pc_ - start_),
name ? name : "uint32_t");
if (checkAvailable(4)) {
#ifdef V8_TARGET_LITTLE_ENDIAN
byte b0 = pc_[0];
byte b1 = pc_[1];
byte b2 = pc_[2];
byte b3 = pc_[3];
#else
byte b3 = pc_[0];
byte b2 = pc_[1];
byte b1 = pc_[2];
byte b0 = pc_[3];
#endif
uint32_t val = static_cast<uint32_t>(b3 << 24) |
static_cast<uint32_t>(b2 << 16) |
static_cast<uint32_t>(b1 << 8) | b0;
uint32_t val = read_u32(pc_);
TRACE("%02x %02x %02x %02x = %u\n", pc_[0], pc_[1], pc_[2], pc_[3], val);
pc_ += 4;
return val;
} else {
error("expected 4 bytes, but fell off end");
return traceOffEnd<uint32_t>();
}
return traceOffEnd<uint32_t>();
}
// Reads a LEB128 variable-length 32-bit integer and advances {pc_}.
uint32_t u32v(int* length, const char* name = nullptr) {
uint32_t consume_u32v(int* length, const char* name = nullptr) {
TRACE(" +%d %-20s: ", static_cast<int>(pc_ - start_),
name ? name : "varint");
if (!checkAvailable(1)) {
error("expected at least 1 byte, but fell off end");
return traceOffEnd<uint32_t>();
}
if (checkAvailable(1)) {
const byte* pos = pc_;
const byte* end = pc_ + 5;
if (end > limit_) end = limit_;
const byte* pos = pc_;
const byte* end = pc_ + 5;
if (end > limit_) end = limit_;
uint32_t result = 0;
int shift = 0;
byte b = 0;
while (pc_ < end) {
b = *pc_++;
TRACE("%02x ", b);
result = result | ((b & 0x7F) << shift);
if ((b & 0x80) == 0) break;
shift += 7;
}
uint32_t result = 0;
int shift = 0;
byte b = 0;
while (pc_ < end) {
b = *pc_++;
TRACE("%02x ", b);
result = result | ((b & 0x7F) << shift);
if ((b & 0x80) == 0) break;
shift += 7;
}
*length = static_cast<int>(pc_ - pos);
if (pc_ == end && (b & 0x80)) {
error(pc_ - 1, "varint too large");
} else {
TRACE("= %u\n", result);
*length = static_cast<int>(pc_ - pos);
if (pc_ == end && (b & 0x80)) {
error(pc_ - 1, "varint too large");
} else {
TRACE("= %u\n", result);
}
return result;
}
return result;
return traceOffEnd<uint32_t>();
}
// Check that at least {size} bytes exist between {pc_} and {limit_}.
......@@ -208,6 +233,7 @@ class Decoder {
start_ = start;
pc_ = start;
limit_ = end;
end_ = end;
error_pc_ = nullptr;
error_pt_ = nullptr;
error_msg_.Reset(nullptr);
......@@ -220,6 +246,7 @@ class Decoder {
const byte* start_;
const byte* pc_;
const byte* limit_;
const byte* end_;
const byte* error_pc_;
const byte* error_pt_;
base::SmartArrayPointer<char> error_msg_;
......
This diff is collapsed.
......@@ -2378,9 +2378,6 @@ TEST(Run_WasmCallEmpty) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST(Run_WasmCallF32StackParameter) {
// Build the target function.
LocalType param_types[20];
......@@ -2432,8 +2429,6 @@ TEST(Run_WasmCallF64StackParameter) {
CHECK_EQ(256.5, result);
}
#endif
TEST(Run_WasmCallVoid) {
const byte kMemOffset = 8;
......
......@@ -251,9 +251,6 @@ TEST_F(WasmDecoderTest, Int64Const) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, Float32Const) {
byte code[] = {kExprF32Const, 0, 0, 0, 0};
float* ptr = reinterpret_cast<float*>(code + 1);
......@@ -273,8 +270,6 @@ TEST_F(WasmDecoderTest, Float64Const) {
}
}
#endif
TEST_F(WasmDecoderTest, Int32Const_off_end) {
byte code[] = {kExprI32Const, 0xaa, 0xbb, 0xcc, 0x44};
......@@ -532,16 +527,11 @@ TEST_F(WasmDecoderTest, ExprBlock1b) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, ExprBlock1c) {
static const byte code[] = {kExprBlock, 1, kExprF32Const, 0, 0, 0, 0};
EXPECT_VERIFIES(&env_f_ff, code);
}
#endif
TEST_F(WasmDecoderTest, IfEmpty) {
static const byte code[] = {kExprIf, kExprGetLocal, 0, kExprNop};
......@@ -704,9 +694,6 @@ TEST_F(WasmDecoderTest, ReturnVoid2) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, ReturnVoid3) {
EXPECT_VERIFIES_INLINE(&env_v_v, kExprI8Const, 0);
EXPECT_VERIFIES_INLINE(&env_v_v, kExprI32Const, 0, 0, 0, 0);
......@@ -717,8 +704,6 @@ TEST_F(WasmDecoderTest, ReturnVoid3) {
EXPECT_VERIFIES_INLINE(&env_v_i, kExprGetLocal, 0);
}
#endif
TEST_F(WasmDecoderTest, Unreachable1) {
EXPECT_VERIFIES_INLINE(&env_v_v, kExprUnreachable);
......@@ -881,9 +866,6 @@ TEST_F(WasmDecoderTest, MacrosStmt) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, MacrosBreak) {
EXPECT_VERIFIES_INLINE(&env_v_v, WASM_LOOP(1, WASM_BREAK(0)));
......@@ -895,8 +877,6 @@ TEST_F(WasmDecoderTest, MacrosBreak) {
WASM_LOOP(1, WASM_BREAKV(0, WASM_F64(0.0))));
}
#endif
TEST_F(WasmDecoderTest, MacrosContinue) {
EXPECT_VERIFIES_INLINE(&env_v_v, WASM_LOOP(1, WASM_CONTINUE(0)));
......@@ -1263,9 +1243,6 @@ TEST_F(WasmDecoderTest, CallsWithTooFewArguments) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, CallsWithSpilloverArgs) {
static LocalType a_i_ff[] = {kAstI32, kAstF32, kAstF32};
FunctionSig sig_i_ff(1, 2, a_i_ff);
......@@ -1329,8 +1306,6 @@ TEST_F(WasmDecoderTest, CallsWithMismatchedSigs3) {
EXPECT_FAILURE_INLINE(env, WASM_CALL_FUNCTION(1, WASM_F32(17.6)));
}
#endif
TEST_F(WasmDecoderTest, SimpleIndirectCalls) {
FunctionEnv* env = &env_i_i;
......@@ -1573,9 +1548,6 @@ TEST_F(WasmDecoderTest, BreakNesting3) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, BreaksWithMultipleTypes) {
EXPECT_FAILURE_INLINE(
&env_i_i,
......@@ -1593,8 +1565,6 @@ TEST_F(WasmDecoderTest, BreaksWithMultipleTypes) {
WASM_BRV_IF(0, WASM_ZERO, WASM_I8(11))));
}
#endif
TEST_F(WasmDecoderTest, BreakNesting_6_levels) {
for (int mask = 0; mask < 64; mask++) {
......@@ -1628,9 +1598,6 @@ TEST_F(WasmDecoderTest, BreakNesting_6_levels) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, ExprBreak_TypeCheck) {
FunctionEnv* envs[] = {&env_i_i, &env_l_l, &env_f_ff, &env_d_dd};
for (size_t i = 0; i < arraysize(envs); i++) {
......@@ -1653,8 +1620,6 @@ TEST_F(WasmDecoderTest, ExprBreak_TypeCheck) {
WASM_F64(1.2)));
}
#endif
TEST_F(WasmDecoderTest, ExprBreak_TypeCheckAll) {
byte code1[] = {WASM_BLOCK(2,
......@@ -1835,9 +1800,6 @@ TEST_F(WasmDecoderTest, TableSwitch2) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, TableSwitch1b) {
EXPECT_VERIFIES_INLINE(&env_i_i, WASM_TABLESWITCH_OP(1, 1, WASM_CASE(0)),
WASM_TABLESWITCH_BODY(WASM_GET_LOCAL(0), WASM_ZERO));
......@@ -1849,8 +1811,6 @@ TEST_F(WasmDecoderTest, TableSwitch1b) {
WASM_TABLESWITCH_BODY(WASM_ZERO, WASM_F64(0.0)));
}
#endif
TEST_F(WasmDecoderTest, TableSwitch_br1) {
for (int depth = 0; depth < 2; depth++) {
byte code[] = {WASM_BLOCK(1, WASM_TABLESWITCH_OP(0, 1, WASM_CASE_BR(depth)),
......@@ -1882,17 +1842,12 @@ TEST_F(WasmDecoderTest, TableSwitch_invalid_case_ref) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, TableSwitch1_br) {
EXPECT_VERIFIES_INLINE(
&env_i_i, WASM_TABLESWITCH_OP(1, 1, WASM_CASE(0)),
WASM_TABLESWITCH_BODY(WASM_GET_LOCAL(0), WASM_BRV(0, WASM_ZERO)));
}
#endif
TEST_F(WasmDecoderTest, TableSwitch2_br) {
EXPECT_VERIFIES_INLINE(
......@@ -1916,9 +1871,6 @@ TEST_F(WasmDecoderTest, TableSwitch2x2) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, ExprBreakNesting1) {
EXPECT_VERIFIES_INLINE(&env_v_v, WASM_BLOCK(1, WASM_BRV(0, WASM_ZERO)));
EXPECT_VERIFIES_INLINE(&env_v_v, WASM_BLOCK(1, WASM_BR(0)));
......@@ -1936,8 +1888,6 @@ TEST_F(WasmDecoderTest, ExprBreakNesting1) {
EXPECT_VERIFIES_INLINE(&env_v_v, WASM_LOOP(1, WASM_BR(1)));
}
#endif
TEST_F(WasmDecoderTest, Select) {
EXPECT_VERIFIES_INLINE(
......@@ -1946,9 +1896,6 @@ TEST_F(WasmDecoderTest, Select) {
}
// TODO(tizer): Fix on arm and reenable.
#if !V8_TARGET_ARCH_ARM && !V8_TARGET_ARCH_ARM64
TEST_F(WasmDecoderTest, Select_TypeCheck) {
EXPECT_FAILURE_INLINE(&env_i_i, WASM_SELECT(WASM_F32(9.9), WASM_GET_LOCAL(0),
WASM_GET_LOCAL(0)));
......@@ -1960,8 +1907,6 @@ TEST_F(WasmDecoderTest, Select_TypeCheck) {
&env_i_i, WASM_SELECT(WASM_F32(9.9), WASM_GET_LOCAL(0), WASM_I64(0)));
}
#endif
class WasmOpcodeLengthTest : public TestWithZone {
public:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment