Skip to content

Commit d0956cf

Browse files
committed
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
2 parents c380cf8 + 08f555c commit d0956cf

File tree

6 files changed

+155
-67
lines changed

6 files changed

+155
-67
lines changed

builtin/init-db.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -515,13 +515,14 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
515515
saved = shared_repository;
516516
shared_repository = 0;
517517
switch (safe_create_leading_directories_const(argv[0])) {
518-
case -3:
518+
case SCLD_OK:
519+
case SCLD_PERMS:
520+
break;
521+
case SCLD_EXISTS:
519522
errno = EEXIST;
520523
/* fallthru */
521-
case -1:
522-
die_errno(_("cannot mkdir %s"), argv[0]);
523-
break;
524524
default:
525+
die_errno(_("cannot mkdir %s"), argv[0]);
525526
break;
526527
}
527528
shared_repository = saved;

cache.h

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,29 @@ enum sharedrepo {
737737
};
738738
int git_config_perm(const char *var, const char *value);
739739
int adjust_shared_perm(const char *path);
740-
int safe_create_leading_directories(char *path);
741-
int safe_create_leading_directories_const(const char *path);
740+
741+
/*
742+
* Create the directory containing the named path, using care to be
743+
* somewhat safe against races. Return one of the scld_error values
744+
* to indicate success/failure.
745+
*
746+
* SCLD_VANISHED indicates that one of the ancestor directories of the
747+
* path existed at one point during the function call and then
748+
* suddenly vanished, probably because another process pruned the
749+
* directory while we were working. To be robust against this kind of
750+
* race, callers might want to try invoking the function again when it
751+
* returns SCLD_VANISHED.
752+
*/
753+
enum scld_error {
754+
SCLD_OK = 0,
755+
SCLD_FAILED = -1,
756+
SCLD_PERMS = -2,
757+
SCLD_EXISTS = -3,
758+
SCLD_VANISHED = -4
759+
};
760+
enum scld_error safe_create_leading_directories(char *path);
761+
enum scld_error safe_create_leading_directories_const(const char *path);
762+
742763
int mkdir_in_gitdir(const char *path);
743764
extern void home_config_paths(char **global, char **xdg, char *file);
744765
extern char *expand_user_path(const char *path);

