Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
tmpfiles: symlinks are followed in non-terminal path components (CVE-2018-6954) #7986
Sorry to keep harassing you with these. I think
Same disclaimer: I'm running tmpfiles from a git checkout, but not booting systemd. Now I have
I start the service once, so that
Now, as the owner (mjo) of
The 500,000 dummy files buy some time to swap in the symlink before the loop gets to
And while that is busy looping on the 500,000 dummy files, swap out
(I've used "mv" because "rm" is ironically slow at deleting my own dummy files.)
The end result is that I wind up as the owner of
hmm, yeah, the current code there sucks. when traversing recursively through directory trees we should never use absolute paths, but always operate with openat() and friends and only operate on one level at a time. We are doing that at most other places these days, but tmpfiles is a hold-out in this regard...
referenced this issue
Jan 24, 2018
@poettering You will eventually hit the same issue I am hitting with OpenRC. There are valid cases for symlinks, e.g. at least /var/run and /var/lock, so if you force all non-terminal path components to not be symlinks you might break things. I'm not sure there is much that can be done about this.
@williamh well, if we descend into the tree to re-chown() one level at a time, and strictly only use openat() then there never are non-terminal symlinks we have to care about really, all of them are terminal.
See https://github.com/systemd/systemd/blob/master/src/core/chown-recursive.c#L62 for example, which is a recursive chown() used for systemd's DynamicUser=1 functionality that re-chown()s a directory tree for the UID assigned to a service shortly before invoking the service, if that's necessary.
As you can see we'll never ever descend into symlink stuff, there's a superficial check through the stat data, but then when we are about to enter the subdir, we open it with O_DIRECTORY|O_NOFOLLOW which means if the thing gets replaced by a symlink right there we'll fail the whole thing, and hence are protected.
And yeah, we should make tmpfiles.c work the same way. The only reason we currently don't is that tmpfiles.c is much much older.
(in fact the code I linked isn't perfect either. By making use of O_PATH we could make the whole thing even better, as the user couldn't replace the inode between our stat and openat calls at all)
referenced this issue
Feb 5, 2018
@fbuihuu yes, something like that. I added a number of comments there. Any chance you can rework this accordingly and post as PR for further review?
Key though should be that we operate as long as we can with O_PATH fds of everything (because that pins the node and allows us to operate on it, adjusting xattrs, acls, mode and ownership while being sure that it wont be replaced underneath us). Only when we actually want a regular fd (for the chattr ioctl) or a directory fd (to enumerate child nodes) we should convert the O_PATH fd into a regular fd by opening its /proc/self/fd/%i link. With that approach we can neatly avoid most issues, as we nothing can change under our view...
Hmm... I don't see why O_PATH is the key here and where its "pin the node" feature comes from. As long as a file/dir is opened (with or without O_PATH flag) and you get a valid file descriptor, it remains "pinned" until the fd is closed, no ?
In my understanding the key here was to use ...at() syscalls as we only resolve pathnames only once.
O_PATH might be useful in our case but only because it doesn't involve access to the filesystem object which makes the operation slighly cheaper, nothing else.
O_PATH allows us to pin any kind of file object, i.e. also symlinks, device nodes, sockets, regardless of the type, and without following/triggering it, and we can apply all kinds of attribute changes to it.
If we wouldn't use O_PATH then we can only realistically deal with regular files and directories, but given that these are not the only kind of nodes handling them is always racy: we wouldn't want to open() device nodes or fifos (since doing so as non-trivial effects on drivers and apps behind them), and we can't open sockets or symlinks (since that's simply not defined or does something weird), but if we'd check beforehand what they are and only then decide whether to open() them then things would be racy since they might have been swapped out in the meantime. Moreover, not using O_PATH always means that for regular files/dirs we could use fchmod()/fchown(), but for symlinks/device nodes/sockets/symlinks we'd have to use chmod()/chown()/… on the paths. O_PATH makes all these races and different codepaths go away: we can open the node right-away, regardless of its type and then invoke chmod()/chown()/… via the /proc/self/fd/%i pseudo-symlink, which works for all kinds of objects the same and requires no checking of type beforehand.
Hence yes, O_PATH is required to fix these races fully.
added a commit
Mar 5, 2018
I don't think this is entirely fixed... the recursive part looks good, but the tmpfiles.d entries still wind up making one call to e.g.
Now after "foo" is created, I can
The problem is pretty much the same: the initial
Well we're supposed to follow symlinks in the path except for the final component... so I don't how such (dangerous) tmpfiles directives could be fixed.
Unless we introduce a safe mode (perhaps enabled by default) which makes tmpfiles refuse to follow symlinks which are not owned by root.
The spec says only "does not follow symlinks", so it would be easy to interpret that in whichever way is convenient =)
The real question is whether or not changing this behavior would break anything. It certainly could, if people have