CVE-2017-18232: Insufficient Information (NVD-CWE-noinfo)
value: The Serial Attached SCSI (SAS) implementation in the Linux kernel through 4.15.9 mishandles a mutex within libsas, which allows local users to cause a denial of service (deadlock) by triggering certain error-handling code.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.5
Published: 2018-03-15T04:29Z
Repository: https://github.com/torvalds/linux
Language: C
Files changed: drivers/scsi/libsas/sas_ata.c, drivers/scsi/libsas/sas_discover.c, drivers/scsi/libsas/sas_expander.c, drivers/scsi/libsas/sas_internal.h, drivers/scsi/libsas/sas_port.c, include/scsi/libsas.h, include/scsi/scsi_transport_sas.h
Commit date: 2018-01-10 23:24:02 -0500
Commit message: scsi: libsas: direct call probe and destruct

In commit 87c8331fcf72 ("[SCSI] libsas: prevent domain rediscovery
competing with ata error handling") introduced disco mutex to prevent
rediscovery competing with ata error handling and put the whole
revalidation in the mutex. But the rphy add/remove needs to wait for the
error handling which also grabs the disco mutex. This may leads to dead
lock.So the probe and destruct event were introduce to do the rphy
add/remove asynchronously and out of the lock.

The asynchronously processed workers makes the whole discovery process
not atomic, the other events may interrupt the process. For example,
if a loss of signal event inserted before the probe event, the
sas_deform_port() is called and the port will be deleted.

And sas_port_delete() may run before the destruct event, but the
port-x:x is the top parent of end device or expander. This leads to
a kernel WARNING such as:

[   82.042979] sysfs group 'power' not found for kobject 'phy-1:0:22'
[   82.042983] ------------[ cut here ]------------
[   82.042986] WARNING: CPU: 54 PID: 1714 at fs/sysfs/group.c:237
sysfs_remove_group+0x94/0xa0
[   82.043059] Call trace:
[   82.043082] [<ffff0000082e7624>] sysfs_remove_group+0x94/0xa0
[   82.043085] [<ffff00000864e320>] dpm_sysfs_remove+0x60/0x70
[   82.043086] [<ffff00000863ee10>] device_del+0x138/0x308
[   82.043089] [<ffff00000869a2d0>] sas_phy_delete+0x38/0x60
[   82.043091] [<ffff00000869a86c>] do_sas_phy_delete+0x6c/0x80
[   82.043093] [<ffff00000863dc20>] device_for_each_child+0x58/0xa0
[   82.043095] [<ffff000008696f80>] sas_remove_children+0x40/0x50
[   82.043100] [<ffff00000869d1bc>] sas_destruct_devices+0x64/0xa0
[   82.043102] [<ffff0000080e93bc>] process_one_work+0x1fc/0x4b0
[   82.043104] [<ffff0000080e96c0>] worker_thread+0x50/0x490
[   82.043105] [<ffff0000080f0364>] kthread+0xfc/0x128
[   82.043107] [<ffff0000080836c0>] ret_from_fork+0x10/0x50

Make probe and destruct a direct call in the disco and revalidate function,
but put them outside the lock. The whole discovery or revalidate won't
be interrupted by other events. And the DISCE_PROBE and DISCE_DESTRUCT
event are deleted as a result of the direct call.

Introduce a new list to destruct the sas_port and put the port delete after
the destruct. This makes sure the right order of destroying the sysfs
kobject and fix the warning above.

In sas_ex_revalidate_domain() have a loop to find all broadcasted
device, and sometimes we have a chance to find the same expander twice.
Because the sas_port will be deleted at the end of the whole revalidate
process, sas_port with the same name cannot be added before this.
Otherwise the sysfs will complain of creating duplicate filename. Since
the LLDD will send broadcast for every device change, we can only
process one expander's revalidation.

[mkp: kbuild test robot warning]

Signed-off-by: Jason Yan <yanaijie@huawei.com>
CC: John Garry <john.garry@huawei.com>
CC: Johannes Thumshirn <jthumshirn@suse.de>
CC: Ewan Milne <emilne@redhat.com>
CC: Christoph Hellwig <hch@lst.de>
CC: Tomas Henzl <thenzl@redhat.com>
CC: Dan Williams <dan.j.williams@intel.com>
Reviewed-by: Hannes Reinecke <hare@suse.com>
Signed-off-by: Martin K. Petersen <martin.petersen@oracle.com>

Diff: diff --git a/drivers/scsi/libsas/sas_ata.c b/drivers/scsi/libsas/sas_ata.c
index 70be4425ae0b..2b3637b40dde 100644
--- a/drivers/scsi/libsas/sas_ata.c
+++ b/drivers/scsi/libsas/sas_ata.c
@@ -728,11 +728,10 @@ int sas_discover_sata(struct domain_device *dev)
 
 	res = sas_notify_lldd_dev_found(dev);
 	if (res)
 		return res;
 
-	sas_discover_event(dev->port, DISCE_PROBE);
 	return 0;
 }
 
 static void async_sas_ata_eh(void *data, async_cookie_t cookie)
 {
diff --git a/drivers/scsi/libsas/sas_discover.c b/drivers/scsi/libsas/sas_discover.c
index 14f714d05767..e4fd078e4175 100644
--- a/drivers/scsi/libsas/sas_discover.c
+++ b/drivers/scsi/libsas/sas_discover.c
@@ -210,17 +210,13 @@ void sas_notify_lldd_dev_gone(struct domain_device *dev)
 		i->dft->lldd_dev_gone(dev);
 		sas_put_device(dev);
 	}
 }
 
