When sharing a dma-buf between components of different trust levels, the allocator may need to hand out a read-only view of a buffer it holds with read-write access. Currently there is no mechanism to do this: the file flags set at allocation time are fixed for the lifetime of the dma-buf, and dup(2) and dup3(2) cannot change the access mode of the new fd.
Add DMA_BUF_IOCTL_DERIVE, which takes a struct dma_buf_derive carrying the desired access flags and returns a new file descriptor for the same buffer with those flags applied. Permission escalation is rejected with EACCES.
The new fd aliases the same struct dma_buf, same dma_resv, same exporter ops, same underlying memory. Importers that attach to either fd operate on the same object and observe the same fence timeline.
To support multiple struct file instances sharing one struct dma_buf, two small internal adjustments are required. First, move __dma_buf_list_del() to dma_buf_release() so that list removal fires exactly once when the dentry is destroyed. Second, update dma_buf_file_release() to call dma_buf_put() only for the files that are not primary dmabuf files, leaving the primary fd's refcount managed by the normal dentry lifecycle.
Finally, enforce the access restriction in dma_buf_mmap_internal(): a shared writable mapping (MAP_SHARED + PROT_WRITE) on a read-only fd is rejected with -EACCES. Without this check, O_RDONLY on a dma-buf fd would be cosmetic, as the VFS does not enforce f_mode for writable mmap on anonymous inodes.
Signed-off-by: Albert Esteve aesteve@redhat.com --- drivers/dma-buf/dma-buf.c | 58 +++++++++++++++++++++++++++++++++++++++++++- include/uapi/linux/dma-buf.h | 28 +++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/drivers/dma-buf/dma-buf.c b/drivers/dma-buf/dma-buf.c index 71f37544a5c61..34a3872365730 100644 --- a/drivers/dma-buf/dma-buf.c +++ b/drivers/dma-buf/dma-buf.c @@ -180,6 +180,7 @@ static void dma_buf_release(struct dentry *dentry) */ BUG_ON(dmabuf->cb_in.active || dmabuf->cb_out.active);
+ __dma_buf_list_del(dmabuf); dmabuf->ops->release(dmabuf);
if (dmabuf->resv == (struct dma_resv *)&dmabuf[1]) @@ -193,10 +194,13 @@ static void dma_buf_release(struct dentry *dentry)
static int dma_buf_file_release(struct inode *inode, struct file *file) { + struct dma_buf *dmabuf = file->private_data; + if (!is_dma_buf_file(file)) return -EINVAL;
- __dma_buf_list_del(file->private_data); + if (file != dmabuf->file) + dma_buf_put(dmabuf);
return 0; } @@ -232,6 +236,11 @@ static int dma_buf_mmap_internal(struct file *file, struct vm_area_struct *vma) if (!is_dma_buf_file(file)) return -EINVAL;
+ if ((vma->vm_flags & VM_WRITE) && + (vma->vm_flags & VM_SHARED) && + !(file->f_mode & FMODE_WRITE)) + return -EACCES; + dmabuf = file->private_data;
/* check if buffer supports mmap */ @@ -537,6 +546,50 @@ static long dma_buf_import_sync_file(struct dma_buf *dmabuf, } #endif
+static const struct file_operations dma_buf_fops; + +static int dma_buf_ioctl_derive(struct dma_buf *dmabuf, struct file *file, + void __user *udata) +{ + struct dma_buf_derive params; + struct file *new_file; + int new_fd; + + if (copy_from_user(¶ms, udata, sizeof(params))) + return -EFAULT; + + if (params.flags & ~(O_ACCMODE | O_CLOEXEC)) + return -EINVAL; + + /* Escalating permissions is not allowed. */ + if ((params.flags & O_ACCMODE) == O_RDWR && + !(file->f_mode & FMODE_WRITE)) + return -EACCES; + + new_file = alloc_file_clone(dmabuf->file, params.flags, &dma_buf_fops); + if (IS_ERR(new_file)) + return PTR_ERR(new_file); + + get_dma_buf(dmabuf); + new_file->private_data = dmabuf; + + new_fd = get_unused_fd_flags(params.flags & O_CLOEXEC ? O_CLOEXEC : 0); + if (new_fd < 0) { + fput(new_file); + return new_fd; + } + + params.fd = new_fd; + if (copy_to_user(udata, ¶ms, sizeof(params))) { + put_unused_fd(new_fd); + fput(new_file); + return -EFAULT; + } + + fd_install(new_fd, new_file); + return 0; +} + static long dma_buf_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { @@ -587,6 +640,9 @@ static long dma_buf_ioctl(struct file *file, return dma_buf_import_sync_file(dmabuf, (const void __user *)arg); #endif
+ case DMA_BUF_IOCTL_DERIVE: + return dma_buf_ioctl_derive(dmabuf, file, (void __user *)arg); + default: return -ENOTTY; } diff --git a/include/uapi/linux/dma-buf.h b/include/uapi/linux/dma-buf.h index e827c9d20c5d3..d0cf616228e55 100644 --- a/include/uapi/linux/dma-buf.h +++ b/include/uapi/linux/dma-buf.h @@ -168,6 +168,33 @@ struct dma_buf_import_sync_file { __s32 fd; };
+/** + * struct dma_buf_derive - Obtain a dma-buf fd with reduced access permissions + * + * Userspace can perform a DMA_BUF_IOCTL_DERIVE to obtain a second file + * descriptor for the same dma-buf with a subset of the calling fd's + * permissions. This allows a producer holding read-write access to hand a + * read-only view to a less-privileged consumer without giving up its own + * write access or allocating a separate buffer. + * + * Unlike first-export ioctls, the new fd is not a re-export. It shares the + * same reservation object, exporter ops, and underlying memory as the + * original. + * + * The requested permissions must not exceed those of the calling fd. + */ +struct dma_buf_derive { + /** + * @flags: Requested access flags. + * + * Accepts O_RDONLY or O_RDWR, optionally combined with O_CLOEXEC. + * All other bits must be zero. + */ + __u32 flags; + /** @fd: Returned file descriptor with the requested permissions */ + __s32 fd; +}; + #define DMA_BUF_BASE 'b' #define DMA_BUF_IOCTL_SYNC _IOW(DMA_BUF_BASE, 0, struct dma_buf_sync)
@@ -179,5 +206,6 @@ struct dma_buf_import_sync_file { #define DMA_BUF_SET_NAME_B _IOW(DMA_BUF_BASE, 1, __u64) #define DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR(DMA_BUF_BASE, 2, struct dma_buf_export_sync_file) #define DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW(DMA_BUF_BASE, 3, struct dma_buf_import_sync_file) +#define DMA_BUF_IOCTL_DERIVE _IOWR(DMA_BUF_BASE, 4, struct dma_buf_derive)
#endif