9ns: do not allow binding a symlink onto any other name
authorBarret Rhoden <brho@cs.berkeley.edu>
Wed, 24 Apr 2019 01:07:22 +0000 (21:07 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Wed, 24 Apr 2019 01:59:37 +0000 (21:59 -0400)
Due to how walk() handles mount points, allowing binds pointing to
symlinks is difficult enough to warrant not supporting that feature.

Specifically, walk() only follows mount points for intermediate parts of
a path, e.g. /this/this/and/this/but-not-this.  If we allowed binding to
symlinks, any of those mounts could be symlinks.  The intermediate ones
are fine, but since walk() does not call domount() and follow the last
path name, we could be sitting on a symlink when walk() returns.

This might not be a huge problem, but all callers of walk() would need
to handle this - in particular namec().  Most everyone calls domount(),
so it is tempting to have domount() call walk_symlink().  However, note
the existing of undomount() (used by dotdot).  You can undo a mount, but
you can't undo a symlink follow.  So I bet you could concoct a situation
where you wanted to undomount(), but the mount/bind was to a symlink and
you're busted.

Given that binds and symlinks are largely independent implementations of
a similar concept, I don't see a need to allow a bind to point to a
symlink.

The naming around binding and symlinks can be a little confusing.  To be
clear, the scenario is:

$ ln -s file symlink
$ /bin/bind symlink /some/other/name

Both of these are in the style:
PROGRAM ACTUAL-THING-WE-WANT LOCATION-FOR-THING

- ln -s calls it TARGET and LINK_NAME
- Our bind calls it SRC_PATH and ONTO_PATH
- Plan 9's bind called it NEW OLD
- Linux's mount --bind calls it OLD NEW
- Another way to think of this is WHAT WHERE, especially when you think
  of binding devices into the namespace.

This means that when you walk to /some/other/name, you'll get the
symlink, and with the various 'follow' flags set, you'd expect to get
'file'.  Prior to this commit, walk() would get the symlink, not the
file.  Now, the bind just fails.

Note you can still bind 'from' a symlink, i.e. ONTO_PATH.  So you could
do this:

$ ln -s file1 symlink
$ cat symlink # get file1's contents
$ /bin/bind file2 symlink
$ cat symlink # get file2's contents

The file 'symlink' is still a symlink, but 'file2' was bound over it in
the namespace.  Someone else's namespace will still see symlink ->
file1.  If you do a stat or ls -l in the namespace with the bind, you'll
just see the info for 'file2'.

Reported-by: syzbot+2b1a1f196f3612be7660@syzkaller.appspotmail.com
Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
kern/src/ns/chan.c

index ae16089..459e532 100644 (file)
@@ -426,6 +426,14 @@ int cmount(struct chan *new, struct chan *old, int flag, char *spec)
        struct mhead *m, **l, *mh;
        struct mount *nm, *f, *um, **h;
 
+       /* Can't bind pointing to a symlink, since it vastly complicates namec
+        * and walk.  In particular, walk() only follows mounts on the
+        * intermediate path elements.  Grep 'ntry - 1'.  Because of that,
+        * walk() can end on a symlink.  Having domount() follow symlinks is a
+        * pain: undomount. */
+       if (new->qid.type & QTSYMLINK)
+               error(EINVAL, "cannot bind a symlink");
+
        /* Can bind anything onto a symlink's name.  Otherwise, both the old and
         * the new must agree on whether or not it is a directory. */
        if (!(old->qid.type & QTSYMLINK) &&