[PATCH 04/16] devpts: Teach /dev/ptmx to automount the appropriate devpts via path lookup

From: Eric W. Biederman
Date: Fri Apr 15 2016 - 11:48:33 EST


This is in preparation for forcing each mount of devpts to be a
distinct filesystem. The goal of this change is to cleanly allow
each mount of devpts to be a distince filesystem while not
introducing regressions in userspace.

On each open of /dev/ptmx look at the relative path ../pts and see if
devpts is mounted there.

If a devpts filesystem is found via the path lookup mount it's
ptmx node on /dev/ptmx.

If no devpts filesystem is found via the path lookup mount the
system devpts ptmx node on /dev/ptmx. This retains backwards
compatibility for weird setups.

This winds up using 3 new vfs helpers path_parent, path_pts, and
vfs_loopback_mount.

Additionally init_special_inode and follow_automount are updated
with calls to is_dev_ptmx to add a tiny bit of extra code in
those functions to allow /dev/ptmx to hook into the automount
path.

I endeavored to keep the vfs changes clean, but I did not strive for
generality.

Signed-off-by: "Eric W. Biederman" <ebiederm@xxxxxxxxxxxx>
---
fs/devpts/inode.c | 55 +++++++++++++++++++++++++++++++
fs/inode.c | 3 ++
fs/namei.c | 83 +++++++++++++++++++++++++++++++++++++++--------
include/linux/devpts_fs.h | 13 ++++++++
include/linux/namei.h | 2 ++
5 files changed, 143 insertions(+), 13 deletions(-)

diff --git a/fs/devpts/inode.c b/fs/devpts/inode.c
index 4fc6c49b0efd..0b84063a1e14 100644
--- a/fs/devpts/inode.c
+++ b/fs/devpts/inode.c
@@ -17,6 +17,7 @@
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/namei.h>
+#include <linux/fs_struct.h>
#include <linux/slab.h>
#include <linux/mount.h>
#include <linux/tty.h>
@@ -695,6 +696,60 @@ void devpts_pty_kill(struct inode *inode)
inode_unlock(d_inode(root));
}

+static void ptmx_expire_automounts(struct work_struct *work);
+static LIST_HEAD(ptmx_automounts);
+static DECLARE_DELAYED_WORK(ptmx_automount_work, ptmx_expire_automounts);
+static unsigned long ptmx_automount_timeout = 10 * 60 * HZ;
+
+static void ptmx_expire_automounts(struct work_struct *work)
+{
+ struct list_head *list = &ptmx_automounts;
+
+ mark_mounts_for_expiry(list);
+ if (!list_empty(list))
+ schedule_delayed_work(&ptmx_automount_work,
+ ptmx_automount_timeout);
+}
+
+struct vfsmount *ptmx_automount(struct path *input_path)
+{
+ struct vfsmount *newmnt;
+ struct path path;
+ struct dentry *old;
+
+ /* Can the pts filesystem be found with a path walk? */
+ path = *input_path;
+ path_get(&path);
+
+ if ((path_pts(&path) != 0) ||
+ /* Is the path the root of a devpts filesystem? */
+ (path.mnt->mnt_sb->s_magic != DEVPTS_SUPER_MAGIC) ||
+ (path.mnt->mnt_root != path.mnt->mnt_sb->s_root)) {
+ /* No devpts filesystem found use the system devpts */
+ path_put(&path);
+ path.mnt = devpts_mnt;
+ path.dentry = DEVPTS_SB(devpts_mnt->mnt_sb)->ptmx_dentry;
+ path_get(&path);
+ }
+ else {
+ /* Advance path to the ptmx dentry */
+ old = path.dentry;
+ path.dentry = dget(DEVPTS_SB(path.mnt->mnt_sb)->ptmx_dentry);
+ dput(old);
+ }
+
+ newmnt = vfs_loopback_mount(&path);
+ if (IS_ERR(newmnt))
+ goto fail;
+
+ mntget(newmnt);
+ mnt_set_expiry(newmnt, &ptmx_automounts);
+ schedule_delayed_work(&ptmx_automount_work, ptmx_automount_timeout);
+fail:
+ path_put(&path);
+ return newmnt;
+}
+
static int __init init_devpts_fs(void)
{
int err = register_filesystem(&devpts_fs_type);
diff --git a/fs/inode.c b/fs/inode.c
index 69b8b526c194..251330ba336e 100644
--- a/fs/inode.c
+++ b/fs/inode.c
@@ -18,6 +18,7 @@
#include <linux/buffer_head.h> /* for inode_has_buffers */
#include <linux/ratelimit.h>
#include <linux/list_lru.h>
+#include <linux/devpts_fs.h>
#include <trace/events/writeback.h>
#include "internal.h"

@@ -1917,6 +1918,8 @@ void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
+ if (is_dev_ptmx(inode))
+ inode->i_flags |= S_AUTOMOUNT;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
diff --git a/fs/namei.c b/fs/namei.c
index 794f81dce766..a4bdbeec8067 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -35,6 +35,7 @@
#include <linux/fs_struct.h>
#include <linux/posix_acl.h>
#include <linux/hash.h>
+#include <linux/devpts_fs.h>
#include <asm/uaccess.h>

#include "internal.h"
@@ -1087,10 +1088,15 @@ EXPORT_SYMBOL(follow_up);
static int follow_automount(struct path *path, struct nameidata *nd,
bool *need_mntput)
{
+ struct vfsmount *(*automount)(struct path *) = NULL;
struct vfsmount *mnt;
int err;

- if (!path->dentry->d_op || !path->dentry->d_op->d_automount)
+ if (path->dentry->d_op)
+ automount = path->dentry->d_op->d_automount;
+ if (path->dentry->d_inode && is_dev_ptmx(path->dentry->d_inode))
+ automount = ptmx_automount;
+ if (!automount)
return -EREMOTE;

/* We don't want to mount if someone's just doing a stat -
@@ -1113,7 +1119,7 @@ static int follow_automount(struct path *path, struct nameidata *nd,
if (nd->total_link_count >= 40)
return -ELOOP;

- mnt = path->dentry->d_op->d_automount(path);
+ mnt = automount(path);
if (IS_ERR(mnt)) {
/*
* The filesystem is allowed to return -EISDIR here to indicate
@@ -1415,29 +1421,41 @@ static void follow_mount(struct path *path)
}
}

-static int follow_dotdot(struct nameidata *nd)
+static int path_parent(struct path *root, struct path *path)
{
+ int ret = 0;
+
while(1) {
- struct dentry *old = nd->path.dentry;
+ struct dentry *old = path->dentry;

- if (nd->path.dentry == nd->root.dentry &&
- nd->path.mnt == nd->root.mnt) {
+ if (old == root->dentry &&
+ path->mnt == root->mnt) {
break;
}
- if (nd->path.dentry != nd->path.mnt->mnt_root) {
+ if (old != path->mnt->mnt_root) {
/* rare case of legitimate dget_parent()... */
- nd->path.dentry = dget_parent(nd->path.dentry);
+ path->dentry = dget_parent(path->dentry);
dput(old);
- if (unlikely(!path_connected(&nd->path)))
+ if (unlikely(!path_connected(path)))
return -ENOENT;
+ ret = 1;
break;
}
- if (!follow_up(&nd->path))
+ if (!follow_up(path))
break;
}
- follow_mount(&nd->path);
- nd->inode = nd->path.dentry->d_inode;
- return 0;
+ follow_mount(path);
+ return ret;
+}
+
+static int follow_dotdot(struct nameidata *nd)
+{
+ int ret = path_parent(&nd->root, &nd->path);
+ if (ret >= 0) {
+ ret = 0;
+ nd->inode = nd->path.dentry->d_inode;
+ }
+ return ret;
}

