> secretmem has a relatively laissez-faire attitude to accounting the folios
> it allocates.
> 
> The intention is that the memory is treated as if it were mlock()'d and
> thus is limited by the RLIMIT_MEMLOCK limit if the CAP_IPC_LOCK capability
> is not in place (which broadly allows unlimited ranges of mlock()'d
> memory).
> 
> The lifecycle for memfd accounting against this limit is - account on
> map, unaccount on unmap.
> 
> But the lifecycle of memfd folios is allocate on fault, deallocate on inode
> eviction.
> 
> This mismatch is problematic because the folios are unevictable and remain
> so until the inode is evicted.
> 
> This is established using mapping_set_unevictable() setting the
> AS_UNEVICTABLE flag in the relevant address_space data structure.
> 
> This is problematic as it eliminates usual mlock() semantics - mapping
> folios then unmapping them does not clear their unevictable state, since it
> depends on AS_UNEVICTABLE, not PG_mlocked.
> 
> This is checked by folio_evictable() which first checks for AS_UNEVICTABLE
> before looking at PG_mlocked, which is cleared on unmap via
> munlock_vma_folio().
> 
> A user can therefore easily work around the RLIMIT_MEMLOCK limit - simply
> map then unmap and VmLck no longer counts the secretmem range, nor are they
> accounted in the process's RSS even if mapped again, meaning the OOM killer
> won't know to kill the process.
> 
> This can also be achieved by forking the process, as VMA_LOCKED_BIT is
> cleared for VMAs copied to the child process, whose VmLck will be 0.
> 
> A user without the CAP_IPC_LOCK capability can therefore repeatedly
> map/unmap (or map/fork) and consume all available system memory with
> unevictable folios and cause system instability.
> 
> Worse, the OOM killer will not be able to resolve the situation.
> 
> Additionally, this fd can be passed between processes and over fork so a
> per-process limit simply does not make sense.
> 
> There is precedent for addressing this issue - io_uring, perf, skbuff,
> iommufd and xdp all perform the precise lifetime management required here
> using an alternative method - tracking the number of locked pages in
> struct user_struct->locked_vm.
> 
> Add secretmem to this list and account locked folios over the lifetime of
> the inode to reflect the actual lifetime of the folios, while bypassing
> this if the user has the CAP_IPC_LOCK capability.
> 
> As a result the semantics change - the RLIMIT_MEMLOCK limit is per-user not
> per-process to reflect the actual scope of the allocated folios, and this
> limit is shared between secretmem and io_uring, etc.
> 
> However this is reasonable given the need to track this limit in the actual
> scope in which it applies.
> 
> Since GUP rejects secretmem mappings, setting VMA_LOCKED_BIT does not
> result in memory being faulted in on map, another wrinkle with the mlock()
> accounting.
> 
> This also leads to another oddity from the previous implementation -
> mlock_future_ok() was checked on mmap() but since nothing is faulted in
> right away, this check was more or less meaningless. Therefore drop this.
> 
> There is simply no reason to carry on marking the mapping as mlock()'d
> since it's misleading and the lifecycle is now correctly handled, so remove
> this too.
> 
> Additionally, fix the selftest which checks the limit as this now must
> assert SIGBUS on limit violation on fault-in.
> 
> Also assert there that the limit applies to the lifetime of the fd rather
> than the mapping by trying to map a single page past the maximum rlimit -
> previously this would have succeeded as the prior unmap would have reset
> the mlock limit.
> 
> __secretmem_account_pages() is essentially a duplicate of the code that
> io_uring etc. use, but since this is a bug fix that needs backporting,
> defer any de-duplication efforts to a follow-up.

Can the commit message here be a page rather than a folio? ;-)

