mm: close page_mkwrite races

Change page_mkwrite to allow implementations to return with the page
locked, and also change it's callers (in page fault paths) to hold the
lock until the page is marked dirty.  This allows the filesystem to have
full control of page dirtying events coming from the VM.

Rather than simply hold the page locked over the page_mkwrite call, we
call page_mkwrite with the page unlocked and allow callers to return with
it locked, so filesystems can avoid LOR conditions with page lock.

The problem with the current scheme is this: a filesystem that wants to
associate some metadata with a page as long as the page is dirty, will
perform this manipulation in its ->page_mkwrite.  It currently then must
return with the page unlocked and may not hold any other locks (according
to existing page_mkwrite convention).

In this window, the VM could write out the page, clearing page-dirty.  The
filesystem has no good way to detect that a dirty pte is about to be
attached, so it will happily write out the page, at which point, the
filesystem may manipulate the metadata to reflect that the page is no
longer dirty.

It is not always possible to perform the required metadata manipulation in
->set_page_dirty, because that function cannot block or fail.  The
filesystem may need to allocate some data structure, for example.

And the VM cannot mark the pte dirty before page_mkwrite, because
page_mkwrite is allowed to fail, so we must not allow any window where the
page could be written to if page_mkwrite does fail.

This solution of holding the page locked over the 3 critical operations
(page_mkwrite, setting the pte dirty, and finally setting the page dirty)
closes out races nicely, preventing page cleaning for writeout being
initiated in that window.  This provides the filesystem with a strong
synchronisation against the VM here.

