|
|
Log in / Subscribe / Register

A case for variant symlinks

March 23, 2016

This article was contributed by Neil Brown

Variant symlinks are symbolic links that behave differently depending on details of the process that reads or follows the link. They have a history going back at least to the 1980s when various vendors of Unix systems wanted to be compatible with both BSD Unix from UCB (The University of California at Berkeley), and System V Unix from AT&T. Details varied, but the core idea was that some attribute of a process could be used to modify the target of a symlink or to select among multiple options. This would allow, for example, some processes to see /bin as a symlink to /.ucbbin, while others would see /.attbin.

While those issues are long behind us, the desire for variant symlinks still pops up from time to time, most recently in a proposal by Cole Minnaar for a "Variant Symlink Filesystem". The proposed filesystem — currently implemented as an out-of-tree kernel module — takes an extremely simple approach to the problem. The filesystem provides a single directory that contains a single symlink called resolve. When any process reads or follows this link, the filesystem looks though that process's environment for a particular environment variable, specified when the filesystem is mounted, and reports the value of that variable as the content of the symlink.

To use this you would mount a filesystem at some well known location and create links that pass though that location. For example

    # mount -t varsymfs -o UNIVERSE none /.universe
    # ln -s /.universe/resolve/bin /bin

Then:

    $ UNIVERSE=/att ls -lL /bin
would show the contents of /att/bin, while:
    $ UNIVERSE=/ucb ls -lL /bin
would show those of /ucb/bin.

The responses to this proposal were pretty much as would be expected: Minnaar was told that he should use a FUSE filesystem written in user space, or use mount namespaces to give different processes a different view of the system. Minnaar made it clear that this wasn't just a new idea with no history, but was something he has been working on for some time. Both those ideas had been tried and found wanting.

The problem with a FUSE-based solution is performance. Though the special filesystem is not used for any filesystem I/O and is only needed to look up a single symbolic link, a performance decrease can still be measured. By its nature, a variant symbolic link cannot be cached in the VFS layer, so every request would need to go to user space and back into the kernel. Recent work has made symlink lookup largely lockless because, for some workloads, even requiring spinlocks for following a symlink can be too expensive. This was found to be particularly true when compiling code, since searching for include files generates lots of filename lookups. Minnaar identified compilation as a problematic case for FUSE-based variant symlinks too, and even the cost of that spinlock — sufficient to justify a rewrite of the symlink lookup code — is tiny compared to the cost of scheduling a user-space process to provide an answer.

The situation with mount namespaces brings its own set of problems, though of a very different kind. A large part of the focus on namespaces has been the creation of containers to contain processes — once in a container, the process shouldn't be able to get out. Minnaar is not interested in that side at all. He is interested in convenience rather than containment.

The example he sketched was to support multiple versions of packages that require the use of fixed paths. Many packages, such as Perl and Emacs, include a version number in the path names used for finding support files, such as /usr/lib/perl5/site_perl/5.22.1. This allows multiple versions to be installed side by side. Many other packages are not so enlightened, allowing only one version to be installed at a time. It would be possible to fix such packages to support parallel installations, but it seems it was easier to implement variant symlinks. That way each package can behave as though it owns the standard path names and each user can select their preferred package version by setting up some environment variables.

When it comes to convenience, filesystem namespaces have two problems, one that was mentioned and one that wasn't — yet. The first problem is that the Unix shell doesn't have a "chns" command to change namespaces. While you can certainly use nsenter, as David Lang suggested, this creates a new shell rather than adjusting the state of the old shell. There is a good reason that cd or chdir is built into the shell — having it external would be nowhere near as convenient. In the same way, nsenter would only be as convenient as export UNIVERSE=/att if it was built-in.

The second problem is the inevitable combinatorial explosion that namespaces would cause. If there is only a need to select on one axis, ucb or att, then namespaces could be made to work. If independently selecting between versions of a dozen packages is needed, then there would be a need for potentially thousands of namespaces, one for each combination. In practice, this explosion may not happen, but the need to construct namespaces on demand might not be the most convenient approach.

While variant symlinks may well be useful, it would help to have a variety of concrete use-cases to examine so that we could see exactly how they would be used and informed implementation choices could be made. It is easy to "bikeshed" some variations, like whether a constant prefix should be provided at mount time so the environment variable values don't need to start with "/". However, such bikeshedding is likely to focus on the inconsequential and miss the essential. What we need, as Al Viro indicated, is to ask "the right questions for figuring out what requirements" there are, so as to determine "the best way to do it". Whether anything like that occurs remains to be seen.


Index entries for this article
KernelFilesystems
GuestArticlesBrown, Neil


to post comments

A case for variant symlinks

