Files
xen/xen/arch/arm/include/asm/page.h
T
Leonid Komarianskyi 4b8a11b421 xen/arm: Fix size underflow in invalidate_dcache_va_range()
When the start address is not cacheline aligned, the bytes covered by
the first, partial cacheline are subtracted from 'size'. Nothing checks
that the region actually extends beyond that cacheline, so if it does
not, the unsigned subtraction underflows and the loop that follows walks
practically the entire address space.

For example, with a 64-byte cacheline, p = 0x40000001 and size = 1:

    p & cacheline_mask     -> 1, so the partial head is taken
    size -= 64 - 1         -> 1 - 63 underflows to 0xffffffffffffffc2

Instead of a single "dc civac" at 0x40000000, the loop then runs
288230376151711743 times, issuing "dc ivac" from 0x40000040 upwards.
Invalidating without cleaning discards dirty cachelines, so this
corrupts whatever is mapped along the way until Xen takes a data abort.

Moreover, _cache_flush() in common/grant_table.c, reachable from a guest
via GNTTABOP_cache_flush, bounds 'offset' and 'length' against the page
size but requires no alignment, so GNTTAB_CACHE_INVAL with offset = 1
and length = 1 is enough to trigger this. Note that the page may belong
to a different domain (the d != owner path), so the data being discarded
is not necessarily the caller's own.

The subtraction dates back to the fix for XSA-437, so this is a regression
in that change.

Fix this by detecting the underflow instead of letting it happen: if the
region ends inside the first cacheline, there is nothing left to flush
and the function bails out right after cleaning it. Partially covered
cachelines are still cleaned as well as invalidated, so that data outside
of the region is not discarded.

Additionally, drop 'idx' and advance 'p' directly instead. It simplifies
the logic while using fewer variables and less arithmetic.

Note that the partial head is now cleaned using the unaligned start of the
region instead of the start of its cacheline. This is permitted by the
architecture; the Arm ARM (DDI 0487) states:

  "When a DC instruction requires an address argument this takes the form
  of a 64-bit register that holds the VA argument. No alignment
  restrictions apply for this address."

The same applies on Arm32.

Also, switch the asm() in the loop to asm_inline, like the neighbouring
ones: the wrapper expands to a single instruction with a trailing
semicolon, which GCC counts as two statements, overestimating the size
of the function when deciding whether to inline it.

This is CVE-2026-102699, part of XSA-519.

Fixes: 9a216e92de ("xen/arm: page: Handle cache flush of an element at the top of the address space")
Signed-off-by: Leonid Komarianskyi <leonid_komarianskyi@epam.com>
Reviewed-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Julien Grall <julien@xen.org>
2026-10-13 13:32:17 +01:00

348 lines
11 KiB
C