-static void sas_probe_devices(struct work_struct *work)
+static void sas_probe_devices(struct asd_sas_port *port)
 {
 	struct domain_device *dev, *n;
-	struct sas_discovery_event *ev = to_sas_discovery_event(work);
-	struct asd_sas_port *port = ev->port;
-
-	clear_bit(DISCE_PROBE, &port->disc.pending);
 
 	/* devices must be domain members before link recovery and probe */
 	list_for_each_entry(dev, &port->disco_list, disco_list_node) {
 		spin_lock_irq(&port->dev_list_lock);
 		list_add_tail(&dev->dev_list_node, &port->dev_list);
@@ -292,11 +288,10 @@ int sas_discover_end_dev(struct domain_device *dev)
 	int res;
 
 	res = sas_notify_lldd_dev_found(dev);
 	if (res)
 		return res;
-	sas_discover_event(dev->port, DISCE_PROBE);
 
 	return 0;
 }
 
 /* ---------- Device registration and unregistration ---------- */
@@ -351,27 +346,33 @@ static void sas_unregister_common_dev(struct asd_sas_port *port, struct domain_d
 	spin_unlock_irq(&ha->lock);
 
 	sas_put_device(dev);
 }
 
-static void sas_destruct_devices(struct work_struct *work)
+void sas_destruct_devices(struct asd_sas_port *port)
 {
 	struct domain_device *dev, *n;
-	struct sas_discovery_event *ev = to_sas_discovery_event(work);
-	struct asd_sas_port *port = ev->port;
-
-	clear_bit(DISCE_DESTRUCT, &port->disc.pending);
 
 	list_for_each_entry_safe(dev, n, &port->destroy_list, disco_list_node) {
 		list_del_init(&dev->disco_list_node);
 
 		sas_remove_children(&dev->rphy->dev);
 		sas_rphy_delete(dev->rphy);
 		sas_unregister_common_dev(port, dev);
 	}
 }
 
+static void sas_destruct_ports(struct asd_sas_port *port)
+{
+	struct sas_port *sas_port, *p;
+
+	list_for_each_entry_safe(sas_port, p, &port->sas_port_del_list, del_list) {
+		list_del_init(&sas_port->del_list);
+		sas_port_delete(sas_port);
+	}
+}
+
 void sas_unregister_dev(struct asd_sas_port *port, struct domain_device *dev)
 {
 	if (!test_bit(SAS_DEV_DESTROY, &dev->state) &&
 	    !list_empty(&dev->disco_list_node)) {
 		/* this rphy never saw sas_rphy_add */
@@ -382,11 +383,10 @@ void sas_unregister_dev(struct asd_sas_port *port, struct domain_device *dev)
 	}
 
 	if (!test_and_set_bit(SAS_DEV_DESTROY, &dev->state)) {
 		sas_rphy_unlink(dev->rphy);
 		list_move_tail(&dev->disco_list_node, &port->destroy_list);
-		sas_discover_event(dev->port, DISCE_DESTRUCT);
 	}
 }
 
 void sas_unregister_domain_devices(struct asd_sas_port *port, int gone)
 {
@@ -488,10 +488,12 @@ static void sas_discover_domain(struct work_struct *work)
 
 		sas_put_device(dev);
 		port->port_dev = NULL;
 	}
 
+	sas_probe_devices(port);
+
 	SAS_DPRINTK("DONE DISCOVERY on port %d, pid:%d, result:%d\n", port->id,
 		    task_pid_nr(current), error);
 }
 
 static void sas_revalidate_domain(struct work_struct *work)
@@ -521,10 +523,14 @@ static void sas_revalidate_domain(struct work_struct *work)
 
 	SAS_DPRINTK("done REVALIDATING DOMAIN on port %d, pid:%d, res 0x%x\n",
 		    port->id, task_pid_nr(current), res);
  out:
 	mutex_unlock(&ha->disco_mutex);
