@@ -379,6 +379,13 @@ struct ipv6_txoptions *ipv6_renew_options(struct sock *sk,
struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space,
struct ipv6_txoptions *opt);
+int ipv6_opt_validate_tlvs(struct net *net, struct ipv6_opt_hdr *opt,
+ unsigned int optname, bool admin);
+int ipv6_opt_validate_single_tlv(struct net *net, unsigned int optname,
+ unsigned char *tlv, size_t len,
+ bool deleting, bool admin);
+int ipv6_opt_check_perm(struct net *net, struct sock *sk,
+ int optname, bool admin);
struct tlv_tx_param {
unsigned char preferred_order;
unsigned char admin_perm : 2;
@@ -853,10 +853,13 @@ 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, 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 +876,13 @@ 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, 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 +904,13 @@ 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, 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;
@@ -162,6 +162,304 @@ 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_param *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 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_param *tptx;
+ int retc, ret = -EINVAL;
+ 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;
+
+ tptx = tlv_deref_tx_params(tlv[offset]);
+ 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 ipv6_opt_hdr *opt,
+ unsigned int optname, bool admin)
+{
+ return __ipv6_opt_validate_tlvs(net, 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, unsigned int optname,
+ unsigned char *tlv, size_t len,
+ bool deleting, bool admin)
+{
+ struct tlv_tx_param *tptx;
+ unsigned int class;
+ bool deep_check;
+ int ret = 0;
+
+ class = optname_to_tlv_class(optname);
+
+ switch (tlv[0]) {
+ case IPV6_TLV_PAD1:
+ case IPV6_TLV_PADN:
+ return -EINVAL;
+ default:
+ break;
+ }
+
+ if (len < 2)
+ return -EINVAL;
+
+ if (tlv[1] + 2 != len)
+ return -EINVAL;
+
+ rcu_read_lock();
+
+ tptx = tlv_deref_tx_params(tlv[0]);
+
+ 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 is current capabilities allows modify
+ * 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 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, opt, optname, true, admin);
+
+out:
+ txopt_put(old);
+ return retv;
+}
+EXPORT_SYMBOL(ipv6_opt_check_perm);
+
/* Destination options header */
#if IS_ENABLED(CONFIG_IPV6_MIP6)
@@ -400,11 +400,6 @@ static int do_ipv6_setsockopt(struct sock *sk, int level, int optname,
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;
-
/* remove any sticky options header with a zero option
* length, per RFC3542.
*/
@@ -427,6 +422,31 @@ static int do_ipv6_setsockopt(struct sock *sk, int level, int optname,
}
}
+ 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, sk, optname, cap);
+ if (retv < 0) {
+ kfree(new);
+ break;
+ }
+
+ /* Check permissions and other validations on new
+ * TLVs
+ */
+ if (new) {
+ retv = ipv6_opt_validate_tlvs(net, new,
+ optname, cap);
+ if (retv < 0) {
+ kfree(new);
+ break;
+ }
+ }
+ }
+
opt = rcu_dereference_protected(np->opt,
lockdep_sock_is_held(sk));
opt = ipv6_renew_options(sk, opt, optname, new);