Commit e0193fa4 authored by Charlie Jacobsen's avatar Charlie Jacobsen Committed by Vikram Narayanan

Basic lcd module create, run, and destroy.

This code is ugly, but it's working.

Tested with basic module, and appears to be working
properly. I will soon incorporate the patched
modprobe into the kernel tree, and then this code
will be usable by everyone.

The ipc code is still unimplemented. The only
hypercall handled is yield. Also note that other
exit conditions (e.g. external interrupt) have not
been fully tested.

Overview:
-- kernel code calls lcd_create_as_module with
   the module's name
-- lcd_create_as_module loads the module using
   request_lcd_module (request_lcd_module calls
   the patched modprobe to load the module, and
   the patched modprobe calls back into the lcd
   driver via the ioctrl interface to load the
   module)
-- lcd_create_as_module then finds the loaded
   module, spawns a kernel thread and passes off
   the module to it
-- the kernel thread initializes the lcd and
   maps the module inside it, then suspends itself
-- lcd_run_as_module wakes up the kernel thread
   and tells it to run
-- lcd_delete_as_module stops the kernel thread
   and deletes the module from the host kernel

File-by-file details:

arch/x86/include/asm/lcd-domains-arch.h
arch/x86/lcd-domains/lcd-domains-arch-tests.c
arch/x86/lcd-domains/lcd-domains-arch.c
-- lcd was not running in 64-bit mode, and my
   checks had one subtle bug
-- fixed %cr3 load to properly load vmcs first
-- fixed set program counter to use guest virtual
   rather than guest physical address

include/linux/sched.h
-- added struct lcd to task_struct

include/linux/init_task.h
-- lcd pointer set to null when task_struct is
   initialized

include/linux/module.h
kernel/module.c
-- made init_module and delete_module system calls
   callable from kernel code
-- available in module.h via do_sys_init_module and
   do_sys_delete_module
-- simply moved the majority of the guts of the
   system calls into a non-system call, exported
   routine
-- take an extra flag, for_lcd; when set, the init
   code skips over running (and deallocating) the
   module's init code, and the delete code skips
   over running the module exit
-- system calls from user code set for_lcd = 0; this
   ensures existing code still works

include/linux/kmod.h
kernel/kmod.c
kernel/sysctl.c
-- changed __request_module to __do_request_module; takes
   one extra argument, for_lcd
-- __request_module   ==>  __do_request_module with for_lcd = 0
-- request_lcd_module ==>  __do_request_module with for_lcd = 1
-- call_modprobe conditionally uses lcd_modprobe_path, the path
   to a patched modprobe accessible via sysfs

include/lcd-domains/lcd-domains.h
-- added lcd status enum; see source code doc
-- three routines for creating/running/destroying
   lcd's that use modules; see source code doc

include/uapi/linux/lcd-domains.h
-- added interface defns for patched modprobe to call into
   lcd driver for module init; lcd driver loads
   module (via slightly refactored module.c code) on behalf
   of modprobe

virt/lcd-domains/lcd-domains.c
-- implementation of routines for modules inside lcd's
-- implementation of module init / delete for lcd's
   (uses patched module.c code)