Posted Mar 24, 2016 2:22 UTC (Thu) by k8to (guest, #15413) [Link]

The most disturbing part of this article is the implicit acceptance of "hidden" directories by use of the intiial dot. Surely we should have learned by now that hiding things this way just moves the mess around. A fully normal target filepath should be used even for variant target locations like the article discusses.

A case for variant symlinks

Posted Mar 24, 2016 6:22 UTC (Thu) by rsidd (guest, #2582) [Link] (8 responses)

Dragonfly BSD has supported variant symlinks for over 12 years now. FreeBSD started a port a couple of years later but it never seems to have been officially released. It would be nice if a Linux implementation would be compatible. It works like this (from slides by Brooks Davis of FreeBSD)
Symbolic links that change targets based on variables
$ echo bar >  bar; echo baz >  baz
$ ln -s ’${XXX}’ foo
$ ls -l foo
lrwxr-xr-x 1 brooks wheel ... foo ->  ${XXX}
$ varsym XXX=bar cat foo
bar
$ varsym XXX=baz cat foo
baz

A case for variant symlinks

Posted Mar 25, 2016 1:34 UTC (Fri) by khim (subscriber, #9252) [Link] (5 responses)

But then we hit the problem of compatibility. Someone, somewhere have used ${XXX} as a filename, I'm sure. S/he'll be quite sad to find out that Linux just went and broke the world!

We may argue that it was a mistake to allow all symbols (except for '/') in the names of files, but at this point it's kinda too late to change that. Especially for such a "minor" reason: it'll be nice to have "Linux will be compatible with Dragonfly" but I'm pretty sure that "Linux should be compatible with Linux" takes precedence...

A case for variant symlinks

Posted Mar 27, 2016 3:59 UTC (Sun) by giraffedata (guest, #1954) [Link] (3 responses)

Someone, somewhere have used ${XXX} as a filename, I'm sure. S/he'll be quite sad to find out that Linux just went and broke the world!

I think it's much more likely that someone is using 'resolve' as a filename, so the proposal in the article suffers from the same fundamental problem.

A case for variant symlinks

Posted Mar 27, 2016 5:24 UTC (Sun) by viro (subscriber, #7872) [Link] (2 responses)

FWIW, you can have a bunch of processes in different namespace with everything
except one mountpoint shared. /etc/var is bound to itself in all of them. Have your session start with cloning the namespace, then mount --make-private /etc/var,
mount --bind /etc/sessions/<number> /etc/var and you are all set. Bind whatever you like on /etc/sessions/<number>/<var>, and /etc/var/foo will resolve for you to whatever's bound on /etc/sessions/<number>/foo.

You want to switch to the context of session 42? No problem, unshare namespace, mount --make-private /etc/var; mount --bind /etc/sessions/42 /etc/var and there you are. Anything mount/umount anywhere other than /etc/var itself will propagate to all sessions. Want to switch to another sessions? Do the same... Want to change the meaning of /etc/var/splat for session 69? No problem, umount /etc/sessions/69/splat; mount --bind your_new_target /etc/sessions/69/splat and you are all set.

There's really no need for that thing. And with all due respect to Matt, DFBSD occasionally suffers from "FBSD core team had been too willing to say no, somebody ought to try that feature, so let's be that somebody". Sometimes it works, sometimes... AFAICS, variant symlinks had landed there with no better reason than that.

A case for variant symlinks

Posted Mar 27, 2016 21:48 UTC (Sun) by devkev (subscriber, #74096) [Link]

The problem with doing it this way is that (last time I checked at least, which was a while ago now), doing things like mount --make-private and mount --bind requires root. Because the target here is regular users, this means messing around with setuid, sudo or userns, which is terribly inconvenient.

It also makes it hard/impossible for a user process (like an interactive shell) to switch at any time, because generally setuid helpers (and similar) will run before the process has started, and the process later has no way of re-escalating in order to change the filesystem. (Again, I'm not sure if this is still the case; userns in particular seems like it aims to perhaps address this.)

The idea here is to have something that is as convenient for regular users to change as $PATH or $EDITOR - but for software which is too dumb to respect an environment variable, and instead always goes looking for something at a fixed path like /foo/bar/baz. Variant symlinks allow users to easily control what is actually found at that fixed path, just by changing the right environment variable.

A case for variant symlinks

Posted Mar 28, 2016 23:17 UTC (Mon) by koverstreet (subscriber, #4296) [Link]

The trouble I see with the mount namespaces stuff is that it inherently requires pinning kernel memory O(# of variants) - this rules it out for a lot of interesting use cases and makes non root usage inherently problematic.

I don't have any compelling use cases myself though, so I don't have any strong opinions here...

A case for variant symlinks

Posted Mar 27, 2016 4:54 UTC (Sun) by Cyberax (✭ supporter ✭, #52523) [Link]

I've seen files with ${blah} in names. In my experience they usually come from failed substitutions in various log systems.

A case for variant symlinks

Posted Mar 27, 2016 17:21 UTC (Sun) by joey (guest, #328) [Link] (1 responses)

I remember using variant symlinks on linux circa 1997. Presumably via some shady kernel patch. To do crazy NFS client stuff with partially shared trees, IIRC.

A case for variant symlinks

Posted Mar 27, 2016 20:35 UTC (Sun) by neilbrown (subscriber, #359) [Link]

There was a user-space NFS server that supported variant symlinks - it interpolated the client hostname or something similar into properly marked symlinks. I knew about it because someone wanted the kernel NFSd to do the same thing, but I couldn't see the point. Each client trivially has it's own mount namespace so mounting things differently on different clients can solve the same problem.

Maybe you were using that NFS server? I went looking for it recently and couldn't find it. I just tried again and found https://sourceforge.net/projects/clusternfs/ and http://marc.info/?t=99294659600001&r=1&w=2

A case for variant symlinks

Posted Mar 24, 2016 6:52 UTC (Thu) by pabs (subscriber, #43278) [Link]

This article reminds me of Hurd's translators functionality. I'd love to have that in Linux.

https://www.gnu.org/software/hurd/hurd/translator.html

A case for variant symlinks

Posted Mar 24, 2016 10:15 UTC (Thu) by peter-b (guest, #66996) [Link] (2 responses)

I've been using environment modules (http://modules.sourceforge.net/) for a decade, and it's highly unclear from this article what problems variant symlinks solve that can't more-than-adequately be addressed by the use of environment modules...

A case for variant symlinks

Posted Mar 25, 2016 23:51 UTC (Fri) by reubenhwk (guest, #75803) [Link]

Environment variable are a horrible way to provide a user interface...seems like variant symlinks are just as bad.

A case for variant symlinks

Posted Mar 26, 2016 23:45 UTC (Sat) by devkev (subscriber, #74096) [Link]

I agree that environment variables are an excellent user-configurable way of doing this, and that environment modules are ace.

As the article explains, not every piece of software is smart enough to be able to use environment variables to find their files.

"... multiple versions of packages that require the use of fixed paths."

Environment modules can't address this problem. Variant symlinks are a way of fixing all such software "at once", without needing to dig into the guts of each one individually to add the necessary calls to getenv(). Then you can use environment modules nicely with these packages.

A case for variant symlinks

Posted Mar 24, 2016 15:25 UTC (Thu) by madscientist (subscriber, #16861) [Link]

I used variant symlinks (probably that's not what they were called, but the same idea) on DG/UX back in the late 1980's/early 1990's and they were extremely handy.

A case for variant symlinks

Posted Mar 27, 2016 6:56 UTC (Sun) by giraffedata (guest, #1954) [Link] (1 responses)

Is there any precedent for the kernel using a process' environment variables?

A case for variant symlinks

Posted Mar 27, 2016 22:19 UTC (Sun) by devkev (subscriber, #74096) [Link]

The kernel module includes its own code for searching through the process's environment. I can't seem to find any in-tree code which touches env_start/env_end in any meaningful or similar way. So I think the answer is "no".

A case for variant symlinks

Posted Mar 28, 2016 11:50 UTC (Mon) by alankila (guest, #47141) [Link] (29 responses)

IMHO, better just fix the packages to support side-by-side installing, and not make filename lookups any tiny bit more complex and thus difficult to understand than they already are. I would in fact prefer symlinks to be removed altogether, because they tend to confuse the meaning of "..": is it the removal of the last path entry, or access to the actual parent directory dentry?

A case for variant symlinks

Posted Mar 28, 2016 14:33 UTC (Mon) by nybble41 (subscriber, #55106) [Link] (27 responses)

> I would in fact prefer symlinks to be removed altogether, because they tend to confuse the meaning of "..": is it the removal of the last path entry, or access to the actual parent directory dentry?

I would rather keep the symlinks and instead remove the second meaning. Give up on the idea of a single canonical parent directory by eliminating "." and ".." as fixed entries in the directory listing, and instead interpreting them in the context of the stack of inodes actually traversed to reach the current point.

Note that symlinks are not the only case that confuses "..": there are also bind mounts, and the occasional hard-linked directory. (I've run into this latter case often when using the Rational ClearCase filesystem for dynamic views... it won't let you create directory links per se with the "ln" command, but there are plenty of other ways to achieve the same effect, e.g. by moving a directory and then reverting the change to one parent directory and not the other, or by configuring a view to select the version of one directory before the move and the other directory after the move. The same situation could be created easily by any FUSE filesystem.)

A case for variant symlinks

Posted Mar 28, 2016 16:44 UTC (Mon) by viro (subscriber, #7872) [Link] (23 responses)

If you can get .. confused by mount --bind, report a bug. It should not happen. If your filesystem (be it ClearCase or an equally growth-promoting organic product of any other origin) has hardlinks to directories, it's broken. Either due to outright fs image corruption or by design.

A case for variant symlinks

Posted Mar 28, 2016 23:33 UTC (Mon) by nybble41 (subscriber, #55106) [Link] (22 responses)

The confusing part is that there is more than one legitimate parent directory for a bind mount: the directory containing the mount point, and the directory containing the bind target. The ".." entry for a bind mount points to the former, but there may well be cases where the latter would make more sense. For example, a symlink with a ".." relative path inside a bind mount will traverse the mount point rather than referring to the target in the original filesystem:

$ mkdir a
$ mkdir b/c
$ sudo mount --bind a b/c
$ ln -s ../x a/y
$ ls -l a/y b/c/y
lrwxrwxrwx 1 user user 4 Mar 28 00:00 a/y -> ../x
lrwxrwxrwx 1 user user 4 Mar 28 00:00 b/c/y -> ../x
$ touch a/y
$ cat b/c/y
cat: b/c/y: No such file or directory
$ readlink -f a/y b/c/y
/home/user/test/x
/home/user/test/b/x

As for other filesystems... you can call it "broken" all you want, but given the existence of network filesystems and FUSE, tools need to be able to deal with hard-linked directories anyway. In my opinion it would be better to normalize the arrangement rather than treat it as an exceptional condition.

A case for variant symlinks

Posted Mar 29, 2016 2:24 UTC (Tue) by viro (subscriber, #7872) [Link] (21 responses)

'..' _always_ refers to the parent of mountpoint, no matter which kind of mount it is. Both when it occurs in symlink body and when it comes from the pathname itself. Binding is no different from other mounts.

And filesystem with hardlinks to directories *is* broken, period. Sure, NFS server (or FUSE, or...) can produce bogosities and we'd better avoid crashing on that. The same goes for corrupted ISO image, etc. We do avoid crashing, but you get all kinds of unpleasantness for userland. Same as you do if e.g. NFS server ignores locks, etc.

As for your example, it has nothing to do with symlinks. a/../x and b/c/../x will also resolve to different things. Same as they would if you mounted the same fs on a and on b/c - a/.. and b/c/.. already leave the filesystem(s) mounted on a/ and b/c/ resp. And lead into different directories - parents of those mountpoints.

'..' semantics is *NOT* plain "follow the link named '..'" and adding "... and pretend that this entry in root directory is patched at mount time" also isn't enough. In fact, that appendix hadn't been done until v7 - v6 has /usr/../bin resolving to /usr/bin. In v7 they'd fixed _that_ (essentially by faking .. in root directory), but chroot(2) was not handled by that - chroot to /mnt and /.. will refer to global root. That got fixed about a year later, when somebody had spotted that bug - hard to tell who, but 4BSD went through two versions of fix, the later variant being a part of resync with what was to become v8.

And old behaviour was just plain wrong. They hadn't sorted the mount semantics out until _after_ v7, unfortunately. Hell, just look at the mess that was heterogenous filesystems handling - everything from "boot this kernel if you want Berkeley filesystem support, this - if you want the Missed'em'V one, and if you need to access both, too bad, can't do that" to the fun still visible in arch/alpha/kernel/osf_sys.c:osf_mount(), where we had to imitate the oh-so-wonderful API from OSF/1. Research branch had introduced mixing filesystem types (had to - they wanted network filesystem, for starters, and the same work had allowed them to get procfs pretty much for free), but that had happened about 4 years too late ;-/ Pity, that...

A case for variant symlinks

Posted Mar 29, 2016 2:29 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link] (8 responses)

> And filesystem with hardlinks to directories *is* broken, period.
Uhm... Why (apart from the problem with cyclical references)?

A case for variant symlinks

Posted Mar 29, 2016 4:13 UTC (Tue) by viro (subscriber, #7872) [Link] (7 responses)

Because POSIX has the notion of directory being an ancestor of another directory, and it becomes just about impossible to calculate - not without the full search of directory tree. And no, "following .. links until we loop or reach root" does not yield the right semantics. That's just for starters - you also get all kinds of fun problems with caching lookups, getcwd(2) behaviour, etc. It's really a case of "doctor, it hurts when I do it". The best we can do is to try and avoid crashes; if you have two links to a directory, chdir into a subdirectory of one, then lookup on a path going through another will end up either with lookup failing with EBUSY, or getcwd(2) flipping as if our directory had been moved to new location. For rename() done on server it's the best approximation to the right thing, for real multiple links... at least it avoids kernel crashes/deadlocks/etc. Can surprise the userland, but there's really fuck-all we *can* do in that situation. And that's not just getcwd(2) - lookups on ../../../.. will match the other location as well. Until somebody does a lookup going through the original link. Or the third one, etc.

A case for variant symlinks

Posted Mar 29, 2016 4:39 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link] (6 responses)

I'm still not understanding it. We can already have cyclical links with bind mounts or good old symlinks.

Ah... I see it, it's impossible to determine which parent should be used if a user wants to get the ancestor of a directory through getdents() or something similar (after fchdir() for additional fun).

I believe that it's still possible to produce directory hardlinks if directories are always identified by their full paths. It should even be POSIX-compatible if opendir() and friends were saving the path information in the file descriptor metadata inside the kernel.

However, it definitely looks like there's way too much overhead for such a niche feature.

A case for variant symlinks

Posted Mar 29, 2016 13:38 UTC (Tue) by nybble41 (subscriber, #55106) [Link] (5 responses)

> It should even be POSIX-compatible if opendir() and friends were saving the path information in the file descriptor metadata inside the kernel.
> However, it definitely looks like there's way too much overhead for such a niche feature.

Doesn't the kernel do that anyway? How else could you have the /proc/*/fd entries with the original paths?

A case for variant symlinks

Posted Mar 30, 2016 17:28 UTC (Wed) by flussence (guest, #85566) [Link] (2 responses)

Those are "just" soft symlinks - there's no guarantee that a procfs fd points to a currently-existing file, or even something that ever was a valid file (anon shm, sockets, eventfds...)

A case for variant symlinks

Posted Mar 30, 2016 19:19 UTC (Wed) by nybble41 (subscriber, #55106) [Link] (1 responses)

Right, but for disk files the kernel does keep track of the location (vfsmount, dentry) for each FD, which isn't too different from what it would need to track for fchdir() etc.

The /proc/*/fd/* entries are actually a bit special compared to normal symlinks. Even if the path returned by readlink() no longer exists, a process can still open /proc/*/fd/* and obtain a duplicate of the original file descriptor; the open() call operates on the file descriptor rather than following the link. This allows you to obtain a reference to an unlinked file, as long as it's still open somewhere.

A case for variant symlinks

Posted Mar 30, 2016 21:13 UTC (Wed) by viro (subscriber, #7872) [Link]

Egads... Once more, with feeling: suppose a and b are links to the same directory. Should a/x and b/x have the same dentry or separate ones? If it's the same one, how do you reconstruct the path by it? If not, what should happen after rename("a/x", "c/z") to the dentry of b/x?

Note that for different mounts you get the same dentry and separate vfsmounts. However, cross-mount rename() is not allowed and for a very good reason; loop detection on rename is needed and it's hell in any graph other than a tree + some edges leading from that tree to terminal nodes.

While we are at it, what about the questions I'd asked in http://lwn.net/Articles/681667?

A case for variant symlinks

Posted Mar 30, 2016 23:05 UTC (Wed) by bfields (subscriber, #19510) [Link] (1 responses)

"How else could you have the /proc/*/fd entries with the original paths?"

[bfields@pad ~]$ mkdir foo
[bfields@pad ~]$ sleep 100 >foo/P &
[1] 20041
[bfields@pad ~]$ ls -l /proc/20041/fd/1
l-wx------. 1 bfields bfields 64 Mar 30 15:35 /proc/20041/fd/1 -> /home/bfields/foo/P
[bfields@pad ~]$ mv foo bar
[bfields@pad ~]$ ls -l /proc/20041/fd/1
l-wx------. 1 bfields bfields 64 Mar 30 15:35 /proc/20041/fd/1 -> /home/bfields/bar/P

As others have said, it's storing a (vfsmount, dentry) pair and using that to walk up the root to create the path returned by readlink.

A case for variant symlinks

Posted Apr 5, 2016 19:27 UTC (Tue) by mathstuf (subscriber, #69389) [Link]

Which is also how it knows it is broken after (not real output, but just continuing the "session" from above):

$ rm bar/P
$ touch bar/P
$ ls -l /proc/20041/fd/1
l-wx------. 1 bfields bfields 64 Mar 30 15:35 /proc/20041/fd/1 -> /home/bfields/bar/P (deleted)

A case for variant symlinks

Posted Mar 29, 2016 4:58 UTC (Tue) by nybble41 (subscriber, #55106) [Link] (11 responses)

> '..' _always_ refers to the parent of mountpoint, no matter which kind of mount it is.
> As for your example, it has nothing to do with symlinks. a/../x and b/c/../x will also resolve to different things.
> Same as they would if you mounted the same fs on a and on b/c - a/.. and b/c/.. already leave the filesystem(s) mounted on a/ and b/c/ resp.

I never said they didn't. It's still non-intuitive that the same symlink can refer to two different locations depending on how you get to it—the directory it's in no longer has a single canonical parent dentry. That is a relatively recent phenomenon (for non-networked, non-virtual filesystems) since it only occurs when the same filesystem is mounted in more than one place, which most commonly occurs with bind mounts. Before the addition of bind mounts you could generally only mount a given block device at one VFS path at a time. These days, of course, if you try that the result is the same as using mount --bind.

The point wasn't about symlinks at all, it was that mount --bind already creates the equivalent of runtime directory hardlinks with semantics very similar to what I proposed—just at the VFS layer, rather than inside the filesystem, and using ad-hoc exceptions to the normal in-filesystem path lookup rules for ".." (like other mount points, yes) instead of a single set of rules that would apply to all uses of ".." in a pathname.

> And filesystem with hardlinks to directories *is* broken, period.

Only because there is a rule saying "filesystems shall not have directory hardlinks". But everything that deals with filesystems has to allow for that possibility anyway, so why bother pretending? Just assume that there is no such thing as a single authoritative parent directory from the start.

> '..' semantics is *NOT* plain "follow the link named '..'" and adding "... and pretend that this entry in root directory is patched at mount time" also isn't enough. In fact, that appendix hadn't been done until v7 - v6 has /usr/../bin resolving to /usr/bin. In v7 they'd fixed _that_ (essentially by faking .. in root directory), but chroot(2) was not handled by that - chroot to /mnt and /.. will refer to global root. That got fixed about a year later, when somebody had spotted that bug - hard to tell who, but 4BSD went through two versions of fix, the later variant being a part of resync with what was to become v8.

My point exactly. The current system, where ".." is part of the filesystem and selectively overridden by the VFS for mount points, chroot(2), etc., is overly complicated. Just make ".." go back to whichever directory you just came from. Then the chroot issue would be trivial: reset the process's "directory stack" and no ".." can possibly get out of the chroot, since there is no "back" reference to follow.

A case for variant symlinks

Posted Mar 29, 2016 5:22 UTC (Tue) by viro (subscriber, #7872) [Link] (10 responses)

Could you show me the place where we look at the contents of '..' directory entry on disk? In pathname resolution, that is. I believe that am reasonably familiar with the current system, and I am not aware of any place where we would do that. We *do* look at that thing in one place - nfsd finding the pathname corresponding to fhandle it got from server does that in case of many local filesystem. However, that is nowhere near any system calls that would take pathnames as arguments.

As the matter of fact, .. is resolved entirely within fs/{namei,namespace}.c, without any calls of filesystem methods. So may I ask what the hell are you talking about?

A case for variant symlinks

Posted Mar 29, 2016 5:45 UTC (Tue) by viro (subscriber, #7872) [Link] (9 responses)

BTW, going back to where we'd just come from is not enough - consider e.g. ../.. for trivial counterexample. You'll need more than that.

Another fun issue to consider: there's no such thing as _the_ directory symlink is in - multiple hardlinks to a symlink are possible.

And another: loop avoidance in rename(2) and sane locking order.

Have fun. If you come up with something workable, the list to post it on is linux-fsdevel. I'm absolutely serious - any reasonable proposal with enough details to be possible to review (i.e. not a vague handwaving) will be discussed. Knowing your way around the existing algorithms and data structures is a requirement, though - discussion will inevitably contain references to and comparisons with what's currently there, so you'll need to be reasonably familiar with at least the overall structure of the damn thing. Neil's articles on lwn make a sane starting point and point to the areas to RTFS through...

A case for variant symlinks

Posted Mar 29, 2016 13:55 UTC (Tue) by nybble41 (subscriber, #55106) [Link] (8 responses)

> Could you show me the place where we look at the contents of '..' directory entry on disk?

Why? I'm not claiming that anyone is using the ".." directory entry on disk. This is about the logical ".." entry which appears as a fixed entry in the directory listing, with the corresponding assumption that there is exactly one canonical parent for each directory. Nothing to do with how filesystems are stored on disk—or even whether they *are* stored on disk, as this also applies to network and FUSE filesystems.

> BTW, going back to where we'd just come from is not enough - consider e.g. ../.. for trivial counterexample. You'll need more than that.

Yes, that's why I've been saying the system would use a stack of directories for context. "Going back to where we'd just come from" means popping the most local directory off that stack. ../.. means "go back two directories", not "go back and then return to the first directory". All of this is obvious from what has already been stated elsewhere in this thread.

> Another fun issue to consider: there's no such thing as _the_ directory symlink is in - multiple hardlinks to a symlink are possible.

Yes, and this reinforces my point. We don't insist on a single parent directory for files, or device nodes, or symlinks—or even whole filesystems, given the existence of bind mounts—so why must there be a single parent directory for ordinary directories?

A case for variant symlinks

Posted Mar 29, 2016 14:34 UTC (Tue) by viro (subscriber, #7872) [Link] (7 responses)

List of ancestor directories per _process_ won't be enough - consider fchdir(2). It would have to be kept for each opened directory. And that will give you chroot(2) problems right back, not to mention the joy you'll get when somebody does a cross-directory rename(2). Or descriptor-passing between processes with different roots. Now, consider what open(2) becomes in terms of grabbing references on everything in that list, and these *are* going to be heavily shared in the initial elements, so we are talking about cacheline bouncing from hell.

BTW, as for the /proc/*/fd/*, we do not keep the _original_ pathnames for those; what we do keep is a (vfsmount,dentry) pair for opened file, with _current_ ancestors found by that. With multiple links to directories you'd need to keep track of which links have you passed through. What's more, unlink() becomes really messy in terms of cache coherency - if a and b are links to the same directory, and x and y are links to the same file, you want unlink a/x to make a/x and b/x negative, leaving a/y intact. And it gets even more fun when you consider _negative_ cache entries - after that unlink, creation of (new) a/x should locate the negative cache entry for b/x and either turn it positive or evict it.

A case for variant symlinks

Posted Mar 29, 2016 21:24 UTC (Tue) by nybble41 (subscriber, #55106) [Link] (6 responses)

> List of ancestor directories per _process_ won't be enough - consider fchdir(2). It would have to be kept for each opened directory. And that will give you chroot(2) problems right back...

Doesn't fchdir() already cause problems for chroot(2) environments, if you allow a process inside the chroot access to a file descriptor for an outside directory? How is this any different?

> ... not to mention the joy you'll get when somebody does a cross-directory rename(2).

Relative paths based on an existing reference to the moved directory would retrace the original route and ignore the rename. This is more intuitive than entering a directory by one path and leaving it by another because some other process moved it elsewhere in the filesystem in the meantime.

> BTW, as for the /proc/*/fd/*, we do not keep the _original_ pathnames for those; what we do keep is a (vfsmount,dentry) pair for opened file, with _current_ ancestors found by that.

Yes, that is exactly how I thought it worked. The pathnames are reconstructed from the dentries when the links are read. This part wouldn't change much, if at all. The dentry would indicate the path used to open the file.

A case for variant symlinks

Posted Mar 29, 2016 22:12 UTC (Tue) by viro (subscriber, #7872) [Link] (5 responses)

1) fchdir() to something inside the subtree you are chrooted into does *not* cause any problems.

2) define "retrace the original route". I have a process with current directory in /tmp/foo/bar/baz; /tmp/foo is renamed to /tmp/foo2, either by this process or by any other. What should ../barf resolve to before and after that rename?

3) if for a and b being hardlinks to the same directory you want a/x and b/x to have different dentries, please explain how would you deal with operations on one of them affecting the rest, especially since if a/y is a hardlink to a/x, operations on b/x should affect a/x and a/y differently. Starting with unlink(2), but rename a/x to c/z would also be an interesting one - where should an existing dentry for b/x end up?

4) for extra fun, explain how to handle ELOOP in rename(2), and no, "just stop treating that as an error" is not an option - once you allow loops, preventing detached subgraphs on subsequent rename() becomes very hard.

A case for variant symlinks

Posted Mar 29, 2016 23:01 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link] (4 responses)

> 2) define "retrace the original route". I have a process with current directory in /tmp/foo/bar/baz; /tmp/foo is renamed to /tmp/foo2, either by this process or by any other. What should ../barf resolve to before and after that rename?

You can store not the literal directory names but dentries. So simply renames won't be affected.

The structure-changing renames are more complicated. The kernel will have to keep track of open path stacks and edit them. So:
1) /tmp/foo/bar/baz is open. The kernel stores path stack information: <1>/<2>/<3>/<4> (dentry numbers)
2) /tmp/qoo/bar/baz is another hardlink. The kernel stores: <1>/<5>/<3>/<4> (dentry numbers)
3) When /tmp/foo is moved into /tmp/arr/yeah - the kernel checks for open references to foo's dentry <2>. And then edits only these path stacks, so <1>/<2>/<3>/<4> becomes <1>/<6>/<3>/<4> but the other open path (<1>/<5>/<3>/<4> - /tmp/qoo/bar/baz) is untouched.

I don't see the problems with loop handling and I believe that it's possible to make it work without any _logical_ issues. Practical issues are another problem.

A case for variant symlinks

Posted Mar 29, 2016 23:35 UTC (Tue) by viro (subscriber, #7872) [Link] (3 responses)

*UGH*

So you need to be able to locate all those stacks containing given dentry at any point *and* atomically rewrite all of them, possibly extending or shrinking the damn things, and that is even before you start considering the games you need to play with dentry refcounts. With total number of those stacks being that of opened files + running processes. And you need that fun (including the updates of whatever search structures you use to locate the affected ones) on a non-priveleged syscall...

As for the loop handling... The absolute requirement is to have rename() preserving the connectivity of graph - if you can detach a piece of graph with all nodes having incoming edges (i.e. positive link count), you've got a trivial DoS, at the very least. The obvious way to guarantee that is to keep it a directed acyclic graph with directories forming a tree - that's definitely going to satisfy our condition and this property is easy to maintain. You just need to verify that you do not attempt to make a node its own descendent, which is trivial if you keep track of the (only) chain of ancestors. For a single rename that's enough, so combined with fs-wide serialization of actual modifications done by rename(2) (finding the parents can be done outside of that exclusion) it solves the problem.

Have fun doing that with directory hardlinks. Solutions requiring (on any step) a full scan of directory tree do not count...

A case for variant symlinks

Posted Mar 30, 2016 2:02 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link] (2 responses)

> So you need to be able to locate all those stacks containing given dentry at any point *and* atomically rewrite all of them, possibly extending or shrinking the damn things, and that is even before you start considering the games you need to play with dentry refcounts.
Well, I didn't say it's easy or efficient.

I personally wouldn't mind hardlinked directories (I can see where they can be handy), but then I also want O_PONIES.

> You just need to verify that you do not attempt to make a node its own descendent, which is trivial if you keep track of the (only) chain of ancestors.
But that holds just as well for the multi-hardlinked directories. To avoid cycles the FS has to uphold the invariant "no links from a directory to any of its parent directories". It doesn't require full tree scanning.

If loops _are_ allowed then I'm pretty sure the filesystem will require a tracing garbage collector.

A case for variant symlinks

Posted Mar 30, 2016 3:14 UTC (Wed) by viro (subscriber, #7872) [Link] (1 responses)

... except that now you need to maintain the list of all parents through all operations. What's worse, you can't just stop at direct parents - you need traverse all chains of ancestors, however long they might be. With the unique chain to root you already have all of that in RAM - you either get them from pathname resolution, or you've started with fhandles and their decoding includes traversing the chain of ancestors until the damn thing is connected to the root. With multiple links you could very well have never seen the majority of those ancestors, which means that loop detection turns into massive IO. Now add the fact that you need to block all rename() (and link() to directories, and rmdir()) for the duration of that check, and it becomes _very_ unattractive. Locking is also less than pleasant. Once you add that to the costs of maintaining the crap attached to struct file, well...

It's not impossible that somebody would eventually come up with a decent way of doing that stuff. I've never been able to. Neither had *BSD folks. And Thompson, Pike et.al. flat-out decided that all that crap was just too much headache to bother with - even though they started their list of the bad ideas not to duplicate with cross-directory renames, overwriting renames, symlinks and fchdir. On very different VFS architectures, including the one designed pretty much from scratch, with freedom to drop the syscalls considered to be misfeatures. Which is something the rest of us couldn't afford - not without breaking arseloads of userland code.

Again, maybe there is a decent way of doing that just waiting for somebody to invent it. I would love to read it through and try to poke holes in it - got to be interesting. But until somebody does come up with something that survives peer review... Filesystem with multiple hardlinks to directories is a Bloody Bad Idea(tm). Sorry.

A case for variant symlinks

Posted Mar 30, 2016 3:25 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]

I have a feeling that it might be possible to devise a way for the "happy case" (no hardlinks in any parent) to be about as fast as the current case. But once hardlinks start appearing, it'll devolve rapidly (a nice DoS right there).

And I feel that even alternative VFS designs are not going to help unless they introduce a full-scale garbage collector. Which might not even be such a bad idea once we switch to non-volatile memory.

A case for variant symlinks

Posted Mar 28, 2016 17:48 UTC (Mon) by mathstuf (subscriber, #69389) [Link] (1 responses)

> I would rather keep the symlinks and instead remove the second meaning. Give up on the idea of a single canonical parent directory by eliminating "." and ".." as fixed entries in the directory listing, and instead interpreting them in the context of the stack of inodes actually traversed to reach the current point.

This would cause all kinds of problems. Let's say that we know proj1 and proj2 directories are siblings on disk (so ../proj2), but proj1 has a convenience symlink:

In [8]: abs(real('code/proj1/../proj2'))
Out[8]: '/home/user/code/depot/group-foo/proj2'

In [9]: real(abs('code/proj1/../proj2'))
Out[9]: '/home/user/code/proj2'

That second path is junk and doesn't exist. Without post-symlink resolution of .., that relative path is useless depending on how you got to that project's directory. Granted, mount --bind can make the relative path unhelpful (I think) if you bind mount proj1 directly, but that is *far* less common.

A case for variant symlinks

Posted Mar 28, 2016 23:21 UTC (Mon) by nybble41 (subscriber, #55106) [Link]

> Let's say that we know proj1 and proj2 directories are siblings on disk (so ../proj2), but proj1 has a convenience symlink:
> In [8]: abs(real('code/proj1/../proj2'))
> Out[8]: '/home/user/code/depot/group-foo/proj2'
> In [9]: real(abs('code/proj1/../proj2'))
> Out[9]: '/home/user/code/proj2'
> That second path is junk and doesn't exist.

I think you're skipping over the expansion of the symlink to get the second path. The ".." wouldn't be handled first; you'd process each element of the pathname in turn, expanding symlinks as you go. An example, using "|" to represent where the parser is in the path:

$PWD/ | code/proj1/../proj2 # initial state
$PWD/code/ | proj1/../proj2 # traverse "code"
$PWD/code/ | depot/group-foo/proj1/../proj2 # expand "proj1" -> "depot/group-foo/proj1"
$PWD/code/depot/ | group-foo/proj1/../proj2 # traverse "depot"
$PWD/code/depot/group-foo/ | proj1/../proj2 # traverse "group-foo"
$PWD/code/depot/group-foo/proj1/ | ../proj2 # traverse "proj1"
$PWD/code/depot/group-foo/ | proj2 # remove "proj1" and ".."
$PWD/code/depot/group-foo/proj2 | # traverse "proj2"

(A real implementation would naturally use a stack of FDs (or equivalent) for the traversed directories rather than simply rewriting the text, to protect against renames.)

Relative symlinks shouldn't be affected much by this change. You could get the sort of behavior you're describing with directory hardlinks, where the ".." in a symlink goes back to different locations depending on how you got to the link. Of course, that isn't a *change* in behavior since directory hardlinks aren't currently allowed, aside from bind mounts, which already do something very similar by synthesizing ".." based on how you got to the directory rather than using the native filesystem ".." entry.

A case for variant symlinks

Posted Mar 29, 2016 0:00 UTC (Tue) by neilbrown (subscriber, #359) [Link]

For those interested in reading some history, this was posted just short of 30 years ago.

https://groups.google.com/d/msg/comp.unix.wizards/rkPBbdT...

Note that the thread is started by David Korn, author of "ksh".

A case for variant symlinks

Posted Mar 28, 2016 17:37 UTC (Mon) by mathstuf (subscriber, #69389) [Link]

It is always the actual parent dentry. Unless you're in a shell with certain options set (CHASE_DOTS in zsh, bash does it by default, but can be overridden with cd -P and cd -L seems to be some kind of hybrid).

A case for variant symlinks

Posted Mar 29, 2016 13:23 UTC (Tue) by mstone_ (subscriber, #66309) [Link]

IIRC, a kind of variant symlink was used in some multi-level security (MLS) implementations to provide shared directories like /tmp at different levels. (Presumably to eliminate covert channels via filenames?) That dates back to at least the 90s, and clearly wasn't an idea that took the world by storm.


Copyright © 2016, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds