Add a mount notification facility whereby notifications about changes in mount topology and configuration can be received. Note that this only covers vfsmount topology changes and not superblock events. A separate facility will be added for that. Every mount is given a change counter than counts the number of topological rearrangements in which it is involved and the number of attribute changes it undergoes. This allows notification loss to be dealt with. Later patches will provide a way to quickly retrieve this value, along with information about topology and parameters for the superblock. Firstly, a watch queue needs to be created: pipe2(fds, O_NOTIFICATION_PIPE); ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256); then a notification can be set up to report notifications via that queue: struct watch_notification_filter filter = { .nr_filters = 1, .filters = { [0] = { .type = WATCH_TYPE_MOUNT_NOTIFY, .subtype_filter[0] = UINT_MAX, }, }, }; ioctl(fds[1], IOC_WATCH_QUEUE_SET_FILTER, &filter); watch_mount(AT_FDCWD, "/", 0, fds[1], 0x02); In this case, it would let me monitor the mount topology subtree rooted at "/" for events. Mount notifications propagate up the tree towards the root, so a watch will catch all of the events happening in the subtree rooted at the watch. After setting the watch, records will be placed into the queue when, for example, as superblock switches between read-write and read-only. Records are of the following format: struct mount_notification { struct watch_notification watch; __u32 triggered_on; __u32 auxiliary_mount; __u32 topology_changes; __u32 attr_changes; __u32 aux_topology_changes; } *n; Where: n->watch.type will be WATCH_TYPE_MOUNT_NOTIFY. n->watch.subtype will indicate the type of event, such as NOTIFY_MOUNT_NEW_MOUNT. n->watch.info & WATCH_INFO_LENGTH will indicate the length of the record. n->watch.info & WATCH_INFO_ID will be the fifth argument to watch_mount(), shifted. n->watch.info & NOTIFY_MOUNT_IN_SUBTREE if true indicates that the notification was generated in the mount subtree rooted at the watch, and not actually in the watch itself. n->watch.info & NOTIFY_MOUNT_IS_RECURSIVE if true indicates that the notification was generated by an event (eg. SETATTR) that was applied recursively. The notification is only generated for the object that initially triggered it. n->watch.info & NOTIFY_MOUNT_IS_NOW_RO will be used for NOTIFY_MOUNT_READONLY, being set if the mount becomes R/O, and being cleared otherwise, and for NOTIFY_MOUNT_NEW_MOUNT, being set if the new mount is readonly. n->watch.info & NOTIFY_MOUNT_IS_SUBMOUNT if true indicates that the NOTIFY_MOUNT_NEW_MOUNT notification is in response to a mount performed by the kernel (e.g. an automount). n->triggered_on indicates the ID of the mount to which the change was accounted (e.g. the new parent of a new mount). n->axiliary_mount indicates the ID of an additional mount that was affected (e.g. a new mount itself) or 0. n->topology_changes provides the value of the topology change counter of the triggered-on mount at the conclusion of the operation. n->attr_changes provides the value of the attribute change counter of the triggered-on mount at the conclusion of the operation. n->aux_topology_changes provides the value of the topology change counter of the auxiliary mount at the conclusion of the operation. Note that it is permissible for event records to be of variable length - or, at least, the length may be dependent on the subtype. Note also that the queue can be shared between multiple notifications of various types. Signed-off-by: David Howells <dhowells@xxxxxxxxxx> --- Documentation/watch_queue.rst | 12 + arch/alpha/kernel/syscalls/syscall.tbl | 1 arch/arm/tools/syscall.tbl | 1 arch/arm64/include/asm/unistd32.h | 2 arch/ia64/kernel/syscalls/syscall.tbl | 1 arch/m68k/kernel/syscalls/syscall.tbl | 1 arch/microblaze/kernel/syscalls/syscall.tbl | 1 arch/mips/kernel/syscalls/syscall_n32.tbl | 1 arch/mips/kernel/syscalls/syscall_n64.tbl | 1 arch/mips/kernel/syscalls/syscall_o32.tbl | 1 arch/parisc/kernel/syscalls/syscall.tbl | 1 arch/powerpc/kernel/syscalls/syscall.tbl | 1 arch/s390/kernel/syscalls/syscall.tbl | 1 arch/sh/kernel/syscalls/syscall.tbl | 1 arch/sparc/kernel/syscalls/syscall.tbl | 1 arch/x86/entry/syscalls/syscall_32.tbl | 1 arch/x86/entry/syscalls/syscall_64.tbl | 1 arch/xtensa/kernel/syscalls/syscall.tbl | 1 fs/Kconfig | 9 + fs/Makefile | 1 fs/mount.h | 21 ++ fs/mount_notify.c | 228 +++++++++++++++++++++++++++ fs/namespace.c | 22 +++ include/linux/dcache.h | 1 include/linux/syscalls.h | 2 include/uapi/asm-generic/unistd.h | 4 include/uapi/linux/watch_queue.h | 36 ++++ kernel/sys_ni.c | 3 28 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 fs/mount_notify.c diff --git a/Documentation/watch_queue.rst b/Documentation/watch_queue.rst index 849fad6893ef..3e647992be31 100644 --- a/Documentation/watch_queue.rst +++ b/Documentation/watch_queue.rst @@ -8,6 +8,7 @@ opened by userspace. This can be used in conjunction with:: * Key/keyring notifications + * Mount notifications. The notifications buffers can be enabled by: @@ -233,6 +234,11 @@ Any particular buffer can be fed from multiple sources. Sources include: See Documentation/security/keys/core.rst for more information. + * WATCH_TYPE_MOUNT_NOTIFY + + Notifications of this type indicate changes to mount attributes and the + mount topology within the subtree at the indicated point. + Event Filtering =============== @@ -292,9 +298,10 @@ A buffer is created with something like the following:: pipe2(fds, O_TMPFILE); ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256); -It can then be set to receive keyring change notifications:: +It can then be set to receive notifications:: keyctl(KEYCTL_WATCH_KEY, KEY_SPEC_SESSION_KEYRING, fds[1], 0x01); + watch_mount(AT_FDCWD, "/", 0, fds[1], 0x02); The notifications can then be consumed by something like the following:: @@ -331,6 +338,9 @@ The notifications can then be consumed by something like the following:: case WATCH_TYPE_KEY_NOTIFY: saw_key_change(&n.n); break; + case WATCH_TYPE_MOUNT_NOTIFY: + saw_mount_change(&n.n); + break; } p += len; diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl index 5ddd128d4b7a..b6cf8403da35 100644 --- a/arch/alpha/kernel/syscalls/syscall.tbl +++ b/arch/alpha/kernel/syscalls/syscall.tbl @@ -478,3 +478,4 @@ 547 common openat2 sys_openat2 548 common pidfd_getfd sys_pidfd_getfd 549 common faccessat2 sys_faccessat2 +550 common watch_mount sys_watch_mount diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl index d5cae5ffede0..27cc1f53f4a0 100644 --- a/arch/arm/tools/syscall.tbl +++ b/arch/arm/tools/syscall.tbl @@ -452,3 +452,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/arm64/include/asm/unistd32.h b/arch/arm64/include/asm/unistd32.h index 6d95d0c8bf2f..4f9cf98cdf0f 100644 --- a/arch/arm64/include/asm/unistd32.h +++ b/arch/arm64/include/asm/unistd32.h @@ -885,6 +885,8 @@ __SYSCALL(__NR_openat2, sys_openat2) __SYSCALL(__NR_pidfd_getfd, sys_pidfd_getfd) #define __NR_faccessat2 439 __SYSCALL(__NR_faccessat2, sys_faccessat2) +#define __NR_watch_mount 440 +__SYSCALL(__NR_watch_mount, sys_watch_mount) /* * Please add new compat syscalls above this comment and update diff --git a/arch/ia64/kernel/syscalls/syscall.tbl b/arch/ia64/kernel/syscalls/syscall.tbl index 49e325b604b3..fc6d87903781 100644 --- a/arch/ia64/kernel/syscalls/syscall.tbl +++ b/arch/ia64/kernel/syscalls/syscall.tbl @@ -359,3 +359,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl index f71b1bbcc198..c671aa0e4d25 100644 --- a/arch/m68k/kernel/syscalls/syscall.tbl +++ b/arch/m68k/kernel/syscalls/syscall.tbl @@ -438,3 +438,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl index edacc4561f2b..65cc53f129ef 100644 --- a/arch/microblaze/kernel/syscalls/syscall.tbl +++ b/arch/microblaze/kernel/syscalls/syscall.tbl @@ -444,3 +444,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl index f777141f5256..7f034a239930 100644 --- a/arch/mips/kernel/syscalls/syscall_n32.tbl +++ b/arch/mips/kernel/syscalls/syscall_n32.tbl @@ -377,3 +377,4 @@ 437 n32 openat2 sys_openat2 438 n32 pidfd_getfd sys_pidfd_getfd 439 n32 faccessat2 sys_faccessat2 +440 n32 watch_mount sys_watch_mount diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl index da8c76394e17..d39b90de3642 100644 --- a/arch/mips/kernel/syscalls/syscall_n64.tbl +++ b/arch/mips/kernel/syscalls/syscall_n64.tbl @@ -353,3 +353,4 @@ 437 n64 openat2 sys_openat2 438 n64 pidfd_getfd sys_pidfd_getfd 439 n64 faccessat2 sys_faccessat2 +440 n64 watch_mount sys_watch_mount diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl index 13280625d312..09f426cb45b1 100644 --- a/arch/mips/kernel/syscalls/syscall_o32.tbl +++ b/arch/mips/kernel/syscalls/syscall_o32.tbl @@ -426,3 +426,4 @@ 437 o32 openat2 sys_openat2 438 o32 pidfd_getfd sys_pidfd_getfd 439 o32 faccessat2 sys_faccessat2 +440 o32 watch_mount sys_watch_mount diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl index 5a758fa6ec52..52ff3454baa1 100644 --- a/arch/parisc/kernel/syscalls/syscall.tbl +++ b/arch/parisc/kernel/syscalls/syscall.tbl @@ -436,3 +436,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl index f833a3190822..10b7ed3c7a1b 100644 --- a/arch/powerpc/kernel/syscalls/syscall.tbl +++ b/arch/powerpc/kernel/syscalls/syscall.tbl @@ -528,3 +528,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl index bfdcb7633957..86f317bf52df 100644 --- a/arch/s390/kernel/syscalls/syscall.tbl +++ b/arch/s390/kernel/syscalls/syscall.tbl @@ -441,3 +441,4 @@ 437 common openat2 sys_openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount sys_watch_mount diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl index acc35daa1b79..0bb0f0b372c7 100644 --- a/arch/sh/kernel/syscalls/syscall.tbl +++ b/arch/sh/kernel/syscalls/syscall.tbl @@ -441,3 +441,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl index 8004a276cb74..369ab65c1e9a 100644 --- a/arch/sparc/kernel/syscalls/syscall.tbl +++ b/arch/sparc/kernel/syscalls/syscall.tbl @@ -484,3 +484,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl index d8f8a1a69ed1..e760ba92c58d 100644 --- a/arch/x86/entry/syscalls/syscall_32.tbl +++ b/arch/x86/entry/syscalls/syscall_32.tbl @@ -443,3 +443,4 @@ 437 i386 openat2 sys_openat2 438 i386 pidfd_getfd sys_pidfd_getfd 439 i386 faccessat2 sys_faccessat2 +440 i386 watch_mount sys_watch_mount diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl index 78847b32e137..5b58621d4f75 100644 --- a/arch/x86/entry/syscalls/syscall_64.tbl +++ b/arch/x86/entry/syscalls/syscall_64.tbl @@ -360,6 +360,7 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount # # x32-specific system call numbers start at 512 to avoid cache impact diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl index 69d0d73876b3..5b28ee39f70f 100644 --- a/arch/xtensa/kernel/syscalls/syscall.tbl +++ b/arch/xtensa/kernel/syscalls/syscall.tbl @@ -409,3 +409,4 @@ 437 common openat2 sys_openat2 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 +440 common watch_mount sys_watch_mount diff --git a/fs/Kconfig b/fs/Kconfig index a88aa3af73c1..1a55e56d5c54 100644 --- a/fs/Kconfig +++ b/fs/Kconfig @@ -117,6 +117,15 @@ source "fs/verity/Kconfig" source "fs/notify/Kconfig" +config MOUNT_NOTIFICATIONS + bool "Mount topology change notifications" + select WATCH_QUEUE + help + This option provides support for getting change notifications on the + mount tree topology. This makes use of the /dev/watch_queue misc + device to handle the notification buffer and provides the + mount_notify() system call to enable/disable watchpoints. + source "fs/quota/Kconfig" source "fs/autofs/Kconfig" diff --git a/fs/Makefile b/fs/Makefile index 2ce5112b02c8..dd0d87e2ef19 100644 --- a/fs/Makefile +++ b/fs/Makefile @@ -22,6 +22,7 @@ obj-y += no-block.o endif obj-$(CONFIG_PROC_FS) += proc_namespace.o +obj-$(CONFIG_MOUNT_NOTIFICATIONS) += mount_notify.o obj-y += notify/ obj-$(CONFIG_EPOLL) += eventpoll.o diff --git a/fs/mount.h b/fs/mount.h index c7abb7b394d8..1c777f651446 100644 --- a/fs/mount.h +++ b/fs/mount.h @@ -4,6 +4,7 @@ #include <linux/poll.h> #include <linux/ns_common.h> #include <linux/fs_pin.h> +#include <linux/watch_queue.h> struct mnt_namespace { atomic_t count; @@ -78,6 +79,12 @@ struct mount { int mnt_expiry_mark; /* true if marked for expiry */ struct hlist_head mnt_pins; struct hlist_head mnt_stuck_children; +#ifdef CONFIG_MOUNT_NOTIFICATIONS + atomic_t mnt_topology_changes; /* Number of topology changes applied */ + atomic_t mnt_attr_changes; /* Number of attribute changes applied */ + atomic_t mnt_subtree_notifications; /* Number of notifications in subtree */ + struct watch_list *mnt_watchers; /* Watches on dentries within this mount */ +#endif } __randomize_layout; #define MNT_NS_INTERNAL ERR_PTR(-EINVAL) /* distinct from any mnt_namespace */ @@ -159,3 +166,17 @@ static inline bool is_anon_ns(struct mnt_namespace *ns) } extern void mnt_cursor_del(struct mnt_namespace *ns, struct mount *cursor); + +#ifdef CONFIG_MOUNT_NOTIFICATIONS +extern void notify_mount(struct mount *triggered, + struct mount *aux, + enum mount_notification_subtype subtype, + u32 info_flags); +#else +static inline void notify_mount(struct mount *triggered, + struct mount *aux, + enum mount_notification_subtype subtype, + u32 info_flags) +{ +} +#endif diff --git a/fs/mount_notify.c b/fs/mount_notify.c new file mode 100644 index 000000000000..365aac5fa746 --- /dev/null +++ b/fs/mount_notify.c @@ -0,0 +1,228 @@ +// 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@xxxxxxxxxx) + */ + +#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. + */ +static 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, + ¬ify->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; + atomic_inc(&mnt->mnt_subtree_notifications); + } 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(); +} + +/* + * Generate a mount notification. + */ +void notify_mount(struct mount *trigger, + struct mount *aux, + enum mount_notification_subtype subtype, + u32 info_flags) +{ + + struct mount_notification n; + + memset(&n, 0, sizeof(n)); + n.watch.type = WATCH_TYPE_MOUNT_NOTIFY; + n.watch.subtype = subtype; + n.watch.info = info_flags | watch_sizeof(n); + n.triggered_on = trigger->mnt_id; + + switch (subtype) { + case NOTIFY_MOUNT_EXPIRY: + case NOTIFY_MOUNT_READONLY: + case NOTIFY_MOUNT_SETATTR: + n.topology_changes = atomic_read(&trigger->mnt_topology_changes); + n.attr_changes = atomic_inc_return(&trigger->mnt_attr_changes); + break; + + case NOTIFY_MOUNT_NEW_MOUNT: + case NOTIFY_MOUNT_UNMOUNT: + case NOTIFY_MOUNT_MOVE_FROM: + case NOTIFY_MOUNT_MOVE_TO: + n.auxiliary_mount = aux->mnt_id, + n.attr_changes = atomic_read(&trigger->mnt_attr_changes); + n.topology_changes = atomic_inc_return(&trigger->mnt_topology_changes); + n.aux_topology_changes = atomic_inc_return(&aux->mnt_topology_changes); + break; + + default: + BUG(); + } + + post_mount_notification(trigger, &n); +} + +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 (!READ_ONCE(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 << WATCH_INFO_ID__SHIFT; + + 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 { + 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; +} diff --git a/fs/namespace.c b/fs/namespace.c index 4a0f600a3328..73ff5bf0c9af 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -498,6 +498,9 @@ static int mnt_make_readonly(struct mount *mnt) smp_wmb(); mnt->mnt.mnt_flags &= ~MNT_WRITE_HOLD; unlock_mount_hash(); + if (ret == 0) + notify_mount(mnt, NULL, NOTIFY_MOUNT_READONLY, + NOTIFY_MOUNT_IS_NOW_RO); return ret; } @@ -506,6 +509,7 @@ static int __mnt_unmake_readonly(struct mount *mnt) lock_mount_hash(); mnt->mnt.mnt_flags &= ~MNT_READONLY; unlock_mount_hash(); + notify_mount(mnt, NULL, NOTIFY_MOUNT_READONLY, 0); return 0; } @@ -835,6 +839,7 @@ static struct mountpoint *unhash_mnt(struct mount *mnt) */ static void umount_mnt(struct mount *mnt) { + notify_mount(mnt->mnt_parent, mnt, NOTIFY_MOUNT_UNMOUNT, 0); put_mountpoint(unhash_mnt(mnt)); } @@ -1175,6 +1180,11 @@ static void mntput_no_expire(struct mount *mnt) mnt->mnt.mnt_flags |= MNT_DOOMED; rcu_read_unlock(); +#ifdef CONFIG_MOUNT_NOTIFICATIONS + if (mnt->mnt_watchers) + remove_watch_list(mnt->mnt_watchers, mnt->mnt_id); +#endif + list_del(&mnt->mnt_instance); if (unlikely(!list_empty(&mnt->mnt_mounts))) { @@ -1503,6 +1513,7 @@ static void umount_tree(struct mount *mnt, enum umount_tree_flags how) p = list_first_entry(&tmp_list, struct mount, mnt_list); list_del_init(&p->mnt_expire); list_del_init(&p->mnt_list); + ns = p->mnt_ns; if (ns) { ns->mounts--; @@ -2137,7 +2148,10 @@ static int attach_recursive_mnt(struct mount *source_mnt, } if (moving) { unhash_mnt(source_mnt); + notify_mount(source_mnt->mnt_parent, source_mnt, + NOTIFY_MOUNT_MOVE_FROM, 0); attach_mnt(source_mnt, dest_mnt, dest_mp); + notify_mount(dest_mnt, source_mnt, NOTIFY_MOUNT_MOVE_TO, 0); touch_mnt_namespace(source_mnt->mnt_ns); } else { if (source_mnt->mnt_ns) { @@ -2146,6 +2160,11 @@ static int attach_recursive_mnt(struct mount *source_mnt, } mnt_set_mountpoint(dest_mnt, dest_mp, source_mnt); commit_tree(source_mnt); + notify_mount(dest_mnt, source_mnt, NOTIFY_MOUNT_NEW_MOUNT, + (source_mnt->mnt.mnt_sb->s_flags & SB_RDONLY ? + NOTIFY_MOUNT_IS_NOW_RO : 0) | + (source_mnt->mnt.mnt_sb->s_flags & SB_SUBMOUNT ? + NOTIFY_MOUNT_IS_SUBMOUNT : 0)); } hlist_for_each_entry_safe(child, n, &tree_list, mnt_hash) { @@ -2522,6 +2541,8 @@ static void set_mount_attributes(struct mount *mnt, unsigned int mnt_flags) mnt->mnt.mnt_flags = mnt_flags; touch_mnt_namespace(mnt->mnt_ns); unlock_mount_hash(); + notify_mount(mnt, NULL, NOTIFY_MOUNT_SETATTR, + (mnt_flags & SB_RDONLY ? NOTIFY_MOUNT_IS_NOW_RO : 0)); } static void mnt_warn_timestamp_expiry(struct path *mountpoint, struct vfsmount *mnt) @@ -2992,6 +3013,7 @@ void mark_mounts_for_expiry(struct list_head *mounts) propagate_mount_busy(mnt, 1)) continue; list_move(&mnt->mnt_expire, &graveyard); + notify_mount(mnt, NULL, NOTIFY_MOUNT_EXPIRY, 0); } while (!list_empty(&graveyard)) { mnt = list_first_entry(&graveyard, struct mount, mnt_expire); diff --git a/include/linux/dcache.h b/include/linux/dcache.h index a81f0c3cf352..a94c551c62a3 100644 --- a/include/linux/dcache.h +++ b/include/linux/dcache.h @@ -219,6 +219,7 @@ struct dentry_operations { #define DCACHE_PAR_LOOKUP 0x10000000 /* being looked up (with parent locked shared) */ #define DCACHE_DENTRY_CURSOR 0x20000000 #define DCACHE_NORCU 0x40000000 /* No RCU delay for freeing */ +#define DCACHE_MOUNT_WATCH 0x80000000 /* There's a mount watch here */ extern seqlock_t rename_lock; diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h index b951a87da987..88d03fd627ab 100644 --- a/include/linux/syscalls.h +++ b/include/linux/syscalls.h @@ -1005,6 +1005,8 @@ asmlinkage long sys_pidfd_send_signal(int pidfd, int sig, siginfo_t __user *info, unsigned int flags); asmlinkage long sys_pidfd_getfd(int pidfd, int fd, unsigned int flags); +asmlinkage long sys_watch_mount(int dfd, const char __user *path, + unsigned int at_flags, int watch_fd, int watch_id); /* * Architecture-specific system calls diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h index f4a01305d9a6..fcdca8c7d30a 100644 --- a/include/uapi/asm-generic/unistd.h +++ b/include/uapi/asm-generic/unistd.h @@ -857,9 +857,11 @@ __SYSCALL(__NR_openat2, sys_openat2) __SYSCALL(__NR_pidfd_getfd, sys_pidfd_getfd) #define __NR_faccessat2 439 __SYSCALL(__NR_faccessat2, sys_faccessat2) +#define __NR_watch_mount 440 +__SYSCALL(__NR_watch_mount, sys_watch_mount) #undef __NR_syscalls -#define __NR_syscalls 440 +#define __NR_syscalls 441 /* * 32 bit systems traditionally used different diff --git a/include/uapi/linux/watch_queue.h b/include/uapi/linux/watch_queue.h index c3d8320b5d3a..6b6cd2afc590 100644 --- a/include/uapi/linux/watch_queue.h +++ b/include/uapi/linux/watch_queue.h @@ -14,7 +14,8 @@ enum watch_notification_type { WATCH_TYPE_META = 0, /* Special record */ WATCH_TYPE_KEY_NOTIFY = 1, /* Key change event notification */ - WATCH_TYPE__NR = 2 + WATCH_TYPE_MOUNT_NOTIFY = 2, /* Mount topology change notification */ + WATCH_TYPE___NR = 3 }; enum watch_meta_notification_subtype { @@ -101,4 +102,37 @@ struct key_notification { __u32 aux; /* Per-type auxiliary data */ }; +/* + * Type of mount topology change notification. + */ +enum mount_notification_subtype { + NOTIFY_MOUNT_NEW_MOUNT = 0, /* New mount added */ + NOTIFY_MOUNT_UNMOUNT = 1, /* Mount removed manually */ + NOTIFY_MOUNT_EXPIRY = 2, /* Automount expired */ + NOTIFY_MOUNT_READONLY = 3, /* Mount R/O state changed */ + NOTIFY_MOUNT_SETATTR = 4, /* Mount attributes changed */ + NOTIFY_MOUNT_MOVE_FROM = 5, /* Mount moved from here */ + NOTIFY_MOUNT_MOVE_TO = 6, /* Mount moved to here (compare op_id) */ +}; + +#define NOTIFY_MOUNT_IN_SUBTREE WATCH_INFO_FLAG_0 /* Event not actually at watched dentry */ +#define NOTIFY_MOUNT_IS_RECURSIVE WATCH_INFO_FLAG_1 /* Change applied recursively */ +#define NOTIFY_MOUNT_IS_NOW_RO WATCH_INFO_FLAG_2 /* Mount changed to R/O */ +#define NOTIFY_MOUNT_IS_SUBMOUNT WATCH_INFO_FLAG_3 /* New mount is submount */ + +/* + * Mount topology/configuration change notification record. + * - watch.type = WATCH_TYPE_MOUNT_NOTIFY + * - watch.subtype = enum mount_notification_subtype + */ +struct mount_notification { + struct watch_notification watch; /* WATCH_TYPE_MOUNT_NOTIFY */ + __u32 triggered_on; /* The mount that triggered the notification */ + __u32 auxiliary_mount; /* Added/moved/removed mount or 0 */ + __u32 topology_changes; /* trigger: Number of topology changes applied */ + __u32 attr_changes; /* trigger: Number of attribute changes applied */ + __u32 aux_topology_changes; /* aux: Number of topology changes applied */ + __u32 __padding; +}; + #endif /* _UAPI_LINUX_WATCH_QUEUE_H */ diff --git a/kernel/sys_ni.c b/kernel/sys_ni.c index 3b69a560a7ac..3e1c5c9d2efe 100644 --- a/kernel/sys_ni.c +++ b/kernel/sys_ni.c @@ -85,6 +85,9 @@ COND_SYSCALL(ioprio_get); /* fs/locks.c */ COND_SYSCALL(flock); +/* fs/mount_notify.c */ +COND_SYSCALL(watch_mount); + /* fs/namei.c */ /* fs/namespace.c */