diff options
Diffstat (limited to '')
-rw-r--r-- | src/util/edit_file.c | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/src/util/edit_file.c b/src/util/edit_file.c new file mode 100644 index 0000000..9d76b93 --- /dev/null +++ b/src/util/edit_file.c @@ -0,0 +1,353 @@ +/*++ +/* NAME +/* edit_file 3 +/* SUMMARY +/* simple cooperative file updating protocol +/* SYNOPSIS +/* #include <edit_file.h> +/* +/* typedef struct { +/* .in +4 +/* char *tmp_path; /* temp. pathname */ +/* VSTREAM *tmp_fp; /* temp. stream */ +/* /* private members... */ +/* .in -4 +/* } EDIT_FILE; +/* +/* EDIT_FILE *edit_file_open(original_path, output_flags, output_mode) +/* const char *original_path; +/* int output_flags; +/* mode_t output_mode; +/* +/* int edit_file_close(edit_file) +/* EDIT_FILE *edit_file; +/* +/* void edit_file_cleanup(edit_file) +/* EDIT_FILE *edit_file; +/* DESCRIPTION +/* This module implements a simple protocol for cooperative +/* processes to update one file. The idea is to 1) create a +/* new file under a deterministic temporary pathname, 2) +/* populate the new file with updated information, and 3) +/* rename the new file into the place of the original file. +/* This module provides 1) and 3), and leaves 2) to the +/* application. The temporary pathname is deterministic to +/* avoid accumulation of thrash after program crashes. +/* +/* edit_file_open() implements the first phase of the protocol. +/* It creates or opens an output file with a deterministic +/* temporary pathname, obtained by appending the suffix defined +/* with EDIT_FILE_SUFFIX to the specified original file pathname. +/* The original file itself is not opened. edit_file_open() +/* then locks the output file for exclusive access, and verifies +/* that the file still exists under the temporary pathname. +/* At this point in the protocol, the current process controls +/* both the output file content and its temporary pathname. +/* +/* In the second phase, the application opens the original +/* file if needed, and updates the output file via the +/* \fBtmp_fp\fR member of the EDIT_FILE data structure. This +/* phase is not implemented by the edit_file() module. +/* +/* edit_file_close() implements the third and final phase of +/* the protocol. It flushes the output file to persistent +/* storage, and renames the output file from its temporary +/* pathname into the place of the original file. When any of +/* these operations fails, edit_file_close() behaves as if +/* edit_file_cleanup() was called. Regardless of whether these +/* operations succeed, edit_file_close() releases the exclusive +/* lock, closes the output file, and frees up memory that was +/* allocated by edit_file_open(). +/* +/* edit_file_cleanup() aborts the protocol. It discards the +/* output file, releases the exclusive lock, closes the output +/* file, and frees up memory that was allocated by edit_file_open(). +/* +/* Arguments: +/* .IP original_path +/* The pathname of the original file that will be replaced by +/* the output file. The temporary pathname for the output file +/* is obtained by appending the suffix defined with EDIT_FILE_SUFFIX +/* to a copy of the specified original file pathname, and is +/* made available via the \fBtmp_path\fR member of the EDIT_FILE +/* data structure. +/* .IP output_flags +/* Flags for opening the output file. These are as with open(2), +/* except that the O_TRUNC flag is ignored. edit_file_open() +/* always truncates the output file after it has obtained +/* exclusive control over the output file content and temporary +/* pathname. +/* .IP output_mode +/* Permissions for the output file. These are as with open(2), +/* except that the output file is initially created with no +/* group or other access permissions. The specified output +/* file permissions are applied by edit_file_close(). +/* .IP edit_file +/* Pointer to data structure that is returned upon successful +/* completion by edit_file_open(), and that must be passed to +/* edit_file_close() or edit_file_cleanup(). +/* DIAGNOSTICS +/* Fatal errors: memory allocation failure, fstat() failure, +/* unlink() failure, lock failure, ftruncate() failure. +/* +/* edit_file_open() immediately returns a null pointer when +/* it cannot open the output file. +/* +/* edit_file_close() returns zero on success, VSTREAM_EOF on +/* failure. +/* +/* With both functions, the global errno variable indicates +/* the nature of the problem. All errors are relative to the +/* temporary output's pathname. With both functions, this +/* pathname is not available via the EDIT_FILE data structure, +/* because that structure was already destroyed, or not created. +/* BUGS +/* In the non-error case, edit_file_open() will not return +/* until it obtains exclusive control over the output file +/* content and temporary pathname. Applications that are +/* concerned about deadlock should protect the edit_file_open() +/* call with a watchdog timer. +/* +/* When interrupted, edit_file_close() may leave behind a +/* world-readable output file under the temporary pathname. +/* On some systems this can be used to inflict a shared-lock +/* DOS on the protocol. Applications that are concerned about +/* maximal safety should protect the edit_file_close() call +/* with sigdelay() and sigresume() calls, but this introduces +/* the risk that the program will get stuck forever. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Based on code originally by: +/* Victor Duchovni +/* Morgan Stanley +/* +/* Packaged into one module with minor improvements by: +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/stat.h> +#include <stdio.h> /* rename(2) */ +#include <errno.h> + + /* + * This mask selects all permission bits in the st_mode stat data. There is + * no portable definition (unlike S_IFMT, which is defined for the file type + * bits). For example, BSD / Linux have ALLPERMS, while Solaris has S_IAMB. + */ +#define FILE_PERM_MASK \ + (S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO) + +/* Utility Library. */ + +#include <msg.h> +#include <vstream.h> +#include <mymalloc.h> +#include <stringops.h> +#include <myflock.h> +#include <edit_file.h> +#include <warn_stat.h> + + /* + * Do we reuse and truncate an output file that persists after a crash, or + * do we unlink it and create a new file? + */ +#define EDIT_FILE_REUSE_AFTER_CRASH + + /* + * Protocol internals: the temporary file permissions. + */ +#define EDIT_FILE_MODE (S_IRUSR | S_IWUSR) /* temp file mode */ + + /* + * Make complex operations more readable. We could use functions, instead. + * The main thing is that we keep the _alloc and _free code together. + */ +#define EDIT_FILE_ALLOC(ep, path, mode) do { \ + (ep) = (EDIT_FILE *) mymalloc(sizeof(EDIT_FILE)); \ + (ep)->final_path = mystrdup(path); \ + (ep)->final_mode = (mode); \ + (ep)->tmp_path = concatenate((path), EDIT_FILE_SUFFIX, (char *) 0); \ + (ep)->tmp_fp = 0; \ + } while (0) + +#define EDIT_FILE_FREE(ep) do { \ + myfree((ep)->final_path); \ + myfree((ep)->tmp_path); \ + myfree((void *) (ep)); \ + } while (0) + +/* edit_file_open - open and lock file with deterministic temporary pathname */ + +EDIT_FILE *edit_file_open(const char *path, int flags, mode_t mode) +{ + struct stat before_lock; + struct stat after_lock; + int saved_errno; + EDIT_FILE *ep; + + /* + * Initialize. Do not bother to optimize for the error case. + */ + EDIT_FILE_ALLOC(ep, path, mode); + + /* + * As long as the output file can be opened under the temporary pathname, + * this code can loop or block forever. + * + * Applications that are concerned about deadlock should protect the + * edit_file_open() call with a watchdog timer. + */ + for ( /* void */ ; /* void */ ; (void) vstream_fclose(ep->tmp_fp)) { + + /* + * Try to open the output file under the temporary pathname. This + * succeeds or fails immediately. To avoid creating a shared-lock DOS + * opportunity after we crash, we create the output file with no + * group or other permissions, and set the final permissions at the + * end (this is one reason why we try to get exclusive control over + * the output file instead of the original file). We postpone file + * truncation until we have obtained exclusive control over the file + * content and temporary pathname. If the open operation fails, we + * give up immediately. The caller can retry the call if desirable. + * + * XXX If we replace the vstream_fopen() call by safe_open(), then we + * should replace the stat() call below by lstat(). + */ + if ((ep->tmp_fp = vstream_fopen(ep->tmp_path, flags & ~(O_TRUNC), + EDIT_FILE_MODE)) == 0) { + saved_errno = errno; + EDIT_FILE_FREE(ep); + errno = saved_errno; + return (0); + } + + /* + * At this point we may have opened an existing output file that was + * already locked. Try to lock the open file exclusively. This may + * take some time. + */ + if (myflock(vstream_fileno(ep->tmp_fp), INTERNAL_LOCK, + MYFLOCK_OP_EXCLUSIVE) < 0) + msg_fatal("lock %s: %m", ep->tmp_path); + + /* + * At this point we have an exclusive lock, but some other process + * may have renamed or removed the output file while we were waiting + * for the lock. If that is the case, back out and try again. + */ + if (fstat(vstream_fileno(ep->tmp_fp), &before_lock) < 0) + msg_fatal("open %s: %m", ep->tmp_path); + if (stat(ep->tmp_path, &after_lock) < 0 + || before_lock.st_dev != after_lock.st_dev + || before_lock.st_ino != after_lock.st_ino +#ifdef HAS_ST_GEN + || before_lock.st_gen != after_lock.st_gen +#endif + /* No need to compare st_rdev or st_nlink here. */ + ) { + continue; + } + + /* + * At this point we have exclusive control over the output file + * content and its temporary pathname (within the rules of the + * cooperative protocol). But wait, there is more. + * + * There are many opportunities for trouble when opening a pre-existing + * output file. Here are just a few. + * + * - Victor observes that a system crash in the middle of the + * final-phase rename() operation may result in the output file + * having both the temporary pathname and the final pathname. In that + * case we must not write to the output file. + * + * - Wietse observes that crashes may also leave the output file in + * other inconsistent states. To avoid permission-related trouble, we + * simply refuse to work with an output file that has the wrong + * temporary permissions. This won't stop the shared-lock DOS if we + * crash after changing the file permissions, though. + * + * To work around these crash-related problems, remove the temporary + * pathname, back out, and try again. + */ + if (!S_ISREG(after_lock.st_mode) +#ifndef EDIT_FILE_REUSE_AFTER_CRASH + || after_lock.st_size > 0 +#endif + || after_lock.st_nlink > 1 + || (after_lock.st_mode & FILE_PERM_MASK) != EDIT_FILE_MODE) { + if (unlink(ep->tmp_path) < 0 && errno != ENOENT) + msg_fatal("unlink %s: %m", ep->tmp_path); + continue; + } + + /* + * Settle the final details. + */ +#ifdef EDIT_FILE_REUSE_AFTER_CRASH + if (ftruncate(vstream_fileno(ep->tmp_fp), 0) < 0) + msg_fatal("truncate %s: %m", ep->tmp_path); +#endif + return (ep); + } +} + +/* edit_file_cleanup - clean up without completing the protocol */ + +void edit_file_cleanup(EDIT_FILE *ep) +{ + + /* + * Don't touch the file after we lose the exclusive lock! + */ + if (unlink(ep->tmp_path) < 0 && errno != ENOENT) + msg_fatal("unlink %s: %m", ep->tmp_path); + (void) vstream_fclose(ep->tmp_fp); + EDIT_FILE_FREE(ep); +} + +/* edit_file_close - rename the file into place and close the file */ + +int edit_file_close(EDIT_FILE *ep) +{ + VSTREAM *fp = ep->tmp_fp; + int fd = vstream_fileno(fp); + int saved_errno; + + /* + * The rename/unlock portion of the protocol is relatively simple. The + * only things that really matter here are that we change permissions as + * late as possible, and that we rename the file to its final pathname + * before we lose the exclusive lock. + * + * Applications that are concerned about maximal safety should protect the + * edit_file_close() call with sigdelay() and sigresume() calls. It is + * not safe for us to call these functions directly, because the calls do + * not nest. It is also not nice to force every caller to run with + * interrupts turned off. + */ + if (vstream_fflush(fp) < 0 + || fchmod(fd, ep->final_mode) < 0 +#ifdef HAS_FSYNC + || fsync(fd) < 0 +#endif + || rename(ep->tmp_path, ep->final_path) < 0) { + saved_errno = errno; + edit_file_cleanup(ep); + errno = saved_errno; + return (VSTREAM_EOF); + } else { + (void) vstream_fclose(ep->tmp_fp); + EDIT_FILE_FREE(ep); + return (0); + } +} |