git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v6 24/39] lockfile: avoid transitory invalid states

From
Michael Haggerty <mhagger@alum.mit.edu>
Date
Sep 26, 2014, 10:08 UTC
Message-ID
<1411726119-31598-25-git-send-email-mhagger@alum.mit.edu>
In-Reply-To
<1411726119-31598-1-git-send-email-mhagger@alum.mit.edu>

Because remove_lock_file() can be called any time by the signal handler, it is important that any lock_file objects that are in the lock_file_list are always in a valid state. And since lock_file objects are often reused (but are never removed from lock_file_list), that means we have to be careful whenever mutating a lock_file object to always keep it in a well-defined state.

This was formerly not the case, because part of the state was encoded by setting lk->filename to the empty string vs. a valid filename. It is wrong to assume that this string can be updated atomically; for example, even

    strcpy(lk->filename, value)
is unsafe.  But the old code was even more reckless; for example,
    strcpy(lk->filename, path);
    if (!(flags & LOCK_NODEREF))
            resolve_symlink(lk->filename, max_path_len);
    strcat(lk->filename, ".lock");

During the call to resolve_symlink(), lk->filename contained the name of the file that was being locked, not the name of the lockfile. If a signal were raised during that interval, then the signal handler would have deleted the valuable file!

We could probably continue to use the filename field to encode the state by being careful to write characters 1..N-1 of the filename first, and then overwrite the NUL at filename[0] with the first character of the filename, but that would be awkward and error-prone.

So, instead of using the filename field to determine whether the lock_file object is active, add a new field "lock_file::active" for this purpose. Be careful to set this field only when filename really contains the name of a file that should be deleted on cleanup.

Helped-by: Johannes Sixt <j6t@kdbg.org>
Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 cache.h      |  1 +
 lockfile.c   | 37 ++++++++++++++++++++++++++-----------
 read-cache.c |  1 +
 3 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/cache.h b/cache.h
