Commit d00551b4 authored by David S. Miller's avatar David S. Miller

Merge branch 'master' of git://git.kernel.org/pub/scm/linux/kernel/git/klassert/ipsec

Steffen Klassert says:

====================
pull request (net): ipsec 2021-08-04

1) Fix a sysbot reported memory leak in xfrm_user_rcv_msg.
   From Pavel Skripkin.

2) Revert "xfrm: policy: Read seqcount outside of rcu-read side
   in xfrm_policy_lookup_bytype". This commit tried to fix a
   lockin bug, but only cured some of the symptoms. A proper
   fix is applied on top of this revert.

3) Fix a locking bug on xfrm state hash resize. A recent change
   on sequence counters accidentally repaced a spinlock by a mutex.
   Fix from Frederic Weisbecker.

4) Fix possible user-memory-access in xfrm_user_rcv_msg_compat().
   From Dmitry Safonov.

5) Add initialiation sefltest fot xfrm_spdattr_type_t.
   From Dmitry Safonov.
====================
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents ff0ee9df 480e93e1
...@@ -75,6 +75,7 @@ struct netns_xfrm { ...@@ -75,6 +75,7 @@ struct netns_xfrm {
#endif #endif
spinlock_t xfrm_state_lock; spinlock_t xfrm_state_lock;
seqcount_spinlock_t xfrm_state_hash_generation; seqcount_spinlock_t xfrm_state_hash_generation;
seqcount_spinlock_t xfrm_policy_hash_generation;
spinlock_t xfrm_policy_lock; spinlock_t xfrm_policy_lock;
struct mutex xfrm_cfg_mutex; struct mutex xfrm_cfg_mutex;
......
...@@ -298,8 +298,16 @@ static int xfrm_xlate64(struct sk_buff *dst, const struct nlmsghdr *nlh_src) ...@@ -298,8 +298,16 @@ static int xfrm_xlate64(struct sk_buff *dst, const struct nlmsghdr *nlh_src)
len = nlmsg_attrlen(nlh_src, xfrm_msg_min[type]); len = nlmsg_attrlen(nlh_src, xfrm_msg_min[type]);
nla_for_each_attr(nla, attrs, len, remaining) { nla_for_each_attr(nla, attrs, len, remaining) {
int err = xfrm_xlate64_attr(dst, nla); int err;
switch (type) {
case XFRM_MSG_NEWSPDINFO:
err = xfrm_nla_cpy(dst, nla, nla_len(nla));
break;
default:
err = xfrm_xlate64_attr(dst, nla);
break;
}
if (err) if (err)
return err; return err;
} }
...@@ -341,7 +349,8 @@ static int xfrm_alloc_compat(struct sk_buff *skb, const struct nlmsghdr *nlh_src ...@@ -341,7 +349,8 @@ static int xfrm_alloc_compat(struct sk_buff *skb, const struct nlmsghdr *nlh_src
/* Calculates len of translated 64-bit message. */ /* Calculates len of translated 64-bit message. */
static size_t xfrm_user_rcv_calculate_len64(const struct nlmsghdr *src, static size_t xfrm_user_rcv_calculate_len64(const struct nlmsghdr *src,
struct nlattr *attrs[XFRMA_MAX+1]) struct nlattr *attrs[XFRMA_MAX + 1],
int maxtype)
{ {
size_t len = nlmsg_len(src); size_t len = nlmsg_len(src);
...@@ -358,10 +367,20 @@ static size_t xfrm_user_rcv_calculate_len64(const struct nlmsghdr *src, ...@@ -358,10 +367,20 @@ static size_t xfrm_user_rcv_calculate_len64(const struct nlmsghdr *src,
case XFRM_MSG_POLEXPIRE: case XFRM_MSG_POLEXPIRE:
len += 8; len += 8;
break; break;
case XFRM_MSG_NEWSPDINFO:
/* attirbutes are xfrm_spdattr_type_t, not xfrm_attr_type_t */
return len;
default: default:
break; break;
} }
/* Unexpected for anything, but XFRM_MSG_NEWSPDINFO, please
* correct both 64=>32-bit and 32=>64-bit translators to copy
* new attributes.
*/
if (WARN_ON_ONCE(maxtype))
return len;
if (attrs[XFRMA_SA]) if (attrs[XFRMA_SA])
len += 4; len += 4;
if (attrs[XFRMA_POLICY]) if (attrs[XFRMA_POLICY])
...@@ -440,7 +459,8 @@ static int xfrm_xlate32_attr(void *dst, const struct nlattr *nla, ...@@ -440,7 +459,8 @@ static int xfrm_xlate32_attr(void *dst, const struct nlattr *nla,
static int xfrm_xlate32(struct nlmsghdr *dst, const struct nlmsghdr *src, static int xfrm_xlate32(struct nlmsghdr *dst, const struct nlmsghdr *src,
struct nlattr *attrs[XFRMA_MAX+1], struct nlattr *attrs[XFRMA_MAX+1],
size_t size, u8 type, struct netlink_ext_ack *extack) size_t size, u8 type, int maxtype,
struct netlink_ext_ack *extack)
{ {
size_t pos; size_t pos;
int i; int i;
...@@ -520,6 +540,25 @@ static int xfrm_xlate32(struct nlmsghdr *dst, const struct nlmsghdr *src, ...@@ -520,6 +540,25 @@ static int xfrm_xlate32(struct nlmsghdr *dst, const struct nlmsghdr *src,
} }
pos = dst->nlmsg_len; pos = dst->nlmsg_len;
if (maxtype) {
/* attirbutes are xfrm_spdattr_type_t, not xfrm_attr_type_t */
WARN_ON_ONCE(src->nlmsg_type != XFRM_MSG_NEWSPDINFO);
for (i = 1; i <= maxtype; i++) {
int err;
if (!attrs[i])
continue;
/* just copy - no need for translation */
err = xfrm_attr_cpy32(dst, &pos, attrs[i], size,
nla_len(attrs[i]), nla_len(attrs[i]));
if (err)
return err;
}
return 0;
}
for (i = 1; i < XFRMA_MAX + 1; i++) { for (i = 1; i < XFRMA_MAX + 1; i++) {
int err; int err;
...@@ -564,7 +603,7 @@ static struct nlmsghdr *xfrm_user_rcv_msg_compat(const struct nlmsghdr *h32, ...@@ -564,7 +603,7 @@ static struct nlmsghdr *xfrm_user_rcv_msg_compat(const struct nlmsghdr *h32,
if (err < 0) if (err < 0)
return ERR_PTR(err); return ERR_PTR(err);
len = xfrm_user_rcv_calculate_len64(h32, attrs); len = xfrm_user_rcv_calculate_len64(h32, attrs, maxtype);
/* The message doesn't need translation */ /* The message doesn't need translation */
if (len == nlmsg_len(h32)) if (len == nlmsg_len(h32))
return NULL; return NULL;
...@@ -574,7 +613,7 @@ static struct nlmsghdr *xfrm_user_rcv_msg_compat(const struct nlmsghdr *h32, ...@@ -574,7 +613,7 @@ static struct nlmsghdr *xfrm_user_rcv_msg_compat(const struct nlmsghdr *h32,
if (!h64) if (!h64)
return ERR_PTR(-ENOMEM); return ERR_PTR(-ENOMEM);
err = xfrm_xlate32(h64, h32, attrs, len, type, extack); err = xfrm_xlate32(h64, h32, attrs, len, type, maxtype, extack);
if (err < 0) { if (err < 0) {
kvfree(h64); kvfree(h64);
return ERR_PTR(err); return ERR_PTR(err);
......
...@@ -241,7 +241,7 @@ static void ipcomp_free_tfms(struct crypto_comp * __percpu *tfms) ...@@ -241,7 +241,7 @@ static void ipcomp_free_tfms(struct crypto_comp * __percpu *tfms)
break; break;
} }
WARN_ON(!pos); WARN_ON(list_entry_is_head(pos, &ipcomp_tfms_list, list));
if (--pos->users) if (--pos->users)
return; return;
......
...@@ -155,7 +155,6 @@ static struct xfrm_policy_afinfo const __rcu *xfrm_policy_afinfo[AF_INET6 + 1] ...@@ -155,7 +155,6 @@ static struct xfrm_policy_afinfo const __rcu *xfrm_policy_afinfo[AF_INET6 + 1]
__read_mostly; __read_mostly;
static struct kmem_cache *xfrm_dst_cache __ro_after_init; static struct kmem_cache *xfrm_dst_cache __ro_after_init;
static __read_mostly seqcount_mutex_t xfrm_policy_hash_generation;
static struct rhashtable xfrm_policy_inexact_table; static struct rhashtable xfrm_policy_inexact_table;
static const struct rhashtable_params xfrm_pol_inexact_params; static const struct rhashtable_params xfrm_pol_inexact_params;
...@@ -585,7 +584,7 @@ static void xfrm_bydst_resize(struct net *net, int dir) ...@@ -585,7 +584,7 @@ static void xfrm_bydst_resize(struct net *net, int dir)
return; return;
spin_lock_bh(&net->xfrm.xfrm_policy_lock); spin_lock_bh(&net->xfrm.xfrm_policy_lock);
write_seqcount_begin(&xfrm_policy_hash_generation); write_seqcount_begin(&net->xfrm.xfrm_policy_hash_generation);
odst = rcu_dereference_protected(net->xfrm.policy_bydst[dir].table, odst = rcu_dereference_protected(net->xfrm.policy_bydst[dir].table,
lockdep_is_held(&net->xfrm.xfrm_policy_lock)); lockdep_is_held(&net->xfrm.xfrm_policy_lock));
...@@ -596,7 +595,7 @@ static void xfrm_bydst_resize(struct net *net, int dir) ...@@ -596,7 +595,7 @@ static void xfrm_bydst_resize(struct net *net, int dir)
rcu_assign_pointer(net->xfrm.policy_bydst[dir].table, ndst); rcu_assign_pointer(net->xfrm.policy_bydst[dir].table, ndst);
net->xfrm.policy_bydst[dir].hmask = nhashmask; net->xfrm.policy_bydst[dir].hmask = nhashmask;
write_seqcount_end(&xfrm_policy_hash_generation); write_seqcount_end(&net->xfrm.xfrm_policy_hash_generation);
spin_unlock_bh(&net->xfrm.xfrm_policy_lock); spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
synchronize_rcu(); synchronize_rcu();
...@@ -1245,7 +1244,7 @@ static void xfrm_hash_rebuild(struct work_struct *work) ...@@ -1245,7 +1244,7 @@ static void xfrm_hash_rebuild(struct work_struct *work)
} while (read_seqretry(&net->xfrm.policy_hthresh.lock, seq)); } while (read_seqretry(&net->xfrm.policy_hthresh.lock, seq));
spin_lock_bh(&net->xfrm.xfrm_policy_lock); spin_lock_bh(&net->xfrm.xfrm_policy_lock);
write_seqcount_begin(&xfrm_policy_hash_generation); write_seqcount_begin(&net->xfrm.xfrm_policy_hash_generation);
/* make sure that we can insert the indirect policies again before /* make sure that we can insert the indirect policies again before
* we start with destructive action. * we start with destructive action.
...@@ -1354,7 +1353,7 @@ static void xfrm_hash_rebuild(struct work_struct *work) ...@@ -1354,7 +1353,7 @@ static void xfrm_hash_rebuild(struct work_struct *work)
out_unlock: out_unlock:
__xfrm_policy_inexact_flush(net); __xfrm_policy_inexact_flush(net);
write_seqcount_end(&xfrm_policy_hash_generation); write_seqcount_end(&net->xfrm.xfrm_policy_hash_generation);
spin_unlock_bh(&net->xfrm.xfrm_policy_lock); spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
mutex_unlock(&hash_resize_mutex); mutex_unlock(&hash_resize_mutex);
...@@ -2091,15 +2090,12 @@ static struct xfrm_policy *xfrm_policy_lookup_bytype(struct net *net, u8 type, ...@@ -2091,15 +2090,12 @@ static struct xfrm_policy *xfrm_policy_lookup_bytype(struct net *net, u8 type,
if (unlikely(!daddr || !saddr)) if (unlikely(!daddr || !saddr))
return NULL; return NULL;
retry:
sequence = read_seqcount_begin(&xfrm_policy_hash_generation);
rcu_read_lock(); rcu_read_lock();
retry:
do {
sequence = read_seqcount_begin(&net->xfrm.xfrm_policy_hash_generation);
chain = policy_hash_direct(net, daddr, saddr, family, dir); chain = policy_hash_direct(net, daddr, saddr, family, dir);
if (read_seqcount_retry(&xfrm_policy_hash_generation, sequence)) { } while (read_seqcount_retry(&net->xfrm.xfrm_policy_hash_generation, sequence));
rcu_read_unlock();
goto retry;
}
ret = NULL; ret = NULL;
hlist_for_each_entry_rcu(pol, chain, bydst) { hlist_for_each_entry_rcu(pol, chain, bydst) {
...@@ -2130,15 +2126,11 @@ static struct xfrm_policy *xfrm_policy_lookup_bytype(struct net *net, u8 type, ...@@ -2130,15 +2126,11 @@ static struct xfrm_policy *xfrm_policy_lookup_bytype(struct net *net, u8 type,
} }
skip_inexact: skip_inexact:
if (read_seqcount_retry(&xfrm_policy_hash_generation, sequence)) { if (read_seqcount_retry(&net->xfrm.xfrm_policy_hash_generation, sequence))
rcu_read_unlock();
goto retry; goto retry;
}
if (ret && !xfrm_pol_hold_rcu(ret)) { if (ret && !xfrm_pol_hold_rcu(ret))
rcu_read_unlock();
goto retry; goto retry;
}
fail: fail:
rcu_read_unlock(); rcu_read_unlock();
...@@ -4089,6 +4081,7 @@ static int __net_init xfrm_net_init(struct net *net) ...@@ -4089,6 +4081,7 @@ static int __net_init xfrm_net_init(struct net *net)
/* Initialize the per-net locks here */ /* Initialize the per-net locks here */
spin_lock_init(&net->xfrm.xfrm_state_lock); spin_lock_init(&net->xfrm.xfrm_state_lock);
spin_lock_init(&net->xfrm.xfrm_policy_lock); spin_lock_init(&net->xfrm.xfrm_policy_lock);
seqcount_spinlock_init(&net->xfrm.xfrm_policy_hash_generation, &net->xfrm.xfrm_policy_lock);
mutex_init(&net->xfrm.xfrm_cfg_mutex); mutex_init(&net->xfrm.xfrm_cfg_mutex);
rv = xfrm_statistics_init(net); rv = xfrm_statistics_init(net);
...@@ -4133,7 +4126,6 @@ void __init xfrm_init(void) ...@@ -4133,7 +4126,6 @@ void __init xfrm_init(void)
{ {
register_pernet_subsys(&xfrm_net_ops); register_pernet_subsys(&xfrm_net_ops);
xfrm_dev_init(); xfrm_dev_init();
seqcount_mutex_init(&xfrm_policy_hash_generation, &hash_resize_mutex);
xfrm_input_init(); xfrm_input_init();
#ifdef CONFIG_XFRM_ESPINTCP #ifdef CONFIG_XFRM_ESPINTCP
......
...@@ -2811,6 +2811,16 @@ static int xfrm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh, ...@@ -2811,6 +2811,16 @@ static int xfrm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
err = link->doit(skb, nlh, attrs); err = link->doit(skb, nlh, attrs);
/* We need to free skb allocated in xfrm_alloc_compat() before
* returning from this function, because consume_skb() won't take
* care of frag_list since netlink destructor sets
* sbk->head to NULL. (see netlink_skb_destructor())
*/
if (skb_has_frag_list(skb)) {
kfree_skb(skb_shinfo(skb)->frag_list);
skb_shinfo(skb)->frag_list = NULL;
}
err: err:
kvfree(nlh64); kvfree(nlh64);
return err; return err;
......
...@@ -484,13 +484,16 @@ enum desc_type { ...@@ -484,13 +484,16 @@ enum desc_type {
MONITOR_ACQUIRE, MONITOR_ACQUIRE,
EXPIRE_STATE, EXPIRE_STATE,
EXPIRE_POLICY, EXPIRE_POLICY,
SPDINFO_ATTRS,
}; };
const char *desc_name[] = { const char *desc_name[] = {
"create tunnel", "create tunnel",
"alloc spi", "alloc spi",
"monitor acquire", "monitor acquire",
"expire state", "expire state",
"expire policy" "expire policy",
"spdinfo attributes",
""
}; };
struct xfrm_desc { struct xfrm_desc {
enum desc_type type; enum desc_type type;
...@@ -1593,6 +1596,155 @@ static int xfrm_expire_policy(int xfrm_sock, uint32_t *seq, ...@@ -1593,6 +1596,155 @@ static int xfrm_expire_policy(int xfrm_sock, uint32_t *seq,
return ret; return ret;
} }
static int xfrm_spdinfo_set_thresh(int xfrm_sock, uint32_t *seq,
unsigned thresh4_l, unsigned thresh4_r,
unsigned thresh6_l, unsigned thresh6_r,
bool add_bad_attr)
{
struct {
struct nlmsghdr nh;
union {
uint32_t unused;
int error;
};
char attrbuf[MAX_PAYLOAD];
} req;
struct xfrmu_spdhthresh thresh;
memset(&req, 0, sizeof(req));
req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(req.unused));
req.nh.nlmsg_type = XFRM_MSG_NEWSPDINFO;
req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.nh.nlmsg_seq = (*seq)++;
thresh.lbits = thresh4_l;
thresh.rbits = thresh4_r;
if (rtattr_pack(&req.nh, sizeof(req), XFRMA_SPD_IPV4_HTHRESH, &thresh, sizeof(thresh)))
return -1;
thresh.lbits = thresh6_l;
thresh.rbits = thresh6_r;
if (rtattr_pack(&req.nh, sizeof(req), XFRMA_SPD_IPV6_HTHRESH, &thresh, sizeof(thresh)))
return -1;
if (add_bad_attr) {
BUILD_BUG_ON(XFRMA_IF_ID <= XFRMA_SPD_MAX + 1);
if (rtattr_pack(&req.nh, sizeof(req), XFRMA_IF_ID, NULL, 0)) {
pr_err("adding attribute failed: no space");
return -1;
}
}
if (send(xfrm_sock, &req, req.nh.nlmsg_len, 0) < 0) {
pr_err("send()");
return -1;
}
if (recv(xfrm_sock, &req, sizeof(req), 0) < 0) {
pr_err("recv()");
return -1;
} else if (req.nh.nlmsg_type != NLMSG_ERROR) {
printk("expected NLMSG_ERROR, got %d", (int)req.nh.nlmsg_type);
return -1;
}
if (req.error) {
printk("NLMSG_ERROR: %d: %s", req.error, strerror(-req.error));
return -1;
}
return 0;
}
static int xfrm_spdinfo_attrs(int xfrm_sock, uint32_t *seq)
{
struct {
struct nlmsghdr nh;
union {
uint32_t unused;
int error;
};
char attrbuf[MAX_PAYLOAD];
} req;
if (xfrm_spdinfo_set_thresh(xfrm_sock, seq, 32, 31, 120, 16, false)) {
pr_err("Can't set SPD HTHRESH");
return KSFT_FAIL;
}
memset(&req, 0, sizeof(req));
req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(req.unused));
req.nh.nlmsg_type = XFRM_MSG_GETSPDINFO;
req.nh.nlmsg_flags = NLM_F_REQUEST;
req.nh.nlmsg_seq = (*seq)++;
if (send(xfrm_sock, &req, req.nh.nlmsg_len, 0) < 0) {
pr_err("send()");
return KSFT_FAIL;
}
if (recv(xfrm_sock, &req, sizeof(req), 0) < 0) {
pr_err("recv()");
return KSFT_FAIL;
} else if (req.nh.nlmsg_type == XFRM_MSG_NEWSPDINFO) {
size_t len = NLMSG_PAYLOAD(&req.nh, sizeof(req.unused));
struct rtattr *attr = (void *)req.attrbuf;
int got_thresh = 0;
for (; RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) {
if (attr->rta_type == XFRMA_SPD_IPV4_HTHRESH) {
struct xfrmu_spdhthresh *t = RTA_DATA(attr);
got_thresh++;
if (t->lbits != 32 || t->rbits != 31) {
pr_err("thresh differ: %u, %u",
t->lbits, t->rbits);
return KSFT_FAIL;
}
}
if (attr->rta_type == XFRMA_SPD_IPV6_HTHRESH) {
struct xfrmu_spdhthresh *t = RTA_DATA(attr);
got_thresh++;
if (t->lbits != 120 || t->rbits != 16) {
pr_err("thresh differ: %u, %u",
t->lbits, t->rbits);
return KSFT_FAIL;
}
}
}
if (got_thresh != 2) {
pr_err("only %d thresh returned by XFRM_MSG_GETSPDINFO", got_thresh);
return KSFT_FAIL;
}
} else if (req.nh.nlmsg_type != NLMSG_ERROR) {
printk("expected NLMSG_ERROR, got %d", (int)req.nh.nlmsg_type);
return KSFT_FAIL;
} else {
printk("NLMSG_ERROR: %d: %s", req.error, strerror(-req.error));
return -1;
}
/* Restore the default */
if (xfrm_spdinfo_set_thresh(xfrm_sock, seq, 32, 32, 128, 128, false)) {
pr_err("Can't restore SPD HTHRESH");
return KSFT_FAIL;
}
/*
* At this moment xfrm uses nlmsg_parse_deprecated(), which
* implies NL_VALIDATE_LIBERAL - ignoring attributes with
* (type > maxtype). nla_parse_depricated_strict() would enforce
* it. Or even stricter nla_parse().
* Right now it's not expected to fail, but to be ignored.
*/
if (xfrm_spdinfo_set_thresh(xfrm_sock, seq, 32, 32, 128, 128, true))
return KSFT_PASS;
return KSFT_PASS;
}
static int child_serv(int xfrm_sock, uint32_t *seq, static int child_serv(int xfrm_sock, uint32_t *seq,
unsigned int nr, int cmd_fd, void *buf, struct xfrm_desc *desc) unsigned int nr, int cmd_fd, void *buf, struct xfrm_desc *desc)
{ {
...@@ -1717,6 +1869,9 @@ static int child_f(unsigned int nr, int test_desc_fd, int cmd_fd, void *buf) ...@@ -1717,6 +1869,9 @@ static int child_f(unsigned int nr, int test_desc_fd, int cmd_fd, void *buf)
case EXPIRE_POLICY: case EXPIRE_POLICY:
ret = xfrm_expire_policy(xfrm_sock, &seq, nr, &desc); ret = xfrm_expire_policy(xfrm_sock, &seq, nr, &desc);
break; break;
case SPDINFO_ATTRS:
ret = xfrm_spdinfo_attrs(xfrm_sock, &seq);
break;
default: default:
printk("Unknown desc type %d", desc.type); printk("Unknown desc type %d", desc.type);
exit(KSFT_FAIL); exit(KSFT_FAIL);
...@@ -1994,8 +2149,10 @@ static int write_proto_plan(int fd, int proto) ...@@ -1994,8 +2149,10 @@ static int write_proto_plan(int fd, int proto)
* sizeof(xfrm_user_polexpire) = 168 | sizeof(xfrm_user_polexpire) = 176 * sizeof(xfrm_user_polexpire) = 168 | sizeof(xfrm_user_polexpire) = 176
* *
* Check the affected by the UABI difference structures. * Check the affected by the UABI difference structures.
* Also, check translation for xfrm_set_spdinfo: it has it's own attributes
* which needs to be correctly copied, but not translated.
*/ */
const unsigned int compat_plan = 4; const unsigned int compat_plan = 5;
static int write_compat_struct_tests(int test_desc_fd) static int write_compat_struct_tests(int test_desc_fd)
{ {
struct xfrm_desc desc = {}; struct xfrm_desc desc = {};
...@@ -2019,6 +2176,10 @@ static int write_compat_struct_tests(int test_desc_fd) ...@@ -2019,6 +2176,10 @@ static int write_compat_struct_tests(int test_desc_fd)
if (__write_desc(test_desc_fd, &desc)) if (__write_desc(test_desc_fd, &desc))
return -1; return -1;
desc.type = SPDINFO_ATTRS;
if (__write_desc(test_desc_fd, &desc))
return -1;
return 0; return 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