213 lines
6.2 KiB
C
213 lines
6.2 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
/*
|
|
* Ashmem compatability for memfd
|
|
*
|
|
* Copyright (c) 2025, Google LLC.
|
|
* Author: Isaac J. Manjarres <isaacmanjarres@google.com>
|
|
*/
|
|
|
|
#include <asm-generic/mman-common.h>
|
|
#include <linux/capability.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/memfd.h>
|
|
#include <linux/uaccess.h>
|
|
|
|
#include "memfd-ashmem-shim.h"
|
|
#include "memfd-ashmem-shim-internal.h"
|
|
|
|
/* memfd file names all start with memfd: */
|
|
#define MEMFD_PREFIX "memfd:"
|
|
#define MEMFD_PREFIX_LEN (sizeof(MEMFD_PREFIX) - 1)
|
|
|
|
static const char *get_memfd_name(struct file *file)
|
|
{
|
|
/* This pointer is always valid, so no need to check if it's NULL. */
|
|
const char *file_name = file->f_path.dentry->d_name.name;
|
|
|
|
if (file_name != strstr(file_name, MEMFD_PREFIX))
|
|
return NULL;
|
|
|
|
return file_name;
|
|
}
|
|
|
|
static long get_name(struct file *file, void __user *name)
|
|
{
|
|
const char *file_name = get_memfd_name(file);
|
|
size_t len;
|
|
|
|
if (!file_name)
|
|
return -EINVAL;
|
|
|
|
/* Strip MEMFD_PREFIX to retain compatibility with ashmem driver. */
|
|
file_name = &file_name[MEMFD_PREFIX_LEN];
|
|
|
|
/*
|
|
* The expectation is that the user provided buffer is ASHMEM_NAME_LEN in size, which is
|
|
* larger than the maximum size of a name for a memfd buffer, so the name should always fit
|
|
* within the given buffer.
|
|
*
|
|
* However, we should ensure that the string will indeed fit in the user provided buffer.
|
|
*
|
|
* Add 1 to the copy size to account for the NUL terminator
|
|
*/
|
|
len = strlen(file_name) + 1;
|
|
if (len > ASHMEM_NAME_LEN)
|
|
return -EINVAL;
|
|
|
|
return copy_to_user(name, file_name, len) ? -EFAULT : 0;
|
|
}
|
|
|
|
static long get_prot_mask(struct file *file)
|
|
{
|
|
long prot_mask = PROT_READ | PROT_EXEC;
|
|
long seals = memfd_fcntl(file, F_GET_SEALS, 0);
|
|
|
|
if (seals < 0)
|
|
return seals;
|
|
|
|
/* memfds are readable and executable by default. Only writability can be changed. */
|
|
if (!(seals & (F_SEAL_WRITE | F_SEAL_FUTURE_WRITE)))
|
|
prot_mask |= PROT_WRITE;
|
|
|
|
return prot_mask;
|
|
}
|
|
|
|
static long set_prot_mask(struct file *file, unsigned long prot)
|
|
{
|
|
long curr_prot = get_prot_mask(file);
|
|
long ret = 0;
|
|
|
|
if (curr_prot < 0)
|
|
return curr_prot;
|
|
|
|
/*
|
|
* memfds are always readable and executable; there is no way to remove either mapping
|
|
* permission, nor is there a known usecase that requires it.
|
|
*
|
|
* Attempting to remove either of these mapping permissions will return successfully, but
|
|
* will be a nop, as the buffer will still be mappable with these permissions.
|
|
*/
|
|
prot |= PROT_READ | PROT_EXEC;
|
|
|
|
/* Only allow permissions to be removed. */
|
|
if ((curr_prot & prot) != prot)
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* Removing PROT_WRITE:
|
|
*
|
|
* We could prevent any other mappings from having write permissions by adding the
|
|
* F_SEAL_WRITE mapping. However, that would conflict with known usecases where it is
|
|
* desirable to maintain an existing writable mapping, but forbid future writable mappings.
|
|
*
|
|
* To support those usecases, we use F_SEAL_FUTURE_WRITE.
|
|
*/
|
|
if (!(prot & PROT_WRITE))
|
|
ret = memfd_fcntl(file, F_ADD_SEALS, F_SEAL_FUTURE_WRITE);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* memfd_ashmem_shim_ioctl - ioctl handler for ashmem commands
|
|
* @file: The shmem file.
|
|
* @cmd: The ioctl command.
|
|
* @arg: The argument for the ioctl command.
|
|
*
|
|
* The purpose of this handler is to allow old applications to continue working
|
|
* on newer kernels by allowing them to invoke ashmem ioctl commands on memfds.
|
|
*
|
|
* The ioctl handler attempts to retain as much compatibility with the ashmem
|
|
* driver as possible.
|
|
*/
|
|
long memfd_ashmem_shim_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
long ret = -ENOTTY;
|
|
unsigned long inode_nr;
|
|
|
|
switch (cmd) {
|
|
/*
|
|
* Older applications won't create memfds and try to use ASHMEM_SET_NAME/ASHMEM_SET_SIZE on
|
|
* them intentionally.
|
|
*
|
|
* Instead, we can end up in this scenario if an old application receives a memfd that was
|
|
* created by another process.
|
|
*
|
|
* However, the current process shouldn't expect to be able to reliably [re]name/size a
|
|
* buffer that was shared with it, since the process that shared that buffer with it, or
|
|
* any other process that references the buffer could have already mapped it.
|
|
*
|
|
* Additionally in the case of ASHMEM_SET_SIZE, when processes create memfds that are going
|
|
* to be shared with other processes in Android, they also specify the size of the memory
|
|
* region and seal the file against any size changes. Therefore, ASHMEM_SET_SIZE should not
|
|
* be supported anyway.
|
|
*
|
|
* Therefore, it is reasonable to return -EINVAL here, as if the buffer was already mapped.
|
|
*/
|
|
case ASHMEM_SET_NAME:
|
|
case ASHMEM_SET_SIZE:
|
|
ret = -EINVAL;
|
|
break;
|
|
case ASHMEM_GET_NAME:
|
|
ret = get_name(file, (void __user *)arg);
|
|
break;
|
|
case ASHMEM_GET_SIZE:
|
|
ret = i_size_read(file_inode(file));
|
|
break;
|
|
case ASHMEM_SET_PROT_MASK:
|
|
ret = set_prot_mask(file, arg);
|
|
break;
|
|
case ASHMEM_GET_PROT_MASK:
|
|
ret = get_prot_mask(file);
|
|
break;
|
|
/*
|
|
* Unpinning ashmem buffers was deprecated with the release of Android 10,
|
|
* as it did not yield any remarkable benefits. Therefore, ignore pinning
|
|
* related requests.
|
|
*
|
|
* This makes it so that memory is always "pinned" or never entirely freed
|
|
* until all references to the ashmem buffer are dropped. The memory occupied
|
|
* by the buffer is still subject to being reclaimed (swapped out) under memory
|
|
* pressure, but that is not the same as being freed.
|
|
*
|
|
* This makes it so that:
|
|
*
|
|
* 1. Memory is always pinned and therefore never purged.
|
|
* 2. Requests to unpin memory (make it a candidate for being freed) are ignored.
|
|
*/
|
|
case ASHMEM_PIN:
|
|
ret = ASHMEM_NOT_PURGED;
|
|
break;
|
|
case ASHMEM_UNPIN:
|
|
ret = 0;
|
|
break;
|
|
case ASHMEM_GET_PIN_STATUS:
|
|
ret = ASHMEM_IS_PINNED;
|
|
break;
|
|
case ASHMEM_PURGE_ALL_CACHES:
|
|
ret = capable(CAP_SYS_ADMIN) ? 0 : -EPERM;
|
|
break;
|
|
case ASHMEM_GET_FILE_ID:
|
|
inode_nr = file_inode(file)->i_ino;
|
|
if (copy_to_user((void __user *)arg, &inode_nr, sizeof(inode_nr)))
|
|
ret = -EFAULT;
|
|
else
|
|
ret = 0;
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
long memfd_ashmem_shim_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
if (cmd == COMPAT_ASHMEM_SET_SIZE)
|
|
cmd = ASHMEM_SET_SIZE;
|
|
else if (cmd == COMPAT_ASHMEM_SET_PROT_MASK)
|
|
cmd = ASHMEM_SET_PROT_MASK;
|
|
|
|
return memfd_ashmem_shim_ioctl(file, cmd, arg);
|
|
}
|
|
#endif
|