> Reported-by: Daehyeon Ko <[email protected]>
> Closes: 
> https://lore.kernel.org/linux-mm/[email protected]/
> Fixes: 1507f51255c9 ("mm: introduce memfd_secret system call to create 
> "secret" memory areas")
> Cc: [email protected]
> Signed-off-by: Lorenzo Stoakes (ARM) <[email protected]>
>
> diff --git a/include/linux/sched/user.h b/include/linux/sched/user.h
> index 4cc52698e214..8d7e5521f7cd 100644
> --- a/include/linux/sched/user.h
> +++ b/include/linux/sched/user.h
> @@ -25,7 +25,8 @@ struct user_struct {
>  
>  #if defined(CONFIG_PERF_EVENTS) || defined(CONFIG_BPF_SYSCALL) || \
>       defined(CONFIG_NET) || defined(CONFIG_IO_URING) || \
> -     defined(CONFIG_VFIO_PCI_ZDEV_KVM) || IS_ENABLED(CONFIG_IOMMUFD)
> +     defined(CONFIG_VFIO_PCI_ZDEV_KVM) || IS_ENABLED(CONFIG_IOMMUFD) || \
> +     defined(CONFIG_SECRETMEM)
>       atomic_long_t locked_vm;
>  #endif
>  #ifdef CONFIG_WATCH_QUEUE
> diff --git a/mm/secretmem.c b/mm/secretmem.c
> index d29865075b6e..537fe5b1222f 100644
> --- a/mm/secretmem.c
> +++ b/mm/secretmem.c
> @@ -18,6 +18,8 @@
>  #include <linux/secretmem.h>
>  #include <linux/set_memory.h>
>  #include <linux/sched/signal.h>
> +#include <linux/sched/user.h>
> +#include <linux/cred.h>
>  
>  #include <uapi/linux/magic.h>
>  
> @@ -47,10 +49,107 @@ bool secretmem_active(void)
>       return !!atomic_read(&secretmem_users);
>  }
>  
> +struct secretmem_inode_state {
> +     struct user_struct      *user;
> +     atomic_long_t           nr_pages_accounted;
> +};
> +
> +static bool __secretmem_account_pages(struct user_struct *user,
> +             unsigned long nr_pages)
> +{
> +     unsigned long page_limit, cur_pages, new_pages;
> +
> +     if (!nr_pages)
> +             return true;
> +
> +     page_limit = rlimit(RLIMIT_MEMLOCK) >> PAGE_SHIFT;
> +
> +     cur_pages = atomic_long_read(&user->locked_vm);
> +     do {
> +             new_pages = cur_pages + nr_pages;
> +             if (new_pages > page_limit)
> +                     return false;
> +     } while (!atomic_long_try_cmpxchg(&user->locked_vm,
> +                                       &cur_pages, new_pages));
> +     return true;
> +}
> +
> +static bool secretmem_account_folio(struct secretmem_inode_state *state,
> +             const struct folio *folio)
> +{
> +     unsigned long nr_pages;
> +
> +     if (!state)
> +             return true;
> +
> +     nr_pages = folio_nr_pages(folio);
> +     if (!__secretmem_account_pages(state->user, nr_pages))
> +             return false;
> +
> +     atomic_long_add(nr_pages, &state->nr_pages_accounted);
> +     return true;
> +}
> +
> +static void __secretmem_unaccount_pages(struct secretmem_inode_state *state,
> +             unsigned long nr_pages)
> +{
> +     atomic_long_sub(nr_pages, &state->user->locked_vm);
> +     atomic_long_sub(nr_pages, &state->nr_pages_accounted);
> +}
> +
> +static void secretmem_unaccount_folio(struct secretmem_inode_state *state,
> +             struct folio *folio)
> +{
> +     if (!state)
> +             return;
> +
> +     __secretmem_unaccount_pages(state, folio_nr_pages(folio));
> +}
> +
> +static void secretmem_unaccount_all_folios(struct secretmem_inode_state 
> *state)
> +{
> +     unsigned long nr_pages_accounted;
> +
> +     if (!state)
> +             return;
> +
> +     nr_pages_accounted = atomic_long_read(&state->nr_pages_accounted);
> +     __secretmem_unaccount_pages(state, nr_pages_accounted);
> +}
> +
> +static void secretmem_destroy_inode_priv(struct inode *inode)
> +{
> +     struct secretmem_inode_state *state = inode->i_private;
> +
> +     if (!state)
> +             return;
> +
> +     secretmem_unaccount_all_folios(state);
> +     free_uid(state->user);
> +     kfree(state);
> +}
> +
> +static int secretmem_init_inode_priv(struct inode *inode)

Can _init and _destroy live closer to their callers please?

> +{
> +     struct secretmem_inode_state *state;
> +
> +     if (ns_capable_noaudit(&init_user_ns, CAP_IPC_LOCK))
> +             return 0;

The check happens only on inode creation, so if a privileged process,
e.g. a container engine, passes the fd to an unprivileged process, the
accounting issue remains.

> +
> +     state = kzalloc_obj(*state, GFP_KERNEL);

GFP_KERNEL is implicit for kzalloc_obj(), isn't it?

> +     if (!state)
> +             return -ENOMEM;
> +
> +     state->user = get_uid(current_user());
> +     inode->i_private = state;
> +     return 0;
> +}
> +
>  static vm_fault_t secretmem_fault(struct vm_fault *vmf)
>  {
>       struct address_space *mapping = vmf->vma->vm_file->f_mapping;
>       struct inode *inode = file_inode(vmf->vma->vm_file);
> +     struct secretmem_inode_state *state = inode->i_private;
>       pgoff_t offset = vmf->pgoff;
>       gfp_t gfp = vmf->gfp_mask;
>       unsigned long addr;
> @@ -72,8 +171,15 @@ static vm_fault_t secretmem_fault(struct vm_fault *vmf)
>                       goto out;
>               }
>  
> +             if (!secretmem_account_folio(state, folio)) {

Oh, this is getting really hairy :(
We need better gotos in secretmem_fault(), it could be a nice
preparation patch, but I don't feel very strongly about it.

> +                     folio_put(folio);
> +                     ret = VM_FAULT_SIGBUS;
> +                     goto out;
> +             }
> +
>               err = set_direct_map_invalid_noflush(folio_page(folio, 0));
>               if (err) {
> +                     secretmem_unaccount_folio(state, folio);
>                       folio_put(folio);
>                       ret = vmf_error(err);
>                       goto out;
> @@ -82,6 +188,7 @@ static vm_fault_t secretmem_fault(struct vm_fault *vmf)
>               __folio_mark_uptodate(folio);
>               err = filemap_add_folio(mapping, folio, offset, gfp);
>               if (unlikely(err)) {
> +                     secretmem_unaccount_folio(state, folio);
>                       /*
>                        * If a split of large page was required, it
>                        * already happened when we marked the page invalid
> @@ -115,19 +222,17 @@ static const struct vm_operations_struct 
> secretmem_vm_ops = {
>  static int secretmem_release(struct inode *inode, struct file *file)
>  {
>       atomic_dec(&secretmem_users);
> +     secretmem_destroy_inode_priv(inode);
> +
>       return 0;
>  }
>  
>  static int secretmem_mmap_prepare(struct vm_area_desc *desc)
>  {
> -     const unsigned long len = vma_desc_size(desc);
> -
>       if (!vma_desc_test_any(desc, VMA_SHARED_BIT, VMA_MAYSHARE_BIT))
>               return -EINVAL;
>  
> -     vma_desc_set_flags(desc, VMA_LOCKED_BIT, VMA_DONTDUMP_BIT);
> -     if (!mlock_future_ok(desc->mm, /*is_vma_locked=*/ true, len))
> -             return -EAGAIN;
> +     vma_desc_set_flags(desc, VMA_DONTDUMP_BIT);
>       desc->vm_ops = &secretmem_vm_ops;
>  
>       return 0;
> @@ -192,15 +297,23 @@ static struct file *secretmem_file_create(unsigned long 
> flags)
>       struct file *file;
>       struct inode *inode;
>       const char *anon_name = "[secretmem]";
> +     int err;
>  
>       inode = anon_inode_make_secure_inode(secretmem_mnt->mnt_sb, anon_name, 
> NULL);
>       if (IS_ERR(inode))
>               return ERR_CAST(inode);
>  
> +     err = secretmem_init_inode_priv(inode);
> +     if (err)
> +             goto err_free_inode;
> +
>       file = alloc_file_pseudo(inode, secretmem_mnt, "secretmem",
>                                O_RDWR | O_LARGEFILE, &secretmem_fops);
> -     if (IS_ERR(file))
> +     if (IS_ERR(file)) {
> +             secretmem_destroy_inode_priv(inode);

Please use goto for error handling here.

> +             err = PTR_ERR(file);
>               goto err_free_inode;
> +     }
>  
>       mapping_set_gfp_mask(inode->i_mapping, GFP_USER);
>       mapping_set_unevictable(inode->i_mapping);
> @@ -218,7 +331,7 @@ static struct file *secretmem_file_create(unsigned long 
> flags)
>  
>  err_free_inode:
>       iput(inode);
> -     return file;
> +     return ERR_PTR(err);
>  }
>  
>  SYSCALL_DEFINE1(memfd_secret, unsigned int, flags)
> diff --git a/tools/testing/selftests/mm/memfd_secret.c 
> b/tools/testing/selftests/mm/memfd_secret.c
> index aac4f795c327..626e7033b72f 100644
> --- a/tools/testing/selftests/mm/memfd_secret.c
> +++ b/tools/testing/selftests/mm/memfd_secret.c
> @@ -15,6 +15,8 @@
>  #include <sys/resource.h>
>  #include <sys/capability.h>
>  
> +#include <setjmp.h>
> +#include <signal.h>
>  #include <stdlib.h>
>  #include <string.h>
>  #include <unistd.h>
> @@ -22,6 +24,8 @@
>  #include <stdio.h>
>  #include <fcntl.h>
>  
> +#include <sys/mman.h>
> +
>  #include "kselftest.h"
>  
>  #define fail(fmt, ...) ksft_test_result_fail(fmt, ##__VA_ARGS__)
> @@ -31,6 +35,7 @@
>  #ifdef __NR_memfd_secret
>  
>  #define PATTERN      0x55
> +#define MLOCK_LIMIT_CAP      (8UL << 20)

A comment about why capping at 8MB would be nice.

>  static const int prot = PROT_READ | PROT_WRITE;
>  static const int mode = MAP_SHARED;
> @@ -39,6 +44,13 @@ static unsigned long page_size;
>  static unsigned long mlock_limit_cur;
>  static unsigned long mlock_limit_max;
>  
> +static sigjmp_buf fault_env;
> +
> +static void sigbus_handler(int sig)
> +{
> +     siglongjmp(fault_env, 1);
> +}
> +
>  static int memfd_secret(unsigned int flags)
>  {
>       return syscall(__NR_memfd_secret, flags);
> @@ -57,10 +69,32 @@ static void test_file_apis(int fd)
>               pass("file IO is blocked as expected\n");
>  }
>  
> -static void test_mlock_limit(int fd)
> +/* GUP disallows automatic fault-in of secretmem, so do it manually. */
> +static bool fault_in_secretmem(char *mem, size_t len)
> +{
> +     if (sigsetjmp(fault_env, 1))
> +             return false;
> +     memset(mem, PATTERN, len);
> +     return true;
> +}
> +
> +static void test_mlock_limit(void)
>  {
>       size_t len;
>       char *mem;
> +     int fd;
> +
> +     /* Locked pages have an inode lifetime, so need a new fd. */
> +     fd = memfd_secret(0);
> +     if (fd < 0) {
> +             fail("memfd_secret failed: %s\n", strerror(errno));
> +             return;
> +     }
> +
> +     if (ftruncate(fd, mlock_limit_max * 2)) {
> +             fail("ftruncate failed: %s\n", strerror(errno));
> +             goto out_close;
> +     }

Can't we ftruncate to this size in main and be done?

>  
>       len = mlock_limit_cur;
>       if (len % page_size != 0)
> @@ -69,19 +103,48 @@ static void test_mlock_limit(int fd)
>       mem = mmap(NULL, len, prot, mode, fd, 0);
>       if (mem == MAP_FAILED) {
>               fail("unable to mmap secret memory\n");
> -             return;
> +             goto out_close;
> +     }
> +
> +     if (!fault_in_secretmem(mem, len)) {
> +             munmap(mem, len);
> +             fail("unable to fault in secret memory\n");
> +             goto out_close;

Please don't mix cleanup styles, goto err_do_cleanup is the preferred
way.

-- 
Sincerely yours,
Mike.


Reply via email to