// SPDX-License-Identifier: GPL-2.0 /* * Ashmem compatability for memfd * * Copyright (c) 2025, Google LLC. * Author: Isaac J. Manjarres */ #include #include #include #include #include #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