Commit 8003a573 authored by Linus Torvalds's avatar Linus Torvalds

Merge tag 'nfs-for-4.4-2' of git://git.linux-nfs.org/projects/trondmy/linux-nfs

Pull NFS client bugfixes from Trond Myklebust:
 "Highlights include:

  Stable patches:
   - Fix a NFSv4 callback identifier leak that was also causing client
     crashes
   - Fix NFSv4 callback decoding issues when incoming requests are
     truncated
   - Don't declare the attribute cache valid when we call
     nfs_update_inode with an empty attribute structure.
   - Resend LAYOUTGET when there is a race that changes the seqid

  Bugfixes:
   - Fix a number of issues with the NFSv4.2 CLONE ioctl()
   - Properly set NFS v4.2 NFSDBG_FACILITY
   - NFSv4 referrals are broken; Cleanup FATTR4_WORD0_FS_LOCATIONS after
     decoding success
   - Use sliding delay when LAYOUTGET gets NFS4ERR_DELAY
   - Ensure that attrcache is revalidated after a SETATTR"

* tag 'nfs-for-4.4-2' of git://git.linux-nfs.org/projects/trondmy/linux-nfs:
  nfs4: resend LAYOUTGET when there is a race that changes the seqid
  nfs: if we have no valid attrs, then don't declare the attribute cache valid
  nfs: ensure that attrcache is revalidated after a SETATTR
  nfs4: limit callback decoding to received bytes
  nfs4: start callback_ident at idr 1
  nfs: use sliding delay when LAYOUTGET gets NFS4ERR_DELAY
  NFS4: Cleanup FATTR4_WORD0_FS_LOCATIONS after decoding success
  NFS: Properly set NFS v4.2 NFSDBG_FACILITY
  nfs: reduce the amount of ifdefs for v4.2 in nfs4file.c
  nfs: use btrfs ioctl defintions for clone
  nfs: allow intra-file CLONE
  nfs: offer native ioctls even if CONFIG_COMPAT is set
  nfs: pass on count for CLONE operations
