Commit 6f1b5a2b authored by Alexei Starovoitov's avatar Alexei Starovoitov

Merge branch 'bpf-kselftest-improvements'

Daniel Borkmann says:

====================
First one unifies the often repeated rlimit handling and the
second one enables and adds run-time tests for BPF tail calls
which provides useful coverage in particular for JITs.
====================
Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
parents 3808b519 b33eb735
#include <sys/resource.h>
#include <stdio.h>
static __attribute__((constructor)) void bpf_rlimit_ctor(void)
{
struct rlimit rlim_old, rlim_new = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY,
};
getrlimit(RLIMIT_MEMLOCK, &rlim_old);
/* For the sake of running the test cases, we temporarily
* set rlimit to infinity in order for kernel to focus on
* errors from actual test cases and not getting noise
* from hitting memlock limits. The limit is on per-process
* basis and not a global one, hence destructor not really
* needed here.
*/
if (setrlimit(RLIMIT_MEMLOCK, &rlim_new) < 0) {
perror("Unable to lift memlock rlimit");
/* Trying out lower limit, but expect potential test
* case failures from this!
*/
rlim_new.rlim_cur = rlim_old.rlim_cur + (1UL << 20);
rlim_new.rlim_max = rlim_old.rlim_max + (1UL << 20);
setrlimit(RLIMIT_MEMLOCK, &rlim_new);
}
}
...@@ -9,8 +9,6 @@ ...@@ -9,8 +9,6 @@
#include <stddef.h> #include <stddef.h>
#include <stdbool.h> #include <stdbool.h>
#include <sys/resource.h>
#include <linux/unistd.h> #include <linux/unistd.h>
#include <linux/filter.h> #include <linux/filter.h>
#include <linux/bpf_perf_event.h> #include <linux/bpf_perf_event.h>
...@@ -19,6 +17,7 @@ ...@@ -19,6 +17,7 @@
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include "../../../include/linux/filter.h" #include "../../../include/linux/filter.h"
#include "bpf_rlimit.h"
#ifndef ARRAY_SIZE #ifndef ARRAY_SIZE
# define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) # define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
...@@ -702,9 +701,6 @@ static int do_test(unsigned int from, unsigned int to) ...@@ -702,9 +701,6 @@ static int do_test(unsigned int from, unsigned int to)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
unsigned int from = 0, to = ARRAY_SIZE(tests); unsigned int from = 0, to = ARRAY_SIZE(tests);
struct rlimit rinf = { RLIM_INFINITY, RLIM_INFINITY };
setrlimit(RLIMIT_MEMLOCK, &rinf);
if (argc == 3) { if (argc == 3) {
unsigned int l = atoi(argv[argc - 2]); unsigned int l = atoi(argv[argc - 2]);
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
#include <errno.h> #include <errno.h>
#include <assert.h> #include <assert.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h>
#include <linux/bpf.h> #include <linux/bpf.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include <bpf/libbpf.h> #include <bpf/libbpf.h>
#include "cgroup_helpers.h" #include "cgroup_helpers.h"
#include "bpf_rlimit.h"
#define DEV_CGROUP_PROG "./dev_cgroup.o" #define DEV_CGROUP_PROG "./dev_cgroup.o"
...@@ -25,15 +25,11 @@ ...@@ -25,15 +25,11 @@
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
struct rlimit limit = { RLIM_INFINITY, RLIM_INFINITY };
struct bpf_object *obj; struct bpf_object *obj;
int error = EXIT_FAILURE; int error = EXIT_FAILURE;
int prog_fd, cgroup_fd; int prog_fd, cgroup_fd;
__u32 prog_cnt; __u32 prog_cnt;
if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0)
perror("Unable to lift memlock rlimit");
if (bpf_prog_load(DEV_CGROUP_PROG, BPF_PROG_TYPE_CGROUP_DEVICE, if (bpf_prog_load(DEV_CGROUP_PROG, BPF_PROG_TYPE_CGROUP_DEVICE,
&obj, &prog_fd)) { &obj, &prog_fd)) {
printf("Failed to load DEV_CGROUP program\n"); printf("Failed to load DEV_CGROUP program\n");
......
...@@ -22,10 +22,11 @@ ...@@ -22,10 +22,11 @@
#include <unistd.h> #include <unistd.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include "bpf_util.h" #include "bpf_util.h"
#include "bpf_rlimit.h"
struct tlpm_node { struct tlpm_node {
struct tlpm_node *next; struct tlpm_node *next;
...@@ -736,17 +737,11 @@ static void test_lpm_multi_thread(void) ...@@ -736,17 +737,11 @@ static void test_lpm_multi_thread(void)
int main(void) int main(void)
{ {
struct rlimit limit = { RLIM_INFINITY, RLIM_INFINITY }; int i;
int i, ret;
/* we want predictable, pseudo random tests */ /* we want predictable, pseudo random tests */
srand(0xf00ba1); srand(0xf00ba1);
/* allow unlimited locked memory */
ret = setrlimit(RLIMIT_MEMLOCK, &limit);
if (ret < 0)
perror("Unable to lift memlock rlimit");
test_lpm_basic(); test_lpm_basic();
test_lpm_order(); test_lpm_order();
...@@ -755,11 +750,8 @@ int main(void) ...@@ -755,11 +750,8 @@ int main(void)
test_lpm_map(i); test_lpm_map(i);
test_lpm_ipaddr(); test_lpm_ipaddr();
test_lpm_delete(); test_lpm_delete();
test_lpm_get_next_key(); test_lpm_get_next_key();
test_lpm_multi_thread(); test_lpm_multi_thread();
printf("test_lpm: OK\n"); printf("test_lpm: OK\n");
......
...@@ -16,10 +16,11 @@ ...@@ -16,10 +16,11 @@
#include <time.h> #include <time.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/resource.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include "bpf_util.h" #include "bpf_util.h"
#include "bpf_rlimit.h"
#define LOCAL_FREE_TARGET (128) #define LOCAL_FREE_TARGET (128)
#define PERCPU_FREE_TARGET (4) #define PERCPU_FREE_TARGET (4)
...@@ -613,7 +614,6 @@ static void test_lru_sanity6(int map_type, int map_flags, int tgt_free) ...@@ -613,7 +614,6 @@ static void test_lru_sanity6(int map_type, int map_flags, int tgt_free)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
int map_types[] = {BPF_MAP_TYPE_LRU_HASH, int map_types[] = {BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH}; BPF_MAP_TYPE_LRU_PERCPU_HASH};
int map_flags[] = {0, BPF_F_NO_COMMON_LRU}; int map_flags[] = {0, BPF_F_NO_COMMON_LRU};
...@@ -621,8 +621,6 @@ int main(int argc, char **argv) ...@@ -621,8 +621,6 @@ int main(int argc, char **argv)
setbuf(stdout, NULL); setbuf(stdout, NULL);
assert(!setrlimit(RLIMIT_MEMLOCK, &r));
nr_cpus = bpf_num_possible_cpus(); nr_cpus = bpf_num_possible_cpus();
assert(nr_cpus != -1); assert(nr_cpus != -1);
printf("nr_cpus:%d\n\n", nr_cpus); printf("nr_cpus:%d\n\n", nr_cpus);
......
...@@ -17,13 +17,14 @@ ...@@ -17,13 +17,14 @@
#include <stdlib.h> #include <stdlib.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/resource.h>
#include <linux/bpf.h> #include <linux/bpf.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include <bpf/libbpf.h> #include <bpf/libbpf.h>
#include "bpf_util.h" #include "bpf_util.h"
#include "bpf_rlimit.h"
static int map_flags; static int map_flags;
...@@ -1126,10 +1127,6 @@ static void run_all_tests(void) ...@@ -1126,10 +1127,6 @@ static void run_all_tests(void)
int main(void) int main(void)
{ {
struct rlimit rinf = { RLIM_INFINITY, RLIM_INFINITY };
setrlimit(RLIMIT_MEMLOCK, &rinf);
map_flags = 0; map_flags = 0;
run_all_tests(); run_all_tests();
......
...@@ -26,7 +26,6 @@ typedef __u16 __sum16; ...@@ -26,7 +26,6 @@ typedef __u16 __sum16;
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/resource.h>
#include <sys/types.h> #include <sys/types.h>
#include <fcntl.h> #include <fcntl.h>
...@@ -34,9 +33,11 @@ typedef __u16 __sum16; ...@@ -34,9 +33,11 @@ typedef __u16 __sum16;
#include <linux/err.h> #include <linux/err.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include <bpf/libbpf.h> #include <bpf/libbpf.h>
#include "test_iptunnel_common.h" #include "test_iptunnel_common.h"
#include "bpf_util.h" #include "bpf_util.h"
#include "bpf_endian.h" #include "bpf_endian.h"
#include "bpf_rlimit.h"
static int error_cnt, pass_cnt; static int error_cnt, pass_cnt;
...@@ -965,10 +966,6 @@ static void test_stacktrace_map() ...@@ -965,10 +966,6 @@ static void test_stacktrace_map()
int main(void) int main(void)
{ {
struct rlimit rinf = { RLIM_INFINITY, RLIM_INFINITY };
setrlimit(RLIMIT_MEMLOCK, &rinf);
test_pkt_access(); test_pkt_access();
test_xdp(); test_xdp();
test_l4lb_all(); test_l4lb_all();
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
#include <assert.h> #include <assert.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/resource.h>
#include <linux/filter.h> #include <linux/filter.h>
#include <linux/bpf.h> #include <linux/bpf.h>
...@@ -21,6 +20,7 @@ ...@@ -21,6 +20,7 @@
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include "../../../include/linux/filter.h" #include "../../../include/linux/filter.h"
#include "bpf_rlimit.h"
static struct bpf_insn prog[BPF_MAXINSNS]; static struct bpf_insn prog[BPF_MAXINSNS];
...@@ -184,11 +184,9 @@ static void do_test(uint32_t *tests, int start_insns, int fd_map, ...@@ -184,11 +184,9 @@ static void do_test(uint32_t *tests, int start_insns, int fd_map,
int main(void) int main(void)
{ {
struct rlimit rinf = { RLIM_INFINITY, RLIM_INFINITY };
uint32_t tests = 0; uint32_t tests = 0;
int i, fd_map; int i, fd_map;
setrlimit(RLIMIT_MEMLOCK, &rinf);
fd_map = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), fd_map = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int),
sizeof(int), 1, BPF_F_NO_PREALLOC); sizeof(int), 1, BPF_F_NO_PREALLOC);
assert(fd_map > 0); assert(fd_map > 0);
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
#include <linux/bpf.h> #include <linux/bpf.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <fcntl.h> #include <fcntl.h>
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include <bpf/libbpf.h> #include <bpf/libbpf.h>
#include "bpf_util.h" #include "bpf_util.h"
#include "bpf_rlimit.h"
#include <linux/perf_event.h> #include <linux/perf_event.h>
#include "test_tcpbpf.h" #include "test_tcpbpf.h"
...@@ -44,7 +44,6 @@ static int bpf_find_map(const char *test, struct bpf_object *obj, ...@@ -44,7 +44,6 @@ static int bpf_find_map(const char *test, struct bpf_object *obj,
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
struct rlimit limit = { RLIM_INFINITY, RLIM_INFINITY };
const char *file = "test_tcpbpf_kern.o"; const char *file = "test_tcpbpf_kern.o";
struct tcpbpf_globals g = {0}; struct tcpbpf_globals g = {0};
int cg_fd, prog_fd, map_fd; int cg_fd, prog_fd, map_fd;
...@@ -57,9 +56,6 @@ int main(int argc, char **argv) ...@@ -57,9 +56,6 @@ int main(int argc, char **argv)
int pid; int pid;
int rv; int rv;
if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0)
perror("Unable to lift memlock rlimit");
if (argc > 1 && strcmp(argv[1], "-d") == 0) if (argc > 1 && strcmp(argv[1], "-d") == 0)
debug_flag = true; debug_flag = true;
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
#include <limits.h> #include <limits.h>
#include <sys/capability.h> #include <sys/capability.h>
#include <sys/resource.h>
#include <linux/unistd.h> #include <linux/unistd.h>
#include <linux/filter.h> #include <linux/filter.h>
...@@ -41,7 +40,7 @@ ...@@ -41,7 +40,7 @@
# define CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS 1 # define CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS 1
# endif # endif
#endif #endif
#include "bpf_rlimit.h"
#include "../../../include/linux/filter.h" #include "../../../include/linux/filter.h"
#ifndef ARRAY_SIZE #ifndef ARRAY_SIZE
...@@ -2589,6 +2588,62 @@ static struct bpf_test tests[] = { ...@@ -2589,6 +2588,62 @@ static struct bpf_test tests[] = {
.result_unpriv = REJECT, .result_unpriv = REJECT,
.result = ACCEPT, .result = ACCEPT,
}, },
{
"runtime/jit: tail_call within bounds, prog once",
.insns = {
BPF_MOV64_IMM(BPF_REG_3, 0),
BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 1),
BPF_EXIT_INSN(),
},
.fixup_prog = { 1 },
.result = ACCEPT,
.retval = 42,
},
{
"runtime/jit: tail_call within bounds, prog loop",
.insns = {
BPF_MOV64_IMM(BPF_REG_3, 1),
BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 1),
BPF_EXIT_INSN(),
},
.fixup_prog = { 1 },
.result = ACCEPT,
.retval = 41,
},
{
"runtime/jit: tail_call within bounds, no prog",
.insns = {
BPF_MOV64_IMM(BPF_REG_3, 2),
BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 1),
BPF_EXIT_INSN(),
},
.fixup_prog = { 1 },
.result = ACCEPT,
.retval = 1,
},
{
"runtime/jit: tail_call out of bounds",
.insns = {
BPF_MOV64_IMM(BPF_REG_3, 256),
BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(),
},
.fixup_prog = { 1 },
.result = ACCEPT,
.retval = 2,
},
{ {
"runtime/jit: pass negative index to tail_call", "runtime/jit: pass negative index to tail_call",
.insns = { .insns = {
...@@ -2596,11 +2651,12 @@ static struct bpf_test tests[] = { ...@@ -2596,11 +2651,12 @@ static struct bpf_test tests[] = {
BPF_LD_MAP_FD(BPF_REG_2, 0), BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call), BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 0), BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(), BPF_EXIT_INSN(),
}, },
.fixup_prog = { 1 }, .fixup_prog = { 1 },
.result = ACCEPT, .result = ACCEPT,
.retval = 2,
}, },
{ {
"runtime/jit: pass > 32bit index to tail_call", "runtime/jit: pass > 32bit index to tail_call",
...@@ -2609,11 +2665,12 @@ static struct bpf_test tests[] = { ...@@ -2609,11 +2665,12 @@ static struct bpf_test tests[] = {
BPF_LD_MAP_FD(BPF_REG_2, 0), BPF_LD_MAP_FD(BPF_REG_2, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call), BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 0), BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(), BPF_EXIT_INSN(),
}, },
.fixup_prog = { 2 }, .fixup_prog = { 2 },
.result = ACCEPT, .result = ACCEPT,
.retval = 42,
}, },
{ {
"stack pointer arithmetic", "stack pointer arithmetic",
...@@ -11279,16 +11336,61 @@ static int create_map(uint32_t size_value, uint32_t max_elem) ...@@ -11279,16 +11336,61 @@ static int create_map(uint32_t size_value, uint32_t max_elem)
return fd; return fd;
} }
static int create_prog_dummy1(void)
{
struct bpf_insn prog[] = {
BPF_MOV64_IMM(BPF_REG_0, 42),
BPF_EXIT_INSN(),
};
return bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog,
ARRAY_SIZE(prog), "GPL", 0, NULL, 0);
}
static int create_prog_dummy2(int mfd, int idx)
{
struct bpf_insn prog[] = {
BPF_MOV64_IMM(BPF_REG_3, idx),
BPF_LD_MAP_FD(BPF_REG_2, mfd),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
BPF_FUNC_tail_call),
BPF_MOV64_IMM(BPF_REG_0, 41),
BPF_EXIT_INSN(),
};
return bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog,
ARRAY_SIZE(prog), "GPL", 0, NULL, 0);
}
static int create_prog_array(void) static int create_prog_array(void)
{ {
int fd; int p1key = 0, p2key = 1;
int mfd, p1fd, p2fd;
fd = bpf_create_map(BPF_MAP_TYPE_PROG_ARRAY, sizeof(int), mfd = bpf_create_map(BPF_MAP_TYPE_PROG_ARRAY, sizeof(int),
sizeof(int), 4, 0); sizeof(int), 4, 0);
if (fd < 0) if (mfd < 0) {
printf("Failed to create prog array '%s'!\n", strerror(errno)); printf("Failed to create prog array '%s'!\n", strerror(errno));
return -1;
}
return fd; p1fd = create_prog_dummy1();
p2fd = create_prog_dummy2(mfd, p2key);
if (p1fd < 0 || p2fd < 0)
goto out;
if (bpf_map_update_elem(mfd, &p1key, &p1fd, BPF_ANY) < 0)
goto out;
if (bpf_map_update_elem(mfd, &p2key, &p2fd, BPF_ANY) < 0)
goto out;
close(p2fd);
close(p1fd);
return mfd;
out:
close(p2fd);
close(p1fd);
close(mfd);
return -1;
} }
static int create_map_in_map(void) static int create_map_in_map(void)
...@@ -11543,8 +11645,6 @@ static int do_test(bool unpriv, unsigned int from, unsigned int to) ...@@ -11543,8 +11645,6 @@ static int do_test(bool unpriv, unsigned int from, unsigned int to)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
struct rlimit rinf = { RLIM_INFINITY, RLIM_INFINITY };
struct rlimit rlim = { 1 << 20, 1 << 20 };
unsigned int from = 0, to = ARRAY_SIZE(tests); unsigned int from = 0, to = ARRAY_SIZE(tests);
bool unpriv = !is_admin(); bool unpriv = !is_admin();
...@@ -11572,6 +11672,5 @@ int main(int argc, char **argv) ...@@ -11572,6 +11672,5 @@ int main(int argc, char **argv)
return EXIT_FAILURE; return EXIT_FAILURE;
} }
setrlimit(RLIMIT_MEMLOCK, unpriv ? &rlim : &rinf);
return do_test(unpriv, from, to); return do_test(unpriv, from, to);
} }
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h>
#include <linux/bpf.h> #include <linux/bpf.h>
#include <linux/filter.h> #include <linux/filter.h>
...@@ -12,6 +11,8 @@ ...@@ -12,6 +11,8 @@
#include <bpf/bpf.h> #include <bpf/bpf.h>
#include "bpf_rlimit.h"
#define LOG_SIZE (1 << 20) #define LOG_SIZE (1 << 20)
#define err(str...) printf("ERROR: " str) #define err(str...) printf("ERROR: " str)
...@@ -133,16 +134,11 @@ static void test_log_bad(char *log, size_t log_len, int log_level) ...@@ -133,16 +134,11 @@ static void test_log_bad(char *log, size_t log_len, int log_level)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
struct rlimit limit = { RLIM_INFINITY, RLIM_INFINITY };
char full_log[LOG_SIZE]; char full_log[LOG_SIZE];
char log[LOG_SIZE]; char log[LOG_SIZE];
size_t want_len; size_t want_len;
int i; int i;
/* allow unlimited locked memory to have more consistent error code */
if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0)
perror("Unable to lift memlock rlimit");
memset(log, 1, LOG_SIZE); memset(log, 1, LOG_SIZE);
/* Test incorrect attr */ /* Test incorrect attr */
......
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