+
+	sas_destruct_devices(port);
+	sas_destruct_ports(port);
+	sas_probe_devices(port);
 }
 
 /* ---------- Events ---------- */
 
 static void sas_chain_work(struct sas_ha_struct *ha, struct sas_work *sw)
@@ -576,14 +582,12 @@ void sas_init_disc(struct sas_discovery *disc, struct asd_sas_port *port)
 	int i;
 
 	static const work_func_t sas_event_fns[DISC_NUM_EVENTS] = {
 		[DISCE_DISCOVER_DOMAIN] = sas_discover_domain,
 		[DISCE_REVALIDATE_DOMAIN] = sas_revalidate_domain,
-		[DISCE_PROBE] = sas_probe_devices,
 		[DISCE_SUSPEND] = sas_suspend_devices,
 		[DISCE_RESUME] = sas_resume_devices,
-		[DISCE_DESTRUCT] = sas_destruct_devices,
 	};
 
 	disc->pending = 0;
 	for (i = 0; i < DISC_NUM_EVENTS; i++) {
 		INIT_SAS_WORK(&disc->disc_work[i].work, sas_event_fns[i]);
diff --git a/drivers/scsi/libsas/sas_expander.c b/drivers/scsi/libsas/sas_expander.c
index a8a57b0593e3..7444d40e261c 100644
--- a/drivers/scsi/libsas/sas_expander.c
+++ b/drivers/scsi/libsas/sas_expander.c
@@ -1914,11 +1914,12 @@ static void sas_unregister_devs_sas_addr(struct domain_device *parent,
 	memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE);
 	if (phy->port) {
 		sas_port_delete_phy(phy->port, phy->phy);
 		sas_device_set_phy(found, phy->port);
 		if (phy->port->num_phys == 0)
-			sas_port_delete(phy->port);
+			list_add_tail(&phy->port->del_list,
+				&parent->port->sas_port_del_list);
 		phy->port = NULL;
 	}
 }
 
 static int sas_discover_bfs_by_root_level(struct domain_device *root,
@@ -2122,11 +2123,11 @@ int sas_ex_revalidate_domain(struct domain_device *port_dev)
 {
 	int res;
 	struct domain_device *dev = NULL;
 
 	res = sas_find_bcast_dev(port_dev, &dev);
-	while (res == 0 && dev) {
+	if (res == 0 && dev) {
 		struct expander_device *ex = &dev->ex_dev;
 		int i = 0, phy_id;
 
 		do {
 			phy_id = -1;
@@ -2134,13 +2135,10 @@ int sas_ex_revalidate_domain(struct domain_device *port_dev)
 			if (phy_id == -1)
 				break;
 			res = sas_rediscover(dev, phy_id);
 			i = phy_id + 1;
 		} while (i < ex->num_phys);
-
-		dev = NULL;
-		res = sas_find_bcast_dev(port_dev, &dev);
 	}
 	return res;
 }
 
 void sas_smp_handler(struct bsg_job *job, struct Scsi_Host *shost,
diff --git a/drivers/scsi/libsas/sas_internal.h b/drivers/scsi/libsas/sas_internal.h
index d8826a747690..50e12d662ffe 100644
--- a/drivers/scsi/libsas/sas_internal.h
+++ b/drivers/scsi/libsas/sas_internal.h
@@ -99,10 +99,11 @@ int sas_get_report_phy_sata(struct domain_device *dev, int phy_id,
 			    struct smp_resp *rps_resp);
 int sas_try_ata_reset(struct asd_sas_phy *phy);
 void sas_hae_reset(struct work_struct *work);
 
 void sas_free_device(struct kref *kref);
+void sas_destruct_devices(struct asd_sas_port *port);
 
 extern const work_func_t sas_phy_event_fns[PHY_NUM_EVENTS];
 extern const work_func_t sas_port_event_fns[PORT_NUM_EVENTS];
 
 #ifdef CONFIG_SCSI_SAS_HOST_SMP
diff --git a/drivers/scsi/libsas/sas_port.c b/drivers/scsi/libsas/sas_port.c
index 64722f42b256..f07e55d3aa73 100644
--- a/drivers/scsi/libsas/sas_port.c
+++ b/drivers/scsi/libsas/sas_port.c
@@ -64,10 +64,11 @@ static void sas_resume_port(struct asd_sas_phy *phy)
 		int i, rc;
 
 		rc = sas_notify_lldd_dev_found(dev);
 		if (rc) {
 			sas_unregister_dev(port, dev);
+			sas_destruct_devices(port);
 			continue;
 		}
 
 		if (dev->dev_type == SAS_EDGE_EXPANDER_DEVICE || dev->dev_type == SAS_FANOUT_EXPANDER_DEVICE) {
 			dev->ex_dev.ex_change_count = -1;
@@ -218,10 +219,11 @@ void sas_deform_port(struct asd_sas_phy *phy, int gone)
 	if (dev)
 		dev->pathways--;
 
 	if (port->num_phys == 1) {
 		sas_unregister_domain_devices(port, gone);
+		sas_destruct_devices(port);
 		sas_port_delete(port->port);
 		port->port = NULL;
 	} else {
 		sas_port_delete_phy(port->port, phy->phy);
 		sas_device_set_phy(dev, port->port);
@@ -315,10 +317,11 @@ static void sas_init_port(struct asd_sas_port *port,
 	memset(port, 0, sizeof(*port));
 	port->id = i;
 	INIT_LIST_HEAD(&port->dev_list);
 	INIT_LIST_HEAD(&port->disco_list);
 	INIT_LIST_HEAD(&port->destroy_list);
+	INIT_LIST_HEAD(&port->sas_port_del_list);
 	spin_lock_init(&port->phy_list_lock);
 	INIT_LIST_HEAD(&port->phy_list);
 	port->ha = sas_ha;
 
 	spin_lock_init(&port->dev_list_lock);
diff --git a/include/scsi/libsas.h b/include/scsi/libsas.h
index 6255bb5ed1e4..1cab6f7af425 100644
--- a/include/scsi/libsas.h
+++ b/include/scsi/libsas.h
@@ -80,14 +80,12 @@ enum phy_event {
 };
 
 enum discover_event {
 	DISCE_DISCOVER_DOMAIN   = 0U,
 	DISCE_REVALIDATE_DOMAIN,
-	DISCE_PROBE,
 	DISCE_SUSPEND,
 	DISCE_RESUME,
-	DISCE_DESTRUCT,
 	DISC_NUM_EVENTS,
 };
 
 /* ---------- Expander Devices ---------- */
 
@@ -260,10 +258,11 @@ struct asd_sas_port {
 	struct domain_device *port_dev;
 	spinlock_t dev_list_lock;
 	struct list_head dev_list;
 	struct list_head disco_list;
 	struct list_head destroy_list;
+	struct list_head sas_port_del_list;
 	enum   sas_linkrate linkrate;
 
 	struct sas_work work;
 	int suspended;
 
diff --git a/include/scsi/scsi_transport_sas.h b/include/scsi/scsi_transport_sas.h
index 62895b405933..05ec927a3c72 100644
--- a/include/scsi/scsi_transport_sas.h
+++ b/include/scsi/scsi_transport_sas.h
@@ -154,10 +154,11 @@ struct sas_port {
 	/* the other end of the link */
 	struct sas_rphy		*rphy;
 
 	struct mutex		phy_list_mutex;
 	struct list_head	phy_list;
+	struct list_head	del_list; /* libsas only */
 };
 
 #define dev_to_sas_port(d) \
 	container_of((d), struct sas_port, dev)
 #define transport_class_to_sas_port(dev) \

Vulnerable code: 	sas_discover_event(dev->port, DISCE_PROBE);
static void sas_probe_devices(struct work_struct *work)
	struct sas_discovery_event *ev = to_sas_discovery_event(work);
	struct asd_sas_port *port = ev->port;

	clear_bit(DISCE_PROBE, &port->disc.pending);
	sas_discover_event(dev->port, DISCE_PROBE);
static void sas_destruct_devices(struct work_struct *work)
	struct sas_discovery_event *ev = to_sas_discovery_event(work);
	struct asd_sas_port *port = ev->port;

	clear_bit(DISCE_DESTRUCT, &port->disc.pending);
		sas_discover_event(dev->port, DISCE_DESTRUCT);
		[DISCE_PROBE] = sas_probe_devices,
		[DISCE_DESTRUCT] = sas_destruct_devices,
			sas_port_delete(phy->port);
	while (res == 0 && dev) {

		dev = NULL;
		res = sas_find_bcast_dev(port_dev, &dev);
	DISCE_PROBE,
	DISCE_DESTRUCT,
Fixed code: static void sas_probe_devices(struct asd_sas_port *port)
void sas_destruct_devices(struct asd_sas_port *port)
static void sas_destruct_ports(struct asd_sas_port *port)
{
	struct sas_port *sas_port, *p;

	list_for_each_entry_safe(sas_port, p, &port->sas_port_del_list, del_list) {
		list_del_init(&sas_port->del_list);
		sas_port_delete(sas_port);
	}
}

	sas_probe_devices(port);


	sas_destruct_devices(port);
	sas_destruct_ports(port);
	sas_probe_devices(port);
			list_add_tail(&phy->port->del_list,
				&parent->port->sas_port_del_list);
	if (res == 0 && dev) {
void sas_destruct_devices(struct asd_sas_port *port);
			sas_destruct_devices(port);
		sas_destruct_devices(port);
	INIT_LIST_HEAD(&port->sas_port_del_list);
	struct list_head sas_port_del_list;
	struct list_head	del_list; /* libsas only */