/*
@@ -2374,6 +2392,45 @@ struct dentry *lookup_one_len_unlocked(const char *name,
}
EXPORT_SYMBOL(lookup_one_len_unlocked);

+#ifdef CONFIG_UNIX98_PTYS
+int path_pts(struct path *path)
+{
+ /* A pathwalk of "../pts" with no permission checks. */
+ struct dentry *child, *parent = path->dentry;
+ struct qstr this;
+ struct path root;
+ int ret;
+
+ get_fs_root(current->fs, &root);
+ ret = path_parent(&root, path);
+ path_put(&root);
+ if (ret != 1)
+ return -ENOENT;
+
+ if (!d_can_lookup(parent))
+ return -ENOENT;
+
+ this.name = "pts";
+ this.len = 3;
+ this.hash = full_name_hash(this.name, this.len);
+ if (parent->d_flags & DCACHE_OP_HASH) {
+ int err = parent->d_op->d_hash(parent, &this);
+ if (err < 0)
+ return err;
+ }
+ inode_lock(parent->d_inode);
+ child = d_lookup(parent, &this);
+ inode_unlock(parent->d_inode);
+ if (!child)
+ return -ENOENT;
+
+ path->dentry = child;
+ dput(parent);
+ follow_mount(path);
+ return 0;
+}
+#endif
+
int user_path_at_empty(int dfd, const char __user *name, unsigned flags,
struct path *path, int *empty)
{
diff --git a/include/linux/devpts_fs.h b/include/linux/devpts_fs.h
index ff2b7c274435..5b2f6d6cd386 100644
--- a/include/linux/devpts_fs.h
+++ b/include/linux/devpts_fs.h
@@ -19,9 +19,12 @@
#define PTMX_MINOR 2

#ifdef CONFIG_UNIX98_PTYS
+#include <linux/major.h>

extern struct file_operations ptmx_fops;

+struct vfsmount *ptmx_automount(struct path *path);
+
int devpts_new_index(struct inode *ptmx_inode);
void devpts_kill_index(struct inode *ptmx_inode, int idx);
void devpts_add_ref(struct inode *ptmx_inode);
@@ -34,6 +37,10 @@ void *devpts_get_priv(struct inode *pts_inode);
/* unlink */
void devpts_pty_kill(struct inode *inode);

+static inline bool is_dev_ptmx(struct inode *inode)
+{
+ return inode->i_rdev == MKDEV(TTYAUX_MAJOR, PTMX_MINOR);
+}
#else

/* Dummy stubs in the no-pty case */
@@ -52,6 +59,12 @@ static inline void *devpts_get_priv(struct inode *pts_inode)
}
static inline void devpts_pty_kill(struct inode *inode) { }

+#define ptmx_automount NULL
+
+static inline bool is_dev_ptmx(struct inode *inode)
+{
+ return false;
+}
#endif


diff --git a/include/linux/namei.h b/include/linux/namei.h
index 77d01700daf7..f29abda31e6d 100644
--- a/include/linux/namei.h
+++ b/include/linux/namei.h
@@ -45,6 +45,8 @@ enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
#define LOOKUP_ROOT 0x2000
#define LOOKUP_EMPTY 0x4000

+extern int path_pts(struct path *path);
+
extern int user_path_at_empty(int, const char __user *, unsigned, struct path *, int *empty);

static inline int user_path_at(int dfd, const char __user *name, unsigned flags,
--
2.8.1