iommu: fix double spin_lock_irqsave on `device_domain_lock'
PATCH 1/1 iommu: fix double spin_lock_irqsave on `device_domain_lock'
From: Iago Abal <mail-nS/[email protected]>
The EBA code analyzer (https://github.com/models-team/eba) reported the following double lock:
1. In function `disable_dmar_iommu' defined at 1706;
2. the lock `device_domain_lock' is first taken in line 1714:
// FIRST spin_lock_irqsave(&device_domain_lock, flags);
3. enter the `list_for_each_entry_safe' loop at 1715;
4. call function `dmar_remove_one_dev_info' (defined at 4851) in line 1726;
5. finally, the lock is taken a second time in line 4857:
// SECOND: DOUBLE LOCK !!! spin_lock_irqsave(&device_domain_lock, flags);
In addition, within that same loop, there is also a call to domain_exit', which calls to domain_remove_dev_info', which also spin_lock on `device_domain_lock'.
I fixed the potential deadlock by releasing the device_domain_lock' during the execution of the loop body. This seems to respect the locking assumptions made by the rest of the code: both dmar_remove_one_dev_info' and domain_exit' will (directly or indiretly) take that look, so they should not be called with it held. Function domain_type_is_vm_or_si' just checks `domain->flags' and there seem to be no concurrent writes to this field.
--- drivers/iommu/intel-iommu.c | 4 ++++ 1 file changed, 4 insertions(+)
drivers/iommu/intel-iommu.c +4 -0
@@ -1721,12 +1721,16 @@ static void disable_dmar_iommu(struct intel_iommu *iommu)if (!info->dev || !info->domain)continue ; +spin_unlock_irqrestore (&device_domain_lock, flags); + domain = info->domain;dmar_remove_one_dev_info (domain, info->dev);if (!domain_type_is_vm_or_si (domain))domain_exit (domain); + +spin_lock_irqsave (&device_domain_lock, flags); }spin_unlock_irqrestore (&device_domain_lock, flags);
Signature · 1 line
1.9.1
A message that is not in the archive (sent off-list or not yet received): 1476694480-4251-1-git-send-email-iari-YidNj35/[email protected]
Quoted 10 lines · Iago Abal
drivers/iommu/intel-iommu.c +2
-0@@ -1721,12 +1721,16 @@ static void disable_dmar_iommu(struct intel_iommu *iommu)if (!info->dev || !info->domain)continue ; +spin_unlock_irqrestore (&device_domain_lock, flags); +
drivers/iommu/intel-iommu.c static void disable_dmar_iommu(struct intel_iommu *iommu) · line 1735
Quoted 10 lines
drivers/iommu/intel-iommu.c +2
-0domain = info->domain;dmar_remove_one_dev_info (domain, info->dev);if (!domain_type_is_vm_or_si (domain))domain_exit (domain); + +spin_lock_irqsave (&device_domain_lock, flags); }spin_unlock_irqrestore (&device_domain_lock, flags);
No, you can't just release the lock to re-aquire it in dmar_remove_one_dev_info(). This introduces new races, as the list your are walking is no longer protected by the lock. The right solution is to call a variant of dmar_remove_one_dev_info() which does not take the lock. It turns out this function already exists, so the patch looks like below. Can you check if this is still correct and resubmit your patch?
drivers/iommu/intel-iommu.c +1 -1
@@ -1723,7 +1723,7 @@ static void disable_dmar_iommu(struct intel_iommu *iommu) domain = info->domain; -dmar_remove_one_dev_info (domain, info->dev); +__dmar_remove_one_dev_info (info);if (!domain_type_is_vm_or_si (domain))domain_exit (domain);
A message that is not in the archive (sent off-list or not yet received): [email protected]
Quoted 13 lines · Joerg Roedel
No, you can't just release the lock to re-aquire it in
dmar_remove_one_dev_info(). This introduces new races, as the list your are walking is no longer protected by the lock. The right solution is to call a variant ofdmar_remove_one_dev_info()which does not take the lock. It turns out this function already exists, so the patch looks like below. Can you check if this is still correct and resubmit your patch?drivers/iommu/intel-iommu.c +0
-0
drivers/iommu/intel-iommu.c static void disable_dmar_iommu(struct intel_iommu *iommu) · line 1729
Quoted 8 lines
drivers/iommu/intel-iommu.c +1
-1@@ -1723,7 +1723,7 @@ static void disable_dmar_iommu(struct intel_iommu *iommu) domain = info->domain; -dmar_remove_one_dev_info (domain, info->dev); +__dmar_remove_one_dev_info (info);if (!domain_type_is_vm_or_si (domain))domain_exit (domain);
That patch was actually my first attempt at fixing the problem, but I ran the tool and I found a second possibility of deadlock:domain_exit' calls to domain_remove_dev_info', which also spin_locks on `device_domain_lock'.
Alternatively I could add another `__domain_exit' function that doesn't take the lock.
Would that be fine?
-- iago
A message that is not in the archive (sent off-list or not yet received): CAGbDTvqDAEcLPNxHa6wvjTwpotDbZSSDL3MocVgj=LTMFG8Okw-JsoAwUIsXosN+BqQ9rBEUg@public.gmane.org
PATCH 1/1 iommu/vt-d: Fix dead-locks in disable_dmar_iommu() path
Quoted 7 lines · Iago Abal
That patch was actually my first attempt at fixing the problem, but I ran the tool and I found a second possibility of deadlock:
domain_exit' calls todomain_remove_dev_info', which alsospin_lockson `device_domain_lock'.Alternatively I could add another `__domain_exit' function that doesn't take the lock.
So that is actually not easy to do, I'd rather go with the simpler solution of dropping the lock for domain_exit() invocation and then re-start walking the list afterwards, like in the below patch:
Quoted 1 line
From
bea64033dd7b5fb6296eda8266acab6364ce1554Mon Sep 17 00:00:00 2001
From: Joerg Roedel <[email protected]>
Date: Tue, 8 Nov 2016 15:08:26 +0100
Subject: [PATCH] iommu/vt-d: Fix dead-locks in disable_dmar_iommu() path
It turns out that the disable_dmar_iommu() code-path tried to get the device_domain_lock recursivly, which will dead-lock when this code runs on dmar removal. Fix both code-paths that could lead to the dead-lock.
--- drivers/iommu/intel-iommu.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-)
drivers/iommu/intel-iommu.c +12 -2
@@ -1711,6 +1711,7 @@ static void disable_dmar_iommu(struct intel_iommu *iommu)if (!iommu->domains || !iommu->domain_ids)return ; +again:spin_lock_irqsave (&device_domain_lock, flags);list_for_each_entry_safe (info, tmp, &device_domain_list, global) {struct dmar_domain *domain;
@@ -1723,10 +1724,19 @@ static void disable_dmar_iommu(struct intel_iommu *iommu) domain = info->domain; -dmar_remove_one_dev_info (domain, info->dev); +__dmar_remove_one_dev_info (info); -if (!domain_type_is_vm_or_si (domain)) +if (!domain_type_is_vm_or_si (domain)) { +/* +* The domain_exit() function can't be called under +* device_domain_lock, as it takes this lock itself. +* So release the lock here and re-run the loop +* afterwards. +*/ +spin_unlock_irqrestore (&device_domain_lock, flags);domain_exit (domain); + goto again; + } }spin_unlock_irqrestore (&device_domain_lock, flags);
Signature · 1 line
2.6.6