Merge branch 'mh/safe-create-leading-directories'

Code clean-up and protection against concurrent write access to the
ref namespace.

* mh/safe-create-leading-directories:
  rename_tmp_log(): on SCLD_VANISHED, retry
  rename_tmp_log(): limit the number of remote_empty_directories() attempts
  rename_tmp_log(): handle a possible mkdir/rmdir race
  rename_ref(): extract function rename_tmp_log()
  remove_dir_recurse(): handle disappearing files and directories
  remove_dir_recurse(): tighten condition for removing unreadable dir
  lock_ref_sha1_basic(): if locking fails with ENOENT, retry
  lock_ref_sha1_basic(): on SCLD_VANISHED, retry
  safe_create_leading_directories(): add new error value SCLD_VANISHED
  cmd_init_db(): when creating directories, handle errors conservatively
  safe_create_leading_directories(): introduce enum for return values
  safe_create_leading_directories(): always restore slash at end of loop
  safe_create_leading_directories(): split on first of multiple slashes
  safe_create_leading_directories(): rename local variable
  safe_create_leading_directories(): add explicit "slash" pointer
  safe_create_leading_directories(): reduce scope of local variable
  safe_create_leading_directories(): fix format of "if" chaining
This commit is contained in:
Junio C Hamano 2014-01-27 10:45:33 -08:00
commit d0956cfa8e
6 changed files with 155 additions and 67 deletions

View File

@ -515,13 +515,14 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
saved = shared_repository;
shared_repository = 0;
switch (safe_create_leading_directories_const(argv[0])) {
case -3:
case SCLD_OK:
case SCLD_PERMS:
break;
case SCLD_EXISTS:
errno = EEXIST;
/* fallthru */
case -1:
die_errno(_("cannot mkdir %s"), argv[0]);
break;
default:
die_errno(_("cannot mkdir %s"), argv[0]);
break;
}
shared_repository = saved;

25
cache.h
View File

@ -737,8 +737,29 @@ enum sharedrepo {
};
int git_config_perm(const char *var, const char *value);
int adjust_shared_perm(const char *path);
int safe_create_leading_directories(char *path);
int safe_create_leading_directories_const(const char *path);
/*
* Create the directory containing the named path, using care to be
* somewhat safe against races. Return one of the scld_error values
* to indicate success/failure.
*
* SCLD_VANISHED indicates that one of the ancestor directories of the
* path existed at one point during the function call and then
* suddenly vanished, probably because another process pruned the
* directory while we were working. To be robust against this kind of
* race, callers might want to try invoking the function again when it
* returns SCLD_VANISHED.
*/
enum scld_error {
SCLD_OK = 0,
SCLD_FAILED = -1,
SCLD_PERMS = -2,
SCLD_EXISTS = -3,
SCLD_VANISHED = -4
};
enum scld_error safe_create_leading_directories(char *path);
enum scld_error safe_create_leading_directories_const(const char *path);
int mkdir_in_gitdir(const char *path);
extern void home_config_paths(char **global, char **xdg, char *file);
extern char *expand_user_path(const char *path);

27
dir.c
View File

