# HG changeset patch # User Christian Brabandt # Date 1511712006 -3600 # Node ID 1d41836582b369fafc659f5703c54d272f1b2126 # Parent e5a87e955a15a046c27899a67d88afd2c9958606 patch 8.0.1345: race condition between stat() and open() for viminfo commit https://github.com/vim/vim/commit/c41838aa01ef99540e2737c42e9b1283e3da5e26 Author: Bram Moolenaar Date: Sun Nov 26 16:50:41 2017 +0100 patch 8.0.1345: race condition between stat() and open() for viminfo Problem: Race condition between stat() and open() for the viminfo temp file. (Simon Ruderich) Solution: use open() with O_EXCL to atomically check if the file exists. Don't try using a temp file, renaming it will fail anyway. diff --git a/src/ex_cmds.c b/src/ex_cmds.c --- a/src/ex_cmds.c +++ b/src/ex_cmds.c @@ -1825,7 +1825,6 @@ write_viminfo(char_u *file, int forceit) FILE *fp_out = NULL; /* output viminfo file */ char_u *tempname = NULL; /* name of temp viminfo file */ stat_T st_new; /* mch_stat() of potential new file */ - char_u *wp; #if defined(UNIX) || defined(VMS) mode_t umask_save; #endif @@ -1847,27 +1846,29 @@ write_viminfo(char_u *file, int forceit) fp_in = mch_fopen((char *)fname, READBIN); if (fp_in == NULL) { + int fd; + /* if it does exist, but we can't read it, don't try writing */ if (mch_stat((char *)fname, &st_new) == 0) goto end; -#if defined(UNIX) || defined(VMS) - /* - * For Unix we create the .viminfo non-accessible for others, - * because it may contain text from non-accessible documents. - */ - umask_save = umask(077); -#endif - fp_out = mch_fopen((char *)fname, WRITEBIN); -#if defined(UNIX) || defined(VMS) - (void)umask(umask_save); -#endif + + /* Create the new .viminfo non-accessible for others, because it may + * contain text from non-accessible documents. It is up to the user to + * widen access (e.g. to a group). This may also fail if there is a + * race condition, then just give up. */ + fd = mch_open((char *)fname, + O_CREAT|O_EXTRA|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600); + if (fd < 0) + goto end; + fp_out = fdopen(fd, WRITEBIN); } else { /* * There is an existing viminfo file. Create a temporary file to * write the new viminfo into, in the same directory as the - * existing viminfo file, which will be renamed later. + * existing viminfo file, which will be renamed once all writing is + * successful. */ #ifdef UNIX /* @@ -1901,12 +1902,18 @@ write_viminfo(char_u *file, int forceit) #endif /* - * Make tempname. + * Make tempname, find one that does not exist yet. + * Beware of a race condition: If someone logs out and all Vim + * instances exit at the same time a temp file might be created between + * stat() and open(). Use mch_open() with O_EXCL to avoid that. * May try twice: Once normal and once with shortname set, just in * case somebody puts his viminfo file in an 8.3 filesystem. */ for (;;) { + int next_char = 'z'; + char_u *wp; + tempname = buf_modname( #ifdef UNIX shortname, @@ -1924,127 +1931,129 @@ write_viminfo(char_u *file, int forceit) break; /* - * Check if tempfile already exists. Never overwrite an - * existing file! + * Try a series of names. Change one character, just before + * the extension. This should also work for an 8.3 + * file name, when after adding the extension it still is + * the same file as the original. */ - if (mch_stat((char *)tempname, &st_new) == 0) + wp = tempname + STRLEN(tempname) - 5; + if (wp < gettail(tempname)) /* empty file name? */ + wp = gettail(tempname); + for (;;) { -#ifdef UNIX /* - * Check if tempfile is same as original file. May happen - * when modname() gave the same file back. E.g. silly - * link, or file name-length reached. Try again with - * shortname set. + * Check if tempfile already exists. Never overwrite an + * existing file! */ - if (!shortname && st_new.st_dev == st_old.st_dev - && st_new.st_ino == st_old.st_ino) + if (mch_stat((char *)tempname, &st_new) == 0) { - vim_free(tempname); - tempname = NULL; - shortname = TRUE; - continue; - } -#endif - /* - * Try another name. Change one character, just before - * the extension. This should also work for an 8.3 - * file name, when after adding the extension it still is - * the same file as the original. - */ - wp = tempname + STRLEN(tempname) - 5; - if (wp < gettail(tempname)) /* empty file name? */ - wp = gettail(tempname); - for (*wp = 'z'; mch_stat((char *)tempname, &st_new) == 0; - --*wp) - { +#ifdef UNIX /* - * They all exist? Must be something wrong! Don't - * write the viminfo file then. + * Check if tempfile is same as original file. May happen + * when modname() gave the same file back. E.g. silly + * link, or file name-length reached. Try again with + * shortname set. */ - if (*wp == 'a') + if (!shortname && st_new.st_dev == st_old.st_dev + && st_new.st_ino == st_old.st_ino) { - EMSG2(_("E929: Too many viminfo temp files, like %s!"), - tempname); vim_free(tempname); tempname = NULL; + shortname = TRUE; break; } +#endif } - } - break; - } - - if (tempname != NULL) - { + else + { + /* Try creating the file exclusively. This may fail if + * another Vim tries to do it at the same time. */ #ifdef VMS - /* fdopen() fails for some reason */ - umask_save = umask(077); - fp_out = mch_fopen((char *)tempname, WRITEBIN); - (void)umask(umask_save); + /* fdopen() fails for some reason */ + umask_save = umask(077); + fp_out = mch_fopen((char *)tempname, WRITEBIN); + (void)umask(umask_save); #else - int fd; - - /* Use mch_open() to be able to use O_NOFOLLOW and set file - * protection: - * Unix: same as original file, but strip s-bit. Reset umask to - * avoid it getting in the way. - * Others: r&w for user only. */ + int fd; + + /* Use mch_open() to be able to use O_NOFOLLOW and set file + * protection: + * Unix: same as original file, but strip s-bit. Reset + * umask to avoid it getting in the way. + * Others: r&w for user only. */ # ifdef UNIX - umask_save = umask(0); - fd = mch_open((char *)tempname, - O_CREAT|O_EXTRA|O_EXCL|O_WRONLY|O_NOFOLLOW, - (int)((st_old.st_mode & 0777) | 0600)); - (void)umask(umask_save); + umask_save = umask(0); + fd = mch_open((char *)tempname, + O_CREAT|O_EXTRA|O_EXCL|O_WRONLY|O_NOFOLLOW, + (int)((st_old.st_mode & 0777) | 0600)); + (void)umask(umask_save); # else - fd = mch_open((char *)tempname, - O_CREAT|O_EXTRA|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600); + fd = mch_open((char *)tempname, + O_CREAT|O_EXTRA|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600); +# endif + if (fd < 0) + { + fp_out = NULL; +# ifdef EEXIST + /* Avoid trying lots of names while the problem is lack + * of premission, only retry if the file already + * exists. */ + if (errno != EEXIST) + break; # endif - if (fd < 0) - fp_out = NULL; - else - fp_out = fdopen(fd, WRITEBIN); + } + else + fp_out = fdopen(fd, WRITEBIN); #endif /* VMS */ - - /* - * If we can't create in the same directory, try creating a - * "normal" temp file. This is just an attempt, renaming the temp - * file might fail as well. - */ - if (fp_out == NULL) - { - vim_free(tempname); - if ((tempname = vim_tempname('o', TRUE)) != NULL) - fp_out = mch_fopen((char *)tempname, WRITEBIN); + if (fp_out != NULL) + break; + } + + /* Assume file exists, try again with another name. */ + if (next_char == 'a' - 1) + { + /* They all exist? Must be something wrong! Don't write + * the viminfo file then. */ + EMSG2(_("E929: Too many viminfo temp files, like %s!"), + tempname); + break; + } + *wp = next_char; + --next_char; } + if (tempname != NULL) + break; + /* continue if shortname was set */ + } + #if defined(UNIX) && defined(HAVE_FCHOWN) + if (tempname != NULL && fp_out != NULL) + { + stat_T tmp_st; + /* * Make sure the original owner can read/write the tempfile and * otherwise preserve permissions, making sure the group matches. */ - if (fp_out != NULL) + if (mch_stat((char *)tempname, &tmp_st) >= 0) { - stat_T tmp_st; - - if (mch_stat((char *)tempname, &tmp_st) >= 0) - { - if (st_old.st_uid != tmp_st.st_uid) - /* Changing the owner might fail, in which case the - * file will now owned by the current user, oh well. */ - ignored = fchown(fileno(fp_out), st_old.st_uid, -1); - if (st_old.st_gid != tmp_st.st_gid - && fchown(fileno(fp_out), -1, st_old.st_gid) == -1) - /* can't set the group to what it should be, remove - * group permissions */ - (void)mch_setperm(tempname, 0600); - } - else - /* can't stat the file, set conservative permissions */ + if (st_old.st_uid != tmp_st.st_uid) + /* Changing the owner might fail, in which case the + * file will now owned by the current user, oh well. */ + ignored = fchown(fileno(fp_out), st_old.st_uid, -1); + if (st_old.st_gid != tmp_st.st_gid + && fchown(fileno(fp_out), -1, st_old.st_gid) == -1) + /* can't set the group to what it should be, remove + * group permissions */ (void)mch_setperm(tempname, 0600); } -#endif - } - } + else + /* can't stat the file, set conservative permissions */ + (void)mch_setperm(tempname, 0600); + } + } +#endif /* * Check if the new viminfo file can be written to. diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -772,6 +772,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1345, +/**/ 1344, /**/ 1343,