[Sorry, I know this is long...]
Post by Kevin J. McCarthyThe calling function (e.g. _maildir_commit_message()) is not trying to
safe_rename() to the same filename over and over though. It is trying
to find an available target filename, and keeps modifying the target
filename using an incremented Counter and time(). So it makes sense for
it to retry if the target filename already existed. Or am I
misunderstanding your point?
OK. I haven't looked that closely at it (clearly).
In that case, it basically is analogous to mkstemp, i.e. it tries to
create a file with a particlar pattern, fails if it can't create it
safely, and retries with a different pattern. But those functions
will fail after TMP_MAX retries, and I think this code should do the
same. That prevents the DoS (infinite loop) and lets Mutt alert the
user that something funny is going on... This should never happen
unless the user is being attacked in some fashion.
On my system, TMP_MAX is defined as 238328. I think that's plenty of
retries! And since it's defined by the system, it's not exactly
arbitrary... =8^)
Post by Kevin J. McCarthyPost by Derek MartinPost by Kevin J. McCarthySteffen's cautions apply to dotlock code, which is a different case and
is not affected by this change.
It's fundamentally the same thing though. The mechanism for dotlock
[...]
If the link succeeds, we have the lock, but the rc from link is
unreliable, so...
This is the important question. Under what circumstances is the rc from
the link() unreliable? Are you saying a "0" success return value should
not be trusted?
I read it as you do too, but 1. I think it's ambiguous enough that I
would not be 100% confident in that interpretation, and 2. this is for
Linux, and it's not clear that other platforms wouldn't have different
semantics (or even different file systems on Linux). I will note
that, for example, GnuPG always ignores the return value from link()
(common/dotlock.c:1070):
/* We ignore the return value of link() because it is unreliable. */
(void) link (h->tname, h->lockname);
if (stat (h->tname, &sb))
{
saveerrno = errno;
my_error_1 ("lock not made: Oops: stat of tmp file failed: %s\n",
strerror (errno));
/* In theory this might be a severe error: It is possible
that link succeeded but stat failed due to changed
permissions. We can't do anything about it, though. */
my_set_errno (saveerrno);
return -1;
}
Post by Kevin J. McCarthyPost by Derek Martin- The stat comparison is supposed to catch the case where the return
value from link() incorrectly indicates failure, but it does not do
that because it only happens when link() succeeds. That defeats the
purpose of doing the check at all.
No, the stat comparison was a double check for if the link __succeeds__.
That's not usually how it's done, as far as I can tell/remember. I
think I understand why, and I would refer you again to the GnuPG code
above as an example. It always ignores the link return value and does
the stat comparison.
Remember, the reason safe_rename() exists is because rename() has a
race condition on NFS--it is required to be atomic by the spec but on
NFS it is not. Therefore rename should be used only as a last resort,
and only if we're pretty sure link() actually failed.
If your link() return value is reliable, there's no reason to do the
stat comparison. If only the error case is unreliable, then on error
the stat comparison is NECESSARY, to determine if link() actually
succeeded despite the error. If neither is reliable, you should just
always ignore link()'s RV and always do the stat comparison.
So again, I think safe_rename() is wrong, regarless, and the change
you made actually has no practical effect. If it were doing the right
thing, your change would prevent it from catching the case where
Post by Kevin J. McCarthyThere is already failure handling code - reverting to trying a rename()
for certain errno values. It was merely Vincent's hypothesis that the
check was in the wrong place, but I don't believe that is correct.
Note that if the link() actually succeeded but incorrectly reported a
failure, the rename will also fail, in the sense that there will be
two links to the file, where the old link will not be removed. This
can be demonstrated on a local file system with the following short
program:
-=-=-=-=-
$ cat rename.c
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
char *old = "linkfile";
char *new = "linktarg";
int main(int argc, char **argv)
{
int fd;
// Show status of both files
system("ls -li linkfile linktarg");
// create the "old" file
printf("creating %s...\n", old);
if ((fd = open(old, O_CREAT, 0644)) == 1){
fprintf(stderr, "creating %s failed\n", old);
return 1;
}
close(fd);
system("ls -li linkfile linktarg");
// On a local file system, this should succeed
if (link(old, new) == -1) fprintf(stderr, "link fail\n");
else printf("link(%s, %s) succeeded.\n", old, new);
system("ls -li linkfile linktarg");
// simulate rename after incorrect link fail
if (rename(old, new) == -1) fprintf(stderr, "rename fail\n");
else printf("rename(%s, %s) succeeded.\n", old, new);
system("ls -li linkfile linktarg");
return 0;
}
$ ./rn
ls: cannot access 'linkfile': No such file or directory
ls: cannot access 'linktarg': No such file or directory
creating linkfile...
ls: cannot access 'linktarg': No such file or directory
27026462 -rw-r--r-- 1 demartin staff 0 Aug 23 14:50 linkfile
link(linkfile, linktarg) succeeded.
27026462 -rw-r--r-- 2 demartin staff 0 Aug 23 14:50 linkfile
27026462 -rw-r--r-- 2 demartin staff 0 Aug 23 14:50 linktarg
rename(linkfile, linktarg) succeeded.
27026462 -rw-r--r-- 2 demartin staff 0 Aug 23 14:50 linkfile
27026462 -rw-r--r-- 2 demartin staff 0 Aug 23 14:50 linktarg
So, I think this further supports what I'm saying. To restate my
various points more clearly, the algorithm for safe_rename() should be
this:
-=-=-=-
try a regular link and get its return value.
stat the old and new files, and compare. if the same, the link worked!
if they are different {
try rename.
if rename fails {
return failure.
}
}
unlink(old), ignore ENOENT, but report other errors.
-=-=-=-
The reason for the last is, the unlink() serves to clean up BOTH the
case where the link succeeded, AND the case where rename() succeeded
but didn't clean up the original link. However normally if rename
succeeds, the old link will get cleaned up, so unlink will fail in
that case, with ENOENT.
Alternatively, if you're convinced that the link() success status is
reliable, you can do the stat comparison conditionally, only if it
returns failure. Again, the reason to do this is to determine if the
link actually succeeded, but brokenly reported failure due to NFS
quirks.
The former (ignoring link's return value and always doing the stat
comparison) is probably safest for most users. The latter will
apparently be less problematic for SSHFS-like quirks, but as we saw
there's an SSHFS option that makes it work without this behavior.
Post by Kevin J. McCarthyYour point about stat vs lstat sounds important, but let's first resolve
whether the double-check needs to be restored or not.
Sure. Also note that rename() will possibly have different symlink
semantics, depending on which link() symlink behavior is in effect on
your platform. That may make this even more complicated.
Isn't POSIX fun? =8^)
--
Derek D. Martin http://www.pizzashack.org/ GPG Key ID: 0xDFBEAD02
-=-=-=-=-
This message is posted from an invalid address. Replying to it will result in
undeliverable mail due to spam prevention. Sorry for the inconvenience.