KVM: MMU: rmap_write_protect() hugepage iteration bug
rmap_next() expects the "spte" argument to be NULL whenever there's only one remaining entry in the descriptor. That is, it was not designed to handle changes in the chain while iterating.
This bug cripples rmap_write_protect() so that it won't nuke all writable large mappings to a particular gfn.
arch/x86/kvm/mmu.c +3 -1
@@ -632,17 +632,19 @@ static void rmap_write_protect(struct kvm *kvm, u64 gfn) rmapp =gfn_to_rmap (kvm, gfn,1 ); spte =rmap_next (kvm, rmapp,NULL );while (spte) { +u64 *next_spte;BUG_ON (!spte);BUG_ON (!(*spte &PT_PRESENT_MASK ));BUG_ON ((*spte & (PT_PAGE_SIZE_MASK |PT_PRESENT_MASK )) != (PT_PAGE_SIZE_MASK |PT_PRESENT_MASK ));pgprintk ("rmap_write_protect(large): spte %p %llx %lld\n" , spte, *spte, gfn); + next_spte =rmap_next (kvm, rmapp, spte);if (is_writeble_pte (*spte)) {rmap_remove (kvm, spte); --kvm->stat.lpages;set_shadow_pte (spte, shadow_trap_nonpresent_pte); write_protected =1 ; } - spte =rmap_next (kvm, rmapp, spte); + spte = next_spte; }if (write_protected)
Quoted 7 lines · Marcelo Tosatti
rmap_next()expects the "spte" argument to be NULL whenever there's only one remaining entry in the descriptor. That is, it was not designed to handle changes in the chain while iterating.This bug cripples
rmap_write_protect()so that it won't nuke all writable large mappings to a particular gfn.
Better than before but the patched version will still not nuke all writeable sptes (I did exactly the same mistake once).
I found out the trouble the hard way with a trace of ksm kprobe wp_notifier faults, and that's this reordering here:
desc->shadow_ptes[i] = desc->shadow_ptes[j]; desc->shadow_ptes[j] = NULL;
In short there's no way to mix rmap_next and rmap_remove in the same loop as rmap_remove will reorder stuff in the array, so using the next_spte as pointer will lead to missing the sptes after the next_spte that have been reordered in place of the spte just before the next_spte. This was quite subtle issue as it's not immediately evident the first time you read rmap_remove internals.
rmap_remove invocation requires to restart from scratch (or alternatively to avoid restarting from scratch, we'd need to extend the rmap methods to provide that functionality, something like rmap_next_safe that would be robust against rmap_remove if used in the same way you did now with rmap_next).
For now this will fix it for real.
arch/x86/kvm/mmu.c +3 -2
@@ -641,8 +641,9 @@ static void rmap_write_protect(struct kvm *kvm, u64 gfn) --kvm->stat.lpages;set_shadow_pte (spte, shadow_trap_nonpresent_pte); write_protected =1 ; - } - spte =rmap_next (kvm, rmapp, spte); + spte =rmap_next (kvm, rmapp,NULL ); + }else + spte =rmap_next (kvm, rmapp, spte); }if (write_protected)
Hi Andrea,
Quoted 11 lines · Andrea Arcangeli
Quoted 7 lines · Marcelo Tosatti
rmap_next()expects the "spte" argument to be NULL whenever there's only one remaining entry in the descriptor. That is, it was not designed to handle changes in the chain while iterating.This bug cripples
rmap_write_protect()so that it won't nuke all writable large mappings to a particular gfn.Better than before but the patched version will still not nuke all writeable sptes (I did exactly the same mistake once).
Why not posting it at the time?
Quoted 5 lines
I found out the trouble the hard way with a trace of ksm kprobe
wp_notifierfaults, and that's this reordering here:desc->shadow_ptes[i] = desc->shadow_ptes[j]; desc->shadow_ptes[j] = NULL;
You're right.
Quoted 7 lines
In short there's no way to mix
rmap_nextandrmap_removein the same loop asrmap_removewill reorder stuff in the array, so using thenext_spteas pointer will lead to missing the sptes after thenext_sptethat have been reordered in place of the spte just before thenext_spte. This was quite subtle issue as it's not immediately evident the first time you readrmap_removeinternals.
Quoted 7 lines
rmap_removeinvocation requires to restart from scratch (or alternatively to avoid restarting from scratch, we'd need to extend the rmap methods to provide that functionality, something likermap_next_safethat would be robust againstrmap_removeif used in the same way you did now withrmap_next).For now this will fix it for real.
This makes no difference since rmap_next() still can't handle the case where rmap_remove() left a single entry in the array and "spte" has been passed as non-NULL:
while (desc) {
for (i = 0; i < RMAP_EXT && desc->shadow_ptes[i]; ++i) {
if (prev_spte == spte)
return desc->shadow_ptes[i];
prev_spte = desc->shadow_ptes[i];
}
desc = desc->more;
}Other than the BUG_ON()'s at the beginning of the while() loop.
How bad you think restarting is? Unless there are workloads with very large hugepage chains most of the data should be cached in L1. Also the fact that a 4K page has just been shadowed inside a large region makes it unlikely for the same chain to be examined again soon.
arch/x86/kvm/mmu.c +0 -0
@@ -640,6 +640,7 @@ static void rmap_write_protect(struct kvm *kvm, u64
gfn)
rmap_remove(kvm, spte);
--kvm->stat.lpages;
set_shadow_pte(spte,shadow_trap_nonpresent_pte);
+ spte = NULL;
write_protected = 1;
}
spte = rmap_next(kvm, rmapp, spte);Quoted 5 lines · Marcelo Tosatti
How bad you think restarting is? Unless there are workloads with very large hugepage chains most of the data should be cached in L1. Also the fact that a 4K page has just been shadowed inside a large region makes it unlikely for the same chain to be examined again soon.
Non-pae guests will have a chain entry for every mm_struct, since the large ptes reside in the pgd and that is replicated for every mm_struct. So it's easy to have very long chains.
Signature · 1 line
error compiling committee.c: too many arguments to function
Quoted 10 lines · Avi Kivity
Quoted 5 lines · Marcelo Tosatti
How bad you think restarting is? Unless there are workloads with very large hugepage chains most of the data should be cached in L1. Also the fact that a 4K page has just been shadowed inside a large region makes it unlikely for the same chain to be examined again soon.
Non-pae guests will have a chain entry for every
mm_struct, since the large ptes reside in the pgd and that is replicated for everymm_struct. So it's easy to have very long chains.
Anyway I applied the patch as a stopgap.
Signature · 1 line
error compiling committee.c: too many arguments to function
Quoted 8 lines · Avi Kivity
Quoted 7 lines · Avi Kivity
Quoted 5 lines · Marcelo Tosatti
How bad you think restarting is? Unless there are workloads with very large hugepage chains most of the data should be cached in L1. Also the fact that a 4K page has just been shadowed inside a large region makes it unlikely for the same chain to be examined again soon.
Quoted 6 lines
Quoted 3 lines
Non-pae guests will have a chain entry for every
mm_struct, since the large ptes reside in the pgd and that is replicated for everymm_struct. So it's easy to have very long chains.Anyway I applied the patch as a stopgap.
btw, please send a signoff for 2.6.26.
Signature · 1 line
error compiling committee.c: too many arguments to function
Quoted 3 lines · Marcelo Tosatti
Quoted 1 line
writeable sptes (I did exactly the same mistake once).
Why not posting it at the time?
See the comments in the mail I posted a few days ago with subject "[patch] kvm with mmu notifier v18". I posted the incompatibility with rmap_next and rmap_remove in the same loop, the day after I fixed it in ksm.
Quoted 3 lines
This makes no difference since
rmap_next()still can't handle the case wherermap_remove()left a single entry in the array and "spte" has been passed as non-NULL:
I don't see the problem, with the fixed code we never pass spte not-NULL after any rmap_remove run.
Quoted 1 line
How bad you think restarting is? Unless there are workloads with very
It probably worth to optimize it with rmap_next_safe in the long run, but it may not be urgent as it may be lost in the noise even if the chains are fairly long. To be sure you could try to profile it and see if rmap_write_protect goes up in the opreport.
Quoted 12 lines
arch/x86/kvm/mmu.c +1
-0@@ -640,6 +640,7 @@ static void rmap_write_protect(struct kvm *kvm, u64 gfn)rmap_remove (kvm ,spte ); --kvm->stat.lpages;set_shadow_pte (spte,shadow_trap_nonpresent_pte); + spte =NULL ; write_protected =1 ; }
Quoted 1 line
spte = rmap_next(kvm, rmapp, spte);
This is shorter and functionally equivalent to my proposed fix, so you seem to agree this makes a difference by posting this patch, and I sure agree with the above fix too!
Quoted 10 lines · Andrea Arcangeli
Quoted 3 lines · Marcelo Tosatti
Quoted 1 line
writeable sptes (I did exactly the same mistake once).
Why not posting it at the time?
See the comments in the mail I posted a few days ago with subject "[patch] kvm with mmu notifier v18". I posted the incompatibility with
rmap_nextandrmap_removein the same loop, the day after I fixed it in ksm.
Quoted 6 lines
Quoted 3 lines
This makes no difference since
rmap_next()still can't handle the case wherermap_remove()left a single entry in the array and "spte" has been passed as non-NULL:I don't see the problem, with the fixed code we never pass spte not-NULL after any
rmap_removerun.
We do. The case is were you have two entries in the array. rmap_remove will first remove the entry at index 0, and move the entry at index 1 to 0.
Then we call "rmap_next()" with a non-NULL spte, which will skip the only remaining entry at index 0. IOW rmap_next() requires the spte argument to be NULL if the array has one valid entry.
Quoted 7 lines
Quoted 2 lines
How bad you think restarting is? Unless there are workloads with very
It probably worth to optimize it with
rmap_next_safein the long run, but it may not be urgent as it may be lost in the noise even if the chains are fairly long. To be sure you could try to profile it and see ifrmap_write_protectgoes up in the opreport.
Will do.
Quoted 17 lines
Quoted 12 lines
arch/x86/kvm/mmu.c +1
-0@@ -640,6 +640,7 @@ static void rmap_write_protect(struct kvm *kvm, u64 gfn)rmap_remove (kvm ,spte ); --kvm->stat.lpages;set_shadow_pte (spte,shadow_trap_nonpresent_pte); + spte =NULL ; write_protected =1 ; }Quoted 1 line
spte = rmap_next(kvm, rmapp, spte);This is shorter and functionally equivalent to my proposed fix, so you seem to agree this makes a difference by posting this patch, and I sure agree with the above fix too!
Quoted 7 lines · Marcelo Tosatti
We do. The case is were you have two entries in the array.
rmap_removewill first remove the entry at index 0, and move the entry at index 1 to 0.Then we call "
rmap_next()" with a non-NULL spte, which will skip the only remaining entry at index 0. IOWrmap_next()requires the spte argument to be NULL if the array has one valid entry.
How exactly we could call rmap_next with not-NULL spte after any rmap_remove with your last patch applied?
Quoted 4 lines
Quoted 2 lines
chains are fairly long. To be sure you could try to profile it and see if
rmap_write_protectgoes up in the opreport.Will do.
Cool, thanks!
Quoted 11 lines · Andrea Arcangeli
Quoted 7 lines · Marcelo Tosatti
We do. The case is were you have two entries in the array.
rmap_removewill first remove the entry at index 0, and move the entry at index 1 to 0.Then we call "
rmap_next()" with a non-NULL spte, which will skip the only remaining entry at index 0. IOWrmap_next()requires the spte argument to be NULL if the array has one valid entry.How exactly we could call
rmap_nextwith not-NULL spte after anyrmap_removewith your last patch applied?
With the original code.
Quoted 1 line · Marcelo Tosatti
With the original code.
Ah sure! ;)