mm: gup: retain synchronicity of concurrent FOLL_LONGTERM R/O pins taken while child exits

David reported a theoretical SMP race discovered upstream, might
affect downstream too. GUP R/O pins can be taken by GUP-fast (or a
concurrent GUP) until the pte has been cleared by __wp_page_copy.

Unsharing a readonly page under a short term R/O pin is always safe,
so the above scenario doesn't matter for short term GUP R/O pins.

However if two FOLL_LONGTERM R/O (!FOLL_WRITE) GUP try to pin the same
page concurrently while a child is exiting (i.e while mapcount changes
from from 2 to 1), the second FOLL_LONGTERM could succeed taking the
PIN without requiring the unshare (by seeing mapcount == 1), while the
first FOLL_LONGTERM (that has seen mapcount == 2) hasn't reached the
__wp_page_copy yet.

The fix is to repeat the can_read_pin_swap_page() check in
wp_page_unshare(), but after clearing the pte to stop GUP-fast too. So
if a R/O pin could have been taken without requiring unshare,
wp_page_unshare will also notice that there's nothing to unshare
anymore and GUP will try again, not invoking the COR fault next time
around.

This only affects concurrent FOLL_LONGTERM !FOLL_WRITE on a readonly
pte while there's a concurrent child exit.

This has never been reproduced and it was found upstream through code
review only.

Reported-by: David Hildenbrand <david@redhat.com>
Signed-off-by: Andrea Arcangeli <aarcange@redhat.com>
2 files changed