git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH v2 0/3] Fix a race condition when reading loose refs
@ 2013-06-19  6:36 Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 1/3] resolve_ref_unsafe(): extract function handle_missing_loose_ref() Michael Haggerty
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Michael Haggerty @ 2013-06-19  6:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Jeff King, Johan Herland, Thomas Rast, git, Michael Haggerty

This is v2 of mh/loose-refs-race-with-pack-ref

I took Peff's suggestion to use gotos rather than an infinite loop in
the last patch, which means that there is no need for the old patch
03/04.

Michael Haggerty (3):
  resolve_ref_unsafe(): extract function handle_missing_loose_ref()
  resolve_ref_unsafe(): handle the case of an SHA-1 within loop
  resolve_ref_unsafe(): close race condition reading loose refs

 refs.c | 106 ++++++++++++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 72 insertions(+), 34 deletions(-)

-- 
1.8.3.1

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH v2 1/3] resolve_ref_unsafe(): extract function handle_missing_loose_ref()
  2013-06-19  6:36 [PATCH v2 0/3] Fix a race condition when reading loose refs Michael Haggerty
@ 2013-06-19  6:36 ` Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 2/3] resolve_ref_unsafe(): handle the case of an SHA-1 within loop Michael Haggerty
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: Michael Haggerty @ 2013-06-19  6:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Jeff King, Johan Herland, Thomas Rast, git, Michael Haggerty

The nesting was getting a bit out of hand, and it's about to get
worse.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 refs.c | 56 +++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 35 insertions(+), 21 deletions(-)

diff --git a/refs.c b/refs.c
index 42a7e17..ab1f99e 100644
--- a/refs.c
+++ b/refs.c
@@ -1197,6 +1197,37 @@ static struct ref_entry *get_packed_ref(const char *refname)
 	return find_ref(get_packed_refs(&ref_cache), refname);
 }
 
+/*
+ * A loose ref file doesn't exist; check for a packed ref.  The
+ * options are forwarded from resolve_safe_unsafe().
+ */
+static const char *handle_missing_loose_ref(const char *refname,
+					    unsigned char *sha1,
+					    int reading,
+					    int *flag)
+{
+	struct ref_entry *entry;
+
+	/*
+	 * The loose reference file does not exist; check for a packed
+	 * reference.
+	 */
+	entry = get_packed_ref(refname);
+	if (entry) {
+		hashcpy(sha1, entry->u.value.sha1);
+		if (flag)
+			*flag |= REF_ISPACKED;
+		return refname;
+	}
+	/* The reference is not a packed reference, either. */
+	if (reading) {
+		return NULL;
+	} else {
+		hashclr(sha1);
+		return refname;
+	}
+}
+
 const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)
 {
 	int depth = MAXDEPTH;
@@ -1222,28 +1253,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		git_snpath(path, sizeof(path), "%s", refname);
 
 		if (lstat(path, &st) < 0) {
-			struct ref_entry *entry;
-
-			if (errno != ENOENT)
+			if (errno == ENOENT)
+				return handle_missing_loose_ref(refname, sha1,
+								reading, flag);
+			else
 				return NULL;
-			/*
-			 * The loose reference file does not exist;
-			 * check for a packed reference.
-			 */
-			entry = get_packed_ref(refname);
-			if (entry) {
-				hashcpy(sha1, entry->u.value.sha1);
-				if (flag)
-					*flag |= REF_ISPACKED;
-				return refname;
-			}
-			/* The reference is not a packed reference, either. */
-			if (reading) {
-				return NULL;
-			} else {
-				hashclr(sha1);
-				return refname;
-			}
 		}
 
 		/* Follow "normalized" - ie "refs/.." symlinks by hand */
-- 
1.8.3.1

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v2 2/3] resolve_ref_unsafe(): handle the case of an SHA-1 within loop
  2013-06-19  6:36 [PATCH v2 0/3] Fix a race condition when reading loose refs Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 1/3] resolve_ref_unsafe(): extract function handle_missing_loose_ref() Michael Haggerty
@ 2013-06-19  6:36 ` Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 3/3] resolve_ref_unsafe(): close race condition reading loose refs Michael Haggerty
  2013-06-19 16:01 ` [PATCH v2 0/3] Fix a race condition when " Jeff King
  3 siblings, 0 replies; 6+ messages in thread
From: Michael Haggerty @ 2013-06-19  6:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Jeff King, Johan Herland, Thomas Rast, git, Michael Haggerty

There is only one "break" statement within the loop, which jumps to
the code after the loop that handles the case of a file that holds a
SHA-1.  So move that code from below the loop into the if statement
where the break was previously located.  This makes the logic flow
more local.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 refs.c | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/refs.c b/refs.c
index ab1f99e..867cf9f 100644
--- a/refs.c
+++ b/refs.c
@@ -1300,8 +1300,19 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		/*
 		 * Is it a symbolic ref?
 		 */
-		if (prefixcmp(buffer, "ref:"))
-			break;
+		if (prefixcmp(buffer, "ref:")) {
+			/*
+			 * Please note that FETCH_HEAD has a second
+			 * line containing other data.
+			 */
+			if (get_sha1_hex(buffer, sha1) ||
+			    (buffer[40] != '\0' && !isspace(buffer[40]))) {
+				if (flag)
+					*flag |= REF_ISBROKEN;
+				return NULL;
+			}
+			return refname;
+		}
 		if (flag)
 			*flag |= REF_ISSYMREF;
 		buf = buffer + 4;
@@ -1314,13 +1325,6 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		}
 		refname = strcpy(refname_buffer, buf);
 	}
-	/* Please note that FETCH_HEAD has a second line containing other data. */
-	if (get_sha1_hex(buffer, sha1) || (buffer[40] != '\0' && !isspace(buffer[40]))) {
-		if (flag)
-			*flag |= REF_ISBROKEN;
-		return NULL;
-	}
-	return refname;
 }
 
 char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)
-- 
1.8.3.1

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v2 3/3] resolve_ref_unsafe(): close race condition reading loose refs
  2013-06-19  6:36 [PATCH v2 0/3] Fix a race condition when reading loose refs Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 1/3] resolve_ref_unsafe(): extract function handle_missing_loose_ref() Michael Haggerty
  2013-06-19  6:36 ` [PATCH v2 2/3] resolve_ref_unsafe(): handle the case of an SHA-1 within loop Michael Haggerty
