diff --git a/include/net/ipv6.h b/include/net/ipv6.h
index 3d2995f..ec1a161 100644
--- a/include/net/ipv6.h
+++ b/include/net/ipv6.h
@@ -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
diff --git a/net/ipv6/datagram.c b/net/ipv6/datagram.c
index ee4a4e5..d029da0 100644
--- a/net/ipv6/datagram.c
+++ b/net/ipv6/datagram.c
@@ -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;
diff --git a/net/ipv6/exthdrs_core.c b/net/ipv6/exthdrs_core.c
index 09799bcd..2c721ee 100644
--- a/net/ipv6/exthdrs_core.c
+++ b/net/ipv6/exthdrs_core.c
@@ -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, },
diff --git a/net/ipv6/ipv6_sockglue.c b/net/ipv6/ipv6_sockglue.c
index 40f21fe..dcc7d49 100644
--- a/net/ipv6/ipv6_sockglue.c
+++ b/net/ipv6/ipv6_sockglue.c
@@ -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;