parents d0bc387d 4f2e9dce
...@@ -78,7 +78,8 @@ static __be32 *read_buf(struct xdr_stream *xdr, int nbytes) ...@@ -78,7 +78,8 @@ static __be32 *read_buf(struct xdr_stream *xdr, int nbytes)
p = xdr_inline_decode(xdr, nbytes); p = xdr_inline_decode(xdr, nbytes);
if (unlikely(p == NULL)) if (unlikely(p == NULL))
printk(KERN_WARNING "NFS: NFSv4 callback reply buffer overflowed!\n"); printk(KERN_WARNING "NFS: NFSv4 callback reply buffer overflowed "
"or truncated request.\n");
return p; return p;
} }
...@@ -889,6 +890,7 @@ static __be32 nfs4_callback_compound(struct svc_rqst *rqstp, void *argp, void *r ...@@ -889,6 +890,7 @@ static __be32 nfs4_callback_compound(struct svc_rqst *rqstp, void *argp, void *r
struct cb_compound_hdr_arg hdr_arg = { 0 }; struct cb_compound_hdr_arg hdr_arg = { 0 };
struct cb_compound_hdr_res hdr_res = { NULL }; struct cb_compound_hdr_res hdr_res = { NULL };
struct xdr_stream xdr_in, xdr_out; struct xdr_stream xdr_in, xdr_out;
struct xdr_buf *rq_arg = &rqstp->rq_arg;
__be32 *p, status; __be32 *p, status;
struct cb_process_state cps = { struct cb_process_state cps = {
.drc_status = 0, .drc_status = 0,
...@@ -900,7 +902,8 @@ static __be32 nfs4_callback_compound(struct svc_rqst *rqstp, void *argp, void *r ...@@ -900,7 +902,8 @@ static __be32 nfs4_callback_compound(struct svc_rqst *rqstp, void *argp, void *r
dprintk("%s: start\n", __func__); dprintk("%s: start\n", __func__);
xdr_init_decode(&xdr_in, &rqstp->rq_arg, rqstp->rq_arg.head[0].iov_base); rq_arg->len = rq_arg->head[0].iov_len + rq_arg->page_len;
xdr_init_decode(&xdr_in, rq_arg, rq_arg->head[0].iov_base);
p = (__be32*)((char *)rqstp->rq_res.head[0].iov_base + rqstp->rq_res.head[0].iov_len); p = (__be32*)((char *)rqstp->rq_res.head[0].iov_base + rqstp->rq_res.head[0].iov_len);
xdr_init_encode(&xdr_out, &rqstp->rq_res, p); xdr_init_encode(&xdr_out, &rqstp->rq_res, p);
......
...@@ -618,7 +618,10 @@ void nfs_setattr_update_inode(struct inode *inode, struct iattr *attr, ...@@ -618,7 +618,10 @@ void nfs_setattr_update_inode(struct inode *inode, struct iattr *attr,
nfs_inc_stats(inode, NFSIOS_SETATTRTRUNC); nfs_inc_stats(inode, NFSIOS_SETATTRTRUNC);
nfs_vmtruncate(inode, attr->ia_size); nfs_vmtruncate(inode, attr->ia_size);
} }
nfs_update_inode(inode, fattr); if (fattr->valid)
nfs_update_inode(inode, fattr);
else
NFS_I(inode)->cache_validity |= NFS_INO_INVALID_ATTR;
spin_unlock(&inode->i_lock); spin_unlock(&inode->i_lock);
} }
EXPORT_SYMBOL_GPL(nfs_setattr_update_inode); EXPORT_SYMBOL_GPL(nfs_setattr_update_inode);
...@@ -1824,7 +1827,11 @@ static int nfs_update_inode(struct inode *inode, struct nfs_fattr *fattr) ...@@ -1824,7 +1827,11 @@ static int nfs_update_inode(struct inode *inode, struct nfs_fattr *fattr)
if ((long)fattr->gencount - (long)nfsi->attr_gencount > 0) if ((long)fattr->gencount - (long)nfsi->attr_gencount > 0)
nfsi->attr_gencount = fattr->gencount; nfsi->attr_gencount = fattr->gencount;
} }
invalid &= ~NFS_INO_INVALID_ATTR;
/* Don't declare attrcache up to date if there were no attrs! */
if (fattr->valid != 0)
invalid &= ~NFS_INO_INVALID_ATTR;
/* Don't invalidate the data if we were to blame */ /* Don't invalidate the data if we were to blame */
if (!(S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode) if (!(S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)
|| S_ISLNK(inode->i_mode))) || S_ISLNK(inode->i_mode)))
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
#include "pnfs.h" #include "pnfs.h"
#include "internal.h" #include "internal.h"
#define NFSDBG_FACILITY NFSDBG_PNFS #define NFSDBG_FACILITY NFSDBG_PROC
static int nfs42_set_rw_stateid(nfs4_stateid *dst, struct file *file, static int nfs42_set_rw_stateid(nfs4_stateid *dst, struct file *file,
fmode_t fmode) fmode_t fmode)
...@@ -284,6 +284,7 @@ static int _nfs42_proc_clone(struct rpc_message *msg, struct file *src_f, ...@@ -284,6 +284,7 @@ static int _nfs42_proc_clone(struct rpc_message *msg, struct file *src_f,
.dst_fh = NFS_FH(dst_inode), .dst_fh = NFS_FH(dst_inode),
.src_offset = src_offset, .src_offset = src_offset,
.dst_offset = dst_offset, .dst_offset = dst_offset,
.count = count,
.dst_bitmask = server->cache_consistency_bitmask, .dst_bitmask = server->cache_consistency_bitmask,
}; };
struct nfs42_clone_res res = { struct nfs42_clone_res res = {
......
...@@ -33,7 +33,7 @@ static int nfs_get_cb_ident_idr(struct nfs_client *clp, int minorversion) ...@@ -33,7 +33,7 @@ static int nfs_get_cb_ident_idr(struct nfs_client *clp, int minorversion)
return ret; return ret;
idr_preload(GFP_KERNEL); idr_preload(GFP_KERNEL);
spin_lock(&nn->nfs_client_lock); spin_lock(&nn->nfs_client_lock);
ret = idr_alloc(&nn->cb_ident_idr, clp, 0, 0, GFP_NOWAIT); ret = idr_alloc(&nn->cb_ident_idr, clp, 1, 0, GFP_NOWAIT);
if (ret >= 0) if (ret >= 0)
clp->cl_cb_ident = ret; clp->cl_cb_ident = ret;
spin_unlock(&nn->nfs_client_lock); spin_unlock(&nn->nfs_client_lock);
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include <linux/file.h> #include <linux/file.h>
#include <linux/falloc.h> #include <linux/falloc.h>
#include <linux/nfs_fs.h> #include <linux/nfs_fs.h>
#include <uapi/linux/btrfs.h> /* BTRFS_IOC_CLONE/BTRFS_IOC_CLONE_RANGE */
#include "delegation.h" #include "delegation.h"
#include "internal.h" #include "internal.h"
#include "iostat.h" #include "iostat.h"
...@@ -203,6 +204,7 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd, ...@@ -203,6 +204,7 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
struct fd src_file; struct fd src_file;
struct inode *src_inode; struct inode *src_inode;
unsigned int bs = server->clone_blksize; unsigned int bs = server->clone_blksize;
bool same_inode = false;
int ret; int ret;
/* dst file must be opened for writing */ /* dst file must be opened for writing */
...@@ -221,10 +223,8 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd, ...@@ -221,10 +223,8 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
src_inode = file_inode(src_file.file); src_inode = file_inode(src_file.file);
/* src and dst must be different files */
ret = -EINVAL;
if (src_inode == dst_inode) if (src_inode == dst_inode)
goto out_fput; same_inode = true;
/* src file must be opened for reading */ /* src file must be opened for reading */
if (!(src_file.file->f_mode & FMODE_READ)) if (!(src_file.file->f_mode & FMODE_READ))
...@@ -249,8 +249,16 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd, ...@@ -249,8 +249,16 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
goto out_fput; goto out_fput;
} }
/* verify if ranges are overlapped within the same file */
if (same_inode) {
if (dst_off + count > src_off && dst_off < src_off + count)
goto out_fput;
}
/* XXX: do we lock at all? what if server needs CB_RECALL_LAYOUT? */ /* XXX: do we lock at all? what if server needs CB_RECALL_LAYOUT? */
if (dst_inode < src_inode) { if (same_inode) {
mutex_lock(&src_inode->i_mutex);
} else if (dst_inode < src_inode) {
mutex_lock_nested(&dst_inode->i_mutex, I_MUTEX_PARENT); mutex_lock_nested(&dst_inode->i_mutex, I_MUTEX_PARENT);
mutex_lock_nested(&src_inode->i_mutex, I_MUTEX_CHILD); mutex_lock_nested(&src_inode->i_mutex, I_MUTEX_CHILD);
} else { } else {
...@@ -275,7 +283,9 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd, ...@@ -275,7 +283,9 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
truncate_inode_pages_range(&dst_inode->i_data, dst_off, dst_off + count - 1); truncate_inode_pages_range(&dst_inode->i_data, dst_off, dst_off + count - 1);
out_unlock: out_unlock:
if (dst_inode < src_inode) { if (same_inode) {
mutex_unlock(&src_inode->i_mutex);
} else if (dst_inode < src_inode) {
mutex_unlock(&src_inode->i_mutex); mutex_unlock(&src_inode->i_mutex);
mutex_unlock(&dst_inode->i_mutex); mutex_unlock(&dst_inode->i_mutex);
} else { } else {
...@@ -291,46 +301,31 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd, ...@@ -291,46 +301,31 @@ nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
static long nfs42_ioctl_clone_range(struct file *dst_file, void __user *argp) static long nfs42_ioctl_clone_range(struct file *dst_file, void __user *argp)
{ {
struct nfs_ioctl_clone_range_args args; struct btrfs_ioctl_clone_range_args args;
if (copy_from_user(&args, argp, sizeof(args))) if (copy_from_user(&args, argp, sizeof(args)))
return -EFAULT; return -EFAULT;
return nfs42_ioctl_clone(dst_file, args.src_fd, args.src_off, args.dst_off, args.count); return nfs42_ioctl_clone(dst_file, args.src_fd, args.src_offset,
} args.dest_offset, args.src_length);
#else
static long nfs42_ioctl_clone(struct file *dst_file, unsigned long srcfd,
u64 src_off, u64 dst_off, u64 count)
{
return -ENOTTY;
}
static long nfs42_ioctl_clone_range(struct file *dst_file, void __user *argp)
{
return -ENOTTY;
} }
#endif /* CONFIG_NFS_V4_2 */
long nfs4_ioctl(struct file *file, unsigned int cmd, unsigned long arg) long nfs4_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{ {
void __user *argp = (void __user *)arg; void __user *argp = (void __user *)arg;
switch (cmd) { switch (cmd) {
case NFS_IOC_CLONE: case BTRFS_IOC_CLONE:
return nfs42_ioctl_clone(file, arg, 0, 0, 0); return nfs42_ioctl_clone(file, arg, 0, 0, 0);
case NFS_IOC_CLONE_RANGE: case BTRFS_IOC_CLONE_RANGE:
return nfs42_ioctl_clone_range(file, argp); return nfs42_ioctl_clone_range(file, argp);
} }
return -ENOTTY; return -ENOTTY;
} }
#endif /* CONFIG_NFS_V4_2 */
const struct file_operations nfs4_file_operations = { const struct file_operations nfs4_file_operations = {
#ifdef CONFIG_NFS_V4_2
.llseek = nfs4_file_llseek,
#else
.llseek = nfs_file_llseek,
#endif
.read_iter = nfs_file_read, .read_iter = nfs_file_read,
.write_iter = nfs_file_write, .write_iter = nfs_file_write,
.mmap = nfs_file_mmap, .mmap = nfs_file_mmap,
...@@ -342,14 +337,14 @@ const struct file_operations nfs4_file_operations = { ...@@ -342,14 +337,14 @@ const struct file_operations nfs4_file_operations = {
.flock = nfs_flock, .flock = nfs_flock,
.splice_read = nfs_file_splice_read, .splice_read = nfs_file_splice_read,
.splice_write = iter_file_splice_write, .splice_write = iter_file_splice_write,
#ifdef CONFIG_NFS_V4_2
.fallocate = nfs42_fallocate,
#endif /* CONFIG_NFS_V4_2 */
.check_flags = nfs_check_flags, .check_flags = nfs_check_flags,
.setlease = simple_nosetlease, .setlease = simple_nosetlease,
#ifdef CONFIG_COMPAT #ifdef CONFIG_NFS_V4_2
.llseek = nfs4_file_llseek,
.fallocate = nfs42_fallocate,
.unlocked_ioctl = nfs4_ioctl, .unlocked_ioctl = nfs4_ioctl,
#else
.compat_ioctl = nfs4_ioctl, .compat_ioctl = nfs4_ioctl,
#endif /* CONFIG_COMPAT */ #else
.llseek = nfs_file_llseek,
#endif
}; };
...@@ -7866,7 +7866,7 @@ static void nfs4_layoutget_done(struct rpc_task *task, void *calldata) ...@@ -7866,7 +7866,7 @@ static void nfs4_layoutget_done(struct rpc_task *task, void *calldata)
spin_unlock(&inode->i_lock); spin_unlock(&inode->i_lock);
goto out_restart; goto out_restart;
} }
if (nfs4_async_handle_error(task, server, state, NULL) == -EAGAIN) if (nfs4_async_handle_error(task, server, state, &lgp->timeout) == -EAGAIN)
goto out_restart; goto out_restart;
out: out:
dprintk("<-- %s\n", __func__); dprintk("<-- %s\n", __func__);
......
...@@ -3615,6 +3615,7 @@ static int decode_attr_fs_locations(struct xdr_stream *xdr, uint32_t *bitmap, st ...@@ -3615,6 +3615,7 @@ static int decode_attr_fs_locations(struct xdr_stream *xdr, uint32_t *bitmap, st
status = 0; status = 0;
if (unlikely(!(bitmap[0] & FATTR4_WORD0_FS_LOCATIONS))) if (unlikely(!(bitmap[0] & FATTR4_WORD0_FS_LOCATIONS)))
goto out; goto out;
bitmap[0] &= ~FATTR4_WORD0_FS_LOCATIONS;
status = -EIO; status = -EIO;
/* Ignore borken servers that return unrequested attrs */ /* Ignore borken servers that return unrequested attrs */
if (unlikely(res == NULL)) if (unlikely(res == NULL))
......
...@@ -872,33 +872,38 @@ send_layoutget(struct pnfs_layout_hdr *lo, ...@@ -872,33 +872,38 @@ send_layoutget(struct pnfs_layout_hdr *lo,
dprintk("--> %s\n", __func__); dprintk("--> %s\n", __func__);
lgp = kzalloc(sizeof(*lgp), gfp_flags); /*
if (lgp == NULL) * Synchronously retrieve layout information from server and
return NULL; * store in lseg. If we race with a concurrent seqid morphing
* op, then re-send the LAYOUTGET.
*/
do {
lgp = kzalloc(sizeof(*lgp), gfp_flags);
if (lgp == NULL)
return NULL;
i_size = i_size_read(ino);
lgp->args.minlength = PAGE_CACHE_SIZE;
if (lgp->args.minlength > range->length)
lgp->args.minlength = range->length;
if (range->iomode == IOMODE_READ) {
if (range->offset >= i_size)
lgp->args.minlength = 0;
else if (i_size - range->offset < lgp->args.minlength)
lgp->args.minlength = i_size - range->offset;
}
lgp->args.maxcount = PNFS_LAYOUT_MAXSIZE;
lgp->args.range = *range;
lgp->args.type = server->pnfs_curr_ld->id;
lgp->args.inode = ino;
lgp->args.ctx = get_nfs_open_context(ctx);
lgp->gfp_flags = gfp_flags;
lgp->cred = lo->plh_lc_cred;
i_size = i_size_read(ino); lseg = nfs4_proc_layoutget(lgp, gfp_flags);
} while (lseg == ERR_PTR(-EAGAIN));
lgp->args.minlength = PAGE_CACHE_SIZE;
if (lgp->args.minlength > range->length)
lgp->args.minlength = range->length;
if (range->iomode == IOMODE_READ) {
if (range->offset >= i_size)
lgp->args.minlength = 0;
else if (i_size - range->offset < lgp->args.minlength)
lgp->args.minlength = i_size - range->offset;
}
lgp->args.maxcount = PNFS_LAYOUT_MAXSIZE;
lgp->args.range = *range;
lgp->args.type = server->pnfs_curr_ld->id;
lgp->args.inode = ino;
lgp->args.ctx = get_nfs_open_context(ctx);
lgp->gfp_flags = gfp_flags;
lgp->cred = lo->plh_lc_cred;
/* Synchronously retrieve layout information from server and
* store in lseg.
*/
lseg = nfs4_proc_layoutget(lgp, gfp_flags);
if (IS_ERR(lseg)) { if (IS_ERR(lseg)) {
switch (PTR_ERR(lseg)) { switch (PTR_ERR(lseg)) {
case -ENOMEM: case -ENOMEM:
...@@ -1687,6 +1692,7 @@ pnfs_layout_process(struct nfs4_layoutget *lgp) ...@@ -1687,6 +1692,7 @@ pnfs_layout_process(struct nfs4_layoutget *lgp)
/* existing state ID, make sure the sequence number matches. */ /* existing state ID, make sure the sequence number matches. */
if (pnfs_layout_stateid_blocked(lo, &res->stateid)) { if (pnfs_layout_stateid_blocked(lo, &res->stateid)) {
dprintk("%s forget reply due to sequence\n", __func__); dprintk("%s forget reply due to sequence\n", __func__);
status = -EAGAIN;
goto out_forget_reply; goto out_forget_reply;
} }
pnfs_set_layout_stateid(lo, &res->stateid, false); pnfs_set_layout_stateid(lo, &res->stateid, false);
......
...@@ -251,6 +251,7 @@ struct nfs4_layoutget { ...@@ -251,6 +251,7 @@ struct nfs4_layoutget {
struct nfs4_layoutget_res res; struct nfs4_layoutget_res res;
struct rpc_cred *cred; struct rpc_cred *cred;
gfp_t gfp_flags; gfp_t gfp_flags;
long timeout;
}; };
struct nfs4_getdeviceinfo_args { struct nfs4_getdeviceinfo_args {
......
...@@ -33,17 +33,6 @@ ...@@ -33,17 +33,6 @@
#define NFS_PIPE_DIRNAME "nfs" #define NFS_PIPE_DIRNAME "nfs"
/* NFS ioctls */
/* Let's follow btrfs lead on CLONE to avoid messing userspace */
#define NFS_IOC_CLONE _IOW(0x94, 9, int)
#define NFS_IOC_CLONE_RANGE _IOW(0x94, 13, int)
struct nfs_ioctl_clone_range_args {
__s64 src_fd;
__u64 src_off, count;
__u64 dst_off;
};
/* /*
* NFS stats. The good thing with these values is that NFSv3 errors are * NFS stats. The good thing with these values is that NFSv3 errors are
* a superset of NFSv2 errors (with the exception of NFSERR_WFLUSH which * a superset of NFSv2 errors (with the exception of NFSERR_WFLUSH which
......
...@@ -353,12 +353,20 @@ void xprt_complete_bc_request(struct rpc_rqst *req, uint32_t copied) ...@@ -353,12 +353,20 @@ void xprt_complete_bc_request(struct rpc_rqst *req, uint32_t copied)
{ {
struct rpc_xprt *xprt = req->rq_xprt; struct rpc_xprt *xprt = req->rq_xprt;
struct svc_serv *bc_serv = xprt->bc_serv; struct svc_serv *bc_serv = xprt->bc_serv;
struct xdr_buf *rq_rcv_buf = &req->rq_rcv_buf;
spin_lock(&xprt->bc_pa_lock); spin_lock(&xprt->bc_pa_lock);
list_del(&req->rq_bc_pa_list); list_del(&req->rq_bc_pa_list);
xprt_dec_alloc_count(xprt, 1); xprt_dec_alloc_count(xprt, 1);
spin_unlock(&xprt->bc_pa_lock); spin_unlock(&xprt->bc_pa_lock);
if (copied <= rq_rcv_buf->head[0].iov_len) {
rq_rcv_buf->head[0].iov_len = copied;
rq_rcv_buf->page_len = 0;
} else {
rq_rcv_buf->page_len = copied - rq_rcv_buf->head[0].iov_len;
}
req->rq_private_buf.len = copied; req->rq_private_buf.len = copied;
set_bit(RPC_BC_PA_IN_USE, &req->rq_bc_pa_state); set_bit(RPC_BC_PA_IN_USE, &req->rq_bc_pa_state);
......
...@@ -1363,6 +1363,7 @@ bc_svc_process(struct svc_serv *serv, struct rpc_rqst *req, ...@@ -1363,6 +1363,7 @@ bc_svc_process(struct svc_serv *serv, struct rpc_rqst *req,
memcpy(&rqstp->rq_addr, &req->rq_xprt->addr, rqstp->rq_addrlen); memcpy(&rqstp->rq_addr, &req->rq_xprt->addr, rqstp->rq_addrlen);
memcpy(&rqstp->rq_arg, &req->rq_rcv_buf, sizeof(rqstp->rq_arg)); memcpy(&rqstp->rq_arg, &req->rq_rcv_buf, sizeof(rqstp->rq_arg));
memcpy(&rqstp->rq_res, &req->rq_snd_buf, sizeof(rqstp->rq_res)); memcpy(&rqstp->rq_res, &req->rq_snd_buf, sizeof(rqstp->rq_res));
rqstp->rq_arg.len = req->rq_private_buf.len;
/* reset result send buffer "put" position */ /* reset result send buffer "put" position */
resv->iov_len = 0; resv->iov_len = 0;
......
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