- Sage needs this race closed for ceph filesystem.
- Trond for NFS (http://bugzilla.kernel.org/show_bug.cgi?id=12913).
- I need it for fsblock.
- I suspect other filesystems may need it too (eg. btrfs).
- I have converted buffer.c to the new locking. Even simple block allocation
  under dirty pages might be susceptible to i_size changing under partial page
  at the end of file (we also have a buffer.c-side problem here, but it cannot
  be fixed properly without this patch).
- Other filesystems (eg. NFS, maybe btrfs) will need to change their
  page_mkwrite functions themselves.

[ This also moves page_mkwrite another step closer to fault, which should
  eventually allow page_mkwrite to be moved into ->fault, and thus avoiding a
  filesystem calldown and page lock/unlock cycle in __do_fault. ]

[akpm@linux-foundation.org: fix derefs of NULL ->mapping]
Cc: Sage Weil <sage@newdream.net>
Cc: Trond Myklebust <trond.myklebust@fys.uio.no>
Signed-off-by: Nick Piggin <npiggin@suse.de>
Cc: Valdis Kletnieks <Valdis.Kletnieks@vt.edu>
Cc: <stable@kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
This commit is contained in:
Nick Piggin 2009-04-30 15:08:16 -07:00 committed by Linus Torvalds
parent a5fc1abe43
commit b827e496c8
3 changed files with 98 additions and 44 deletions

View File

@ -512,16 +512,24 @@ locking rules:
BKL mmap_sem PageLocked(page) BKL mmap_sem PageLocked(page)
open: no yes open: no yes
close: no yes close: no yes
fault: no yes fault: no yes can return with page locked
page_mkwrite: no yes no page_mkwrite: no yes can return with page locked
access: no yes access: no yes
->page_mkwrite() is called when a previously read-only page is ->fault() is called when a previously not present pte is about
about to become writeable. The file system is responsible for to be faulted in. The filesystem must find and return the page associated
protecting against truncate races. Once appropriate action has been with the passed in "pgoff" in the vm_fault structure. If it is possible that
taking to lock out truncate, the page range should be verified to be the page may be truncated and/or invalidated, then the filesystem must lock
within i_size. The page mapping should also be checked that it is not the page, then ensure it is not already truncated (the page lock will block
NULL. subsequent truncate), and then return with VM_FAULT_LOCKED, and the page
locked. The VM will unlock the page.
->page_mkwrite() is called when a previously read-only pte is
about to become writeable. The filesystem again must ensure that there are
no truncate/invalidate races, and then return with the page locked. If
the page has been truncated, the filesystem should not look up a new page
like the ->fault() handler, but simply return with VM_FAULT_NOPAGE, which
will cause the VM to retry the fault.
->access() is called when get_user_pages() fails in ->access() is called when get_user_pages() fails in
acces_process_vm(), typically used to debug a process through acces_process_vm(), typically used to debug a process through

View File

@ -2397,7 +2397,8 @@ block_page_mkwrite(struct vm_area_struct *vma, struct vm_fault *vmf,
if ((page->mapping != inode->i_mapping) || if ((page->mapping != inode->i_mapping) ||
(page_offset(page) > size)) { (page_offset(page) > size)) {
/* page got truncated out from underneath us */ /* page got truncated out from underneath us */
goto out_unlock; unlock_page(page);
goto out;
} }
/* page is wholly or partially inside EOF */ /* page is wholly or partially inside EOF */
@ -2411,14 +2412,15 @@ block_page_mkwrite(struct vm_area_struct *vma, struct vm_fault *vmf,
ret = block_commit_write(page, 0, end); ret = block_commit_write(page, 0, end);
if (unlikely(ret)) { if (unlikely(ret)) {
unlock_page(page);
if (ret == -ENOMEM) if (ret == -ENOMEM)
ret = VM_FAULT_OOM; ret = VM_FAULT_OOM;
else /* -ENOSPC, -EIO, etc */ else /* -ENOSPC, -EIO, etc */
ret = VM_FAULT_SIGBUS; ret = VM_FAULT_SIGBUS;
} } else
ret = VM_FAULT_LOCKED;
out_unlock: out:
unlock_page(page);
return ret; return ret;
} }

View File

@ -1971,6 +1971,15 @@ static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
ret = tmp; ret = tmp;
goto unwritable_page; goto unwritable_page;
} }
if (unlikely(!(tmp & VM_FAULT_LOCKED))) {
lock_page(old_page);
if (!old_page->mapping) {
ret = 0; /* retry the fault */
unlock_page(old_page);
goto unwritable_page;
}
} else
VM_BUG_ON(!PageLocked(old_page));
/* /*
* Since we dropped the lock we need to revalidate * Since we dropped the lock we need to revalidate
@ -1980,9 +1989,11 @@ static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
*/ */
page_table = pte_offset_map_lock(mm, pmd, address, page_table = pte_offset_map_lock(mm, pmd, address,
&ptl); &ptl);
page_cache_release(old_page); if (!pte_same(*page_table, orig_pte)) {
if (!pte_same(*page_table, orig_pte)) unlock_page(old_page);
page_cache_release(old_page);
goto unlock; goto unlock;
}
page_mkwrite = 1; page_mkwrite = 1;
} }
@ -2094,9 +2105,6 @@ gotten:
unlock: unlock:
pte_unmap_unlock(page_table, ptl); pte_unmap_unlock(page_table, ptl);
if (dirty_page) { if (dirty_page) {
if (vma->vm_file)
file_update_time(vma->vm_file);
/* /*
* Yes, Virginia, this is actually required to prevent a race * Yes, Virginia, this is actually required to prevent a race
* with clear_page_dirty_for_io() from clearing the page dirty * with clear_page_dirty_for_io() from clearing the page dirty
@ -2105,16 +2113,41 @@ unlock:
* *
* do_no_page is protected similarly. * do_no_page is protected similarly.
*/ */
wait_on_page_locked(dirty_page); if (!page_mkwrite) {
set_page_dirty_balance(dirty_page, page_mkwrite); wait_on_page_locked(dirty_page);
set_page_dirty_balance(dirty_page, page_mkwrite);
}
put_page(dirty_page); put_page(dirty_page);
if (page_mkwrite) {
struct address_space *mapping = dirty_page->mapping;
set_page_dirty(dirty_page);
unlock_page(dirty_page);
page_cache_release(dirty_page);
if (mapping) {
/*
* Some device drivers do not set page.mapping
* but still dirty their pages
*/
balance_dirty_pages_ratelimited(mapping);
}
}
/* file_update_time outside page_lock */
if (vma->vm_file)
file_update_time(vma->vm_file);
} }
return ret; return ret;
oom_free_new: oom_free_new:
page_cache_release(new_page); page_cache_release(new_page);
oom: oom:
if (old_page) if (old_page) {
if (page_mkwrite) {
unlock_page(old_page);
page_cache_release(old_page);
}
page_cache_release(old_page); page_cache_release(old_page);
}
return VM_FAULT_OOM; return VM_FAULT_OOM;
unwritable_page: unwritable_page:
@ -2664,27 +2697,22 @@ static int __do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
int tmp; int tmp;
unlock_page(page); unlock_page(page);
vmf.flags |= FAULT_FLAG_MKWRITE; vmf.flags = FAULT_FLAG_WRITE|FAULT_FLAG_MKWRITE;
tmp = vma->vm_ops->page_mkwrite(vma, &vmf); tmp = vma->vm_ops->page_mkwrite(vma, &vmf);
if (unlikely(tmp & if (unlikely(tmp &
(VM_FAULT_ERROR | VM_FAULT_NOPAGE))) { (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
ret = tmp; ret = tmp;
anon = 1; /* no anon but release vmf.page */ goto unwritable_page;
goto out_unlocked;
}
lock_page(page);
/*
* XXX: this is not quite right (racy vs
* invalidate) to unlock and relock the page
* like this, however a better fix requires
* reworking page_mkwrite locking API, which
* is better done later.
*/
if (!page->mapping) {
ret = 0;
anon = 1; /* no anon but release vmf.page */
goto out;
} }
if (unlikely(!(tmp & VM_FAULT_LOCKED))) {
lock_page(page);
if (!page->mapping) {
ret = 0; /* retry the fault */
unlock_page(page);
goto unwritable_page;
}
} else
VM_BUG_ON(!PageLocked(page));
page_mkwrite = 1; page_mkwrite = 1;
} }
} }
@ -2736,19 +2764,35 @@ static int __do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
pte_unmap_unlock(page_table, ptl); pte_unmap_unlock(page_table, ptl);
out: out:
unlock_page(vmf.page); if (dirty_page) {
out_unlocked: struct address_space *mapping = page->mapping;
if (anon)
page_cache_release(vmf.page); if (set_page_dirty(dirty_page))
else if (dirty_page) { page_mkwrite = 1;
unlock_page(dirty_page);
put_page(dirty_page);
if (page_mkwrite && mapping) {
/*
* Some device drivers do not set page.mapping but still
* dirty their pages
*/
balance_dirty_pages_ratelimited(mapping);
}
/* file_update_time outside page_lock */
if (vma->vm_file) if (vma->vm_file)
file_update_time(vma->vm_file); file_update_time(vma->vm_file);
} else {
set_page_dirty_balance(dirty_page, page_mkwrite); unlock_page(vmf.page);
put_page(dirty_page); if (anon)
page_cache_release(vmf.page);
} }
return ret; return ret;
unwritable_page:
page_cache_release(page);
return ret;
} }
static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma, static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma,