2
0
mirror of https://github.com/edk2-porting/linux-next.git synced 2024-12-24 05:04:00 +08:00
linux-next/net/smc/smc_pnet.c
Eric Biggers d49baa7e12 net/smc: check for missing nlattrs in SMC_PNETID messages
It's possible to crash the kernel in several different ways by sending
messages to the SMC_PNETID generic netlink family that are missing the
expected attributes:

- Missing SMC_PNETID_NAME => null pointer dereference when comparing
  names.
- Missing SMC_PNETID_ETHNAME => null pointer dereference accessing
  smc_pnetentry::ndev.
- Missing SMC_PNETID_IBNAME => null pointer dereference accessing
  smc_pnetentry::smcibdev.
- Missing SMC_PNETID_IBPORT => out of bounds array access to
  smc_ib_device::pattr[-1].

Fix it by validating that all expected attributes are present and that
SMC_PNETID_IBPORT is nonzero.

Reported-by: syzbot+5cd61039dc9b8bfa6e47@syzkaller.appspotmail.com
Fixes: 6812baabf2 ("smc: establish pnet table management")
Cc: <stable@vger.kernel.org> # v4.11+
Signed-off-by: Eric Biggers <ebiggers@google.com>
Signed-off-by: David S. Miller <davem@davemloft.net>
2018-05-14 16:40:45 -04:00

