blob: 258498cca9bb6168799103325315cdd306dae4ad [file] [log] [blame] [edit]
// 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