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);