index 9f09a89..81c70c7 100644
--- a/cache.h
+++ b/cache.h
@@ -576,6 +576,7 @@ extern int refresh_index(struct index_state *, unsigned int flags, const struct
 
 struct lock_file {
 	struct lock_file *next;
+	volatile sig_atomic_t active;
 	int fd;
 	pid_t owner;
 	char on_list;
diff --git a/lockfile.c b/lockfile.c
index 728ce49..d35ac44 100644
--- a/lockfile.c
+++ b/lockfile.c
@@ -23,7 +23,7 @@
  * used to prevent a forked process from closing a lockfile created by
  * its parent.
  *
- * A lock_file object can be in several states:
+ * The possible states of a lock_file object are as follows:
  *
  * - Uninitialized.  In this state the object's on_list field must be
  *   zero but the rest of its contents need not be initialized.  As
@@ -32,19 +32,27 @@
  *
  * - Locked, lockfile open (after hold_lock_file_for_update(),
  *   hold_lock_file_for_append(), or reopen_lock_file()). In this
- *   state, the lockfile exists, filename holds the filename of the
- *   lockfile, fd holds a file descriptor open for writing to the
- *   lockfile, and owner holds the PID of the process that locked the
- *   file.
+ *   state:
+ *   - the lockfile exists
+ *   - active is set
+ *   - filename holds the filename of the lockfile
+ *   - fd holds a file descriptor open for writing to the lockfile
+ *   - owner holds the PID of the process that locked the file
  *
  * - Locked, lockfile closed (after successful close_lock_file()).
  *   Same as the previous state, except that the lockfile is closed
  *   and fd is -1.
  *
  * - Unlocked (after commit_lock_file(), rollback_lock_file(), a
- *   failed attempt to lock, or a failed close_lock_file()). In this
- *   state, filename[0] == '\0' and fd is -1. The object is left
- *   registered in the lock_file_list, and on_list is set.
+ *   failed attempt to lock, or a failed close_lock_file()).  In this
+ *   state:
+ *   - active is unset
+ *   - filename[0] == '\0' (usually, though there are transitory states
+ *     in which this condition doesn't hold). Client code should *not*
+ *     rely on this fact!
+ *   - fd is -1
+ *   - the object is left registered in the lock_file_list, and
+ *     on_list is set.
  */
 
 static struct lock_file *lock_file_list;
@@ -175,9 +183,13 @@ static int lock_file(struct lock_file *lk, const char *path, int flags)
 		atexit(remove_lock_file);
 	}
 
+	if (lk->active)
+		die("BUG: cannot lock_file(\"%s\") using active struct lock_file",
+		    path);
 	if (!lk->on_list) {
 		/* Initialize *lk and add it to lock_file_list: */
 		lk->fd = -1;
+		lk->active = 0;
 		lk->owner = 0;
 		lk->filename[0] = 0;
 		lk->next = lock_file_list;
@@ -199,6 +211,7 @@ static int lock_file(struct lock_file *lk, const char *path, int flags)
 		return -1;
 	}
 	lk->owner = getpid();
+	lk->active = 1;
 	if (adjust_shared_perm(lk->filename)) {
 		int save_errno = errno;
 		error("cannot fix permission bits on %s", lk->filename);
@@ -298,7 +311,7 @@ int reopen_lock_file(struct lock_file *lk)
 {
 	if (0 <= lk->fd)
 		die(_("BUG: reopen a lockfile that is still open"));
-	if (!lk->filename[0])
+	if (!lk->active)
 		die(_("BUG: reopen a lockfile that has been committed"));
 	lk->fd = open(lk->filename, O_WRONLY);
 	return lk->fd;
@@ -308,7 +321,7 @@ int commit_lock_file(struct lock_file *lk)
 {
 	char result_file[PATH_MAX];
 
-	if (!lk->filename[0])
+	if (!lk->active)
 		die("BUG: attempt to commit unlocked object");
 
 	if (close_lock_file(lk))
@@ -325,6 +338,7 @@ int commit_lock_file(struct lock_file *lk)
 		return -1;
 	}
 
+	lk->active = 0;
 	lk->filename[0] = 0;
 	return 0;
 }
@@ -339,11 +353,12 @@ int hold_locked_index(struct lock_file *lk, int die_on_error)
 
 void rollback_lock_file(struct lock_file *lk)
 {
-	if (!lk->filename[0])
+	if (!lk->active)
 		return;
 
 	if (!close_lock_file(lk)) {
 		unlink_or_warn(lk->filename);
+		lk->active = 0;
 		lk->filename[0] = 0;
 	}
 }
diff --git a/read-cache.c b/read-cache.c
index 5ffb1d7..af69f34 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -2046,6 +2046,7 @@ static int commit_locked_index(struct lock_file *lk)
 			return -1;
 		if (rename(lk->filename, alternate_index_output))
 			return -1;
+		lk->active = 0;
 		lk->filename[0] = 0;
 		return 0;
 	} else {
-- 
2.1.0
Previous: Michael HaggertyNext: Michael Haggerty
Message 34 of 55 in “Lockfile correctness and refactoring”
  1. 00/39 Lockfile correctness and refactoringMichael Haggerty, Sep 26, 2014
  2. 01/39 unable_to_lock_die(): rename function from unable_to_lock_index_die()Michael Haggerty, Sep 26, 2014
  3. 02/39 api-lockfile: revise and expand the documentationMichael Haggerty, Sep 26, 2014
  4. Junio C HamanoSep 26, 2014
  5. Michael HaggertySep 30, 2014
  6. Junio C HamanoSep 30, 2014
  7. Michael HaggertyOct 1, 2014
  8. Junio C HamanoSep 26, 2014
  9. Michael HaggertySep 30, 2014
  10. Jeff KingSep 30, 2014
  11. Junio C HamanoSep 30, 2014
  12. Michael HaggertyOct 1, 2014
  13. 03/39 close_lock_file(): exit (successfully) if file is already closedMichael Haggerty, Sep 26, 2014
  14. 04/39 rollback_lock_file(): do not clear filename redundantlyMichael Haggerty, Sep 26, 2014
  15. 05/39 rollback_lock_file(): exit early if lock is not activeMichael Haggerty, Sep 26, 2014
  16. 06/39 rollback_lock_file(): set fd to -1Michael Haggerty, Sep 26, 2014
  17. 07/39 lockfile: unlock file if lockfile permissions cannot be adjustedMichael Haggerty, Sep 26, 2014
  18. 08/39 hold_lock_file_for_append(): release lock on errorsMichael Haggerty, Sep 26, 2014
  19. 09/39 lock_file(): always initialize and register lock_file objectMichael Haggerty, Sep 26, 2014
  20. 10/39 lockfile.c: document the various states of lock_file objectsMichael Haggerty, Sep 26, 2014
  21. 11/39 cache.h: define constants LOCK_SUFFIX and LOCK_SUFFIX_LENMichael Haggerty, Sep 26, 2014
  22. 12/39 delete_ref_loose(): don't muck around in the lock_file's filenameMichael Haggerty, Sep 26, 2014
  23. 13/39 prepare_index(): declare return value to be (const char *)Michael Haggerty, Sep 26, 2014
  24. 14/39 write_packed_entry_fn(): convert cb_data into a (const int *)Michael Haggerty, Sep 26, 2014
  25. 15/39 lock_file(): exit early if lockfile cannot be openedMichael Haggerty, Sep 26, 2014
  26. 16/39 remove_lock_file(): call rollback_lock_file()Michael Haggerty, Sep 26, 2014
  27. 17/39 commit_lock_file(): inline temporary variableMichael Haggerty, Sep 26, 2014
  28. 18/39 commit_lock_file(): die() if called for unlocked lockfile objectMichael Haggerty, Sep 26, 2014
  29. 19/39 close_lock_file(): if close fails, roll backMichael Haggerty, Sep 26, 2014
  30. 20/39 commit_lock_file(): rollback lock file on failure to renameMichael Haggerty, Sep 26, 2014
  31. 21/39 api-lockfile: document edge casesMichael Haggerty, Sep 26, 2014
  32. 22/39 dump_marks(): remove a redundant call to rollback_lock_file()Michael Haggerty, Sep 26, 2014
  33. 23/39 git_config_set_multivar_in_file(): avoid call to rollback_lock_file()Michael Haggerty, Sep 26, 2014
  34. 24/39 lockfile: avoid transitory invalid statesMichael Haggerty, Sep 26, 2014
  35. 25/39 struct lock_file: declare some fields volatileMichael Haggerty, Sep 26, 2014
  36. 26/39 try_merge_strategy(): remove redundant lock_file allocationMichael Haggerty, Sep 26, 2014
  37. 27/39 try_merge_strategy(): use a statically-allocated lock_file objectMichael Haggerty, Sep 26, 2014
  38. Junio C HamanoSep 26, 2014
  39. Michael HaggertySep 30, 2014
  40. Junio C HamanoSep 30, 2014
  41. 28/39 commit_lock_file(): use a strbuf to manage temporary spaceMichael Haggerty, Sep 26, 2014
  42. 29/39 Change lock_file::filename into a strbufMichael Haggerty, Sep 26, 2014
  43. 30/39 resolve_symlink(): use a strbuf for internal scratch spaceMichael Haggerty, Sep 26, 2014
  44. 31/39 resolve_symlink(): take a strbuf parameterMichael Haggerty, Sep 26, 2014
  45. 32/39 trim_last_path_component(): replace last_path_elm()Michael Haggerty, Sep 26, 2014
  46. 33/39 Extract a function commit_lock_file_to()Michael Haggerty, Sep 26, 2014
  47. 34/39 Rename LOCK_NODEREF to LOCK_NO_DEREFMichael Haggerty, Sep 26, 2014
  48. 35/39 lockfile.c: rename static functionsMichael Haggerty, Sep 26, 2014
  49. 36/39 get_locked_file_path(): new functionMichael Haggerty, Sep 26, 2014
  50. 37/39 hold_lock_file_for_append(): restore errno before returningMichael Haggerty, Sep 26, 2014
  51. 38/39 Move read_index() definition to read-cache.cMichael Haggerty, Sep 26, 2014
  52. 39/39 lockfile.h: extract new header file for the functions in lockfile.cMichael Haggerty, Sep 26, 2014
  53. Junio C HamanoSep 26, 2014
  54. Michael HaggertySep 30, 2014
  55. Junio C HamanoSep 26, 2014

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.