@@ -457,6 +457,19 @@ int exthdrs_init(struct tlv_param_table *tlv_param_table,
void exthdrs_fini(struct tlv_param_table *tlv_param_table,
struct genl_family *tlv_nl_family);
+int ipv6_opt_validate_tlvs(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ struct ipv6_opt_hdr *opt,
+ unsigned int optname, bool admin);
+int ipv6_opt_validate_single_tlv(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ unsigned int optname,
+ unsigned char *tlv, size_t len,
+ bool deleting, bool admin);
+int ipv6_opt_check_perm(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ struct sock *sk, int optname, bool admin);
+
/* tlv_get_proc assumes rcu_read_lock is held */
static inline struct tlv_proc *tlv_get_proc(
struct tlv_param_table *tlv_param_table,
@@ -473,6 +486,13 @@ bool ipv6_opt_accepted(const struct sock *sk, const struct sk_buff *skb,
struct ipv6_txoptions *ipv6_update_options(struct sock *sk,
struct ipv6_txoptions *opt);
+struct ipv6_txoptions *txoptions_from_opt(struct sock *sk,
+ struct tlv_param_table
+ *tlv_param_table,
+ struct ipv6_txoptions *opt,
+ int optname, char __user *optval,
+ unsigned int optlen);
+
static inline bool ipv6_accept_ra(struct inet6_dev *idev)
{
/* If forwarding is enabled, RA are not accepted unless the special
@@ -853,10 +853,14 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk,
err = -EINVAL;
goto exit_f;
}
- if (!ns_capable(net->user_ns, CAP_NET_RAW)) {
- err = -EPERM;
+
+ err = ipv6_opt_validate_tlvs(net, &ipv6_tlv_param_table,
+ hdr, IPV6_HOPOPTS,
+ ns_capable(net->user_ns,
+ CAP_NET_RAW));
+ if (err < 0)
goto exit_f;
- }
+
opt->opt_nflen += len;
opt->hopopt = hdr;
break;
@@ -873,10 +877,14 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk,
err = -EINVAL;
goto exit_f;
}
- if (!ns_capable(net->user_ns, CAP_NET_RAW)) {
- err = -EPERM;
+
+ err = ipv6_opt_validate_tlvs(net, &ipv6_tlv_param_table,
+ hdr, IPV6_DSTOPTS,
+ ns_capable(net->user_ns,
+ CAP_NET_RAW));
+ if (err < 0)
goto exit_f;
- }
+
if (opt->dst1opt) {
err = -EINVAL;
goto exit_f;
@@ -898,10 +906,14 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk,
err = -EINVAL;
goto exit_f;
}
- if (!ns_capable(net->user_ns, CAP_NET_RAW)) {
- err = -EPERM;
+
+ err = ipv6_opt_validate_tlvs(net, &ipv6_tlv_param_table,
+ hdr, cmsg->cmsg_type,
+ ns_capable(net->user_ns,
+ CAP_NET_RAW));
+ if (err < 0)
goto exit_f;
- }
+
if (cmsg->cmsg_type == IPV6_DSTOPTS) {
opt->opt_flen += len;
opt->dst1opt = hdr;
@@ -421,6 +421,313 @@ struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space,
}
EXPORT_SYMBOL_GPL(ipv6_fixup_options);
+/* TLV validation functions */
+
+/* Validate a single non-padding TLV */
+static int __ipv6_opt_validate_single_tlv(struct net *net, unsigned char *tlv,
+ struct tlv_tx_params *tptx,
+ unsigned int class, bool *deep_check,
+ bool deleting, bool admin)
+{
+ if (tlv[0] < 2) /* Must be non-padding */
+ return -EINVAL;
+
+ /* Check permissions */
+ switch (admin ? tptx->admin_perm : tptx->user_perm) {
+ case IPV6_TLV_PERM_NO_CHECK:
+ /* Allowed with no deep checks */
+ *deep_check = false;
+ return 0;
+ case IPV6_TLV_PERM_WITH_CHECK:
+ /* Allowed with deep checks */
+ *deep_check = true;
+ break;
+ default:
+ /* No permission */
+ return -EPERM;
+ }
+
+ /* Perform deep checks on the TLV */
+
+ /* Check class */
+ if ((tptx->class & class) != class)
+ return -EINVAL;
+
+ /* Don't bother checking lengths when deleting, the TLV is only
+ * needed here for lookup
+ */
+ if (deleting) {
+ /* Don't bother with deep checks when deleting */
+ *deep_check = false;
+ } else {
+ /* Check length */
+ if (tlv[1] < tptx->min_data_len || tlv[1] > tptx->max_data_len)
+ return -EINVAL;
+
+ /* Check length alignment */
+ if ((tlv[1] % (tptx->data_len_mult + 1)) != tptx->data_len_off)
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static unsigned int optname_to_tlv_class(int optname)
+{
+ switch (optname) {
+ case IPV6_HOPOPTS:
+ return IPV6_TLV_CLASS_FLAG_HOPOPT;
+ case IPV6_RTHDRDSTOPTS:
+ return IPV6_TLV_CLASS_FLAG_RTRDSTOPT;
+ case IPV6_DSTOPTS:
+ return IPV6_TLV_CLASS_FLAG_DSTOPT;
+ default:
+ return -1U;
+ }
+}
+
+static int __ipv6_opt_validate_tlvs(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ struct ipv6_opt_hdr *opt,
+ unsigned int optname, bool deleting,
+ bool admin)
+{
+ unsigned int max_len = 0, max_cnt = 0, cnt = 0;
+ unsigned char *tlv = (unsigned char *)opt;
+ bool deep_check, did_deep_check = false;
+ unsigned int opt_len, tlv_len, offset;
+ unsigned int padding = 0, numpad = 0;
+ unsigned char prev_tlv_order = 0;
+ struct tlv_tx_params *tptx;
+ int retc, ret = -EINVAL;
+ struct tlv_proc *tproc;
+ unsigned int class;
+
+ opt_len = ipv6_optlen(opt);
+ offset = sizeof(*opt);
+
+ class = optname_to_tlv_class(optname);
+
+ switch (optname) {
+ case IPV6_HOPOPTS:
+ max_len = net->ipv6.sysctl.max_hbh_opts_len;
+ max_cnt = net->ipv6.sysctl.max_hbh_opts_cnt;
+ break;
+ case IPV6_RTHDRDSTOPTS:
+ case IPV6_DSTOPTS:
+ max_len = net->ipv6.sysctl.max_dst_opts_len;
+ max_cnt = net->ipv6.sysctl.max_dst_opts_cnt;
+ break;
+ }
+
+ rcu_read_lock();
+
+ while (offset < opt_len) {
+ switch (tlv[offset]) {
+ case IPV6_TLV_PAD1:
+ tlv_len = 1;
+ padding++;
+ numpad++;
+ break;
+ case IPV6_TLV_PADN:
+ if (offset + 1 >= opt_len)
+ goto out;
+
+ tlv_len = tlv[offset + 1] + 2;
+
+ if (offset + tlv_len > opt_len)
+ goto out;
+
+ padding += tlv_len;
+ numpad++;
+ break;
+ default:
+ if (offset + 1 >= opt_len)
+ goto out;
+
+ tlv_len = tlv[offset + 1] + 2;
+
+ if (offset + tlv_len > opt_len)
+ goto out;
+
+ tproc = tlv_get_proc(tlv_param_table, tlv[offset]);
+ tptx = &tproc->params.t;
+
+ retc = __ipv6_opt_validate_single_tlv(net, &tlv[offset],
+ tptx, class,
+ &deep_check,
+ deleting, admin);
+ if (retc < 0) {
+ ret = retc;
+ goto out;
+ }
+
+ if (deep_check) {
+ /* Check for too many options */
+ if (++cnt > max_cnt) {
+ ret = -E2BIG;
+ goto out;
+ }
+
+ /* Check order */
+ if (tptx->preferred_order < prev_tlv_order)
+ goto out;
+
+ /* Check alignment */
+ if ((offset % (tptx->align_mult + 1)) !=
+ tptx->align_off)
+ goto out;
+
+ /* Check for right amount of padding */
+ if (numpad > 1 || padding > tptx->align_mult)
+ goto out;
+
+ prev_tlv_order = tptx->preferred_order;
+ }
+
+ padding = 0;
+ numpad = 0;
+ did_deep_check = true;
+ }
+ offset += tlv_len;
+ }
+
+ /* If we did at least one deep check apply length limit */
+ if (did_deep_check && opt_len > max_len) {
+ ret = -EMSGSIZE;
+ goto out;
+ }
+
+ /* All good */
+ ret = 0;
+out:
+ rcu_read_unlock();
+
+ return ret;
+}
+
+/**
+ * __ipv6_opt_validate_tlvs - Validate TLVs.
+ * @net: Current net
+ * @opt: The option header
+ * @optname: IPV6_HOPOPTS, IPV6_RTHDRDSTOPTS, or IPV6_DSTOPTS
+ * @admin: Set for privileged user
+ *
+ * Description:
+ * Walks the TLVs in a list to verify that the TLV lengths and other
+ * parameters are in bounds for a Destination or Hop-by-Hop option.
+ * Return -EINVAL is there is a problem, zero otherwise.
+ */
+int ipv6_opt_validate_tlvs(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ struct ipv6_opt_hdr *opt, unsigned int optname,
+ bool admin)
+{
+ return __ipv6_opt_validate_tlvs(net, tlv_param_table,
+ opt, optname, false, admin);
+}
+EXPORT_SYMBOL(ipv6_opt_validate_tlvs);
+
+/**
+ * ipv6_opt_validate_single - Check that a single TLV is valid.
+ * @net: Current net
+ * @optname: IPV6_HOPOPTS, IPV6_RTHDRDSTOPTS, or IPV6_DSTOPTS
+ * @tlv: The TLV as array of bytes
+ * @len: Length of buffer holding TLV
+ *
+ * Description:
+ * Validates a single TLV. The TLV must be non-padding type. The length
+ * of the TLV (as determined by the second byte that gives length of the
+ * option data) must match @len.
+ */
+int ipv6_opt_validate_single_tlv(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ unsigned int optname,
+ unsigned char *tlv, size_t len,
+ bool deleting, bool admin)
+{
+ struct tlv_tx_params *tptx;
+ struct tlv_proc *tproc;
+ unsigned int class;
+ bool deep_check;
+ int ret = 0;
+
+ class = optname_to_tlv_class(optname);
+
+ if (tlv[0] < 2)
+ return -EINVAL;
+
+ if (len < 2)
+ return -EINVAL;
+
+ if (tlv[1] + 2 != len)
+ return -EINVAL;
+
+ rcu_read_lock();
+
+ tproc = tlv_get_proc(tlv_param_table, tlv[0]);
+ tptx = &tproc->params.t;
+
+ ret = __ipv6_opt_validate_single_tlv(net, tlv, tptx, class,
+ &deep_check, deleting, admin);
+
+ rcu_read_unlock();
+
+ return ret;
+}
+EXPORT_SYMBOL(ipv6_opt_validate_single_tlv);
+
+/**
+ * ipv6_opt_check_perm - Check that current capabilities allows modifying
+ * txopts.
+ * @net: Current net
+ * @sk: the socket
+ * @optname: IPV6_HOPOPTS, IPV6_RTHDRDSTOPTS, or IPV6_DSTOPTS
+ * @admin: Set for privileged user
+ *
+ * Description:
+ *
+ * Checks whether the permissions of TLV that are set on a socket permit
+ * modificationr.
+ *
+ */
+int ipv6_opt_check_perm(struct net *net,
+ struct tlv_param_table *tlv_param_table,
+ struct sock *sk, int optname, bool admin)
+{
+ struct ipv6_txoptions *old = txopt_get(inet6_sk(sk));
+ struct ipv6_opt_hdr *opt;
+ int retv = -EPERM;
+
+ if (!old)
+ return 0;
+
+ switch (optname) {
+ case IPV6_HOPOPTS:
+ opt = old->hopopt;
+ break;
+ case IPV6_RTHDRDSTOPTS:
+ opt = old->dst0opt;
+ break;
+ case IPV6_DSTOPTS:
+ opt = old->dst1opt;
+ break;
+ default:
+ goto out;
+ }
+
+ /* Just call the validate function on the options as being
+ * deleted.
+ */
+ retv = __ipv6_opt_validate_tlvs(net, tlv_param_table, opt, optname,
+ true, admin);
+
+out:
+ txopt_put(old);
+ return retv;
+}
+EXPORT_SYMBOL(ipv6_opt_check_perm);
+
/* TLV parameter table functions and structures */
static void tlv_param_table_release(struct rcu_head *rcu)
@@ -659,6 +966,71 @@ int tlv_unset_params(struct tlv_param_table *tlv_param_table,
}
EXPORT_SYMBOL(tlv_unset_params);
+struct ipv6_txoptions *txoptions_from_opt(struct sock *sk,
+ struct tlv_param_table
+ *tlv_param_table,
+ struct ipv6_txoptions *opt,
+ int optname, char __user *optval,
+ unsigned int optlen)
+{
+ struct ipv6_opt_hdr *new = NULL;
+ struct net *net = sock_net(sk);
+ int retv;
+
+ /* remove any sticky options header with a zero option
+ * length, per RFC3542.
+ */
+ if (optlen == 0) {
+ optval = NULL;
+ } else if (!optval) {
+ return ERR_PTR(-EINVAL);
+ } else if (optlen < sizeof(struct ipv6_opt_hdr) ||
+ optlen & 0x7 || optlen > 8 * 255) {
+ return ERR_PTR(-EINVAL);
+ } else {
+ new = memdup_user(optval, optlen);
+ if (IS_ERR(new))
+ return (struct ipv6_txoptions *)new;
+ if (unlikely(ipv6_optlen(new) > optlen)) {
+ kfree(new);
+ return ERR_PTR(-EINVAL);
+ }
+ }
+
+ if (optname != IPV6_RTHDR) {
+ bool cap = ns_capable(net->user_ns, CAP_NET_RAW);
+
+ /* First check if we have permission to delete
+ * the existing options on the socket.
+ */
+ retv = ipv6_opt_check_perm(net, tlv_param_table,
+ sk, optname, cap);
+ if (retv < 0) {
+ kfree(new);
+ return ERR_PTR(retv);
+ }
+
+ /* Check permissions and other validations on new
+ * TLVs
+ */
+ if (new) {
+ retv = ipv6_opt_validate_tlvs(net,
+ &ipv6_tlv_param_table,
+ new, optname, cap);
+ if (retv < 0) {
+ kfree(new);
+ return ERR_PTR(retv);
+ }
+ }
+ }
+
+ opt = ipv6_renew_options(sk, opt, optname, new);
+ kfree(new);
+
+ return opt;
+}
+EXPORT_SYMBOL(txoptions_from_opt);
+
static const struct nla_policy tlv_nl_policy[IPV6_TLV_ATTR_MAX + 1] = {
[IPV6_TLV_ATTR_TYPE] = { .type = NLA_U8, },
[IPV6_TLV_ATTR_ORDER] = { .type = NLA_U8, },
@@ -398,39 +398,12 @@ static int do_ipv6_setsockopt(struct sock *sk, int level, int optname,
case IPV6_DSTOPTS:
{
struct ipv6_txoptions *opt;
- struct ipv6_opt_hdr *new = NULL;
- /* hop-by-hop / destination options are privileged option */
- retv = -EPERM;
- if (optname != IPV6_RTHDR && !ns_capable(net->user_ns, CAP_NET_RAW))
- break;
+ opt = txoptions_from_opt(sk, &ipv6_tlv_param_table,
+ rcu_dereference_protected(np->opt,
+ lockdep_sock_is_held(sk)),
+ optname, optval, optlen);
- /* remove any sticky options header with a zero option
- * length, per RFC3542.
- */
- if (optlen == 0)
- optval = NULL;
- else if (!optval)
- goto e_inval;
- else if (optlen < sizeof(struct ipv6_opt_hdr) ||
- optlen & 0x7 || optlen > 8 * 255)
- goto e_inval;
- else {
- new = memdup_user(optval, optlen);
- if (IS_ERR(new)) {
- retv = PTR_ERR(new);
- break;
- }
- if (unlikely(ipv6_optlen(new) > optlen)) {
- kfree(new);
- goto e_inval;
- }
- }
-
- opt = rcu_dereference_protected(np->opt,
- lockdep_sock_is_held(sk));
- opt = ipv6_renew_options(sk, opt, optname, new);
- kfree(new);
if (IS_ERR(opt)) {
retv = PTR_ERR(opt);
break;