#ifndef __ARM_PAGE_H__
#define __ARM_PAGE_H__
#include <public/xen.h>
#include <xen/page-size.h>
#include <asm/processor.h>
#include <asm/lpae.h>
#include <asm/sysregs.h>
/* Shareability values for the LPAE entries */
#define LPAE_SH_NON_SHAREABLE 0x0
#define LPAE_SH_UNPREDICTALE 0x1
#define LPAE_SH_OUTER 0x2
#define LPAE_SH_INNER 0x3
/*
* Attribute Indexes.
*
* These are valid in the AttrIndx[2:0] field of an LPAE stage 1 page
* table entry. They are indexes into the bytes of the MAIR*
* registers, as defined below.
*
*/
#define MT_DEVICE_nGnRnE 0x0
#define MT_NORMAL_NC 0x1
#define MT_NORMAL_WT 0x2
#define MT_NORMAL_WB 0x3
#define MT_DEVICE_nGnRE 0x4
#define MT_NORMAL 0x7
/*
* LPAE Memory region attributes. Indexed by the AttrIndex bits of a
* LPAE entry; the 8-bit fields are packed little-endian into MAIR0 and MAIR1.
*
* See section "Device memory" B2.7.2 in ARM DDI 0487B.a for more
* details about the meaning of *G*R*E.
*
* ai encoding
* MT_DEVICE_nGnRnE 000 0000 0000 -- Strongly Ordered/Device nGnRnE
* MT_NORMAL_NC 001 0100 0100 -- Non-Cacheable
* MT_NORMAL_WT 010 1010 1010 -- Write-through
* MT_NORMAL_WB 011 1110 1110 -- Write-back
* MT_DEVICE_nGnRE 100 0000 0100 -- Device nGnRE
* ?? 101
* reserved 110
* MT_NORMAL 111 1111 1111 -- Write-back write-allocate
*
* /!\ It is not possible to combine the definition in MAIRVAL and then
* split because it would result to a 64-bit value that some assembler
* doesn't understand.
*/
#define _MAIR0(attr, mt) (_AC(attr, ULL) << ((mt) * 8))
#define _MAIR1(attr, mt) (_AC(attr, ULL) << (((mt) * 8) - 32))
#define MAIR0VAL (_MAIR0(0x00, MT_DEVICE_nGnRnE)| \
_MAIR0(0x44, MT_NORMAL_NC) | \
_MAIR0(0xaa, MT_NORMAL_WT) | \
_MAIR0(0xee, MT_NORMAL_WB))
#define MAIR1VAL (_MAIR1(0x04, MT_DEVICE_nGnRE) | \
_MAIR1(0xff, MT_NORMAL))
#define MAIRVAL (MAIR1VAL << 32 | MAIR0VAL)
/*
* Layout of the flags used for updating the hypervisor page tables
*
* [0:2] Memory Attribute Index
* [3:4] Permission flags
* [5] Page present
* [6] Only populate page tables
* [7] Superpage mappings is allowed
* [8] Set contiguous bit (internal flag)
*/
#define PAGE_AI_MASK(x) ((x) & 0x7U)
#define _PAGE_XN_BIT 3
#define _PAGE_RO_BIT 4
#define _PAGE_XN (1U << _PAGE_XN_BIT)
#define _PAGE_RO (1U << _PAGE_RO_BIT)
#define PAGE_XN_MASK(x) (((x) >> _PAGE_XN_BIT) & 0x1U)
#define PAGE_RO_MASK(x) (((x) >> _PAGE_RO_BIT) & 0x1U)
#define _PAGE_PRESENT (1U << 5)
#define _PAGE_POPULATE (1U << 6)
#define _PAGE_BLOCK_BIT 7
#define _PAGE_BLOCK (1U << _PAGE_BLOCK_BIT)
#define _PAGE_CONTIG_BIT 8
#define _PAGE_CONTIG (1U << _PAGE_CONTIG_BIT)
/*
* _PAGE_DEVICE and _PAGE_NORMAL are convenience defines. They are not
* meant to be used outside of this header.
*/
#define _PAGE_DEVICE (_PAGE_XN|_PAGE_PRESENT)
#define _PAGE_NORMAL (MT_NORMAL|_PAGE_PRESENT)
#define PAGE_HYPERVISOR_RO (_PAGE_NORMAL|_PAGE_RO|_PAGE_XN)
#define PAGE_HYPERVISOR_RX (_PAGE_NORMAL|_PAGE_RO)
#define PAGE_HYPERVISOR_RW (_PAGE_NORMAL|_PAGE_XN)
#define PAGE_HYPERVISOR PAGE_HYPERVISOR_RW
#define PAGE_HYPERVISOR_NOCACHE (_PAGE_DEVICE|MT_DEVICE_nGnRE)
#define PAGE_HYPERVISOR_WC (_PAGE_DEVICE|MT_NORMAL_NC)
/*
* Stage 2 Memory Type.
*
* These are valid in the MemAttr[3:0] field of an LPAE stage 2 page
* table entry.
*
*/
#define MATTR_DEV 0x1
#define MATTR_MEM_NC 0x5
#define MATTR_MEM 0xf
/* Flags for get_page_from_gva, gvirt_to_maddr etc */
#define GV2M_READ (0u<<0)
#define GV2M_WRITE (1u<<0)
#define GV2M_EXEC (1u<<1)
#ifndef __ASSEMBLER__
#include <xen/errno.h>
#include <xen/types.h>
#include <xen/lib.h>
#include <asm/atomic.h>
#include <asm/system.h>
#if defined(CONFIG_ARM_32)
# include <asm/arm32/page.h>
#elif defined(CONFIG_ARM_64)
# include <asm/arm64/page.h>
#else
# error "unknown ARM variant"
#endif
/* Architectural minimum cacheline size is 4 32-bit words. */
#define MIN_CACHELINE_BYTES 16
/* Min dcache line size on the boot CPU. */
extern size_t dcache_line_bytes;
#define copy_page(dp, sp) memcpy(dp, sp, PAGE_SIZE)
#define clear_page_hot clear_page
#define clear_page_cold clear_page
#define scrub_page_hot(page) memset(page, SCRUB_BYTE_PATTERN, PAGE_SIZE)
#define scrub_page_cold scrub_page_hot
static inline size_t read_dcache_line_bytes(void)
{
register_t ctr;
/* Read CTR */
ctr = READ_SYSREG(CTR_EL0);
/* Bits 16-19 are the log2 number of words in the cacheline. */
return (size_t) (4 << ((ctr >> 16) & 0xf));
}
/* Functions for flushing medium-sized areas.
* if 'range' is large enough we might want to use model-specific
* full-cache flushes. */
static inline int invalidate_dcache_va_range(const void *p, unsigned long size)
{
size_t cacheline_mask = dcache_line_bytes - 1;
unsigned long partial_head;
if ( !size )
return 0;
/* Passing a region that wraps around is illegal */
ASSERT(((uintptr_t)p + size - 1) >= (uintptr_t)p);
dsb(sy); /* So the CPU issues all writes to the range */
/*
* A cacheline only partially covered by the region has to be cleaned as
* well as invalidated, so that data living outside of the region is not
* discarded.
*/
partial_head = (uintptr_t)p & cacheline_mask;
if ( partial_head )
{
unsigned long rem = dcache_line_bytes - partial_head;
asm_inline volatile (
__clean_and_invalidate_dcache_one(0) :: "r" (p) );
p += rem;
/* The region may end within this cacheline, leaving nothing to do. */
if ( __builtin_usubl_overflow(size, rem, &size) )
goto done;
}
for ( ; size >= dcache_line_bytes;
p += dcache_line_bytes, size -= dcache_line_bytes )
asm_inline volatile (__invalidate_dcache_one(0) : : "r" (p));
/* Partial tail must be cleaned as well. */
if ( size > 0 )
asm_inline volatile (
__clean_and_invalidate_dcache_one(0) :: "r" (p) );
done:
dsb(sy); /* So we know the flushes happen before continuing */
return 0;
}
static inline int clean_dcache_va_range(const void *p, unsigned long size)
{
size_t cacheline_mask = dcache_line_bytes - 1;
unsigned long idx = 0;
if ( !size )
return 0;
/* Passing a region that wraps around is illegal */
ASSERT(((uintptr_t)p + size - 1) >= (uintptr_t)p);
dsb(sy); /* So the CPU issues all writes to the range */
size += (uintptr_t)p & cacheline_mask;
size = (size + cacheline_mask) & ~cacheline_mask;
p = (void *)((uintptr_t)p & ~cacheline_mask);
for ( ; size >= dcache_line_bytes;
idx += dcache_line_bytes, size -= dcache_line_bytes )
asm_inline volatile ( __clean_dcache_one(0) : : "r" (p + idx) );
dsb(sy); /* So we know the flushes happen before continuing */
/* ARM callers assume that dcache_* functions cannot fail. */
return 0;
}
static inline int clean_and_invalidate_dcache_va_range
(const void *p, unsigned long size)
{
size_t cacheline_mask = dcache_line_bytes - 1;
unsigned long idx = 0;
if ( !size )
return 0;
/* Passing a region that wraps around is illegal */
ASSERT(((uintptr_t)p + size - 1) >= (uintptr_t)p);
dsb(sy); /* So the CPU issues all writes to the range */
size += (uintptr_t)p & cacheline_mask;
size = (size + cacheline_mask) & ~cacheline_mask;
p = (void *)((uintptr_t)p & ~cacheline_mask);
for ( ; size >= dcache_line_bytes;
idx += dcache_line_bytes, size -= dcache_line_bytes )
asm volatile (__clean_and_invalidate_dcache_one(0) : : "r" (p + idx));
dsb(sy); /* So we know the flushes happen before continuing */
/* ARM callers assume that dcache_* functions cannot fail. */
return 0;
}
/* Macros for flushing a single small item. The predicate is always
* compile-time constant so this will compile down to 3 instructions in
* the common case. */
#define clean_dcache(x) do { \
typeof(x) *_p = &(x); \
if ( sizeof(x) > MIN_CACHELINE_BYTES || sizeof(x) > alignof(x) ) \
clean_dcache_va_range(_p, sizeof(x)); \
else \
asm_inline volatile ( \
"dsb sy;" /* Finish all earlier writes */ \
__clean_dcache_one(0) \
"dsb sy;" /* Finish flush before continuing */ \
: : "r" (_p), "m" (*_p)); \
} while (0)
#define clean_and_invalidate_dcache(x) do { \
typeof(x) *_p = &(x); \
if ( sizeof(x) > MIN_CACHELINE_BYTES || sizeof(x) > alignof(x) ) \
clean_and_invalidate_dcache_va_range(_p, sizeof(x)); \
else \
asm_inline volatile ( \
"dsb sy;" /* Finish all earlier writes */ \
__clean_and_invalidate_dcache_one(0) \
"dsb sy;" /* Finish flush before continuing */ \
: : "r" (_p), "m" (*_p)); \
} while (0)
/*
* Write a pagetable entry.
*
* It is the responsibility of the caller to issue an ISB (if a new entry)
* or a TLB flush (if modified or removed) after write_pte().
*/
static inline void write_pte(lpae_t *p, lpae_t pte)
{
/* Ensure any writes have completed with the old mappings. */
dsb(sy);
/* Safely write the entry. This should always be an atomic write. */
write_atomic(p, pte);
dsb(sy);
}
/* Flush the dcache for an entire page. */
void flush_page_to_ram(unsigned long mfn, bool sync_icache);
/* Print a walk of the hypervisor's page tables for a virtual addr. */
extern void dump_hyp_walk(vaddr_t addr);
static inline uint64_t va_to_par(vaddr_t va)
{
uint64_t par = __va_to_par(va);
/* It is not OK to call this with an invalid VA */
if ( par & PAR_F )
{
dump_hyp_walk(va);
panic_PAR(par);
}
return par;
}
static inline int gva_to_ipa(vaddr_t va, paddr_t *paddr, unsigned int flags)
{
uint64_t par = gva_to_ipa_par(va, flags);
if ( par & PAR_F )
return -EFAULT;
*paddr = (par & PADDR_MASK & PAGE_MASK) | ((unsigned long) va & ~PAGE_MASK);
return 0;
}
/* Bits in the PAR returned by va_to_par */
#define PAR_FAULT 0x1
#endif /* __ASSEMBLER__ */
#endif /* __ARM_PAGE_H__ */
/*
* Local variables:
* mode: C
* c-file-style: "BSD"
* c-basic-offset: 4
* tab-width: 4
* indent-tabs-mode: nil
* End:
*/