Fix refcounting problem in walk_symlink()

The old code would double-close 'symlink' if it ever got into the "walk
succeeded but we were still on a symlink" case.

Recall that walk_symlink() either walks all the way, or not at all.  If
the old code (deleted) failed, we were supposed to close symlink.
However, there was a case where walk_symlink would fail, but it would
also close the chan passed in: when the sub-walk succeeded, but
walk_symlink() still failed.  How could that fail?  With a symlink loop.

If that sounds confusing, it's because it is.  There are actually two
spots of recursion, which might not even have been clear to me when I
wrote this.  The main recursive path is walk -> walk_symlink -> walk ->
walk_symlink.  We normally never got to the "walk succeeded, then call
walk_symlink" (deleted).  We usually only called walk_symlink() from
*within* walk itself.

You could trigger this situation with a no_follow symlink
(rename/remove, flags Aremove, flags no_follow).  Syzkaller basically
did this:
	ln -s x x
	rename x/x whatever
So we were walking the first x, had no_follow set, so walk ->
walk_symlink for the first x.  Since there were more names in the
main/outer namec, we'd get past the first no_follow.  Then that second
walk() would be for "../x", where no_follow would kick in, since there
were no longer any names left.  That walk_symlink() would return the
syml passed in, which appeared to walk to be a success (which it was).

Anyway, this was calling the buggy post-successful-walk() part of
walk_symlink().  The first fix I had was to only cclose(symlink) when that
interior walk_symlink() succeeded.  Although that fixed the bug
(walk_symlink either succeeds xor closes, so don't close on success),
the real problem was having that code at all.

If walk() lands on a symlink, walk itself should try to deal with it (by
calling walk_symlink()).  If walk returns (success or failure), we
should consider the walking of that symlink to be done, one way or
another.  If it is no_follow, we might be on a symlink (if last in the
path).  If it was a mount_point, we could be on a symlink too.

That whole "we're still on a symlink, let's walk it again" was busted,
and it would break when we had no_follow set globally and tried to
follow a legit intermediate symlink that had more than one link to
follow (but not 8 - the max).  And it was confusing.  Hopefully this
code makes a little more sense - esp when realizing that walk() sorts
out symlinks by calling walk_symlink().  walk_symlink() shouldn't call
itself, but it can call walk().  Up to 8 times.

Reported-by: syzbot+9eec51df84779065d6de@syzkaller.appspotmail.com
Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
diff --git a/kern/src/ns/chan.c b/kern/src/ns/chan.c
index 3929813..02f11fd 100644
--- a/kern/src/ns/chan.c
+++ b/kern/src/ns/chan.c
@@ -1684,6 +1684,7 @@
 	struct dir *dir;
 	char *link_name, *link_store;
 	struct chan *from;
+	bool old_nofollow;
 	Elemlist e = {0};
 
 	/* mildly expensive: need to rlock the namespace */
@@ -1727,18 +1728,27 @@
 	kfree(link_store);
 
 	wh->nr_loops++;
+	/* no_follow applies to the outermost walk, i.e. the one that the
+	 * original namec performs.  At this point, we've decided that we're
+	 * going to try and follow a symlink: even if its no_follow, that only
+	 * applies to the last link in the original path.  Our sub-walks are not
+	 * no_follow.
+	 *
+	 * Note the other wh vars need to stay with the walk: nr_loops,
+	 * since its our method of detecting symlink loops, and can_mount, which
+	 * is a property of the overall namec() call. */
+	old_nofollow = wh->no_follow;
+	wh->no_follow = false;
 	if (walk(&from, e.elems, e.ARRAY_SIZEs, wh, NULL) < 0) {
 		cclose(from);
 		from = NULL;
 	} else {
+		/* We can still have a successful walk and have the new 'from'
+		 * be a symlink.  We'd need walk_symlink to return a symlink
+		 * chan, which happens if the symlink is a mount point. */
 		cclose(symlink);
-		if (from->qid.type & QTSYMLINK) {
-			symlink = from;
-			from = walk_symlink(symlink, wh, nr_names_left);
-			if (!from)
-				cclose(symlink);
-		}
 	}
+	wh->no_follow = old_nofollow;
 	wh->nr_loops--;
 
 	kfree(e.name);