diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index 17ea534bc5b0..5963da651da7 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -1128,6 +1128,8 @@ extern pte_t contpte_ptep_get(pte_t *ptep, pte_t orig_pte);
 extern pte_t contpte_ptep_get_lockless(pte_t *orig_ptep);
 extern void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
 				pte_t *ptep, pte_t pte, unsigned int nr);
+extern pte_t contpte_ptep_get_and_clear_full(struct mm_struct *mm,
+				unsigned long addr, pte_t *ptep);
 extern int contpte_ptep_test_and_clear_young(struct vm_area_struct *vma,
 				unsigned long addr, pte_t *ptep);
 extern int contpte_ptep_clear_flush_young(struct vm_area_struct *vma,
@@ -1252,12 +1254,24 @@ static inline void pte_clear(struct mm_struct *mm,
 	__pte_clear(mm, addr, ptep);
 }
 
+#define __HAVE_ARCH_PTEP_GET_AND_CLEAR_FULL
+static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
+				unsigned long addr, pte_t *ptep, int full)
+{
+	pte_t orig_pte = __ptep_get(ptep);
+
+	if (!pte_present(orig_pte) || !pte_cont(orig_pte) || !full) {
+		contpte_try_unfold(mm, addr, ptep, orig_pte);
+		return __ptep_get_and_clear(mm, addr, ptep);
+	} else
+		return contpte_ptep_get_and_clear_full(mm, addr, ptep);
+}
+
 #define __HAVE_ARCH_PTEP_GET_AND_CLEAR
 static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 				unsigned long addr, pte_t *ptep)
 {
-	contpte_try_unfold(mm, addr, ptep, __ptep_get(ptep));
-	return __ptep_get_and_clear(mm, addr, ptep);
+	return ptep_get_and_clear_full(mm, addr, ptep, 0);
 }
 
 #define __HAVE_ARCH_PTEP_TEST_AND_CLEAR_YOUNG
diff --git a/arch/arm64/mm/contpte.c b/arch/arm64/mm/contpte.c
index e8e4a298fd53..0b585d1c4c94 100644
--- a/arch/arm64/mm/contpte.c
+++ b/arch/arm64/mm/contpte.c
@@ -241,6 +241,74 @@ void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
 	} while (addr != end);
 }
 
+pte_t contpte_ptep_get_and_clear_full(struct mm_struct *mm,
+					unsigned long addr, pte_t *ptep)
+{
+	/*
+	 * When doing a full address space teardown, we can avoid unfolding the
+	 * contiguous range, and therefore avoid the associated tlbi. Instead,
+	 * just clear the pte. The caller is promising to call us for every pte,
+	 * so every pte in the range will be cleared by the time the tlbi is
+	 * issued.
+	 *
+	 * However, this approach will leave the ptes in an inconsistent state
+	 * until ptep_get_and_clear_full() has been called for every pte in the
+	 * range. This could cause ptep_get() to fail to return the correct
+	 * access/dirty bits, if ptep_get() calls are interleved with
+	 * ptep_get_and_clear_full() (which they are). Solve this by copying the
+	 * access/dirty bits to every pte in the range so that ptep_get() still
+	 * sees them if we have already cleared pte that the hw chose to update.
+	 * Note that a full teardown will only happen when the process is
+	 * exiting, so we do not expect anymore accesses and therefore no more
+	 * access/dirty bit updates, so there is no race here.
+	 */
+
+	pte_t *orig_ptep = ptep;
+	pte_t pte;
+	bool flags_propagated = false;
+	bool dirty = false;
+	bool young = false;
+	int i;
+
+	/* First, gather access and dirty bits. */
+	ptep = contpte_align_down(orig_ptep);
+	for (i = 0; i < CONT_PTES; i++, ptep++) {
+		pte = __ptep_get(ptep);
+
+		/*
+		 * If we find a zeroed PTE, contpte_ptep_get_and_clear_full()
+		 * must have already been called for it, so we have already
+		 * propagated the flags to the other ptes.
+		 */
+		if (pte_val(pte) == 0) {
+			flags_propagated = true;
+			break;
+		}
+
+		if (pte_dirty(pte))
+			dirty = true;
+
+		if (pte_young(pte))
+			young = true;
+	}
+
+	/* Now copy the access and dirty bits into each pte in the range. */
+	if (!flags_propagated) {
+		ptep = contpte_align_down(orig_ptep);
+		for (i = 0; i < CONT_PTES; i++, ptep++) {
+			pte = __ptep_get(ptep);
+
+			if (dirty)
+				pte = pte_mkdirty(pte);
+
+			if (young)
+				pte = pte_mkyoung(pte);
+		}
+	}
+
+	return __ptep_get_and_clear(mm, addr, orig_ptep);
+}
+
 int contpte_ptep_test_and_clear_young(struct vm_area_struct *vma,
 					unsigned long addr, pte_t *ptep)
 {