550 lines
13 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Shared Memory Communications over RDMA (SMC-R) and RoCE
*
* Generic netlink support functions to configure an SMC-R PNET table
*
* Copyright IBM Corp. 2016
*
* Author(s): Thomas Richter <tmricht@linux.vnet.ibm.com>
*/
#include <linux/module.h>
#include <linux/list.h>
#include <linux/ctype.h>
#include <net/netlink.h>
#include <net/genetlink.h>
#include <uapi/linux/if.h>
#include <uapi/linux/smc.h>
#include <rdma/ib_verbs.h>
#include "smc_pnet.h"
#include "smc_ib.h"
#define SMC_MAX_PNET_ID_LEN 16 /* Max. length of PNET id */
static struct nla_policy smc_pnet_policy[SMC_PNETID_MAX + 1] = {
[SMC_PNETID_NAME] = {
.type = NLA_NUL_STRING,
.len = SMC_MAX_PNET_ID_LEN - 1
},
[SMC_PNETID_ETHNAME] = {
.type = NLA_NUL_STRING,
.len = IFNAMSIZ - 1
},
[SMC_PNETID_IBNAME] = {
.type = NLA_NUL_STRING,
.len = IB_DEVICE_NAME_MAX - 1
},
[SMC_PNETID_IBPORT] = { .type = NLA_U8 }
};
static struct genl_family smc_pnet_nl_family;
/**
* struct smc_pnettable - SMC PNET table anchor
* @lock: Lock for list action
* @pnetlist: List of PNETIDs
*/
static struct smc_pnettable {
rwlock_t lock;
struct list_head pnetlist;
} smc_pnettable = {
.pnetlist = LIST_HEAD_INIT(smc_pnettable.pnetlist),
.lock = __RW_LOCK_UNLOCKED(smc_pnettable.lock)
};
/**
* struct smc_pnetentry - pnet identifier name entry
* @list: List node.
* @pnet_name: Pnet identifier name
* @ndev: pointer to network device.
* @smcibdev: Pointer to IB device.
*/
struct smc_pnetentry {
struct list_head list;
char pnet_name[SMC_MAX_PNET_ID_LEN + 1];
struct net_device *ndev;
struct smc_ib_device *smcibdev;
u8 ib_port;
};
/* Check if two RDMA device entries are identical. Use device name and port
* number for comparison.
*/
static bool smc_pnet_same_ibname(struct smc_pnetentry *pnetelem, char *ibname,
u8 ibport)
{
return pnetelem->ib_port == ibport &&
!strncmp(pnetelem->smcibdev->ibdev->name, ibname,
sizeof(pnetelem->smcibdev->ibdev->name));
}
/* Find a pnetid in the pnet table.
*/
static struct smc_pnetentry *smc_pnet_find_pnetid(char *pnet_name)
{
struct smc_pnetentry *pnetelem, *found_pnetelem = NULL;
read_lock(&smc_pnettable.lock);
list_for_each_entry(pnetelem, &smc_pnettable.pnetlist, list) {
if (!strncmp(pnetelem->pnet_name, pnet_name,
sizeof(pnetelem->pnet_name))) {
found_pnetelem = pnetelem;
break;
}
}
read_unlock(&smc_pnettable.lock);
return found_pnetelem;
}
/* Remove a pnetid from the pnet table.
*/
static int smc_pnet_remove_by_pnetid(char *pnet_name)
{
struct smc_pnetentry *pnetelem, *tmp_pe;
int rc = -ENOENT;
write_lock(&smc_pnettable.lock);
list_for_each_entry_safe(pnetelem, tmp_pe, &smc_pnettable.pnetlist,
list) {
if (!strncmp(pnetelem->pnet_name, pnet_name,
sizeof(pnetelem->pnet_name))) {
list_del(&pnetelem->list);
dev_put(pnetelem->ndev);
kfree(pnetelem);
rc = 0;
break;
}
}
write_unlock(&smc_pnettable.lock);
return rc;
}
/* Remove a pnet entry mentioning a given network device from the pnet table.
*/
static int smc_pnet_remove_by_ndev(struct net_device *ndev)
{
struct smc_pnetentry *pnetelem, *tmp_pe;
int rc = -ENOENT;
write_lock(&smc_pnettable.lock);
list_for_each_entry_safe(pnetelem, tmp_pe, &smc_pnettable.pnetlist,
list) {
if (pnetelem->ndev == ndev) {
list_del(&pnetelem->list);
dev_put(pnetelem->ndev);
kfree(pnetelem);
rc = 0;
break;
}
}
write_unlock(&smc_pnettable.lock);
return rc;
}
/* Remove a pnet entry mentioning a given ib device from the pnet table.
*/
int smc_pnet_remove_by_ibdev(struct smc_ib_device *ibdev)
{
struct smc_pnetentry *pnetelem, *tmp_pe;
int rc = -ENOENT;
write_lock(&smc_pnettable.lock);
list_for_each_entry_safe(pnetelem, tmp_pe, &smc_pnettable.pnetlist,
list) {
if (pnetelem->smcibdev == ibdev) {
list_del(&pnetelem->list);
dev_put(pnetelem->ndev);
kfree(pnetelem);
rc = 0;
break;
}
}
write_unlock(&smc_pnettable.lock);
return rc;
}
/* Append a pnetid to the end of the pnet table if not already on this list.
*/
static int smc_pnet_enter(struct smc_pnetentry *new_pnetelem)
{
struct smc_pnetentry *pnetelem;
int rc = -EEXIST;
write_lock(&smc_pnettable.lock);
list_for_each_entry(pnetelem, &smc_pnettable.pnetlist, list) {
if (!strncmp(pnetelem->pnet_name, new_pnetelem->pnet_name,
sizeof(new_pnetelem->pnet_name)) ||
!strncmp(pnetelem->ndev->name, new_pnetelem->ndev->name,
sizeof(new_pnetelem->ndev->name)) ||
smc_pnet_same_ibname(pnetelem,
new_pnetelem->smcibdev->ibdev->name,
new_pnetelem->ib_port)) {
dev_put(pnetelem->ndev);
goto found;
}
}
list_add_tail(&new_pnetelem->list, &smc_pnettable.pnetlist);
rc = 0;
found:
write_unlock(&smc_pnettable.lock);
return rc;
}
/* The limit for pnetid is 16 characters.
* Valid characters should be (single-byte character set) a-z, A-Z, 0-9.
* Lower case letters are converted to upper case.
* Interior blanks should not be used.
*/
static bool smc_pnetid_valid(const char *pnet_name, char *pnetid)
{
char *bf = skip_spaces(pnet_name);
size_t len = strlen(bf);
char *end = bf + len;
if (!len)
return false;
while (--end >= bf && isspace(*end))
;
if (end - bf >= SMC_MAX_PNET_ID_LEN)
return false;
while (bf <= end) {
if (!isalnum(*bf))
return false;
*pnetid++ = islower(*bf) ? toupper(*bf) : *bf;
bf++;
}
*pnetid = '\0';
return true;
}
/* Find an infiniband device by a given name. The device might not exist. */
static struct smc_ib_device *smc_pnet_find_ib(char *ib_name)
{
struct smc_ib_device *ibdev;
spin_lock(&smc_ib_devices.lock);
list_for_each_entry(ibdev, &smc_ib_devices.list, list) {
if (!strncmp(ibdev->ibdev->name, ib_name,
sizeof(ibdev->ibdev->name))) {
goto out;
}
}
ibdev = NULL;
out:
spin_unlock(&smc_ib_devices.lock);
return ibdev;
}
/* Parse the supplied netlink attributes and fill a pnetentry structure.
* For ethernet and infiniband device names verify that the devices exist.
*/
static int smc_pnet_fill_entry(struct net *net, struct smc_pnetentry *pnetelem,
struct nlattr *tb[])
{
char *string, *ibname;
int rc;
memset(pnetelem, 0, sizeof(*pnetelem));
INIT_LIST_HEAD(&pnetelem->list);
rc = -EINVAL;
if (!tb[SMC_PNETID_NAME])
goto error;
string = (char *)nla_data(tb[SMC_PNETID_NAME]);
if (!smc_pnetid_valid(string, pnetelem->pnet_name))
goto error;
rc = -EINVAL;
if (!tb[SMC_PNETID_ETHNAME])
goto error;
rc = -ENOENT;
string = (char *)nla_data(tb[SMC_PNETID_ETHNAME]);
pnetelem->ndev = dev_get_by_name(net, string);
if (!pnetelem->ndev)
goto error;
rc = -EINVAL;
if (!tb[SMC_PNETID_IBNAME])
goto error;
rc = -ENOENT;
ibname = (char *)nla_data(tb[SMC_PNETID_IBNAME]);
ibname = strim(ibname);
pnetelem->smcibdev = smc_pnet_find_ib(ibname);
if (!pnetelem->smcibdev)
goto error;
rc = -EINVAL;
if (!tb[SMC_PNETID_IBPORT])
goto error;
pnetelem->ib_port = nla_get_u8(tb[SMC_PNETID_IBPORT]);
if (pnetelem->ib_port < 1 || pnetelem->ib_port > SMC_MAX_PORTS)
goto error;
return 0;
error:
if (pnetelem->ndev)
dev_put(pnetelem->ndev);
return rc;
}
/* Convert an smc_pnetentry to a netlink attribute sequence */
static int smc_pnet_set_nla(struct sk_buff *msg, struct smc_pnetentry *pnetelem)
{
if (nla_put_string(msg, SMC_PNETID_NAME, pnetelem->pnet_name) ||
nla_put_string(msg, SMC_PNETID_ETHNAME, pnetelem->ndev->name) ||
nla_put_string(msg, SMC_PNETID_IBNAME,
pnetelem->smcibdev->ibdev->name) ||
nla_put_u8(msg, SMC_PNETID_IBPORT, pnetelem->ib_port))
return -1;
return 0;
}
/* Retrieve one PNETID entry */
static int smc_pnet_get(struct sk_buff *skb, struct genl_info *info)
{
struct smc_pnetentry *pnetelem;
struct sk_buff *msg;
void *hdr;
int rc;
if (!info->attrs[SMC_PNETID_NAME])
return -EINVAL;
pnetelem = smc_pnet_find_pnetid(
(char *)nla_data(info->attrs[SMC_PNETID_NAME]));
if (!pnetelem)
return -ENOENT;
msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
if (!msg)
return -ENOMEM;
hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
&smc_pnet_nl_family, 0, SMC_PNETID_GET);
if (!hdr) {
rc = -EMSGSIZE;
goto err_out;
}
if (smc_pnet_set_nla(msg, pnetelem)) {
rc = -ENOBUFS;
goto err_out;
}
genlmsg_end(msg, hdr);
return genlmsg_reply(msg, info);
err_out:
nlmsg_free(msg);
return rc;
}
static int smc_pnet_add(struct sk_buff *skb, struct genl_info *info)
{
struct net *net = genl_info_net(info);
struct smc_pnetentry *pnetelem;
int rc;
pnetelem = kzalloc(sizeof(*pnetelem), GFP_KERNEL);
if (!pnetelem)
return -ENOMEM;
rc = smc_pnet_fill_entry(net, pnetelem, info->attrs);
if (!rc)
rc = smc_pnet_enter(pnetelem);
if (rc) {
kfree(pnetelem);
return rc;
}
rc = smc_ib_remember_port_attr(pnetelem->smcibdev, pnetelem->ib_port);
if (rc)
smc_pnet_remove_by_pnetid(pnetelem->pnet_name);
return rc;
}
static int smc_pnet_del(struct sk_buff *skb, struct genl_info *info)
{
if (!info->attrs[SMC_PNETID_NAME])
return -EINVAL;
return smc_pnet_remove_by_pnetid(
(char *)nla_data(info->attrs[SMC_PNETID_NAME]));
}
static int smc_pnet_dump_start(struct netlink_callback *cb)
{
cb->args[0] = 0;
return 0;
}
static int smc_pnet_dumpinfo(struct sk_buff *skb,
u32 portid, u32 seq, u32 flags,
struct smc_pnetentry *pnetelem)
{
void *hdr;
hdr = genlmsg_put(skb, portid, seq, &smc_pnet_nl_family,
flags, SMC_PNETID_GET);
if (!hdr)
return -ENOMEM;
if (smc_pnet_set_nla(skb, pnetelem) < 0) {
genlmsg_cancel(skb, hdr);
return -EMSGSIZE;
}
genlmsg_end(skb, hdr);
return 0;
}
static int smc_pnet_dump(struct sk_buff *skb, struct netlink_callback *cb)
{
struct smc_pnetentry *pnetelem;
int idx = 0;
read_lock(&smc_pnettable.lock);
list_for_each_entry(pnetelem, &smc_pnettable.pnetlist, list) {
if (idx++ < cb->args[0])
continue;
if (smc_pnet_dumpinfo(skb, NETLINK_CB(cb->skb).portid,
cb->nlh->nlmsg_seq, NLM_F_MULTI,
pnetelem)) {
--idx;
break;
}
}
cb->args[0] = idx;
read_unlock(&smc_pnettable.lock);
return skb->len;
}
/* Remove and delete all pnetids from pnet table.
*/
static int smc_pnet_flush(struct sk_buff *skb, struct genl_info *info)
{
struct smc_pnetentry *pnetelem, *tmp_pe;
write_lock(&smc_pnettable.lock);
list_for_each_entry_safe(pnetelem, tmp_pe, &smc_pnettable.pnetlist,
list) {
list_del(&pnetelem->list);
dev_put(pnetelem->ndev);
kfree(pnetelem);
}
write_unlock(&smc_pnettable.lock);
return 0;
}
/* SMC_PNETID generic netlink operation definition */
static const struct genl_ops smc_pnet_ops[] = {
{
.cmd = SMC_PNETID_GET,
.flags = GENL_ADMIN_PERM,
.policy = smc_pnet_policy,
.doit = smc_pnet_get,
.dumpit = smc_pnet_dump,
.start = smc_pnet_dump_start
},
{
.cmd = SMC_PNETID_ADD,
.flags = GENL_ADMIN_PERM,
.policy = smc_pnet_policy,
.doit = smc_pnet_add
},
{
.cmd = SMC_PNETID_DEL,
.flags = GENL_ADMIN_PERM,
.policy = smc_pnet_policy,
.doit = smc_pnet_del
},
{
.cmd = SMC_PNETID_FLUSH,
.flags = GENL_ADMIN_PERM,
.policy = smc_pnet_policy,
.doit = smc_pnet_flush
}
};
/* SMC_PNETID family definition */
static struct genl_family smc_pnet_nl_family = {
.hdrsize = 0,
.name = SMCR_GENL_FAMILY_NAME,
.version = SMCR_GENL_FAMILY_VERSION,
.maxattr = SMC_PNETID_MAX,
.netnsok = true,
.module = THIS_MODULE,
.ops = smc_pnet_ops,
.n_ops = ARRAY_SIZE(smc_pnet_ops)
};
static int smc_pnet_netdev_event(struct notifier_block *this,
unsigned long event, void *ptr)
{
struct net_device *event_dev = netdev_notifier_info_to_dev(ptr);
switch (event) {
case NETDEV_REBOOT:
case NETDEV_UNREGISTER:
smc_pnet_remove_by_ndev(event_dev);
default:
break;
}
return NOTIFY_DONE;
}
static struct notifier_block smc_netdev_notifier = {
.notifier_call = smc_pnet_netdev_event
};
int __init smc_pnet_init(void)
{
int rc;
rc = genl_register_family(&smc_pnet_nl_family);
if (rc)
return rc;
rc = register_netdevice_notifier(&smc_netdev_notifier);
if (rc)
genl_unregister_family(&smc_pnet_nl_family);
return rc;
}
void smc_pnet_exit(void)
{
smc_pnet_flush(NULL, NULL);
unregister_netdevice_notifier(&smc_netdev_notifier);
genl_unregister_family(&smc_pnet_nl_family);
}
/* PNET table analysis for a given sock:
* determine ib_device and port belonging to used internal TCP socket
* ethernet interface.
*/
void smc_pnet_find_roce_resource(struct sock *sk,
struct smc_ib_device **smcibdev, u8 *ibport)
{
struct dst_entry *dst = sk_dst_get(sk);
struct smc_pnetentry *pnetelem;
*smcibdev = NULL;
*ibport = 0;
if (!dst)
return;
if (!dst->dev)
goto out_rel;
read_lock(&smc_pnettable.lock);
list_for_each_entry(pnetelem, &smc_pnettable.pnetlist, list) {
if (dst->dev == pnetelem->ndev) {
if (smc_ib_port_active(pnetelem->smcibdev,
pnetelem->ib_port)) {
*smcibdev = pnetelem->smcibdev;
*ibport = pnetelem->ib_port;
}
break;
}
}
read_unlock(&smc_pnettable.lock);
out_rel:
dst_release(dst);
}