Subject: Re: funlink() for fun!
To: der Mouse <mouse@Rodents.Montreal.QC.CA>
From: Greg A. Woods <woods@weird.com>
List: tech-kern
Date: 07/11/2003 18:57:17
[ On Friday, July 11, 2003 at 16:48:54 (-0400), der Mouse wrote: ]
> Subject: Re: funlink() for fun!
>
> However, unlink() does not operate on a file, except as a side effect;
> it operates on a link to a file.  The file is affected, sometimes very
> mildly, sometimes severely, but the critical point is that the pathname
> is not serving just to locate the file, but rather to locate a
> particular link to the file.

Hmmm... no, I'd still say it's normally the other way around (where
"normal" is the case of one link).  The file (inode and the storage it
points to) is operated on (i.e. freed), and the zeroing out of the inode
number in the directory entry is only a side-effect.  At least that's
the way I see it if you want to talk about it in terms of degrees of
affect.  :-)

The critical point is that the caller must assume the file and its
content is gone for good if the unlink() succeeds (unless the caller
created another link, or at least knows of one, _and_ knows that other
link is still safe and secure).

I agree that the pathname is just serving to locate the file (thus my
other argument about unix filesystems being primarily just flat inode
tables :-).  The only logical difference between unlink() and funlink()
(or any other similar pair of filename/file-descriptor system calls) is
the time at which the filename is used to locate the file.  The only
trick with funlink() is that you either have to cache the filename in
the kernel when you first open the file (and then safely confirm it's
still the same file before you unlink it) or else you have to go hunting
again for the filename and at that point you can only safely unlink the
file if it has a link count of one as otherwise you can't tell if you've
found the right filename.  If you cache the filename and you still
implement the ftw() search should the filename prove invalid then you
can increase the likelyhood that funlink() will "do the right thing",
but of course if the file or one of its parent directories is renamed
between the open() and the funlink() thus invalidating the cached
filename then you still can't unlink the file you find with the ftw()
unless its link count is only one, so funlink() is always going to be a
little less reliable, and potentially a lot more costly, than
fchdir(safe_open(dirname())); unlink(basename()).

>  Since file descriptors are on files, not
> on links to files, funlink() doesn't really make sense.

Oh, of course it does -- file descriptors are handles to open files, and
files can only be opened if you know their name.  In the trivial case
where you might funlink(fileno(stdin)), for example, then the intent is
to unlink the file the parent process opened and connected to the
child's stdin.  Of course that begs the question as to why the parent
process wasn't programmed to just wait around for the child to exit and
then do the unlink() itself; or alternately why the parent process
didn't just hand the original pathname to the child process.

> Yes, it should be able to.  But since you can't open "." if your
> current directory is execute-only, it can't.  (It'd also be nice to be
> able to do one syscall (funlink of this flavor) instead of three
> (fchdir, unlink, fchdir).)

I think funlink() is always going to incur a lot more overhead, either
globally to the hole system or individually to the caller, no matter
which direction you choose to try to optimise it in, than doing a pair
of extra fchdir()s will ever incur.  It's an interesting mental
exercise, but I don't think it a realistic solution to secure
programming problems with unlink(2).

However if you're really worried about not being able to open(".") then
I'm all for your O_NOACCESS flag!  ;-)   [or openpwd(), see below]

> Which is why you can't do funlink(), because unlink doesn't operate on
> files; it operates on links to files.  The file is operated on only in
> that it's garbage-collected once it's no longer referenceable.  (Which
> may be when its refcount goes to zero, or it may be an indeterminate
> time later.)

Again, when a file and its storage is garbage collected is irrelevant.
The caller must assume it's gone for good once unlink() returns
successfully.  Just because there was another link doesn't mean even the
caller can find it in time to prevent the ultimate destruction of the
underlying file and its storage.

> > I've always though the only extremely serious omission in the f*()
> > function call set has been faccess()
> 
> It doesn't really make sense, unless you also add fopen() (which name
> has unfortunately already been preempted by stdio), to re-open a file
> with potentially different access rights.  Otherwise, faccess() doesn't
> tell you anything of use.

faccess() is the only way to do the kind of check access() does without
risking the TOCTOU race condition inherent in access().  If you get the
wrong answer from facess() then you just close() the file and no harm is
done (unless the file is on an NFS server that crashes between the
faccess() and the close() :-).

I.e. You don't actually need to re-open a file with different access
rights.  open_as_user() may eliminate the need for access()/faccess() or
it may not, but conversely faccess() definitely does not eliminate the
need for open_as_user() which must still be implemented for the likes of
NFS where superuser privileges may not have any effect on remote
filesystems (such as NFS).

> > (and of course there should not have ever been an access() call since
> > it is inherently insecure);
> 
> How so?

It is absolutely impossible for a privileged process to use access()
safely, especially if the target file is on any filesystem where
sensitive data lives.  (despite the fact access() was intended primarily
for the use of privileged processes)

>  Provided you realize what it does, and more importantly what
> it doesn't do, there's nothing wrong with it.  (In particular, access()
> with F_OK is useful to non-setid programs.)

Well, access() still not safe against TOCTOU race conditions even when
it's used by non-privileged programs.  While such problems may not
constitute actual vulnerabilities, it's still not really safe to use.

Besides for non-privileged programs it's probably still more useful to
look at the actual mode bits and ownerships after an fstat() than it is
to use the very limited semantics of access().

You always have to lstat() what you think you're going to open, then
open() it to get a secure handle on it, and finally fstat() it once more
to make sure you did get what you think you got.  Only then is it safe
to examine what you actually got to see if it has the attributes you're
looking for.  At that point of course faccess() could be a library call
that accepts the struct stat from the fstat() call (or does its own
fstat() again), though paranoid programmers such as myself might still
prefer that the exact same code that implements the rules the kernel
would use to do the same check "in real life" also be used to do the
faccess() check as well.  :-)

> > It could also be very useful for secure programming if we had an
> > O_MKDIR flag for open() that would [combine mkdir() and open()].
> > O_MKDIR may even eliminate enough of the cases where you'd use your
> > O_NOACCESS to make O_NOACCESS strictly unnecessary.
> 
> No, it wouldn't.  You still couldn't save and restore your current
> directory by opening "." and fchdir()ing back there if your current
> directory is execute-only, even with O_MKDIR, without O_NOACCESS.

If your process is running as root then it sure as heck can!  ;-)

I would suggest that quite often you don't really need to fchdir() back
to where you started from when you go off to create and manage temporary
files (but of course that doesn't mean you shouldn't be able to do so).

I think O_NOACCESS is only needed for non-privileged programs (and
maybe, just maybe, for privileged programs that have to access
sufficiently brain-damaged remote filesystems where even true superuser
privileges doen't buy the rights to actually open directories even just
for reading).  If that would be a significant benefit then, like I say,
I am indeed all for O_NOACCESS too!

On the other hand wouldn't all need for O_NOACCESS be eliminated if
there were something like openpwd(2)?   Hmmm... maybe not because you
might want to be able to open a directory that you have no rights on
just to do an fstat() on it, but then again if you have no rights on it
an lstat() or stat() would suffice -- there's no need to use fstat().

-- 
								Greg A. Woods

+1 416 218-0098;            <g.a.woods@ieee.org>;           <woods@robohack.ca>
Planix, Inc. <woods@planix.com>; VE3TCP; Secrets of the Weird <woods@weird.com>