@ -1511,8 +1511,13 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
dir = opendir(path->buf);
if (!dir) {
/* an empty dir could be removed even if it is unreadble */
if (!keep_toplevel)
if (errno == ENOENT)
return keep_toplevel ? -1 : 0;
else if (errno == EACCES && !keep_toplevel)
/*
* An empty dir could be removable even if it
* is unreadable:
*/
return rmdir(path->buf);
else
return -1;
@ -1528,13 +1533,21 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
strbuf_setlen(path, len);
strbuf_addstr(path, e->d_name);
if (lstat(path->buf, &st))
; /* fall thru */
else if (S_ISDIR(st.st_mode)) {
if (lstat(path->buf, &st)) {
if (errno == ENOENT)
/*
* file disappeared, which is what we
* wanted anyway
*/
continue;
/* fall thru */
} else if (S_ISDIR(st.st_mode)) {
if (!remove_dir_recurse(path, flag, &kept_down))
continue; /* happy */
} else if (!only_empty && !unlink(path->buf))
} else if (!only_empty &&
(!unlink(path->buf) || errno == ENOENT)) {
continue; /* happy, too */
}
/* path too long, stat fails, or non-directory still exists */
ret = -1;
@ -1544,7 +1557,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
strbuf_setlen(path, original_len);
if (!ret && !keep_toplevel && !kept_down)
ret = rmdir(path->buf);
ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
else if (kept_up)
/*
* report the uplevel that it is not an error that we

View File

@ -693,7 +693,7 @@ static int make_room_for_path(struct merge_options *o, const char *path)
/* Make sure leading directories are created */
status = safe_create_leading_directories_const(path);
if (status) {
if (status == -3) {
if (status == SCLD_EXISTS) {
/* something else exists */
error(msg, path, _(": perhaps a D/F conflict?"));
return -1;

92
refs.c
View File

@ -2039,6 +2039,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
int type, lflags;
int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
int missing = 0;
int attempts_remaining = 3;
lock = xcalloc(1, sizeof(struct ref_lock));
lock->lock_fd = -1;
@ -2080,7 +2081,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
lock->lk = xcalloc(1, sizeof(struct lock_file));
lflags = LOCK_DIE_ON_ERROR;
lflags = 0;
if (flags & REF_NODEREF) {
refname = orig_refname;
lflags |= LOCK_NODEREF;
@ -2093,13 +2094,32 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
if ((flags & REF_NODEREF) && (type & REF_ISSYMREF))
lock->force_write = 1;
if (safe_create_leading_directories(ref_file)) {
retry:
switch (safe_create_leading_directories(ref_file)) {
case SCLD_OK:
break; /* success */
case SCLD_VANISHED:
if (--attempts_remaining > 0)
goto retry;
/* fall through */
default:
last_errno = errno;
error("unable to create directory for %s", ref_file);
goto error_return;
}
lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, lflags);
if (lock->lock_fd < 0) {
if (errno == ENOENT && --attempts_remaining > 0)
/*
* Maybe somebody just deleted one of the
* directories leading to ref_file. Try
* again:
*/
goto retry;
else
unable_to_lock_index_die(ref_file, errno);
}
return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
error_return:
@ -2508,6 +2528,51 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
*/
#define TMP_RENAMED_LOG "logs/refs/.tmp-renamed-log"
static int rename_tmp_log(const char *newrefname)
{
int attempts_remaining = 4;
retry:
switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
case SCLD_OK:
break; /* success */
case SCLD_VANISHED:
if (--attempts_remaining > 0)
goto retry;
/* fall through */
default:
error("unable to create directory for %s", newrefname);
return -1;
}
if (rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
if ((errno==EISDIR || errno==ENOTDIR) && --attempts_remaining > 0) {
/*
* rename(a, b) when b is an existing
* directory ought to result in ISDIR, but
* Solaris 5.8 gives ENOTDIR. Sheesh.
*/
if (remove_empty_directories(git_path("logs/%s", newrefname))) {
error("Directory not empty: logs/%s", newrefname);
return -1;
}
goto retry;
} else if (errno == ENOENT && --attempts_remaining > 0) {
/*
* Maybe another process just deleted one of
* the directories in the path to newrefname.
* Try again from the beginning.
*/
goto retry;
} else {
error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
newrefname, strerror(errno));
return -1;
}
}
return 0;
}
int rename_ref(const char *oldrefname, const char *newrefname, const char *logmsg)
{
unsigned char sha1[20], orig_sha1[20];
@ -2555,30 +2620,9 @@ int rename_ref(const char *oldrefname, const char *newrefname, const char *logms
}
}
if (log && safe_create_leading_directories(git_path("logs/%s", newrefname))) {
error("unable to create directory for %s", newrefname);
if (log && rename_tmp_log(newrefname))
goto rollback;
}
retry:
if (log && rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
if (errno==EISDIR || errno==ENOTDIR) {
/*
* rename(a, b) when b is an existing
* directory ought to result in ISDIR, but
* Solaris 5.8 gives ENOTDIR. Sheesh.
*/
if (remove_empty_directories(git_path("logs/%s", newrefname))) {
error("Directory not empty: logs/%s", newrefname);
goto rollback;
}
goto retry;
} else {
error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
newrefname, strerror(errno));
goto rollback;
}
}
logmoved = log;
lock = lock_ref_sha1_basic(newrefname, NULL, 0, NULL);

View File

@ -105,50 +105,59 @@ int mkdir_in_gitdir(const char *path)
return adjust_shared_perm(path);
}
int safe_create_leading_directories(char *path)
enum scld_error safe_create_leading_directories(char *path)
{
char *pos = path + offset_1st_component(path);
struct stat st;
char *next_component = path + offset_1st_component(path);
enum scld_error ret = SCLD_OK;
while (pos) {
pos = strchr(pos, '/');
if (!pos)
while (ret == SCLD_OK && next_component) {
struct stat st;
char *slash = strchr(next_component, '/');
if (!slash)
break;
while (*++pos == '/')
;
if (!*pos)
next_component = slash + 1;
while (*next_component == '/')
next_component++;
if (!*next_component)
break;
*--pos = '\0';
*slash = '\0';
if (!stat(path, &st)) {
/* path exists */
if (!S_ISDIR(st.st_mode)) {
*pos = '/';
return -3;
}
}
else if (mkdir(path, 0777)) {
if (!S_ISDIR(st.st_mode))
ret = SCLD_EXISTS;
} else if (mkdir(path, 0777)) {
if (errno == EEXIST &&
!stat(path, &st) && S_ISDIR(st.st_mode)) {
!stat(path, &st) && S_ISDIR(st.st_mode))
; /* somebody created it since we checked */
} else {
*pos = '/';
return -1;
else if (errno == ENOENT)
/*
* Either mkdir() failed because
* somebody just pruned the containing
* directory, or stat() failed because
* the file that was in our way was
* just removed. Either way, inform
* the caller that it might be worth
* trying again:
*/
ret = SCLD_VANISHED;
else
ret = SCLD_FAILED;
} else if (adjust_shared_perm(path)) {
ret = SCLD_PERMS;
}
*slash = '/';
}
else if (adjust_shared_perm(path)) {
*pos = '/';
return -2;
}
*pos++ = '/';
}
return 0;
return ret;
}
int safe_create_leading_directories_const(const char *path)
enum scld_error safe_create_leading_directories_const(const char *path)
{
/* path points to cache entries, so xstrdup before messing with it */
char *buf = xstrdup(path);
int result = safe_create_leading_directories(buf);
enum scld_error result = safe_create_leading_directories(buf);
free(buf);
return result;
}