@ 2013-06-19  6:36 ` Michael Haggerty
  2013-06-19 16:01 ` [PATCH v2 0/3] Fix a race condition when " Jeff King
  3 siblings, 0 replies; 6+ messages in thread
From: Michael Haggerty @ 2013-06-19  6:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Jeff King, Johan Herland, Thomas Rast, git, Michael Haggerty

We read loose references in two steps.  The code is roughly:

    lstat()
    if error ENOENT:
        loose ref is missing; look for corresponding packed ref
    else if S_ISLNK:
        readlink()
        if error:
            report failure
    else if S_ISDIR:
        report failure
    else
        open()
        if error:
            report failure
        read()

The problem is that the first filesystem call, to lstat(), is not
atomic with the second filesystem call, to readlink() or open().
Therefore it is possible for another process to change the file
between our two calls, for example:

* If the other process deletes the file, our second call will fail
  with ENOENT, which we *should* interpret as "loose ref is missing;
  look for corresponding packed ref".  This can arise if the other
  process is pack-refs; it might have just written a new packed-refs
  file containing the old contents of the reference then deleted the
  loose ref.

* If the other process changes a symlink into a plain file, our call
  to readlink() will fail with EINVAL, which we *should* respond to by
  trying to open() and read() the file.

The old code treats the reference as missing in both of these cases,
which is incorrect.

So instead, handle errors more selectively: if the result of
readline()/open() is a failure that is inconsistent with the result of
the previous lstat(), then something is fishy.  In this case jump back
and start over again with a fresh call to lstat().

One race is still possible and undetected: another process could
change the file from a regular file into a symlink between the call to
lstat and the call to open().  The open() call would silently follow
the symlink and not know that something is wrong.  This situation
could be detected in two ways:

* On systems that support O_NOFOLLOW, pass that option to the open().

* On other systems, call fstat() on the fd returned by open() and make
  sure that it agrees with the stat info from the original lstat().

However, we don't use symlinks anymore, so this situation is unlikely.
Moreover, it doesn't appear that treating a symlink as a regular file
would have grave consequences; after all, this is exactly how the code
handles non-relative symlinks.  So this commit leaves that race
unaddressed.

Note that this solves only the part of the race within
resolve_ref_unsafe. In the situation described above, we may still be
depending on a cached view of the packed-refs file; that race will be
dealt with in a future patch.

This problem was reported and diagnosed by Jeff King <peff@peff.net>,
and this solution is derived from his patch.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 refs.c | 28 ++++++++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 867cf9f..70b2c82 100644
--- a/refs.c
+++ b/refs.c
@@ -1252,6 +1252,16 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 
 		git_snpath(path, sizeof(path), "%s", refname);
 