virt/lcd-domains/Kconfig
virt/lcd-domains/Makefile
virt/lcd-domains/lcd-module-load-test.c
virt/lcd-domains/lcd-tests.c
-- added test module for lcd module code
-- test runs automatically when lcd module is inserted
parent e2ddc9fa
......@@ -98,6 +98,10 @@ static inline void * hva2va(hva_t hva)
{
return (void *)hva_val(hva);
}
static inline hva_t va2hva(void *va)
{
return __hva((unsigned long)va);
}
static inline hpa_t hva2hpa(hva_t hva)
{
return (hpa_t){ (unsigned long)__pa(hva2va(hva)) };
......@@ -325,10 +329,10 @@ int lcd_arch_ept_unmap_range(struct lcd_arch *lcd, gpa_t ga_start,
*/
int lcd_arch_ept_gpa_to_hpa(struct lcd_arch *vcpu, gpa_t ga, hpa_t *ha_out);
/**
* Set the lcd's program counter to the guest physical address
* Set the lcd's program counter to the guest virtual address
* a.
*/
int lcd_arch_set_pc(struct lcd_arch *vcpu, gpa_t a);
int lcd_arch_set_pc(struct lcd_arch *vcpu, gva_t a);
/**
* Set the lcd's gva root pointer (for x86, %cr3) to the
* guest physical address a.
......
......@@ -342,7 +342,7 @@ static int test08(void)
printk(KERN_ERR "lcd arch: test08 error setting gva root\n");
goto fail4;
}
ret = lcd_arch_set_pc(lcd, __gpa(0));
ret = lcd_arch_set_pc(lcd, __gva(0));
if (ret) {
printk(KERN_ERR "lcd arch: test08 error setting pc\n");
goto fail5;
......
......@@ -1736,7 +1736,7 @@ static void vmx_setup_vmcs_guest_regs(struct lcd_arch *vcpu)
*
* Intel SDM V3 24.4.1, 3.4.5, 26.3.1.2
*/
vmcs_writel(GUEST_CS_AR_BYTES, 0xD09B);
vmcs_writel(GUEST_CS_AR_BYTES, 0xA09B);
vmcs_writel(GUEST_DS_AR_BYTES, 0x8093);
vmcs_writel(GUEST_ES_AR_BYTES, 0x8093);
vmcs_writel(GUEST_FS_AR_BYTES, 0x8093);
......@@ -2944,14 +2944,14 @@ int lcd_arch_run(struct lcd_arch *vcpu)
/* LCD RUNTIME ENV -------------------------------------------------- */
int lcd_arch_set_pc(struct lcd_arch *vcpu, gpa_t a)
int lcd_arch_set_pc(struct lcd_arch *vcpu, gva_t a)
{
vcpu->regs[LCD_ARCH_REGS_RIP] = gpa_val(a);
vcpu->regs[LCD_ARCH_REGS_RIP] = gva_val(a);
/*
* Must load vmcs to modify it
*/
vmx_get_cpu(vcpu);
vmcs_writel(GUEST_RIP, gpa_val(a));
vmcs_writel(GUEST_RIP, gva_val(a));
vmx_put_cpu(vcpu);
return 0;
}
......@@ -2961,7 +2961,12 @@ int lcd_arch_set_gva_root(struct lcd_arch *vcpu, gpa_t a)
u64 cr3_ptr;
cr3_ptr = gpa_val(a); /* no page write through, etc. ... */
/*
* Must load vmcs to modify it
*/
vmx_get_cpu(vcpu);
vmcs_writel(GUEST_CR3, cr3_ptr);
vmx_put_cpu(vcpu);
return 0;
}
......@@ -4137,7 +4142,7 @@ static int vmx_check_guest_seg(struct lcd_arch *vcpu)
*/
act64 = vmx_getl(vcpu, GUEST_CS_AR_BYTES);
if (vmx_entry_has(vcpu, VM_ENTRY_IA32E_MODE) &&
vmx_seg_l_mode(act64) && !vmx_seg_db(act64)) {
vmx_seg_l_mode(act64) && vmx_seg_db(act64)) {
printk(KERN_ERR "lcd vmx: guest cs improper db/l-mode bits\n");
return -1;
}
......@@ -4308,11 +4313,11 @@ static int vmx_check_guest_rip_rflags(struct lcd_arch *vcpu)
return -1;
}
} else {
lin_addr_width = (cpuid_eax(0x80000008) >> 7) & 0xff;
lin_addr_width = (cpuid_eax(0x80000008) >> 8) & 0xff;
sact64 = (signed long)act64;
if ((sact64 >> lin_addr_width) != 0 ||
(sact64 >> lin_addr_width != -1)) {
printk(KERN_ERR "lcd vmx: guest rip exceeds max linear addr\n");
if ((sact64 >> lin_addr_width) != 0 &&
(sact64 >> lin_addr_width) != -1) {
printk(KERN_ERR "lcd vmx: guest rip 0x%llx exceeds max linear addr\n", act64);
return -1;
}
}
......
#ifndef LCD_DOMAINS_LCD_DOMAINS_H
#define LCD_DOMAINS_LCD_DOMAINS_H
#include <linux/module.h>
#include <asm/lcd-domains-arch.h>
/*
* lcd_status = status of kthread / lcd
* ====================================
*
* LCD_STATUS_UNFORMED = still setting up, or status not set yet
* LCD_STATUS_SUSPENDED = lcd is paused, and kthread is going to asleep / is
* asleep
* LCD_STATUS_RUNNABLE = lcd is paused, and kthread is awake, or should awaken
* (the status should be set to this if the lcd /
* kthread are suspended, and you want it to wake up)
* LCD_STATUS_RUNNING = lcd is running, or is about to run
* LCD_STATUS_KILL = lcd and kthread should die
* LCD_STATUS_DEAD = lcd is not running, most parts have been destroyed
* (lcd_struct is only hanging around to provide status
* info); the kthread is ready to die and be reaped
*/
enum lcd_status {
LCD_STATUS_UNFORMED = 0,
LCD_STATUS_SUSPENDED = 1,
LCD_STATUS_RUNNABLE = 2,
LCD_STATUS_RUNNING = 3,
LCD_STATUS_KILL = 4,
LCD_STATUS_DEAD = 5,
};
struct lcd {
/*
* Display name
*/
char name[MODULE_NAME_LEN];
/*
* Status (enum lcd_status)
*/
int status;
/*
* Arch-dependent state of lcd
*/
......@@ -40,4 +74,36 @@ struct lcd {
} gv;
};
/**
* -- Loads module_name into host kernel. (Note: The module loading code
* expects underscores, _, rather than hyphens. If the module's name
* in the file system is some-module.ko, use the name some_module.)
* -- Spawns a kernel thread that will host the lcd.
* -- The kernel thread will create the lcd and map the module into
* the lcd. The kernel thread will then wait with the lcd's status
* set to LCD_STATUS_SUSPENDED.
* -- Call lcd_run_as_module to start running the lcd.
* -- Returns NULL if we fail to create the kernel thread, or if the
* kernel thread failed to initialize the lcd, etc.
*
* Call lcd_destroy_as_module after a successful return from
* lcd_create_as_module to stop the kthread and remove the module
* from the host kernel.
*/
struct task_struct * lcd_create_as_module(char *module_name);
/**
* Wakes up kthread to start running lcd. Call this after a successful
* return from lcd_create_as_module. Call lcd_destroy_as_module when
* the kthread/lcd are no longer needed.
*/
int lcd_run_as_module(struct task_struct *t);
/**
* Stops the kernel thread (which in turn, destroys the lcd) and removes
* the module from the host kernel.
*
* Note: The kthread checks if it should stop each time the lcd exits in
* the main run loop.
*/
void lcd_destroy_as_module(struct task_struct *t, char *module_name);
#endif /* LCD_DOMAINS_LCD_DOMAINS_H */
......@@ -15,12 +15,11 @@
#include <net/net_namespace.h>
#include <linux/sched/rt.h>
#ifdef CONFIG_LCD
#define INIT_LCD(tsk) \
.sync_rendezvous = LIST_HEAD_INIT(tsk.sync_rendezvous), \
.utcb = NULL,
#ifdef CONFIG_HAVE_LCD
#include <lcd-domains/lcd-domains.h>
#define INIT_LCD .lcd = NULL,
#else
#define INIT_LCD(tsk)
#define INIT_LCD
#endif
#ifdef CONFIG_SMP
......@@ -254,7 +253,7 @@ extern struct task_group root_task_group;
}, \
.thread_group = LIST_HEAD_INIT(tsk.thread_group), \
.thread_node = LIST_HEAD_INIT(init_signals.thread_head), \
.cspace = NULL, \
INIT_LCD \
INIT_IDS \
INIT_PERF_EVENTS(tsk) \
INIT_TRACE_IRQFLAGS \
......
......@@ -30,12 +30,16 @@
#ifdef CONFIG_MODULES
extern char modprobe_path[]; /* for sysctl */
extern char lcd_modprobe_path[]; /* for sysctl */
/* modprobe exit status on success, -ve on error. Return value
* usually useless though. */
extern __printf(2, 3)
int __request_module(bool wait, const char *name, ...);
extern __printf(3, 4)
int __do_request_module(bool wait, int for_lcd, const char *name, ...);
#define __request_module(wait, mod...) __do_request_module(wait,0,mod)
#define request_module(mod...) __request_module(true, mod)
#define request_module_nowait(mod...) __request_module(false, mod)
#define request_lcd_module(mod...) __do_request_module(true,1,mod)
#define try_then_request_module(x, mod...) \
((x) ?: (__request_module(true, mod), (x)))
#else
......
......@@ -1470,10 +1470,7 @@ struct task_struct {
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
#ifdef CONFIG_HAVE_LCD
struct cspace cspace;
struct cap_cache cap_cache;
struct list_head sync_rendezvous;
struct utcb *utcb;
struct lcd *lcd;
#endif
#ifdef CONFIG_SMP
struct llist_node wake_entry;
......
......@@ -25,7 +25,15 @@ struct lcd_blob_info {
unsigned int blob_order;
} __attribute__((packed));
struct lcd_init_module_args {
/* syscall arguments to init_module */
void *module_image;
unsigned long len;
const char *param_values;
} __attribute__((packed));
#define LCD_LOAD_PV_KERNEL _IOR(LCD_MINOR, 0x01, struct lcd_pv_kernel_config)
#define LCD_RUN_BLOB _IOR(LCD_MINOR, 0x02, struct lcd_blob_info)
#define LCD_INIT_MODULE _IOR(LCD_MINOR, 0x03, struct lcd_init_module_args)
#endif /* LCD_DOMAINS_H */
......@@ -56,9 +56,10 @@ static DECLARE_RWSEM(umhelper_sem);
#ifdef CONFIG_MODULES
/*
modprobe_path is set via /proc/sys.
modprobe_path and lcd_modprobe_path is set via /proc/sys.
*/
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
char lcd_modprobe_path[KMOD_PATH_LEN] = "/sbin/lcd-modprobe";
static void free_modprobe_argv(struct subprocess_info *info)
{
......@@ -66,7 +67,7 @@ static void free_modprobe_argv(struct subprocess_info *info)
kfree(info->argv);
}
static int call_modprobe(char *module_name, int wait)
static int call_modprobe(char *module_name, int wait, int for_lcd)
{
struct subprocess_info *info;
static char *envp[] = {
......@@ -77,6 +78,7 @@ static int call_modprobe(char *module_name, int wait)
};
char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
char *__modprobe_path = for_lcd ? lcd_modprobe_path : modprobe_path;
if (!argv)
goto out;
......@@ -84,14 +86,15 @@ static int call_modprobe(char *module_name, int wait)
if (!module_name)
goto free_argv;
argv[0] = modprobe_path;
argv[0] = __modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
info = call_usermodehelper_setup(__modprobe_path, argv, envp,
GFP_KERNEL, NULL, free_modprobe_argv,
NULL);
if (!info)
goto free_module_name;
......@@ -106,8 +109,9 @@ out:
}
/**
* __request_module - try to load a kernel module
* __do_request_module - try to load a kernel module
* @wait: wait (or not) for the operation to complete
* @for_lcd: non-zero if module will be exec'd in lcd
* @fmt: printf style format string for the name of the module
* @...: arguments as specified in the format string
*
......@@ -121,7 +125,7 @@ out:
* If module auto-loading support is disabled then this function
* becomes a no-operation.
*/
int __request_module(bool wait, const char *fmt, ...)
int __do_request_module(bool wait, int for_lcd, const char *fmt, ...)
{
va_list args;
char module_name[MODULE_NAME_LEN];
......@@ -180,12 +184,13 @@ int __request_module(bool wait, const char *fmt, ...)
trace_module_request(module_name, wait, _RET_IP_);
ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);
ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC,
for_lcd);
atomic_dec(&kmod_concurrent);
return ret;
}
EXPORT_SYMBOL(__request_module);
EXPORT_SYMBOL(__do_request_module);
#endif /* CONFIG_MODULES */
static void call_usermodehelper_freeinfo(struct subprocess_info *info)
......
......@@ -932,16 +932,12 @@ SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN-1] = '\0';
if (mutex_lock_interruptible(&module_mutex) != 0)
return -EINTR;
......@@ -981,8 +977,9 @@ SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
goto out;
mutex_unlock(&module_mutex);
/* Final destruction now no one is using it. */
if (mod->exit != NULL)
if (mod->exit != NULL && !for_lcd)
mod->exit();
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
......@@ -1000,6 +997,19 @@ out:
mutex_unlock(&module_mutex);
return ret;
}
EXPORT_SYMBOL(do_sys_delete_module);
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
char name[MODULE_NAME_LEN];
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN-1] = '\0';
return do_sys_delete_module(name, flags, 0);
}
static inline void print_unload_info(struct seq_file *m, struct module *mod)
{
......@@ -3357,7 +3367,7 @@ static void do_free_init(struct rcu_head *head)
* Keep it uninlined to provide a reliable breakpoint target, e.g. for the gdb
* helper command 'lx-symbols'.
*/
static noinline int do_init_module(struct module *mod)
static noinline int do_init_module(struct module *mod, int for_lcd)
{
int ret = 0;
struct mod_initfree *freeinit;
......@@ -3377,7 +3387,7 @@ static noinline int do_init_module(struct module *mod)
do_mod_ctors(mod);
/* Start the module */
if (mod->init != NULL)
if (mod->init != NULL && !for_lcd)
ret = do_one_initcall(mod->init);
if (ret < 0) {
goto fail_free_freeinit;
......@@ -3439,6 +3449,19 @@ static noinline int do_init_module(struct module *mod)
* path, so use actual RCU here.
*/
call_rcu_sched(&freeinit->rcu, do_free_init);
if (!for_lcd) {
/*
* Only free init code if we're not going to run in
* an lcd. If we will run in an lcd, init code will
* be deallocated via free_module in do_sys_delete_module.
*/
unset_module_init_ro_nx(mod);
module_free(mod, mod->module_init);
mod->module_init = NULL;
mod->init_size = 0;
mod->init_ro_size = 0;
mod->init_text_size = 0;
}
mutex_unlock(&module_mutex);
wake_up_all(&module_wq);
......@@ -3572,7 +3595,7 @@ static int unknown_module_param_cb(char *param, char *val, const char *modname,
/* Allocate and load the module: note that size of section 0 is always
zero, and we rely on this for optional sections. */
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
int flags, int for_lcd)
{
struct module *mod;
long err;
......@@ -3857,10 +3880,7 @@ static int load_lcd(struct load_info *info, const char __user *uargs,
/* Done! */
trace_module_load(mod);
lcd = lcd_create();
lcd_move_module(lcd, mod);
lcd_run(lcd);
return 0;
return do_init_module(mod, for_lcd);
bug_cleanup:
/* module_bug_cleanup needs module_mutex protection */
......@@ -3913,8 +3933,8 @@ SYSCALL_DEFINE3(init_lcd, void __user *, umod,
}
#endif
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
int do_sys_init_module(void __user *umod, unsigned long len,
const char __user *uargs, int for_lcd)
{
int err;
struct load_info info = { };
......@@ -3930,7 +3950,14 @@ SYSCALL_DEFINE3(init_module, void __user *, umod,
if (err)
return err;
return load_module(&info, uargs, 0);
return load_module(&info, uargs, 0, for_lcd);
}
EXPORT_SYMBOL(do_sys_init_module);
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
return do_sys_init_module(umod, len, uargs, 0);
}
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
......@@ -3957,7 +3984,7 @@ SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
info.hdr = hdr;
info.len = size;
return load_module(&info, uargs, flags);
return load_module(&info, uargs, flags, 0);
}
static inline int within(unsigned long addr, void *start, unsigned long size)
......
......@@ -657,6 +657,13 @@ static struct ctl_table kern_table[] = {
.mode = 0644,
.proc_handler = proc_dostring,
},
{
.procname = "lcd-modprobe",
.data = &lcd_modprobe_path,
.maxlen = KMOD_PATH_LEN,
.mode = 0644,
.proc_handler = proc_dostring,
},
{
.procname = "modules_disabled",
.data = &modules_disabled,
......
......@@ -6,6 +6,7 @@ config LCD_DOMAINS
tristate "Light-weight Capability Domains"
depends on X86_64
depends on LCD_INTEL
select HAVE_LCD
default y
---help---
Light-weight Capability Domains for Linux.
......@@ -16,3 +17,12 @@ config LCD_INTEL
default y
---help---
Intel-specific code for usings LCDs.
config HAVE_LCD
bool
config LCD_MODULE_LOAD_TEST
tristate
depends on LCD_DOMAINS
default m
......@@ -2,6 +2,10 @@
# One (and only) build file for LCD system.
#
ccflags-y += -Werror
ccflags-y += -Werror
obj-$(CONFIG_LCD_DOMAINS) += lcd-domains.o
obj-$(CONFIG_LCD_DOMAINS) += lcd-domains.o
obj-$(CONFIG_LCD_MODULE_LOAD_TEST) += lcd-module-load-test.o
CFLAGS_lcd-module-load-test.o = -O0
This diff is collapsed.
#include <linux/module.h>
#include <linux/kernel.h>
/*
* Force no inline, so we get a non-trivial jump. Use
* a hack to turn on no inline.
*/
#ifdef noinline
#define lcd_noinline_set
#define lcd_old_noinline noinline
#endif
#undef noinline
static __attribute__ ((noinline)) int foo(int x)
{
return x + 5;
}
#ifdef lcd_noinline_set
#define noinline lcd_old_noinline
#endif
static void lcd_yield(void)
{
asm volatile("mov $6, %rax \n\t"
"vmcall");
/* shouldn't return */
}
static int __init test_init(void)
{
int r;
r = foo(10);
lcd_yield();
/* make compiler happy */
/* (if we actually return, will probably cause ept fault, as we will
* jump to a potentially random place)
*/
return 0;
}
/*
* make module loader happy (so we can unload). we don't actually call
* this before unloading the lcd (yet)
*/
static void __exit test_exit(void)
{
return;
}
module_init(test_init);
module_exit(test_exit);
......@@ -565,6 +565,45 @@ fail1:
return ret;
}
static int test10(void)
{
struct task_struct *t;
int r;
/*
* lcd-module-load-test.c is in virt/lcd-domains/
*/
/*
* Create it
*/
t = lcd_create_as_module("lcd_module_load_test");
if (!t) {
LCD_ERR("create");
goto fail1;
}
/*
* Run it (once)
*/
r = lcd_run_as_module(t);
if (r) {
LCD_ERR("run");
goto fail2;
}
/*
* Tear it down
*/
lcd_destroy_as_module(t, "lcd_module_load_test");
return 0;
fail2:
lcd_destroy_as_module(t, "lcd_module_load_test");
fail1:
return -1;
}
static void lcd_tests(void)
{
if (test01())
......@@ -585,6 +624,8 @@ static void lcd_tests(void)
return;
if (test09())
return;
if (test10())
return;
printk(KERN_ERR "lcd-domains: all tests passed!\n");
return;
}
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