blob: 20644544802ae9d5e0245e05d4feb6da0304921e [file] [log] [blame] [edit]
// SPDX-License-Identifier: GPL-2.0
/* Provide mount topology/attribute change notifications.
*
* Copyright (C) 2019 Red Hat, Inc. All Rights Reserved.
* Written by David Howells (dhowells@redhat.com)
*/
#include <linux/fs.h>
#include <linux/namei.h>
#include <linux/syscalls.h>
#include <linux/slab.h>
#include <linux/security.h>
#include "mount.h"
/*
* Post mount notifications to all watches going rootwards along the tree.
*
* Must be called with the mount_lock held.
*/
void post_mount_notification(struct mount *changed,
struct mount_notification *notify)
{
const struct cred *cred = current_cred();
struct path cursor;
struct mount *mnt;
unsigned seq;
seq = 0;
rcu_read_lock();
restart:
cursor.mnt = &changed->mnt;
cursor.dentry = changed->mnt.mnt_root;
mnt = real_mount(cursor.mnt);
notify->watch.info &= ~NOTIFY_MOUNT_IN_SUBTREE;
read_seqbegin_or_lock(&rename_lock, &seq);
for (;;) {
if (mnt->mnt_watchers &&
!hlist_empty(&mnt->mnt_watchers->watchers)) {
if (cursor.dentry->d_flags & DCACHE_MOUNT_WATCH)
post_watch_notification(mnt->mnt_watchers,
&notify->watch, cred,
(unsigned long)cursor.dentry);
} else {
cursor.dentry = mnt->mnt.mnt_root;
}
notify->watch.info |= NOTIFY_MOUNT_IN_SUBTREE;
if (cursor.dentry == cursor.mnt->mnt_root ||
IS_ROOT(cursor.dentry)) {
struct mount *parent = READ_ONCE(mnt->mnt_parent);
/* Escaped? */
if (cursor.dentry != cursor.mnt->mnt_root)
break;
/* Global root? */
if (mnt == parent)
break;
cursor.dentry = READ_ONCE(mnt->mnt_mountpoint);
mnt = parent;
cursor.mnt = &mnt->mnt;
} else {
cursor.dentry = cursor.dentry->d_parent;
}
}
if (need_seqretry(&rename_lock, seq)) {
seq = 1;
goto restart;
}
done_seqretry(&rename_lock, seq);
rcu_read_unlock();
}
static void release_mount_watch(struct watch *watch)
{
struct dentry *dentry = (struct dentry *)(unsigned long)watch->id;
dput(dentry);
}
/**
* sys_watch_mount - Watch for mount topology/attribute changes
* @dfd: Base directory to pathwalk from or fd referring to mount.
* @filename: Path to mount to place the watch upon
* @at_flags: Pathwalk control flags
* @watch_fd: The watch queue to send notifications to.
* @watch_id: The watch ID to be placed in the notification (-1 to remove watch)
*/
SYSCALL_DEFINE5(watch_mount,
int, dfd,
const char __user *, filename,
unsigned int, at_flags,
int, watch_fd,
int, watch_id)
{
struct watch_queue *wqueue;
struct watch_list *wlist = NULL;
struct watch *watch = NULL;
struct mount *m;
struct path path;
unsigned int lookup_flags =
LOOKUP_DIRECTORY | LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;
int ret;
if (watch_id < -1 || watch_id > 0xff)
return -EINVAL;
if ((at_flags & ~(AT_NO_AUTOMOUNT | AT_EMPTY_PATH)) != 0)
return -EINVAL;
if (at_flags & AT_NO_AUTOMOUNT)
lookup_flags &= ~LOOKUP_AUTOMOUNT;
if (at_flags & AT_EMPTY_PATH)
lookup_flags |= LOOKUP_EMPTY;
ret = user_path_at(dfd, filename, lookup_flags, &path);
if (ret)
return ret;
ret = inode_permission(path.dentry->d_inode, MAY_EXEC);
if (ret)
goto err_path;
wqueue = get_watch_queue(watch_fd);
if (IS_ERR(wqueue))
goto err_path;
m = real_mount(path.mnt);
if (watch_id >= 0) {
ret = -ENOMEM;
if (!m->mnt_watchers) {
wlist = kzalloc(sizeof(*wlist), GFP_KERNEL);
if (!wlist)
goto err_wqueue;
init_watch_list(wlist, release_mount_watch);
}
watch = kzalloc(sizeof(*watch), GFP_KERNEL);
if (!watch)
goto err_wlist;
init_watch(watch, wqueue);
watch->id = (unsigned long)path.dentry;
watch->info_id = (u32)watch_id << 24;
ret = security_watch_mount(watch, &path);
if (ret < 0)
goto err_watch;
down_write(&m->mnt.mnt_sb->s_umount);
if (!m->mnt_watchers) {
m->mnt_watchers = wlist;
wlist = NULL;
}
ret = add_watch_to_object(watch, m->mnt_watchers);
if (ret == 0) {
spin_lock(&path.dentry->d_lock);
path.dentry->d_flags |= DCACHE_MOUNT_WATCH;
spin_unlock(&path.dentry->d_lock);
dget(path.dentry);
watch = NULL;
}
up_write(&m->mnt.mnt_sb->s_umount);
} else {
ret = -EBADSLT;
if (m->mnt_watchers) {
down_write(&m->mnt.mnt_sb->s_umount);
ret = remove_watch_from_object(m->mnt_watchers, wqueue,
(unsigned long)path.dentry,
false);
up_write(&m->mnt.mnt_sb->s_umount);
}
}
err_watch:
kfree(watch);
err_wlist:
kfree(wlist);
err_wqueue:
put_watch_queue(wqueue);
err_path:
path_put(&path);
return ret;
}