@@ -81,6 +81,12 @@ struct vmap_area {
unsigned long flags; /* mark type of vm_map_ram area */
};
+struct checked_vmap_range {
+ unsigned long va_start;
+ unsigned long va_end;
+ struct list_head list;
+};
+
/* archs that select HAVE_ARCH_HUGE_VMAP should override one or more of these */
#ifndef arch_vmap_p4d_supported
static inline bool arch_vmap_p4d_supported(pgprot_t prot)
@@ -125,6 +131,12 @@ static inline pgprot_t arch_vmap_pgprot_tagged(pgprot_t prot)
}
#endif
+#ifndef arch_init_checked_vmap_ranges
+inline void __init arch_init_checked_vmap_ranges(void)
+{
+}
+#endif
+
/*
* Highlevel APIs for driver use
*/
@@ -211,6 +223,8 @@ extern struct vm_struct *__get_vm_area_caller(unsigned long size,
unsigned long flags,
unsigned long start, unsigned long end,
const void *caller);
+int __init create_vmalloc_range_check(unsigned long start_vaddr,
+ unsigned long end_vaddr);
void free_vm_area(struct vm_struct *area);
extern struct vm_struct *remove_vm_area(const void *addr);
extern struct vm_struct *find_vm_area(const void *addr);
@@ -817,6 +817,16 @@ static struct kmem_cache *vmap_area_cachep;
*/
static LIST_HEAD(free_vmap_area_list);
+static struct kmem_cache *vmap_checked_range_cachep;
+
+/*
+ * This linked list is used to record ranges of the vmalloc
+ * region which are checked at allocation time to ensure they
+ * are only allocated within when an explicit allocation
+ * request to that range is made.
+ */
+static LIST_HEAD(checked_range_list);
+
/*
* This augment red-black tree represents the free vmap space.
* All vmap_area objects in this tree are sorted by va->va_start
@@ -1454,6 +1464,23 @@ merge_or_add_vmap_area_augment(struct vmap_area *va,
return va;
}
+static __always_inline bool
+va_is_range_restricted(struct vmap_area *va, unsigned long vstart)
+{
+ struct checked_vmap_range *range, *tmp;
+
+ if (list_empty(&checked_range_list))
+ return false;
+
+ list_for_each_entry_safe(range, tmp, &checked_range_list, list)
+ if (va->va_start >= range->va_start &&
+ va->va_end <= range->va_end &&
+ vstart != range->va_start)
+ return true;
+
+ return false;
+}
+
static __always_inline bool
is_within_this_va(struct vmap_area *va, unsigned long size,
unsigned long align, unsigned long vstart)
@@ -1501,7 +1528,8 @@ find_vmap_lowest_match(struct rb_root *root, unsigned long size,
vstart < va->va_start) {
node = node->rb_left;
} else {
- if (is_within_this_va(va, size, align, vstart))
+ if (!va_is_range_restricted(va, vstart) &&
+ is_within_this_va(va, size, align, vstart))
return va;
/*
@@ -1522,7 +1550,8 @@ find_vmap_lowest_match(struct rb_root *root, unsigned long size,
*/
while ((node = rb_parent(node))) {
va = rb_entry(node, struct vmap_area, rb_node);
- if (is_within_this_va(va, size, align, vstart))
+ if (!va_is_range_restricted(va, vstart) &&
+ is_within_this_va(va, size, align, vstart))
return va;
if (get_subtree_max_size(node->rb_right) >= length &&
@@ -1554,7 +1583,8 @@ find_vmap_lowest_linear_match(struct list_head *head, unsigned long size,
struct vmap_area *va;
list_for_each_entry(va, head, list) {
- if (!is_within_this_va(va, size, align, vstart))
+ if (va_is_range_restricted(va, vstart) ||
+ !is_within_this_va(va, size, align, vstart))
continue;
return va;
@@ -1717,6 +1747,36 @@ va_clip(struct rb_root *root, struct list_head *head,
return 0;
}
+static inline int
+split_and_alloc_va(struct rb_root *root, struct list_head *head, unsigned long addr)
+{
+ struct vmap_area *va;
+ int ret;
+ struct vmap_area *lva = NULL;
+
+ va = __find_vmap_area(addr, root);
+ if (!va) {
+ pr_err("%s: could not find vmap\n", __func__);
+ return -1;
+ }
+
+ lva = kmem_cache_alloc(vmap_area_cachep, GFP_NOWAIT);
+ if (!lva) {
+ pr_err("%s: unable to allocate va for range\n", __func__);
+ return -1;
+ }
+ lva->va_start = addr;
+ lva->va_end = va->va_end;
+ ret = va_clip(root, head, va, addr, va->va_end - addr);
+ if (WARN_ON_ONCE(ret)) {
+ pr_err("%s: unable to clip code base region\n", __func__);
+ kmem_cache_free(vmap_area_cachep, lva);
+ return -1;
+ }
+ insert_vmap_area_augment(lva, NULL, root, head);
+ return 0;
+}
+
static unsigned long
va_alloc(struct vmap_area *va,
struct rb_root *root, struct list_head *head,
@@ -4424,6 +4484,35 @@ int remap_vmalloc_range(struct vm_area_struct *vma, void *addr,
}
EXPORT_SYMBOL(remap_vmalloc_range);
+/**
+ * create_vmalloc_range_check - create a checked range of vmalloc memory
+ * @start_vaddr: The starting vaddr of the code range
+ * @end_vaddr: The ending vaddr of the code range
+ *
+ * Returns: 0 for success, -1 on failure
+ *
+ * This function marks regions within or overlapping the vmalloc region for
+ * requested range checking during allocation. When requesting virtual memory,
+ * if the requested starting vaddr does not explicitly match the starting vaddr
+ * of this range, this range will not be allocated from.
+ */
+int __init create_vmalloc_range_check(unsigned long start_vaddr,
+ unsigned long end_vaddr)
+{
+ struct checked_vmap_range *range;
+
+ range = kmem_cache_alloc(vmap_checked_range_cachep, GFP_NOWAIT);
+ if (split_and_alloc_va(&free_vmap_area_root, &free_vmap_area_list, start_vaddr) ||
+ split_and_alloc_va(&free_vmap_area_root, &free_vmap_area_list, end_vaddr))
+ return -1;
+
+ range->va_start = start_vaddr;
+ range->va_end = end_vaddr;
+
+ list_add(&range->list, &checked_range_list);
+ return 0;
+}
+
void free_vm_area(struct vm_struct *area)
{
struct vm_struct *ret;
@@ -5082,6 +5171,11 @@ void __init vmalloc_init(void)
*/
vmap_area_cachep = KMEM_CACHE(vmap_area, SLAB_PANIC);
+ /*
+ * Create the cache for checked vmap ranges.
+ */
+ vmap_checked_range_cachep = KMEM_CACHE(checked_vmap_range, SLAB_PANIC);
+
for_each_possible_cpu(i) {
struct vmap_block_queue *vbq;
struct vfree_deferred *p;
@@ -5129,4 +5223,6 @@ void __init vmalloc_init(void)
vmap_node_shrinker->count_objects = vmap_node_shrink_count;
vmap_node_shrinker->scan_objects = vmap_node_shrink_scan;
shrinker_register(vmap_node_shrinker);
+
+ arch_init_checked_vmap_ranges();
}
Add an API to the vmalloc infrastructure, create_vmalloc_range_check, which allows for the creation of restricted sub-ranges of vmalloc memory during the init process, which can only be allocated from via vmalloc requests with vaddr start addresses explicitly matching the range's start addresses. Calls to this API can split up to two nodes in the red-black tree. create_vmalloc_range_check restricts vmalloc requests not matching the range's start address to all other locations in the standard vmalloc range, i.e. users of the interface are responsible for requesting only correct and appropriate reservations. The primary intention of this API is supporting ASLR module region allocation regions while not undermining existing security mechanisms by necessitating interleaved code and data pages. To perform range allocation at the appropriate, earliest time, provide a callback arch_init_checked_vmap_ranges rather than maintaining a linked list outside of the vmalloc infrastructure, ensuring all vmap management is still owned by vmalloc.c. Considering some alternatives, i.e. a large change to the vmalloc infrastructure to provide true support for a module code dynamic allocation region, this smaller vstart-based opt-in seems preferable. These changes may need to wait/be rebased on Mike Rapoport's patches at 20240505160628.2323363-11-rppt@kernel.org , but this version is submitted to account for the justification for unrestricted BPF/kprobe code allocations and squashes some bugs the last version created in the bpf selftests. Changes from v4: 20240423095843.446565600-1-mbland@motorola.com - Fix the corruption because of backslash created by SMTP mailer - Add config to permit the reduction of BPF memory to 128MB, i.e. fix issue with the arm64 side of this change implicitly breaking 1636131046-5982-2-git-send-email-alan.maguire@oracle.com "128MB of JIT memory can be quickly exhausted" - Expand modules_alloc region used on arm64 to support larger BPF allocations present in the selftests. Changes from v3: 20240416122254.868007168-1-mbland@motorola.com - Added callbacks into arch-specific code to dynamically partition red-black tree (The freedom of architectures to determine vm area allocation was deemed dangerous since there was no possibility of enforcing that areas were correctly managed.) Changes from v2: 20240220203256.31153-1-mbland@motorola.com - No longer depends on reducing the size of the vmalloc region - Attempted to implement change by allowing architectures to override most abstract public vmalloc interface (Overrides on vmalloc methods were deemed undesirable.) Changes from v1: CAP5Mv+ydhk=Ob4b40ZahGMgT-5+-VEHxtmA=-LkJiEOOU+K6hw@mail.gmail.com - Statically reduced the range of the vmalloc region to support parititoned code ranges (The trade off between space reduction and security was deemed unnecessary.) Signed-off-by: Maxwell Bland <mbland@motorola.com> --- Thanks again to the maintainers for their review, apologies for the mailer and BPF selftest errors on the previous version. This version will still be incompatible with BPF allocation limit stress tests, for clear reasons. I plan to rebase this same exact code on top of Mike Rapoport's recent patchset, but this version is compatible with the current linus upstream. include/linux/vmalloc.h | 14 ++++++ mm/vmalloc.c | 102 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 3 deletions(-) base-commit: ee5b455b0adae9ecafb38b174c648c48f2a3c1a5