dir.c

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,8 +1511,13 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
15111511
flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
15121512
dir = opendir(path->buf);
15131513
if (!dir) {
1514-
/* an empty dir could be removed even if it is unreadble */
1515-
if (!keep_toplevel)
1514+
if (errno == ENOENT)
1515+
return keep_toplevel ? -1 : 0;
1516+
else if (errno == EACCES && !keep_toplevel)
1517+
/*
1518+
* An empty dir could be removable even if it
1519+
* is unreadable:
1520+
*/
15161521
return rmdir(path->buf);
15171522
else
15181523
return -1;
@@ -1528,13 +1533,21 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
15281533

15291534
strbuf_setlen(path, len);
15301535
strbuf_addstr(path, e->d_name);
1531-
if (lstat(path->buf, &st))
1532-
; /* fall thru */
1533-
else if (S_ISDIR(st.st_mode)) {
1536+
if (lstat(path->buf, &st)) {
1537+
if (errno == ENOENT)
1538+
/*
1539+
* file disappeared, which is what we
1540+
* wanted anyway
1541+
*/
1542+
continue;
1543+
/* fall thru */
1544+
} else if (S_ISDIR(st.st_mode)) {
15341545
if (!remove_dir_recurse(path, flag, &kept_down))
15351546
continue; /* happy */
1536-
} else if (!only_empty && !unlink(path->buf))
1547+
} else if (!only_empty &&
1548+
(!unlink(path->buf) || errno == ENOENT)) {
15371549
continue; /* happy, too */
1550+
}
15381551

15391552
/* path too long, stat fails, or non-directory still exists */
15401553
ret = -1;
@@ -1544,7 +1557,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
15441557

15451558
strbuf_setlen(path, original_len);
15461559
if (!ret && !keep_toplevel && !kept_down)
1547-
ret = rmdir(path->buf);
1560+
ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
15481561
else if (kept_up)
15491562
/*
15501563
* report the uplevel that it is not an error that we

merge-recursive.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ static int make_room_for_path(struct merge_options *o, const char *path)
693693
/* Make sure leading directories are created */
694694
status = safe_create_leading_directories_const(path);
695695
if (status) {
696-
if (status == -3) {
696+
if (status == SCLD_EXISTS) {
697697
/* something else exists */
698698
error(msg, path, _(": perhaps a D/F conflict?"));
699699
return -1;

refs.c

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
20392039
int type, lflags;
20402040
int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
20412041
int missing = 0;
2042+
int attempts_remaining = 3;
20422043

20432044
lock = xcalloc(1, sizeof(struct ref_lock));
20442045
lock->lock_fd = -1;
@@ -2080,7 +2081,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
20802081

20812082
lock->lk = xcalloc(1, sizeof(struct lock_file));
20822083

2083-
lflags = LOCK_DIE_ON_ERROR;
2084+
lflags = 0;
20842085
if (flags & REF_NODEREF) {
20852086
refname = orig_refname;
20862087
lflags |= LOCK_NODEREF;
@@ -2093,13 +2094,32 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
20932094
if ((flags & REF_NODEREF) && (type & REF_ISSYMREF))
20942095
lock->force_write = 1;
20952096

2096-
if (safe_create_leading_directories(ref_file)) {
2097+
retry:
2098+
switch (safe_create_leading_directories(ref_file)) {
2099+
case SCLD_OK:
2100+
break; /* success */
2101+
case SCLD_VANISHED:
2102+
if (--attempts_remaining > 0)
2103+
goto retry;
2104+
/* fall through */
2105+
default:
20972106
last_errno = errno;
20982107
error("unable to create directory for %s", ref_file);
20992108
goto error_return;
21002109
}
21012110

21022111
lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, lflags);
2112+
if (lock->lock_fd < 0) {
2113+
if (errno == ENOENT && --attempts_remaining > 0)
2114+
/*
2115+
* Maybe somebody just deleted one of the
2116+
* directories leading to ref_file. Try
2117+
* again:
2118+
*/
2119+
goto retry;
2120+
else
2121+
unable_to_lock_index_die(ref_file, errno);
2122+
}
21032123
return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
21042124

21052125
error_return:
@@ -2508,6 +2528,51 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
25082528
*/
25092529
#define TMP_RENAMED_LOG "logs/refs/.tmp-renamed-log"
25102530

2531+
static int rename_tmp_log(const char *newrefname)
2532+
{
2533+
int attempts_remaining = 4;
2534+
2535+
retry:
2536+
switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
2537+
case SCLD_OK:
2538+
break; /* success */
2539+
case SCLD_VANISHED:
2540+
if (--attempts_remaining > 0)
2541+
goto retry;
2542+
/* fall through */
2543+
default:
2544+
error("unable to create directory for %s", newrefname);
2545+
return -1;
2546+
}
2547+
2548+
if (rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
2549+
if ((errno==EISDIR || errno==ENOTDIR) && --attempts_remaining > 0) {
2550+
/*
2551+
* rename(a, b) when b is an existing
2552+
* directory ought to result in ISDIR, but
2553+
* Solaris 5.8 gives ENOTDIR. Sheesh.
2554+
*/
2555+
if (remove_empty_directories(git_path("logs/%s", newrefname))) {
2556+
error("Directory not empty: logs/%s", newrefname);
2557+
return -1;
2558+
}
2559+
goto retry;
2560+
} else if (errno == ENOENT && --attempts_remaining > 0) {
2561+
/*
2562+
* Maybe another process just deleted one of
2563+
* the directories in the path to newrefname.
2564+
* Try again from the beginning.
2565+
*/
2566+
goto retry;
2567+
} else {
2568+
error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
2569+
newrefname, strerror(errno));
2570+
return -1;
2571+
}
2572+
}
2573+
return 0;
2574+
}
2575+
25112576
int rename_ref(const char *oldrefname, const char *newrefname, const char *logmsg)
25122577
{
25132578
unsigned char sha1[20], orig_sha1[20];
@@ -2555,30 +2620,9 @@ int rename_ref(const char *oldrefname, const char *newrefname, const char *logms
25552620
}
25562621
}
25572622

2558-
if (log && safe_create_leading_directories(git_path("logs/%s", newrefname))) {
2559-
error("unable to create directory for %s", newrefname);
2623+
if (log && rename_tmp_log(newrefname))
25602624
goto rollback;
2561-
}
25622625

2563-
retry:
2564-
if (log && rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
2565-
if (errno==EISDIR || errno==ENOTDIR) {
2566-
/*
2567-
* rename(a, b) when b is an existing
2568-
* directory ought to result in ISDIR, but
2569-
* Solaris 5.8 gives ENOTDIR. Sheesh.
2570-
*/
2571-
if (remove_empty_directories(git_path("logs/%s", newrefname))) {
2572-
error("Directory not empty: logs/%s", newrefname);
2573-
goto rollback;
2574-
}
2575-
goto retry;
2576-
} else {
2577-
error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
2578-
newrefname, strerror(errno));
2579-
goto rollback;
2580-
}
2581-
}
25822626
logmoved = log;
25832627

25842628
lock = lock_ref_sha1_basic(newrefname, NULL, 0, NULL);

sha1_file.c

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -105,50 +105,59 @@ int mkdir_in_gitdir(const char *path)
105105
return adjust_shared_perm(path);
106106
}
107107

108-
int safe_create_leading_directories(char *path)
108+
enum scld_error safe_create_leading_directories(char *path)
109109
{
110-
char *pos = path + offset_1st_component(path);
111-
struct stat st;
110+
char *next_component = path + offset_1st_component(path);
111+
enum scld_error ret = SCLD_OK;
112+
113+
while (ret == SCLD_OK && next_component) {
114+
struct stat st;
115+
char *slash = strchr(next_component, '/');
112116

113-
while (pos) {
114-
pos = strchr(pos, '/');
115-
if (!pos)
117+
if (!slash)
116118
break;
117-
while (*++pos == '/')
118-
;
119-
if (!*pos)
119+
120+
next_component = slash + 1;
121+
while (*next_component == '/')
122+
next_component++;
123+
if (!*next_component)
120124
break;
121-
*--pos = '\0';
125+
126+
*slash = '\0';
122127
if (!stat(path, &st)) {
123128
/* path exists */
124-
if (!S_ISDIR(st.st_mode)) {
125-
*pos = '/';
126-
return -3;
127-
}
128-
}
129-
else if (mkdir(path, 0777)) {
129+
if (!S_ISDIR(st.st_mode))
130+
ret = SCLD_EXISTS;
131+
} else if (mkdir(path, 0777)) {
130132
if (errno == EEXIST &&
131-
!stat(path, &st) && S_ISDIR(st.st_mode)) {
133+
!stat(path, &st) && S_ISDIR(st.st_mode))
132134
; /* somebody created it since we checked */
133-
} else {
134-
*pos = '/';
135-
return -1;
136-
}
137-
}
138-
else if (adjust_shared_perm(path)) {
139-
*pos = '/';
140-
return -2;
135+
else if (errno == ENOENT)
136+
/*
137+
* Either mkdir() failed because
138+
* somebody just pruned the containing
139+
* directory, or stat() failed because
140+
* the file that was in our way was
141+
* just removed. Either way, inform
142+
* the caller that it might be worth
143+
* trying again:
144+
*/
145+
ret = SCLD_VANISHED;
146+
else
147+
ret = SCLD_FAILED;
148+
} else if (adjust_shared_perm(path)) {
149+
ret = SCLD_PERMS;
141150
}
142-
*pos++ = '/';
151+
*slash = '/';
143152
}
144-
return 0;
153+
return ret;
145154
}
146155

147-
int safe_create_leading_directories_const(const char *path)
156+
enum scld_error safe_create_leading_directories_const(const char *path)
148157
{
149158
/* path points to cache entries, so xstrdup before messing with it */
150159
char *buf = xstrdup(path);
151-
int result = safe_create_leading_directories(buf);
160+
enum scld_error result = safe_create_leading_directories(buf);
152161
free(buf);
153162
return result;
154163
}

0 commit comments

Comments
 (0)