diff options
Diffstat (limited to 'mm/mseal.c')
| -rw-r--r-- | mm/mseal.c | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/mm/mseal.c b/mm/mseal.c new file mode 100644 index 000000000..7a8ac66dc --- /dev/null +++ b/mm/mseal.c @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Implement mseal() syscall. + * + * Copyright (c) 2023,2024 Google, Inc. + * + * Author: Jeff Xu <jeffxu@chromium.org> + */ + +#include <linux/mempolicy.h> +#include <linux/minmax.h> +#include <linux/mman.h> +#include <linux/mm.h> +#include <linux/mm_inline.h> +#include <linux/syscalls.h> +#include <linux/sched.h> +#include "internal.h" + +static bool range_contains_unmapped(unsigned long start, unsigned long end) +{ + VMA_ITERATOR(vmi, current->mm, start); + unsigned long prev_end = start; + struct vm_area_struct *vma; + + for_each_vma_range(vmi, vma, end) { + if (vma->vm_start > prev_end) + return true; + + prev_end = vma->vm_end; + } + + return prev_end < end; +} + +static int __mseal_range(unsigned long start, unsigned long end) +{ + VMA_ITERATOR(vmi, current->mm, start); + struct vm_area_struct *vma, *prev; + + /* We know there are no gaps so this will be non-NULL. */ + vma = vma_iter_load(&vmi); + prev = vma_prev(&vmi); + if (start > vma->vm_start) + prev = vma; + + for_each_vma_range(vmi, vma, end) { + const unsigned long curr_start = max(vma->vm_start, start); + const unsigned long curr_end = min(vma->vm_end, end); + + if (!vma_test(vma, VMA_SEALED_BIT)) { + vma_flags_t vma_flags = vma->flags; + + vma_flags_set(&vma_flags, VMA_SEALED_BIT); + + vma = vma_modify_flags(&vmi, prev, vma, curr_start, + curr_end, &vma_flags); + if (IS_ERR(vma)) + return PTR_ERR(vma); + vma_start_write(vma); + vma_set_flags(vma, VMA_SEALED_BIT); + } + + prev = vma; + } + + return 0; +} + +static int mseal_range(unsigned long start, unsigned long end) +{ + int err; + + err = mmap_write_lock_killable(current->mm); + if (err) + return err; + if (range_contains_unmapped(start, end)) + err = -ENOMEM; + else + err = __mseal_range(start, end); + mmap_write_unlock(current->mm); + return err; +} + +/** + * mseal_mmap_page_zero() - If the MMAP_PAGE_ZERO personality is set, mseal() + * the page mapped at address zero. + */ +void mseal_mmap_page_zero(void) +{ + int err; + + if (WARN_ON_ONCE(!(current->personality & MMAP_PAGE_ZERO))) + return; + + err = mseal_range(0, PAGE_SIZE); + if (err) + pr_warn_ratelimited("pid=%d, couldn't seal address 0, ret=%d.\n", + task_pid_nr(current), err); +} + +/* + * Seal VMAs in the specified input range to prevent an attacker replacing what + * is mapped in the range with something else. + * + * Disallows: + * - VMA unmapping, remapping or shrinking. + * - Overwriting the VMA with another one via mmap(), mremap() or similar. + * - Alteration of properties via mprotect()/pkey_mprotect(). + * - Destructive madvise() behaviours (like MADV_DONTNEED) on anonymous read-only + * ranges. + * + * Since unmapped ranges can be mapped at any time, the input range must span + * mapped ranges only. + * + * The flags parameter is currently reserved. + */ +SYSCALL_DEFINE3(mseal, unsigned long, start, size_t, len, unsigned long, flags) +{ + size_t len_aligned; + unsigned long end; + + /* Verify flags not set. */ + if (flags) + return -EINVAL; + + start = untagged_addr(start); + if (!PAGE_ALIGNED(start)) + return -EINVAL; + + len_aligned = PAGE_ALIGN(len); + /* Check to see whether len was rounded up from small -ve to zero. */ + if (len && !len_aligned) + return -EINVAL; + + end = start + len_aligned; + if (end < start) + return -EINVAL; + + if (end == start) + return 0; + + return mseal_range(start, end); +} |
