Commit message: mm: Fix NULL pointer dereference in madvise(MADV_WILLNEED) support

Sasha Levin found a NULL pointer dereference that is due to a missing
page table lock, which in turn is due to the pmd entry in question being
a transparent huge-table entry.

The code - introduced in commit 1998cc048901 ("mm: make
madvise(MADV_WILLNEED) support swap file prefetch") - correctly checks
for this situation using pmd_none_or_trans_huge_or_clear_bad(), but it
turns out that that function doesn't work correctly.

pmd_none_or_trans_huge_or_clear_bad() expected that pmd_bad() would
trigger if the transparent hugepage bit was set, but it doesn't do that
if pmd_numa() is also set. Note that the NUMA bit only gets set on real
NUMA machines, so people trying to reproduce this on most normal
development systems would never actually trigger this.

Fix it by removing the very subtle (and subtly incorrect) expectation,
and instead just checking pmd_trans_huge() explicitly.

Reported-by: Sasha Levin <sasha.levin@oracle.com>
Acked-by: Andrea Arcangeli <aarcange@redhat.com>
[ Additionally remove the now stale test for pmd_trans_huge() inside the
  pmd_bad() case - Linus ]
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
CVE: CVE-2014-8173
CWE: NVD-CWE-Other
Vulnerability description: The pmd_none_or_trans_huge_or_clear_bad function in include/asm-generic/pgtable.h in the Linux kernel before 3.13 on NUMA systems does not properly determine whether a Page Middle Directory (PMD) entry is a transparent huge-table entry, which allows local users to cause a denial of service (NULL pointer dereference and system crash) or possibly have unspecified other impact via a crafted MADV_WILLNEED madvise system call that leverages the absence of a page-table lock.

Project: linux
File: pgtable.h

static inline pmd_t pmd_mksoft_dirty(pmd_t pmd)
{
	return pmd;
}

Project: linux
File: pgtable.h

static inline int track_pfn_copy(struct vm_area_struct *vma)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline void untrack_pfn(struct vm_area_struct *vma,
			       unsigned long pfn, unsigned long size)
{
}

Project: linux
File: pgtable.h

static inline int pmd_numa(pmd_t pmd)
{
	return (pmd_flags(pmd) &
		(_PAGE_NUMA|_PAGE_PRESENT)) == _PAGE_NUMA;
}

Project: linux
File: pgtable.h

static inline pte_t pte_file_mksoft_dirty(pte_t pte)
{
       return pte;
}

Project: linux
File: pgtable.h

static inline pmd_t pmd_mknuma(pmd_t pmd)
{
	pmd = pmd_set_flags(pmd, _PAGE_NUMA);
	return pmd_clear_flags(pmd, _PAGE_PRESENT);
}

Project: linux
File: pgtable.h

static inline pmd_t pmdp_get_and_clear(struct mm_struct *mm,
				       unsigned long address,
				       pmd_t *pmdp)
{
	pmd_t pmd = *pmdp;
	pmd_clear(pmdp);
	return pmd;
}

Project: linux
File: pgtable.h

static inline unsigned long my_zero_pfn(unsigned long addr)
{
	extern unsigned long zero_pfn;
	return zero_pfn;
}

Project: linux
File: pgtable.h

static inline int pte_soft_dirty(pte_t pte)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline pmd_t pmd_mknuma(pmd_t pmd)
{
	return pmd;
}

Project: linux
File: pgtable.h

static inline int track_pfn_insert(struct vm_area_struct *vma, pgprot_t *prot,
				   unsigned long pfn)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline int is_zero_pfn(unsigned long pfn)
{
	extern unsigned long zero_pfn;
	return pfn == zero_pfn;
}

Project: linux
File: pgtable.h

static inline pte_t pte_mksoft_dirty(pte_t pte)
{
	return pte;
}

Project: linux
File: pgtable.h

static inline pte_t pte_mknuma(pte_t pte)
{
	return pte;
}

Project: linux
File: pgtable.h

static inline int pte_file_soft_dirty(pte_t pte)
{
       return 0;
}

Project: linux
File: pgtable.h

static inline int pte_swp_soft_dirty(pte_t pte)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline int pmd_soft_dirty(pmd_t pmd)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline pte_t pte_swp_mksoft_dirty(pte_t pte)
{
	return pte;
}

Project: linux
File: pgtable.h

static inline pte_t pte_mknuma(pte_t pte)
{
	pte = pte_set_flags(pte, _PAGE_NUMA);
	return pte_clear_flags(pte, _PAGE_PRESENT);
}

Project: linux
File: pgtable.h

static inline int is_zero_pfn(unsigned long pfn)
{
	extern unsigned long zero_pfn;
	unsigned long offset_from_zero_pfn = pfn - zero_pfn;
	return offset_from_zero_pfn <= (zero_page_mask >> PAGE_SHIFT);
}

Project: linux
File: pgtable.h

static inline pte_t pte_mknonnuma(pte_t pte)
{
	pte = pte_clear_flags(pte, _PAGE_NUMA);
	return pte_set_flags(pte, _PAGE_PRESENT|_PAGE_ACCESSED);
}

Project: linux
File: pgtable.h

static inline pte_t pte_swp_clear_soft_dirty(pte_t pte)
{
	return pte;
}

Project: linux
File: pgtable.h

static inline int track_pfn_remap(struct vm_area_struct *vma, pgprot_t *prot,
				  unsigned long pfn, unsigned long addr,
				  unsigned long size)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline pmd_t pmd_mknonnuma(pmd_t pmd)
{
	return pmd;
}

Project: linux
File: pgtable.h

static inline int pmd_numa(pmd_t pmd)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline int pmd_none_or_trans_huge_or_clear_bad(pmd_t *pmd)
{
	pmd_t pmdval = pmd_read_atomic(pmd);
	/*
	 * The barrier will stabilize the pmdval in a register or on
	 * the stack so that it will stop changing under the code.
	 *
	 * When CONFIG_TRANSPARENT_HUGEPAGE=y on x86 32bit PAE,
	 * pmd_read_atomic is allowed to return a not atomic pmdval
	 * (for example pointing to an hugepage that has never been
	 * mapped in the pmd). The below checks will only care about
	 * the low part of the pmd with 32bit PAE x86 anyway, with the
	 * exception of pmd_none(). So the important thing is that if
	 * the low part of the pmd is found null, the high part will
	 * be also null or the pmd_none() check below would be
	 * confused.
	 */
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
	barrier();
#endif
	if (pmd_none(pmdval) || pmd_trans_huge(pmdval))
		return 1;
	if (unlikely(pmd_bad(pmdval))) {
		pmd_clear_bad(pmd);
		return 1;
	}
	return 0;
}

Project: linux
File: pgtable.h

static inline pte_t pte_file_clear_soft_dirty(pte_t pte)
{
       return pte;
}

Project: linux
File: pgtable.h

static inline pmd_t pmd_mknonnuma(pmd_t pmd)
{
	pmd = pmd_clear_flags(pmd, _PAGE_NUMA);
	return pmd_set_flags(pmd, _PAGE_PRESENT|_PAGE_ACCESSED);
}

Project: linux
File: pgtable.h

static inline int pte_numa(pte_t pte)
{
	return 0;
}

Project: linux
File: pgtable.h

static inline int pte_numa(pte_t pte)
{
	return (pte_flags(pte) &
		(_PAGE_NUMA|_PAGE_PRESENT)) == _PAGE_NUMA;
}

Project: linux
File: pgtable.h

static inline pte_t pte_mknonnuma(pte_t pte)
{
	return pte;
}