+		/*
+		 * We might have to loop back here to avoid a race
+		 * condition: first we lstat() the file, then we try
+		 * to read it as a link or as a file.  But if somebody
+		 * changes the type of the file (file <-> directory
+		 * <-> symlink) between the lstat() and reading, then
+		 * we don't want to report that as an error but rather
+		 * try again starting with the lstat().
+		 */
+	stat_ref:
 		if (lstat(path, &st) < 0) {
 			if (errno == ENOENT)
 				return handle_missing_loose_ref(refname, sha1,
@@ -1263,8 +1273,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		/* Follow "normalized" - ie "refs/.." symlinks by hand */
 		if (S_ISLNK(st.st_mode)) {
 			len = readlink(path, buffer, sizeof(buffer)-1);
-			if (len < 0)
-				return NULL;
+			if (len < 0) {
+				if (errno == ENOENT || errno == EINVAL)
+					/* inconsistent with lstat; retry */
+					goto stat_ref;
+				else
+					return NULL;
+			}
 			buffer[len] = 0;
 			if (!prefixcmp(buffer, "refs/") &&
 					!check_refname_format(buffer, 0)) {
@@ -1287,8 +1302,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		 * a ref
 		 */
 		fd = open(path, O_RDONLY);
-		if (fd < 0)
-			return NULL;
+		if (fd < 0) {
+			if (errno == ENOENT)
+				/* inconsistent with lstat; retry */
+				goto stat_ref;
+			else
+				return NULL;
+		}
 		len = read_in_full(fd, buffer, sizeof(buffer)-1);
 		close(fd);
 		if (len < 0)
-- 
1.8.3.1

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH v2 0/3] Fix a race condition when reading loose refs
  2013-06-19  6:36 [PATCH v2 0/3] Fix a race condition when reading loose refs Michael Haggerty
                   ` (2 preceding siblings ...)
  2013-06-19  6:36 ` [PATCH v2 3/3] resolve_ref_unsafe(): close race condition reading loose refs Michael Haggerty
@ 2013-06-19 16:01 ` Jeff King
  2013-06-19 17:24   ` Junio C Hamano
  3 siblings, 1 reply; 6+ messages in thread
From: Jeff King @ 2013-06-19 16:01 UTC (permalink / raw)
  To: Michael Haggerty; +Cc: Junio C Hamano, Johan Herland, Thomas Rast, git

On Wed, Jun 19, 2013 at 08:36:25AM +0200, Michael Haggerty wrote:

> I took Peff's suggestion to use gotos rather than an infinite loop in
> the last patch, which means that there is no need for the old patch
> 03/04.

Thanks, this version looks good to me.

I'm sure the Pascal programmers of the world collectively sighed in
disgust at a code review requesting a for loop turn into a goto, but I
think it is more readable than the first version. :)

-Peff

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2 0/3] Fix a race condition when reading loose refs
  2013-06-19 16:01 ` [PATCH v2 0/3] Fix a race condition when " Jeff King
@ 2013-06-19 17:24   ` Junio C Hamano
  0 siblings, 0 replies; 6+ messages in thread
From: Junio C Hamano @ 2013-06-19 17:24 UTC (permalink / raw)
  To: Jeff King; +Cc: Michael Haggerty, Johan Herland, Thomas Rast, git

Jeff King <peff@peff.net> writes:

> On Wed, Jun 19, 2013 at 08:36:25AM +0200, Michael Haggerty wrote:
>
>> I took Peff's suggestion to use gotos rather than an infinite loop in
>> the last patch, which means that there is no need for the old patch
>> 03/04.
>
> Thanks, this version looks good to me.
>
> I'm sure the Pascal programmers of the world collectively sighed in
> disgust at a code review requesting a for loop turn into a goto, but I
> think it is more readable than the first version. :)
>
> -Peff

Thanks, both.  Essentially the first two are the same from the
previous round, and the third one gives us "oops, unexpected
state---let's retry" in a more straight-forward way.

Looks very sensible; will queue.

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2013-06-19 17:24 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2013-06-19  6:36 [PATCH v2 0/3] Fix a race condition when reading loose refs Michael Haggerty
2013-06-19  6:36 ` [PATCH v2 1/3] resolve_ref_unsafe(): extract function handle_missing_loose_ref() Michael Haggerty
2013-06-19  6:36 ` [PATCH v2 2/3] resolve_ref_unsafe(): handle the case of an SHA-1 within loop Michael Haggerty
2013-06-19  6:36 ` [PATCH v2 3/3] resolve_ref_unsafe(): close race condition reading loose refs Michael Haggerty
2013-06-19 16:01 ` [PATCH v2 0/3] Fix a race condition when " Jeff King
2013-06-19 17:24   ` Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).