mremap: fix race between mremap() and page cleanning
Subsystems (6)
PATCH 1/1 mremap: fix race between mremap() and page cleanning
Prior to 3.15, there was a race between zap_pte_range() and page_mkclean() where writes to a page could be lost. Dave Hansen discovered by inspection that there is a similar race between move_ptes() and page_mkclean().
We've been able to reproduce the issue by enlarging the race window with a msleep(), but have not been able to hit it without modifying the code. So, we think it's a real issue, but is difficult or impossible to hit in practice.
The zap_pte_range() issue is fixed by commit 1cf35d47712d("mm: split 'tlb_flush_mmu()' into tlb flushing and memory freeing parts"). And this patch is to fix the race between page_mkclean() and mremap().
Here is one possible way to hit the race: suppose a process mmapped a file with READ | WRITE and SHARED, it has two threads and they are bound to 2 different CPUs, e.g. CPU1 and CPU2. mmap returned X, then thread 1 did a write to addr X so that CPU1 now has a writable TLB for addr X on it. Thread 2 starts mremaping from addr X to Y while thread 1 cleaned the page and then did another write to the old addr X again. The 2nd write from thread 1 could succeed but the value will get lost.
thread 1 thread 2
(bound to CPU1) (bound to CPU2)
1: write 1 to addr X to get a
writeable TLB on this CPU
2: mremap starts
3: move_ptes emptied PTE for addr X
and setup new PTE for addr Y and then dropped PTL for X and Y
4: page laundering for N by doing
fadvise FADV_DONTNEED. When done,
pageframe N is deemed clean.
5: *write 2 to addr X
6: tlb flush for addr X
7: munmap (Y, pagesize) to make the
page unmapped
8: fadvise with FADV_DONTNEED again
to kick the page off the pagecache
9: pread the page from file to verify
the value. If 1 is there, it means
we have lost the written 2.
*the write may or may not cause segmentation fault, it depends on if the TLB is still on the CPU.
Please note that this is only one specific way of how the race could occur, it didn't mean that the race could only occur in exact the above config, e.g. more than 2 threads could be involved and fadvise() could be done in another thread, etc.
For anonymous pages, they could race between mremap() and page reclaim:
THP: a huge PMD is moved by mremap to a new huge PMD, then the new huge PMD gets unmapped/splitted/pagedout before the flush tlb happened for the old huge PMD in move_page_tables() and we could still write data to it. The normal anonymous page has similar situation.
To fix this, check for any dirty PTE in move_ptes()/move_huge_pmd() and if any, did the flush before dropping the PTL. If we did the flush for every move_ptes()/move_huge_pmd() call then we do not need to do the flush in move_pages_tables() for the whole range. But if we didn't, we still need to do the whole range flush.
Alternatively, we can track which part of the range is flushed in move_ptes()/move_huge_pmd() and which didn't to avoid flushing the whole range in move_page_tables(). But that would require multiple tlb flushes for the different sub-ranges and should be less efficient than the single whole range flush.
KBuild test on my Sandybridge desktop doesn't show any noticeable change. v4.9-rc4:
real 5m14.048s user 32m19.800s sys 4m50.320s
With this commit:
real 5m13.888s user 32m19.330s sys 4m51.200s
--- include/linux/huge_mm.h | 2 +- mm/huge_memory.c | 9 ++++++++- mm/mremap.c | 30 +++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 11 deletions(-)
include/linux/huge_mm.h +1 -1
@@ -22,7 +22,7 @@ extern int mincore_huge_pmd(struct vm_area_struct *vma, pmd_t *pmd, unsigned char *vec);extern bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr,unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd); +pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush);extern int change_huge_pmd (struct vm_area_struct *vma,pmd_t *pmd,unsigned long addr,pgprot_t newprot,int prot_numa);
mm/huge_memory.c +8 -1
@@ -1426,11 +1426,12 @@ int zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr,unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd) +pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush) {spinlock_t *old_ptl, *new_ptl;pmd_t pmd;struct mm_struct *mm = vma->vm_mm; +bool force_flush = false; if ((old_addr & ~HPAGE_PMD_MASK) || (new_addr & ~HPAGE_PMD_MASK) ||
@@ -1455,6 +1456,8 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr, new_ptl =pmd_lockptr (mm, new_pmd);if (new_ptl != old_ptl)spin_lock_nested (new_ptl,SINGLE_DEPTH_NESTING ); +if (pmd_present (*old_pmd) &&pmd_dirty (*old_pmd)) + force_flush = true; pmd =pmdp_huge_get_and_clear (mm, old_addr, old_pmd);VM_BUG_ON (!pmd_none (*new_pmd));
@@ -1467,6 +1470,10 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr,set_pmd_at (mm, new_addr, new_pmd,pmd_mksoft_dirty (pmd));if (new_ptl != old_ptl)spin_unlock (new_ptl); +if (force_flush) +flush_tlb_range (vma, old_addr, old_addr +PMD_SIZE ); +else + *need_flush = true;spin_unlock (old_ptl);return true; }
mm/mremap.c +21 -9
@@ -104,11 +104,13 @@ static pte_t move_soft_dirty_pte(pte_t pte)static void move_ptes (struct vm_area_struct *vma,pmd_t *old_pmd,unsigned long old_addr,unsigned long old_end,struct vm_area_struct *new_vma,pmd_t *new_pmd, -unsigned long new_addr,bool need_rmap_locks) +unsigned long new_addr,bool need_rmap_locks,bool *need_flush) {struct mm_struct *mm = vma->vm_mm;pte_t *old_pte, *new_pte, pte;spinlock_t *old_ptl, *new_ptl; +bool force_flush = false; +unsigned long len = old_end - old_addr;/* * When need_rmap_locks is true, we take the i_mmap_rwsem and anon_vma
@@ -146,6 +148,14 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd, new_pte++, new_addr += PAGE_SIZE) {if (pte_none (*old_pte))continue ; + +/* +* We are remapping a dirty PTE, make sure to +* flush TLB before we drop the PTL for the +* old PTE or we may race with page_mkclean(). +*/ +if (pte_present (*old_pte) &&pte_dirty (*old_pte)) + force_flush = true; pte =ptep_get_and_clear (mm, old_addr, old_pte); pte =move_pte (pte, new_vma->vm_page_prot, old_addr, new_addr); pte =move_soft_dirty_pte (pte);
@@ -156,6 +166,10 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,if (new_ptl != old_ptl)spin_unlock (new_ptl);pte_unmap (new_pte -1 ); +if (force_flush) +flush_tlb_range (vma, old_end - len, old_end); +else + *need_flush = true;pte_unmap_unlock (old_pte -1 , old_ptl);if (need_rmap_locks)drop_rmap_locks (vma);
@@ -201,13 +215,12 @@ unsigned long move_page_tables(struct vm_area_struct *vma,if (need_rmap_locks)take_rmap_locks (vma); moved =move_huge_pmd (vma, old_addr, new_addr, - old_end, old_pmd, new_pmd); + old_end, old_pmd, new_pmd, + &need_flush);if (need_rmap_locks)drop_rmap_locks (vma); -if (moved) { - need_flush = true; +if (moved)continue ; - } }split_huge_pmd (vma, old_pmd, old_addr);if (pmd_trans_unstable (old_pmd))
@@ -220,11 +233,10 @@ unsigned long move_page_tables(struct vm_area_struct *vma, extent = next - new_addr;if (extent >LATENCY_LIMIT ) extent =LATENCY_LIMIT ; -move_ptes (vma, old_pmd, old_addr, old_addr + extent, - new_vma, new_pmd, new_addr, need_rmap_locks); - need_flush = true; +move_ptes (vma, old_pmd, old_addr, old_addr + extent, new_vma, + new_pmd, new_addr, need_rmap_locks, &need_flush); } -if (likely (need_flush)) +if (need_flush)flush_tlb_range (vma, old_end-len, old_addr);mmu_notifier_invalidate_range_end (vma->vm_mm, mmun_start, mmun_end);
Signature · 7 lines
2.5.5 -- To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM, see: http://www.linux-mm.org/ . Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
+LKML.
Also attached the kernel patch that enlarges the race window and the user space test code raceremap.c, which you can put in will-it-scale's tests directory and run it as:
# ./raceremap_threads -t 2 -s 1
Make sure "cpu0 runs" appeared in the first line.
If you get SEGFAULT:
[aaron@aaronlu will-it-scale]$ sudo ./raceremap_threads -t 2 -s 1 cpu0 runs
cpu1 runs
cpu0: going to remap
testcase:mremap
warmup
cpu1: going to clean the page
cpu1: going to write 2
min:0 max:0 total:0
min:0 max:0 total:0
cpu0: remap done
Segmentation fault
That means the race doesn't occur.
If you get "*cpu1 wrote 2 gets lost":
[aaron@aaronlu will-it-scale]$ sudo ./raceremap_threads -t 2 -s 1 cpu1 runs
testcase:mremap
warmup
cpu0 runs
cpu0: going to remap
cpu1: going to clean the page
cpu1: going to write 2
cpu1 wrote 2
min:0 max:0 total:0
min:0 max:0 total:0
cpu0: remap done
*cpu1 wrote 2 gets lost
That means the race occurred.
Thanks,
Aaron
Quoted 228 lines · Aaron Lu
Prior to 3.15, there was a race between
zap_pte_range()andpage_mkclean()where writes to a page could be lost. Dave Hansen discovered by inspection that there is a similar race betweenmove_ptes()andpage_mkclean().We've been able to reproduce the issue by enlarging the race window with a
msleep(), but have not been able to hit it without modifying the code. So, we think it's a real issue, but is difficult or impossible to hit in practice.The
zap_pte_range()issue is fixed by commit 1cf35d47712d("mm: split 'tlb_flush_mmu()' into tlb flushing and memory freeing parts"). And this patch is to fix the race betweenpage_mkclean()andmremap().Here is one possible way to hit the race: suppose a process mmapped a file with READ | WRITE and SHARED, it has two threads and they are bound to 2 different CPUs, e.g. CPU1 and CPU2. mmap returned X, then thread 1 did a write to addr X so that CPU1 now has a writable TLB for addr X on it. Thread 2 starts mremaping from addr X to Y while thread 1 cleaned the page and then did another write to the old addr X again. The 2nd write from thread 1 could succeed but the value will get lost.
thread 1 thread 2
(bound to CPU1) (bound to CPU2)1: write 1 to addr X to get a
writeable TLB on this CPU2: mremap starts
3:
move_ptesemptied PTE for addr X
and setup new PTE for addr Y and then dropped PTL for X and Y4: page laundering for N by doing
fadviseFADV_DONTNEED. When done,
pageframe N is deemed clean.5: *write 2 to addr X
6: tlb flush for addr X
7: munmap (Y, pagesize) to make the
page unmapped8: fadvise with
FADV_DONTNEEDagain
to kick the page off the pagecache9: pread the page from file to verify
the value. If 1 is there, it means
we have lost the written 2.*the write may or may not cause segmentation fault, it depends on if the TLB is still on the CPU.
Please note that this is only one specific way of how the race could occur, it didn't mean that the race could only occur in exact the above config, e.g. more than 2 threads could be involved and
fadvise()could be done in another thread, etc.For anonymous pages, they could race between
mremap()and page reclaim:
THP: a huge PMD is moved by mremap to a new huge PMD, then the new huge PMD gets unmapped/splitted/pagedout before the flush tlb happened for the old huge PMD inmove_page_tables()and we could still write data to it. The normal anonymous page has similar situation.To fix this, check for any dirty PTE in move_ptes()/move_huge_pmd() and if any, did the flush before dropping the PTL. If we did the flush for every move_ptes()/move_huge_pmd() call then we do not need to do the flush in
move_pages_tables()for the whole range. But if we didn't, we still need to do the whole range flush.Alternatively, we can track which part of the range is flushed in move_ptes()/move_huge_pmd() and which didn't to avoid flushing the whole range in
move_page_tables(). But that would require multiple tlb flushes for the different sub-ranges and should be less efficient than the single whole range flush.KBuild test on my Sandybridge desktop doesn't show any noticeable change. v4.9-rc4:
real 5m14.048s user 32m19.800s sys 4m50.320sWith this commit:
real 5m13.888s user 32m19.330s sys 4m51.200sReported-by Dave Hansen <[email protected]>Signed-off-by Aaron Lu <[email protected]>--- include/linux/huge_mm.h | 2 +- mm/huge_memory.c | 9 ++++++++- mm/mremap.c | 30 +++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 11 deletions(-)include/linux/huge_mm.h +1
-1@@ -22,7 +22,7 @@ extern int mincore_huge_pmd(struct vm_area_struct *vma, pmd_t *pmd, unsigned char *vec);extern bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr,unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd); +pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush);extern int change_huge_pmd (struct vm_area_struct *vma,pmd_t *pmd,unsigned long addr,pgprot_t newprot,int prot_numa);mm/huge_memory.c +8
-1@@ -1426,11 +1426,12 @@ int zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr,unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd) +pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush) {spinlock_t *old_ptl, *new_ptl;pmd_t pmd;struct mm_struct *mm = vma->vm_mm; +bool force_flush = false; if ((old_addr & ~HPAGE_PMD_MASK) || (new_addr & ~HPAGE_PMD_MASK) ||@@ -1455,6 +1456,8 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr, new_ptl =pmd_lockptr (mm, new_pmd);if (new_ptl != old_ptl)spin_lock_nested (new_ptl,SINGLE_DEPTH_NESTING ); +if (pmd_present (*old_pmd) &&pmd_dirty (*old_pmd)) + force_flush = true; pmd =pmdp_huge_get_and_clear (mm, old_addr, old_pmd);VM_BUG_ON (!pmd_none (*new_pmd));@@ -1467,6 +1470,10 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr,set_pmd_at (mm, new_addr, new_pmd,pmd_mksoft_dirty (pmd));if (new_ptl != old_ptl)spin_unlock (new_ptl); +if (force_flush) +flush_tlb_range (vma, old_addr, old_addr +PMD_SIZE ); +else + *need_flush = true;spin_unlock (old_ptl);return true; }mm/mremap.c +21
-9@@ -104,11 +104,13 @@ static pte_t move_soft_dirty_pte(pte_t pte)static void move_ptes (struct vm_area_struct *vma,pmd_t *old_pmd,unsigned long old_addr,unsigned long old_end,struct vm_area_struct *new_vma,pmd_t *new_pmd, -unsigned long new_addr,bool need_rmap_locks) +unsigned long new_addr,bool need_rmap_locks,bool *need_flush) {struct mm_struct *mm = vma->vm_mm;pte_t *old_pte, *new_pte, pte;spinlock_t *old_ptl, *new_ptl; +bool force_flush = false; +unsigned long len = old_end - old_addr;/* * When need_rmap_locks is true, we take the i_mmap_rwsem and anon_vma @@ -146,6 +148,14 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd, new_pte++, new_addr += PAGE_SIZE) {if (pte_none (*old_pte))continue ; + +/* +* We are remapping a dirty PTE, make sure to +* flush TLB before we drop the PTL for the +* old PTE or we may race with page_mkclean(). +*/ +if (pte_present (*old_pte) &&pte_dirty (*old_pte)) + force_flush = true; pte =ptep_get_and_clear (mm, old_addr, old_pte); pte =move_pte (pte, new_vma->vm_page_prot, old_addr, new_addr); pte =move_soft_dirty_pte (pte);@@ -156,6 +166,10 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,if (new_ptl != old_ptl)spin_unlock (new_ptl);pte_unmap (new_pte -1 ); +if (force_flush) +flush_tlb_range (vma, old_end - len, old_end); +else + *need_flush = true;pte_unmap_unlock (old_pte -1 , old_ptl);if (need_rmap_locks)drop_rmap_locks (vma);@@ -201,13 +215,12 @@ unsigned long move_page_tables(struct vm_area_struct *vma,if (need_rmap_locks)take_rmap_locks (vma); moved =move_huge_pmd (vma, old_addr, new_addr, - old_end, old_pmd, new_pmd); + old_end, old_pmd, new_pmd, + &need_flush);if (need_rmap_locks)drop_rmap_locks (vma); -if (moved) { - need_flush = true; +if (moved)continue ; - } }split_huge_pmd (vma, old_pmd, old_addr);if (pmd_trans_unstable (old_pmd))@@ -220,11 +233,10 @@ unsigned long move_page_tables(struct vm_area_struct *vma, extent = next - new_addr;if (extent >LATENCY_LIMIT ) extent =LATENCY_LIMIT ; -move_ptes (vma, old_pmd, old_addr, old_addr + extent, - new_vma, new_pmd, new_addr, need_rmap_locks); - need_flush = true; +move_ptes (vma, old_pmd, old_addr, old_addr + extent, new_vma, + new_pmd, new_addr, need_rmap_locks, &need_flush); } -if (likely (need_flush)) +if (need_flush)flush_tlb_range (vma, old_end-len, old_addr);mmu_notifier_invalidate_range_end (vma->vm_mm, mmun_start, mmun_end);Signature · 1 line
2.5.5
Quoted 4 lines · Aaron Lu
Prior to 3.15, there was a race between
zap_pte_range()andpage_mkclean()where writes to a page could be lost. Dave Hansen discovered by inspection that there is a similar race betweenmove_ptes()andpage_mkclean().
Ok, patch applied.
I'm not entirely happy with the force_flush vs need_flush games, and I really think this code should be updated to use the same "struct mmu_gather" that we use for the other TLB flushing cases (no need for the page freeing batching, but the tlb_flush_mmu_tlbonly() logic should be the same).
But I guess that's a bigger change, so that wouldn't be approriate for rc5 or stable back-porting anyway. But it would be lovely if somebody could look at that. Hint hint.
Hmm?
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Quoted 6 lines · Linus Torvalds
Quoted 4 lines · Aaron Lu
Prior to 3.15, there was a race between
zap_pte_range()andpage_mkclean()where writes to a page could be lost. Dave Hansen discovered by inspection that there is a similar race betweenmove_ptes()andpage_mkclean().
Quoted 7 lines
Ok, patch applied.
I'm not entirely happy with the
force_flushvsneed_flushgames, and I really think this code should be updated to use the same "structmmu_gather" that we use for the other TLB flushing cases (no need for the page freeing batching, but thetlb_flush_mmu_tlbonly()logic should be the same).
I see.
Quoted 4 lines
But I guess that's a bigger change, so that wouldn't be approriate for rc5 or stable back-porting anyway. But it would be lovely if somebody could look at that. Hint hint.
I'll work on it, thanks for the suggestion.
Regards,
Aaron
Quoted 11 lines
Hmm?
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
cover use mmu gather logic for tlb flush in mremap
Quoted 7 lines · Aaron Lu
Quoted 5 lines · Linus Torvalds
I'm not entirely happy with the
force_flushvsneed_flushgames, and I really think this code should be updated to use the same "structmmu_gather" that we use for the other TLB flushing cases (no need for the page freeing batching, but thetlb_flush_mmu_tlbonly()logic should be the same).
Quoted 8 lines
I see.
Quoted 4 lines
But I guess that's a bigger change, so that wouldn't be approriate for rc5 or stable back-porting anyway. But it would be lovely if somebody could look at that. Hint hint.
I'll work on it, thanks for the suggestion.
So here it is. I'm not quite sure if I've done the right thing in patch 2/2, i.e. should I just use tlb_flush_mmu or export tlb_flush_mmu_tlbonly and then use it in mremap.c. Please take a look and let me know what you think, thanks!
Regards,
Aaron
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
PATCH 1/2 tlb: export tlb_flush_mmu_tlbonly
The mmu gather logic for tlb flush will be used in mremap case so export this function.
--- include/asm-generic/tlb.h | 1 + mm/memory.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-)
include/asm-generic/tlb.h +1 -0
@@ -119,6 +119,7 @@ struct mmu_gather {void tlb_gather_mmu (struct mmu_gather *tlb,struct mm_struct *mm,unsigned long start,unsigned long end);void tlb_flush_mmu (struct mmu_gather *tlb); +void tlb_flush_mmu_tlbonly (struct mmu_gather *tlb);void tlb_finish_mmu (struct mmu_gather *tlb,unsigned long start,unsigned long end); extern bool __tlb_remove_page_size(struct mmu_gather *tlb, struct page *page,
mm/memory.c +1 -1
@@ -238,7 +238,7 @@ void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm, unsigned long__tlb_reset_range (tlb); } -static void tlb_flush_mmu_tlbonly (struct mmu_gather *tlb) +void tlb_flush_mmu_tlbonly (struct mmu_gather *tlb) {if (!tlb->end)return ;
Signature · 7 lines
2.5.5 -- To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM, see: http://www.linux-mm.org/ . Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
PATCH 2/2 mremap: use mmu gather logic for tlb flush in mremap
In reply to Aaron LuAs suggested by Linus, the same mmu gather logic could be used for tlb flush in mremap and this patch just did that.
Note that since there is no page added to "struct mmu_gather" for free during mremap, when tlb needs to be flushed, we can use tlb_flush_mmu or tlb_flush_mmu_tlbonly. Using tlb_flush_mmu could also avoid exporting tlb_flush_mmu_tlbonly. But tlb_flush_mmu_tlbonly *looks* more clear and straightforward so I end up using it.
--- include/linux/huge_mm.h | 6 +++--- mm/huge_memory.c | 13 +++++++------ mm/mremap.c | 30 +++++++++++++++--------------- 3 files changed, 25 insertions(+), 24 deletions(-)
include/linux/huge_mm.h +3 -3
@@ -20,9 +20,9 @@ extern int zap_huge_pmd(struct mmu_gather *tlb,extern int mincore_huge_pmd (struct vm_area_struct *vma,pmd_t *pmd,unsigned long addr,unsigned long end,unsigned char *vec); -extern bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr, -unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush); +extern bool move_huge_pmd (struct mmu_gather *tlb,struct vm_area_struct *vma, +unsigned long old_addr,unsigned long new_addr, +unsigned long old_end,pmd_t *old_pmd,pmd_t *new_pmd);extern int change_huge_pmd (struct vm_area_struct *vma,pmd_t *pmd,unsigned long addr,pgprot_t newprot,int prot_numa);
mm/huge_memory.c +7 -6
@@ -1424,9 +1424,9 @@ int zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,return 1 ; } -bool move_huge_pmd (struct vm_area_struct *vma,unsigned long old_addr, -unsigned long new_addr,unsigned long old_end, -pmd_t *old_pmd,pmd_t *new_pmd,bool *need_flush) +bool move_huge_pmd (struct mmu_gather *tlb,struct vm_area_struct *vma, +unsigned long old_addr,unsigned long new_addr, +unsigned long old_end,pmd_t *old_pmd,pmd_t *new_pmd) {spinlock_t *old_ptl, *new_ptl;pmd_t pmd;
@@ -1456,8 +1456,11 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr, new_ptl =pmd_lockptr (mm, new_pmd);if (new_ptl != old_ptl)spin_lock_nested (new_ptl,SINGLE_DEPTH_NESTING ); + +tlb_remove_pmd_tlb_entry (tlb, old_pmd, old_addr);if (pmd_present (*old_pmd) &&pmd_dirty (*old_pmd)) force_flush = true; + pmd =pmdp_huge_get_and_clear (mm, old_addr, old_pmd);VM_BUG_ON (!pmd_none (*new_pmd));
@@ -1471,9 +1474,7 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr,if (new_ptl != old_ptl)spin_unlock (new_ptl);if (force_flush) -flush_tlb_range (vma, old_addr, old_addr +PMD_SIZE ); -else - *need_flush = true; +tlb_flush_mmu_tlbonly (tlb);spin_unlock (old_ptl);return true; }
mm/mremap.c +15 -15
@@ -25,6 +25,7 @@#include <asm/cacheflush.h> #include <asm/tlbflush.h> +#include <asm/tlb.h> #include "internal.h"
@@ -101,16 +102,15 @@ static pte_t move_soft_dirty_pte(pte_t pte)return pte; } -static void move_ptes (struct vm_area_struct *vma,pmd_t *old_pmd, -unsigned long old_addr,unsigned long old_end, +static void move_ptes (struct mmu_gather *tlb,struct vm_area_struct *vma, +pmd_t *old_pmd,unsigned long old_addr,unsigned long old_end,struct vm_area_struct *new_vma,pmd_t *new_pmd, -unsigned long new_addr,bool need_rmap_locks,bool *need_flush) +unsigned long new_addr,bool need_rmap_locks) {struct mm_struct *mm = vma->vm_mm;pte_t *old_pte, *new_pte, pte;spinlock_t *old_ptl, *new_ptl;bool force_flush = false; -unsigned long len = old_end - old_addr;/* * When need_rmap_locks is true, we take the i_mmap_rwsem and anon_vma
@@ -149,6 +149,7 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,if (pte_none (*old_pte))continue ; +tlb_remove_tlb_entry (tlb, old_pte, old_addr);/* * We are remapping a dirty PTE, make sure to * flush TLB before we drop the PTL for the
@@ -166,10 +167,9 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,if (new_ptl != old_ptl)spin_unlock (new_ptl);pte_unmap (new_pte -1 ); +if (force_flush) -flush_tlb_range (vma, old_end - len, old_end); -else - *need_flush = true; +tlb_flush_mmu_tlbonly (tlb);pte_unmap_unlock (old_pte -1 , old_ptl);if (need_rmap_locks)drop_rmap_locks (vma);
@@ -184,15 +184,16 @@ unsigned long move_page_tables(struct vm_area_struct *vma, {unsigned long extent, next, old_end;pmd_t *old_pmd, *new_pmd; -bool need_flush = false;unsigned long mmun_start;/* For mmu_notifiers */ unsigned long mmun_end;/* For mmu_notifiers */ +struct mmu_gather tlb; old_end = old_addr + len;flush_cache_range (vma, old_addr, old_end); mmun_start = old_addr; mmun_end = old_end; +tlb_gather_mmu (&tlb, vma->vm_mm, mmun_start, mmun_end);mmu_notifier_invalidate_range_start (vma->vm_mm, mmun_start, mmun_end);for (; old_addr < old_end; old_addr += extent, new_addr += extent) {
@@ -214,9 +215,9 @@ unsigned long move_page_tables(struct vm_area_struct *vma,/* See comment in move_ptes() */ if (need_rmap_locks)take_rmap_locks (vma); - moved =move_huge_pmd (vma, old_addr, new_addr, - old_end, old_pmd, new_pmd, - &need_flush); + moved =move_huge_pmd (&tlb, vma, old_addr, + new_addr, old_end, + old_pmd, new_pmd);if (need_rmap_locks)drop_rmap_locks (vma);if (moved)
@@ -233,13 +234,12 @@ unsigned long move_page_tables(struct vm_area_struct *vma, extent = next - new_addr;if (extent >LATENCY_LIMIT ) extent =LATENCY_LIMIT ; -move_ptes (vma, old_pmd, old_addr, old_addr + extent, new_vma, - new_pmd, new_addr, need_rmap_locks, &need_flush); +move_ptes (&tlb, vma, old_pmd, old_addr, old_addr + extent, + new_vma, new_pmd, new_addr, need_rmap_locks); } -if (need_flush) -flush_tlb_range (vma, old_end-len, old_addr);mmu_notifier_invalidate_range_end (vma->vm_mm, mmun_start, mmun_end); +tlb_finish_mmu (&tlb, mmun_start, mmun_end);return len + old_addr - old_end;/* how much done */ }
Signature · 7 lines
2.5.5 -- To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM, see: http://www.linux-mm.org/ . Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Re: [PATCH 2/2] mremap: use mmu gather logic for tlb flush in mremap
Quoted 2 lines · Aaron Lu
As suggested by Linus, the same mmu gather logic could be used for tlb flush in mremap and this patch just did that.
Ok, looking at this patch, I still think it looks like the right thing to do, but I'm admittedly rather less certain of it.
The main advantage of the mmu_gather thing is that it automatically takes care of the TLB flush ranges for us, and that's a big deal during munmap() (where the actual unmapped page range can be _very_ different from the total range), but now that I notice that this doesn't actually remove any other code (in fact, it adds a line), I'm wondering if it's worth it. mremap() is already "dense" in the vma space, unlike munmap (ie you can't move multiple vma's with a single mremap), so the fancy range optimizations that make a difference on some architectures aren't much of an issue.
So I guess the MM people should take a look at this and say whether they think the current state is fine or whether we should do the mmu_gather thing. People?
However, I also independently think I found an actual bug while looking at the code as part of looking at the patch.
This part looks racy:
/*
* We are remapping a dirty PTE, make sure to
* flush TLB before we drop the PTL for the
* old PTE or we may race with page_mkclean().
*/
if (pte_present(*old_pte) && pte_dirty(*old_pte))
force_flush = true;
pte = ptep_get_and_clear(mm, old_addr, old_pte);where the issue is that another thread might make the pte be dirty (in the hardware walker, so no locking of ours make any difference) *after* we checked whether it was dirty, but *before* we removed it from the page tables.
So I think the "check for force-flush" needs to come *after*, and we should do
pte = ptep_get_and_clear(mm, old_addr, old_pte);
if (pte_present(pte) && pte_dirty(pte))
force_flush = true;instead.
This happens for the pmd case too.
So now I'm not sure the mmu_gather thing is worth it, but I'm pretty sure that there remains a (very very) small race that wasn't fixed by the original fix in commit 5d1904204c99 ("mremap: fix race between mremap() and page cleanning").
Aaron, sorry for waffling about this, and asking you to look at a completely different issue instead.
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
PATCH 1/1 mremap: move_ptes: check pte dirty after its removal
Quoted 14 lines · Linus Torvalds
However, I also independently think I found an actual bug while looking at the code as part of looking at the patch.
This part looks racy:
/* * We are remapping a dirty PTE, make sure to * flush TLB before we drop the PTL for the * old PTE or we may race with page_mkclean(). */ if (pte_present(*old_pte) && pte_dirty(*old_pte)) force_flush = true; pte = ptep_get_and_clear(mm, old_addr, old_pte);
Quoted 4 lines
where the issue is that another thread might make the pte be dirty (in the hardware walker, so no locking of ours make any difference) *after* we checked whether it was dirty, but *before* we removed it from the page tables.
Ah, very right. Thanks for the catch!
Quoted 10 lines
So I think the "check for force-flush" needs to come *after*, and we should do
pte = ptep_get_and_clear(mm, old_addr, old_pte); if (pte_present(pte) && pte_dirty(pte)) force_flush = true;instead.
This happens for the pmd case too.
Here is a fix patch, sorry for the trouble.
Re: [PATCH] mremap: move_ptes: check pte dirty after its removal
Quoted 2 lines · Aaron Lu
Here is a fix patch, sorry for the trouble.
I don't think you tested this one.. You've now essentially reverted 5d1904204c99 entirely by making the new force_flush logic a no-op.
Quoted 3 lines
+ pmd = pmdp_huge_get_and_clear(mm, old_addr, old_pmd);
if (pmd_present(*old_pmd) && pmd_dirty(*old_pmd))
force_flush = true;You need to be testing "pmd", not "*old_pmd".
Because now "*old_pmd" will be zeroes.
Quoted 2 lines
if (pte_present(*old_pte) && pte_dirty(*old_pte))
force_flush = true;Similarly here. You need to check "pte", not "*old_pte".
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Re: [PATCH] mremap: move_ptes: check pte dirty after its removal
Quoted 6 lines · Linus Torvalds
Quoted 2 lines · Aaron Lu
Here is a fix patch, sorry for the trouble.
I don't think you tested this one.. You've now essentially reverted
5d1904204c99entirely by making the newforce_flushlogic a no-op.
Right, I just did a build test.
Now I'm doing more tests, sorry for being careless.
Regards,
Aaron
Quoted 14 lines
Quoted 4 lines
+ pmd = pmdp_huge_get_and_clear(mm, old_addr, old_pmd); if (pmd_present(*old_pmd) && pmd_dirty(*old_pmd)) force_flush = true;You need to be testing "pmd", not "
*old_pmd".Because now "
*old_pmd" will be zeroes.Quoted 2 lines
if (pte_present(*old_pte) && pte_dirty(*old_pte)) force_flush = true;Similarly here. You need to check "pte", not "
*old_pte".
Quoted 7 lines
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
PATCH 1/1 mremap: move_ptes: check pte dirty after its removal
In reply to Linus TorvaldsLinus found there still is a race in mremap after commit 5d1904204c99 ("mremap: fix race between mremap() and page cleanning").
As described by Linus:
the issue is that another thread might make the pte be dirty (in the hardware walker, so no locking of ours make any difference) *after* we checked whether it was dirty, but *before* we removed it from the page tables.
Fix it by moving the check after we removed it from the page table.
--- mm/huge_memory.c | 4 ++-- mm/mremap.c | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-)
mm/huge_memory.c +2 -2
@@ -1456,9 +1456,9 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr, new_ptl =pmd_lockptr (mm, new_pmd);if (new_ptl != old_ptl)spin_lock_nested (new_ptl,SINGLE_DEPTH_NESTING ); -if (pmd_present (*old_pmd) &&pmd_dirty (*old_pmd)) - force_flush = true; pmd =pmdp_huge_get_and_clear (mm, old_addr, old_pmd); +if (pmd_present (pmd) &&pmd_dirty (pmd)) + force_flush = true;VM_BUG_ON (!pmd_none (*new_pmd)); if (pmd_move_must_withdraw(new_ptl, old_ptl) &&
mm/mremap.c +6 -2
@@ -149,14 +149,18 @@ static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,if (pte_none (*old_pte))continue ; + pte =ptep_get_and_clear (mm, old_addr, old_pte);/* * We are remapping a dirty PTE, make sure to * flush TLB before we drop the PTL for the * old PTE or we may race with page_mkclean(). +* +* This check has to be done after we removed the +* old PTE from page tables or another thread may +* dirty it after the check and before the removal. */ -if (pte_present (*old_pte) &&pte_dirty (*old_pte)) +if (pte_present (pte) &&pte_dirty (pte)) force_flush = true; - pte =ptep_get_and_clear (mm, old_addr, old_pte); pte =move_pte (pte, new_vma->vm_page_prot, old_addr, new_addr); pte =move_soft_dirty_pte (pte);set_pte_at (mm, new_addr, new_pte, pte);
Signature · 7 lines
2.5.5 -- To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM, see: http://www.linux-mm.org/ . Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Re: [PATCH 2/2] mremap: use mmu gather logic for tlb flush in mremap
In reply to Aaron LuQuoted 8 lines · Aaron Lu
As suggested by Linus, the same mmu gather logic could be used for tlb flush in mremap and this patch just did that.
Note that since there is no page added to "struct
mmu_gather" for free during mremap, when tlb needs to be flushed, we can usetlb_flush_mmuortlb_flush_mmu_tlbonly. Usingtlb_flush_mmucould also avoid exportingtlb_flush_mmu_tlbonly. Buttlb_flush_mmu_tlbonly*looks* more clear and straightforward so I end up using it.
OK, so the code before this patch was passing around a pointer to 'need_flush', and we basically just pass around an mmu_gather instead. It doesn't really simplify the code _too_ much, although it does make it less confusing than when we saw 'need_flush' mixed with 'force_flush' in the code.
tlb_flush_mmu_tlbonly() has exactly one other use: zap_pte_range() for flushing the TLB before pte_unmap_unlock():
if (force_flush)
tlb_flush_mmu_tlbonly(tlb);
pte_unmap_unlock(start_pte, ptl);But, both call-sites are still keeping 'force_flush' to store the information about whether we ever saw a dirty pte. If we moved _that_ logic into the x86 mmu_gather code, we could get rid of all the 'force_flush' tracking in both call sites. It also makes us a bit more future-proof against these page_mkclean() races if we ever grow a third site for clearing ptes.
Instead of exporting and calling tlb_flush_mmu_tlbonly(), we'd need something like tlb_flush_mmu_before_ptl_release() (but with a better name, of course :).
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Re: [PATCH 2/2] mremap: use mmu gather logic for tlb flush in mremap
Quoted 7 lines · Dave Hansen
But, both call-sites are still keeping '
force_flush' to store the information about whether we ever saw a dirty pte. If we moved _that_ logic into the x86mmu_gathercode, we could get rid of all the 'force_flush' tracking in both call sites. It also makes us a bit more future-proof against thesepage_mkclean()races if we ever grow a third site for clearing ptes.
Yeah, that sounds like a nice cleanup and would put all the real state into that mmu_gather structure.
Linus
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>