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:
commit
d0956cfa8e
@ -515,13 +515,14 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
|
|||||||
saved = shared_repository;
|
saved = shared_repository;
|
||||||
shared_repository = 0;
|
shared_repository = 0;
|
||||||
switch (safe_create_leading_directories_const(argv[0])) {
|
switch (safe_create_leading_directories_const(argv[0])) {
|
||||||
case -3:
|
case SCLD_OK:
|
||||||
|
case SCLD_PERMS:
|
||||||
|
break;
|
||||||
|
case SCLD_EXISTS:
|
||||||
errno = EEXIST;
|
errno = EEXIST;
|
||||||
/* fallthru */
|
/* fallthru */
|
||||||
case -1:
|
|
||||||
die_errno(_("cannot mkdir %s"), argv[0]);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
|
die_errno(_("cannot mkdir %s"), argv[0]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
shared_repository = saved;
|
shared_repository = saved;
|
||||||
|
25
cache.h
25
cache.h
@ -737,8 +737,29 @@ enum sharedrepo {
|
|||||||
};
|
};
|
||||||
int git_config_perm(const char *var, const char *value);
|
int git_config_perm(const char *var, const char *value);
|
||||||
int adjust_shared_perm(const char *path);
|
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);
|
int mkdir_in_gitdir(const char *path);
|
||||||
extern void home_config_paths(char **global, char **xdg, char *file);
|
extern void home_config_paths(char **global, char **xdg, char *file);
|
||||||
extern char *expand_user_path(const char *path);
|
extern char *expand_user_path(const char *path);
|
||||||
|
27
dir.c
27
dir.c
@ -1511,8 +1511,13 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
|
|||||||
flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
|
flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
|
||||||
dir = opendir(path->buf);
|
dir = opendir(path->buf);
|
||||||
if (!dir) {
|
if (!dir) {
|
||||||
/* an empty dir could be removed even if it is unreadble */
|
if (errno == ENOENT)
|
||||||
if (!keep_toplevel)
|
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);
|
return rmdir(path->buf);
|
||||||
else
|
else
|
||||||
return -1;
|
return -1;
|
||||||
@ -1528,13 +1533,21 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
|
|||||||
|
|
||||||
strbuf_setlen(path, len);
|
strbuf_setlen(path, len);
|
||||||
strbuf_addstr(path, e->d_name);
|
strbuf_addstr(path, e->d_name);
|
||||||
if (lstat(path->buf, &st))
|
if (lstat(path->buf, &st)) {
|
||||||
; /* fall thru */
|
if (errno == ENOENT)
|
||||||
else if (S_ISDIR(st.st_mode)) {
|
/*
|
||||||
|
* 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))
|
if (!remove_dir_recurse(path, flag, &kept_down))
|
||||||
continue; /* happy */
|
continue; /* happy */
|
||||||
} else if (!only_empty && !unlink(path->buf))
|
} else if (!only_empty &&
|
||||||
|
(!unlink(path->buf) || errno == ENOENT)) {
|
||||||
continue; /* happy, too */
|
continue; /* happy, too */
|
||||||
|
}
|
||||||
|
|
||||||
/* path too long, stat fails, or non-directory still exists */
|
/* path too long, stat fails, or non-directory still exists */
|
||||||
ret = -1;
|
ret = -1;
|
||||||
@ -1544,7 +1557,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
|
|||||||
|
|
||||||
strbuf_setlen(path, original_len);
|
strbuf_setlen(path, original_len);
|
||||||
if (!ret && !keep_toplevel && !kept_down)
|
if (!ret && !keep_toplevel && !kept_down)
|
||||||
ret = rmdir(path->buf);
|
ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
|
||||||
else if (kept_up)
|
else if (kept_up)
|
||||||
/*
|
/*
|
||||||
* report the uplevel that it is not an error that we
|
* report the uplevel that it is not an error that we
|
||||||
|
@ -693,7 +693,7 @@ static int make_room_for_path(struct merge_options *o, const char *path)
|
|||||||
/* Make sure leading directories are created */
|
/* Make sure leading directories are created */
|
||||||
status = safe_create_leading_directories_const(path);
|
status = safe_create_leading_directories_const(path);
|
||||||
if (status) {
|
if (status) {
|
||||||
if (status == -3) {
|
if (status == SCLD_EXISTS) {
|
||||||
/* something else exists */
|
/* something else exists */
|
||||||
error(msg, path, _(": perhaps a D/F conflict?"));
|
error(msg, path, _(": perhaps a D/F conflict?"));
|
||||||
return -1;
|
return -1;
|
||||||
|
92
refs.c
92
refs.c
@ -2039,6 +2039,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
|
|||||||
int type, lflags;
|
int type, lflags;
|
||||||
int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
|
int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
|
||||||
int missing = 0;
|
int missing = 0;
|
||||||
|
int attempts_remaining = 3;
|
||||||
|
|
||||||
lock = xcalloc(1, sizeof(struct ref_lock));
|
lock = xcalloc(1, sizeof(struct ref_lock));
|
||||||
lock->lock_fd = -1;
|
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));
|
lock->lk = xcalloc(1, sizeof(struct lock_file));
|
||||||
|
|
||||||
lflags = LOCK_DIE_ON_ERROR;
|
lflags = 0;
|
||||||
if (flags & REF_NODEREF) {
|
if (flags & REF_NODEREF) {
|
||||||
refname = orig_refname;
|
refname = orig_refname;
|
||||||
lflags |= LOCK_NODEREF;
|
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))
|
if ((flags & REF_NODEREF) && (type & REF_ISSYMREF))
|
||||||
lock->force_write = 1;
|
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;
|
last_errno = errno;
|
||||||
error("unable to create directory for %s", ref_file);
|
error("unable to create directory for %s", ref_file);
|
||||||
goto error_return;
|
goto error_return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, lflags);
|
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;
|
return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
|
||||||
|
|
||||||
error_return:
|
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"
|
#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)
|
int rename_ref(const char *oldrefname, const char *newrefname, const char *logmsg)
|
||||||
{
|
{
|
||||||
unsigned char sha1[20], orig_sha1[20];
|
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))) {
|
if (log && rename_tmp_log(newrefname))
|
||||||
error("unable to create directory for %s", newrefname);
|
|
||||||
goto rollback;
|
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;
|
logmoved = log;
|
||||||
|
|
||||||
lock = lock_ref_sha1_basic(newrefname, NULL, 0, NULL);
|
lock = lock_ref_sha1_basic(newrefname, NULL, 0, NULL);
|
||||||
|
67
sha1_file.c
67
sha1_file.c
@ -105,50 +105,59 @@ int mkdir_in_gitdir(const char *path)
|
|||||||
return adjust_shared_perm(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);
|
char *next_component = path + offset_1st_component(path);
|
||||||
struct stat st;
|
enum scld_error ret = SCLD_OK;
|
||||||
|
|
||||||
while (pos) {
|
while (ret == SCLD_OK && next_component) {
|
||||||
pos = strchr(pos, '/');
|
struct stat st;
|
||||||
if (!pos)
|
char *slash = strchr(next_component, '/');
|
||||||
|
|
||||||
|
if (!slash)
|
||||||
break;
|
break;
|
||||||
while (*++pos == '/')
|
|
||||||
;
|
next_component = slash + 1;
|
||||||
if (!*pos)
|
while (*next_component == '/')
|
||||||
|
next_component++;
|
||||||
|
if (!*next_component)
|
||||||
break;
|
break;
|
||||||
*--pos = '\0';
|
|
||||||
|
*slash = '\0';
|
||||||
if (!stat(path, &st)) {
|
if (!stat(path, &st)) {
|
||||||
/* path exists */
|
/* path exists */
|
||||||
if (!S_ISDIR(st.st_mode)) {
|
if (!S_ISDIR(st.st_mode))
|
||||||
*pos = '/';
|
ret = SCLD_EXISTS;
|
||||||
return -3;
|
} else if (mkdir(path, 0777)) {
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (mkdir(path, 0777)) {
|
|
||||||
if (errno == EEXIST &&
|
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 */
|
; /* somebody created it since we checked */
|
||||||
} else {
|
else if (errno == ENOENT)
|
||||||
*pos = '/';
|
/*
|
||||||
return -1;
|
* 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;
|
||||||
}
|
}
|
||||||
else if (adjust_shared_perm(path)) {
|
*slash = '/';
|
||||||
*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 */
|
/* path points to cache entries, so xstrdup before messing with it */
|
||||||
char *buf = xstrdup(path);
|
char *buf = xstrdup(path);
|
||||||
int result = safe_create_leading_directories(buf);
|
enum scld_error result = safe_create_leading_directories(buf);
|
||||||
free(buf);
|
free(buf);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user