Bug#32890 Crash after repeated create and drop of tables and views

The problem is that CREATE VIEW statements inside prepared statements
weren't being expanded during the prepare phase, which leads to objects
not being allocated in the appropriate memory arenas.

The solution is to perform the validation of CREATE VIEW statements
during the prepare phase of a prepared statement. The validation
during the prepare phase assures that transformations of the parsed
tree will use the permanent arena of the prepared statement.
parent f5cb5fdc
......@@ -1709,4 +1709,156 @@ a b
9999999999999999 14632475938453979136
deallocate prepare stmt;
drop table t1;
drop view if exists v1;
drop table if exists t1;
create table t1 (a int, b int);
insert into t1 values (1,1), (2,2), (3,3);
insert into t1 values (3,1), (1,2), (2,3);
prepare stmt from "create view v1 as select * from t1";
execute stmt;
drop table t1;
create table t1 (a int, b int);
drop view v1;
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `a`,`t1`.`b` AS `b` from `t1`
drop view v1;
prepare stmt from "create view v1 (c,d) as select a,b from t1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `c`,`t1`.`b` AS `d` from `t1`
select * from v1;
c d
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `c`,`t1`.`b` AS `d` from `t1`
select * from v1;
c d
drop view v1;
prepare stmt from "create view v1 (c) as select b+1 from t1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select (`t1`.`b` + 1) AS `c` from `t1`
select * from v1;
c
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select (`t1`.`b` + 1) AS `c` from `t1`
select * from v1;
c
drop view v1;
prepare stmt from "create view v1 (c,d,e,f) as select a,b,a in (select a+2 from t1), a = all (select a from t1) from t1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `c`,`t1`.`b` AS `d`,`t1`.`a` in (select (`t1`.`a` + 2) AS `a+2` from `t1`) AS `e`,`t1`.`a` = all (select `t1`.`a` AS `a` from `t1`) AS `f` from `t1`
select * from v1;
c d e f
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `c`,`t1`.`b` AS `d`,`t1`.`a` in (select (`t1`.`a` + 2) AS `a+2` from `t1`) AS `e`,`t1`.`a` = all (select `t1`.`a` AS `a` from `t1`) AS `f` from `t1`
select * from v1;
c d e f
drop view v1;
prepare stmt from "create or replace view v1 as select 1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select 1 AS `1`
select * from v1;
1
1
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select 1 AS `1`
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select 1 AS `1`
select * from v1;
1
1
drop view v1;
prepare stmt from "create view v1 as select 1, 1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select 1 AS `1`,1 AS `My_exp_1`
select * from v1;
1 My_exp_1
1 1
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select 1 AS `1`,1 AS `My_exp_1`
select * from v1;
1 My_exp_1
1 1
drop view v1;
prepare stmt from "create view v1 (x) as select a from t1 where a > 1";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `x` from `t1` where (`t1`.`a` > 1)
select * from v1;
x
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `t1`.`a` AS `x` from `t1` where (`t1`.`a` > 1)
select * from v1;
x
drop view v1;
prepare stmt from "create view v1 as select * from `t1` `b`";
execute stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `b`.`a` AS `a`,`b`.`b` AS `b` from `t1` `b`
select * from v1;
a b
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
View Create View
v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `b`.`a` AS `a`,`b`.`b` AS `b` from `t1` `b`
select * from v1;
a b
drop view v1;
prepare stmt from "create view v1 (a,b,c) as select * from t1";
execute stmt;
ERROR HY000: View's SELECT and view's field list have different column counts
execute stmt;
ERROR HY000: View's SELECT and view's field list have different column counts
deallocate prepare stmt;
drop table t1;
create temporary table t1 (a int, b int);
prepare stmt from "create view v1 as select * from t1";
execute stmt;
ERROR HY000: View's SELECT refers to a temporary table 't1'
execute stmt;
ERROR HY000: View's SELECT refers to a temporary table 't1'
deallocate prepare stmt;
drop table t1;
prepare stmt from "create view v1 as select * from t1";
ERROR 42S02: Table 'test.t1' doesn't exist
prepare stmt from "create view v1 as select * from `t1` `b`";
ERROR 42S02: Table 'test.t1' doesn't exist
End of 5.0 tests.
......@@ -1824,4 +1824,127 @@ select * from t1 where a = @a and b = @b;
deallocate prepare stmt;
drop table t1;
#
# Bug#32890 Crash after repeated create and drop of tables and views
#
--disable_warnings
drop view if exists v1;
drop table if exists t1;
--enable_warnings
create table t1 (a int, b int);
insert into t1 values (1,1), (2,2), (3,3);
insert into t1 values (3,1), (1,2), (2,3);
prepare stmt from "create view v1 as select * from t1";
execute stmt;
drop table t1;
create table t1 (a int, b int);
drop view v1;
execute stmt;
show create view v1;
drop view v1;
prepare stmt from "create view v1 (c,d) as select a,b from t1";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 (c) as select b+1 from t1";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 (c,d,e,f) as select a,b,a in (select a+2 from t1), a = all (select a from t1) from t1";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create or replace view v1 as select 1";
execute stmt;
show create view v1;
select * from v1;
execute stmt;
show create view v1;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 as select 1, 1";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 (x) as select a from t1 where a > 1";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 as select * from `t1` `b`";
execute stmt;
show create view v1;
select * from v1;
drop view v1;
execute stmt;
deallocate prepare stmt;
show create view v1;
select * from v1;
drop view v1;
prepare stmt from "create view v1 (a,b,c) as select * from t1";
--error ER_VIEW_WRONG_LIST
execute stmt;
--error ER_VIEW_WRONG_LIST
execute stmt;
deallocate prepare stmt;
drop table t1;
create temporary table t1 (a int, b int);
prepare stmt from "create view v1 as select * from t1";
--error ER_VIEW_SELECT_TMPTABLE
execute stmt;
--error ER_VIEW_SELECT_TMPTABLE
execute stmt;
deallocate prepare stmt;
drop table t1;
--error ER_NO_SUCH_TABLE
prepare stmt from "create view v1 as select * from t1";
--error ER_NO_SUCH_TABLE
prepare stmt from "create view v1 as select * from `t1` `b`";
--echo End of 5.0 tests.
......@@ -879,6 +879,23 @@ class Item {
class sp_head;
class Item_basic_constant :public Item
{
public:
/* to prevent drop fixed flag (no need parent cleanup call) */
void cleanup()
{
/*
Restore the original field name as it might not have been allocated
in the statement memory. If the name is auto generated, it must be
done again between subsequent executions of a prepared statement.
*/
if (orig_name)
name= orig_name;
}
};
/*****************************************************************************
The class is a base class for representation of stored routine variables in
the Item-hierarchy. There are the following kinds of SP-vars:
......@@ -1161,7 +1178,7 @@ bool agg_item_charsets(DTCollation &c, const char *name,
Item **items, uint nitems, uint flags, int item_sep);
class Item_num: public Item
class Item_num: public Item_basic_constant
{
public:
Item_num() {} /* Remove gcc warning */
......@@ -1352,7 +1369,7 @@ class Item_field :public Item_ident
friend class st_select_lex_unit;
};
class Item_null :public Item
class Item_null :public Item_basic_constant
{
public:
Item_null(char *name_par=0)
......@@ -1374,8 +1391,6 @@ class Item_null :public Item
bool send(Protocol *protocol, String *str);
enum Item_result result_type () const { return STRING_RESULT; }
enum_field_types field_type() const { return MYSQL_TYPE_NULL; }
/* to prevent drop fixed flag (no need parent cleanup call) */
void cleanup() {}
bool basic_const_item() const { return 1; }
Item *clone_item() { return new Item_null(name); }
bool is_null() { return 1; }
......@@ -1567,8 +1582,6 @@ class Item_int :public Item_num
int save_in_field(Field *field, bool no_conversions);
bool basic_const_item() const { return 1; }
Item *clone_item() { return new Item_int(name,value,max_length); }
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
void print(String *str);
Item_num *neg() { value= -value; return this; }
uint decimal_precision() const
......@@ -1621,8 +1634,6 @@ class Item_decimal :public Item_num
{
return new Item_decimal(name, &decimal_value, decimals, max_length);
}
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
void print(String *str);
Item_num *neg()
{
......@@ -1673,8 +1684,6 @@ class Item_float :public Item_num
String *val_str(String*);
my_decimal *val_decimal(my_decimal *);
bool basic_const_item() const { return 1; }
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
Item *clone_item()
{ return new Item_float(name, value, decimals, max_length); }
Item_num *neg() { value= -value; return this; }
......@@ -1696,7 +1705,7 @@ class Item_static_float_func :public Item_float
};
class Item_string :public Item
class Item_string :public Item_basic_constant
{
public:
Item_string(const char *str,uint length,
......@@ -1780,8 +1789,6 @@ class Item_string :public Item
max_length= str_value.numchars() * collation.collation->mbmaxlen;
}
void print(String *str);
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
};
......@@ -1839,10 +1846,10 @@ class Item_return_int :public Item_int
};
class Item_hex_string: public Item
class Item_hex_string: public Item_basic_constant
{
public:
Item_hex_string(): Item() {}
Item_hex_string() {}
Item_hex_string(const char *str,uint str_length);
enum Type type() const { return VARBIN_ITEM; }
double val_real()
......@@ -1858,8 +1865,6 @@ class Item_hex_string: public Item
enum Item_result result_type () const { return STRING_RESULT; }
enum Item_result cast_to_int_type() const { return INT_RESULT; }
enum_field_types field_type() const { return MYSQL_TYPE_VARCHAR; }
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
void print(String *str);
bool eq(const Item *item, bool binary_cmp) const;
virtual Item *safe_charset_converter(CHARSET_INFO *tocs);
......@@ -2449,7 +2454,7 @@ class Item_trigger_field : public Item_field,
};
class Item_cache: public Item
class Item_cache: public Item_basic_constant
{
protected:
Item *example;
......@@ -2486,8 +2491,6 @@ class Item_cache: public Item
static Item_cache* get_cache(const Item *item);
table_map used_tables() const { return used_table_map; }
virtual void keep_array() {}
// to prevent drop fixed flag (no need parent cleanup call)
void cleanup() {}
void print(String *str);
bool eq_def(Field *field)
{
......
......@@ -1512,6 +1512,45 @@ static bool mysql_test_create_table(Prepared_statement *stmt)
}
/**
@brief Validate and prepare for execution CREATE VIEW statement
@param stmt prepared statement
@note This function handles create view commands.
@retval FALSE Operation was a success.
@retval TRUE An error occured.
*/
static bool mysql_test_create_view(Prepared_statement *stmt)
{
DBUG_ENTER("mysql_test_create_view");
THD *thd= stmt->thd;
LEX *lex= stmt->lex;
SELECT_LEX *select_lex= &lex->select_lex;
bool res= TRUE;
/* Skip first table, which is the view we are creating */
bool link_to_local;
TABLE_LIST *view= lex->unlink_first_table(&link_to_local);
TABLE_LIST *tables= lex->query_tables;
if (create_view_precheck(thd, tables, view, lex->create_view_mode))
goto err;
if (open_normal_and_derived_tables(thd, tables, 0))
goto err;
lex->view_prepare_mode= 1;
res= select_like_stmt_test(stmt, 0, 0);
err:
/* put view back for PS rexecuting */
lex->link_first_table_back(view, link_to_local);
DBUG_RETURN(res);
}
/*
Validate and prepare for execution a multi update statement.
......@@ -1730,6 +1769,7 @@ static bool check_prepared_statement(Prepared_statement *stmt,
my_message(ER_UNSUPPORTED_PS, ER(ER_UNSUPPORTED_PS), MYF(0));
goto error;
}
res= mysql_test_create_view(stmt);
break;
case SQLCOM_DO:
res= mysql_test_do_fields(stmt, tables, lex->insert_list);
......
......@@ -227,104 +227,31 @@ fill_defined_view_parts (THD *thd, TABLE_LIST *view)
return FALSE;
}
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/**
@brief Creating/altering VIEW procedure
@brief CREATE VIEW privileges pre-check.
@param thd thread handler
@param tables tables used in the view
@param views views to create
@param mode VIEW_CREATE_NEW, VIEW_ALTER, VIEW_CREATE_OR_REPLACE
@note This function handles both create and alter view commands.
@retval FALSE Operation was a success.
@retval TRUE An error occured.
*/
bool mysql_create_view(THD *thd, TABLE_LIST *views,
enum_view_create_mode mode)
bool create_view_precheck(THD *thd, TABLE_LIST *tables, TABLE_LIST *view,
enum_view_create_mode mode)
{
LEX *lex= thd->lex;
bool link_to_local;
/* first table in list is target VIEW name => cut off it */
TABLE_LIST *view= lex->unlink_first_table(&link_to_local);
TABLE_LIST *tables= lex->query_tables;
TABLE_LIST *tbl;
SELECT_LEX *select_lex= &lex->select_lex;
#ifndef NO_EMBEDDED_ACCESS_CHECKS
SELECT_LEX *sl;
#endif
SELECT_LEX_UNIT *unit= &lex->unit;
bool res= FALSE;
DBUG_ENTER("mysql_create_view");
/* This is ensured in the parser. */
DBUG_ASSERT(!lex->proc_list.first && !lex->result &&
!lex->param_list.elements && !lex->derived_tables);
if (mode != VIEW_CREATE_NEW)
{
if (mode == VIEW_ALTER &&
fill_defined_view_parts(thd, view))
{
res= TRUE;
goto err;
}
sp_cache_invalidate();
}
bool res= TRUE;
DBUG_ENTER("create_view_precheck");
if (!lex->definer)
{
/*
DEFINER-clause is missing; we have to create default definer in
persistent arena to be PS/SP friendly.
If this is an ALTER VIEW then the current user should be set as
the definer.
*/
Query_arena original_arena;
Query_arena *ps_arena = thd->activate_stmt_arena_if_needed(&original_arena);
if (!(lex->definer= create_default_definer(thd)))
res= TRUE;
if (ps_arena)
thd->restore_active_arena(ps_arena, &original_arena);
if (res)
goto err;
}
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/*
check definer of view:
- same as current user
- current user has SUPER_ACL
*/
if (lex->definer &&
(strcmp(lex->definer->user.str, thd->security_ctx->priv_user) != 0 ||
my_strcasecmp(system_charset_info,
lex->definer->host.str,
thd->security_ctx->priv_host) != 0))
{
if (!(thd->security_ctx->master_access & SUPER_ACL))
{
my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0), "SUPER");
res= TRUE;
goto err;
}
else
{
if (!is_acl_user(lex->definer->host.str,
lex->definer->user.str))
{
push_warning_printf(thd, MYSQL_ERROR::WARN_LEVEL_NOTE,
ER_NO_SUCH_USER,
ER(ER_NO_SUCH_USER),
lex->definer->user.str,
lex->definer->host.str);
}
}
}
/*
Privilege check for view creation:
- user has CREATE VIEW privilege on view table
......@@ -346,10 +273,8 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views,
(check_access(thd, DROP_ACL, view->db, &view->grant.privilege,
0, 0, is_schema_db(view->db)) ||
grant_option && check_grant(thd, DROP_ACL, view, 0, 1, 0))))
{
res= TRUE;
goto err;
}
for (sl= select_lex; sl; sl= sl->next_select())
{
for (tbl= sl->get_table_list(); tbl; tbl= tbl->next_local)
......@@ -363,7 +288,6 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views,
my_error(ER_TABLEACCESS_DENIED_ERROR, MYF(0),
"ANY", thd->security_ctx->priv_user,
thd->security_ctx->priv_host, tbl->table_name);
res= TRUE;
goto err;
}
/*
......@@ -399,10 +323,7 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views,
if (check_access(thd, SELECT_ACL, tbl->db,
&tbl->grant.privilege, 0, 0, test(tbl->schema_table)) ||
grant_option && check_grant(thd, SELECT_ACL, tbl, 0, 1, 0))
{
res= TRUE;
goto err;
}
}
}
}
......@@ -426,8 +347,126 @@ bool mysql_create_view(THD *thd, TABLE_LIST *views,
}
}
}
res= FALSE;
err:
DBUG_RETURN(res || thd->net.report_error);
}
#else
bool create_view_precheck(THD *thd, TABLE_LIST *tables, TABLE_LIST *view,
enum_view_create_mode mode)
{
return FALSE;
}
#endif
/**
@brief Creating/altering VIEW procedure
@param thd thread handler
@param views views to create
@param mode VIEW_CREATE_NEW, VIEW_ALTER, VIEW_CREATE_OR_REPLACE
@note This function handles both create and alter view commands.
@retval FALSE Operation was a success.
@retval TRUE An error occured.
*/
bool mysql_create_view(THD *thd, TABLE_LIST *views,
enum_view_create_mode mode)
{
LEX *lex= thd->lex;
bool link_to_local;
/* first table in list is target VIEW name => cut off it */
TABLE_LIST *view= lex->unlink_first_table(&link_to_local);
TABLE_LIST *tables= lex->query_tables;
TABLE_LIST *tbl;
SELECT_LEX *select_lex= &lex->select_lex;
#ifndef NO_EMBEDDED_ACCESS_CHECKS
SELECT_LEX *sl;
#endif
SELECT_LEX_UNIT *unit= &lex->unit;
bool res= FALSE;
DBUG_ENTER("mysql_create_view");
/* This is ensured in the parser. */
DBUG_ASSERT(!lex->proc_list.first && !lex->result &&
!lex->param_list.elements && !lex->derived_tables);
if (mode != VIEW_CREATE_NEW)
{
if (mode == VIEW_ALTER &&
fill_defined_view_parts(thd, view))
{
res= TRUE;
goto err;
}
sp_cache_invalidate();
}
if (!lex->definer)
{
/*
DEFINER-clause is missing; we have to create default definer in
persistent arena to be PS/SP friendly.
If this is an ALTER VIEW then the current user should be set as
the definer.
*/
Query_arena original_arena;
Query_arena *ps_arena = thd->activate_stmt_arena_if_needed(&original_arena);
if (!(lex->definer= create_default_definer(thd)))
res= TRUE;
if (ps_arena)
thd->restore_active_arena(ps_arena, &original_arena);
if (res)
goto err;
}
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/*
check definer of view:
- same as current user
- current user has SUPER_ACL
*/
if (lex->definer &&
(strcmp(lex->definer->user.str, thd->security_ctx->priv_user) != 0 ||
my_strcasecmp(system_charset_info,
lex->definer->host.str,
thd->security_ctx->priv_host) != 0))
{
if (!(thd->security_ctx->master_access & SUPER_ACL))
{
my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0), "SUPER");
res= TRUE;
goto err;
}
else
{
if (!is_acl_user(lex->definer->host.str,
lex->definer->user.str))
{
push_warning_printf(thd, MYSQL_ERROR::WARN_LEVEL_NOTE,
ER_NO_SUCH_USER,
ER(ER_NO_SUCH_USER),
lex->definer->user.str,
lex->definer->host.str);
}
}
}
#endif
if ((res= create_view_precheck(thd, tables, view, mode)))
goto err;
if (open_and_lock_tables(thd, tables))
{
res= TRUE;
......
......@@ -15,6 +15,9 @@
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
bool create_view_precheck(THD *thd, TABLE_LIST *tables, TABLE_LIST *view,
enum_view_create_mode mode);
bool mysql_create_view(THD *thd, TABLE_LIST *view,
enum_view_create_mode mode);
......
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