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

Re: [PATCH] Teach "git add" and friends to be paranoid

From
Junio C Hamano <gitster@pobox.com>
Date
Feb 18, 2010, 05:36 UTC
Message-ID
<7vocjnqf5c.fsf@alter.siamese.dyndns.org>
In-Reply-To
<alpine.LFD.2.00.1002172350080.1946@xanadu.home>
Nicolas Pitre <nico@fluxnic.net> writes:
> It is likely to have better performance if the buffer is small enough to 
> fit in the CPU L1 cache.  There are two sequencial passes over the 
> buffer: one for the SHA1 computation, and another for the compression, 
> and currently they're sure to trash the L1 cache on each pass.

I did a very unscientific test to hash about 14k paths (arch/ and fs/ from the kernel source) using "git-hash-object -w --stdin-paths" into an empty repository with varying sizes of paranoia buffer (quarter, 1, 4, 8 and 256kB) and saw 8-30% overhead. 256kB did hurt and around 4kB seemed to be optimal for my this small sample load.

In any case, with any size of paranoia, this hurts the sane use case, so I'd introduce an expert switch to disable it, like this.

-- >8 -- When creating a loose object, we normally mmap(2) the entire file, and hash and then compress to write it out in two separate steps for efficiency.

This is perfectly good for the intended use of git---nobody is supposed to be insane enough to expect that it won't break anything to muck with the contents of a file after telling git to index it and before getting the control back from git.

But the nature of breakage caused by such an abuse is rather bad. We will end up with loose object files, whose names do not match what are stored and recovered when uncompressed.

This teaches the index_mem() codepath to be paranoid and hash and compress the data after reading it in core. The contents hashed may not match the contents of the file in an insane use case, but at least this way the result will be internally consistent.

People with saner use of git can regain performance by setting a new configuration variable 'core.volatilefiles' to false to disable this check.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config.txt |    7 ++++
 cache.h                  |    1 +
 config.c                 |    6 +++
 environment.c            |    1 +
 sha1_file.c              |   83 +++++++++++++++++++++++++++++++++++++--------
 5 files changed, 83 insertions(+), 15 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 52786c7..0295aee 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -117,6 +117,14 @@ core.fileMode::
 	the working copy are ignored; useful on broken filesystems like FAT.
 	See linkgit:git-update-index[1]. True by default.
 
+core.volatilefiles::
+	If you modify a file after telling git to record it (e.g. with
+	"git add") but before git finishes the request and gives the
+	control back to you, you may create a broken object (and of course
+	you can keep both halves ;-).  Setting this option to true will
+	tell git to be extra careful to detect the situation and abort.
+	Defaults to true.
+
 core.ignoreCygwinFSTricks::
 	This option is only used by Cygwin implementation of Git. If false,
 	the Cygwin stat() and lstat() functions are used. This may be useful
diff --git a/cache.h b/cache.h
index 231c06d..e5a87cf 100644
--- a/cache.h
+++ b/cache.h
@@ -497,6 +497,7 @@ extern int trust_ctime;
 extern int quote_path_fully;
 extern int has_symlinks;
 extern int ignore_case;
+extern int worktree_files_are_volatile;
 extern int assume_unchanged;
 extern int prefer_symlink_refs;
 extern int log_all_ref_updates;
diff --git a/config.c b/config.c
index 790405a..9898041 100644
--- a/config.c
+++ b/config.c
@@ -360,6 +360,12 @@ static int git_default_core_config(const char *var, const char *value)
 		trust_executable_bit = git_config_bool(var, value);
 		return 0;
 	}
+
+	if (!strcmp(var, "core.volatilefiles")) {
+		worktree_files_are_volatile = git_config_bool(var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "core.trustctime")) {
 		trust_ctime = git_config_bool(var, value);
 		return 0;
diff --git a/environment.c b/environment.c
index e278bce..5d0faf3 100644
--- a/environment.c
+++ b/environment.c
@@ -22,6 +22,7 @@ int is_bare_repository_cfg = -1; /* unspecified */
 int log_all_ref_updates = -1; /* unspecified */
 int warn_ambiguous_refs = 1;
 int repository_format_version;
+int worktree_files_are_volatile = 1; /* yuck */
 const char *git_commit_encoding;
 const char *git_log_output_encoding;
 int shared_repository = PERM_UMASK;
diff --git a/sha1_file.c b/sha1_file.c
index 52d1ead..e126179 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -2335,14 +2335,18 @@ static int create_tmpfile(char *buffer, size_t bufsiz, const char *filename)
 }
 
 static int write_loose_object(const unsigned char *sha1, char *hdr, int hdrlen,
-			      void *buf, unsigned long len, time_t mtime)
+			      void *buf, unsigned long len, time_t mtime,
+			      int paranoid)
 {
 	int fd, size, ret;
 	unsigned char *compressed;
 	z_stream stream;
 	char *filename;
 	static char tmpfile[PATH_MAX];
+	git_SHA_CTX ctx;
 
+	if (!worktree_files_are_volatile)
+		paranoid = 0;
 	filename = sha1_file_name(sha1);
 	fd = create_tmpfile(tmpfile, sizeof(tmpfile), filename);
 	if (fd < 0) {
@@ -2366,12 +2370,41 @@ static int write_loose_object(const unsigned char *sha1, char *hdr, int hdrlen,
 	stream.next_in = (unsigned char *)hdr;
 	stream.avail_in = hdrlen;
 	while (deflate(&stream, 0) == Z_OK)
-		/* nothing */;
+		; /* nothing */
 
 	/* Then the data itself.. */
-	stream.next_in = buf;
-	stream.avail_in = len;
-	ret = deflate(&stream, Z_FINISH);
+	if (paranoid) {
+		unsigned char stablebuf[4096];
+		char *bufptr = buf;
+		unsigned long remainder = len;
+
+		git_SHA1_Init(&ctx);
+		git_SHA1_Update(&ctx, hdr, hdrlen);
+
+		ret = Z_OK;
+		while (remainder) {
+			unsigned long chunklen = remainder;
+
+			if (sizeof(stablebuf) <= chunklen)
+				chunklen = sizeof(stablebuf);
+			memcpy(stablebuf, bufptr, chunklen);
+			git_SHA1_Update(&ctx, stablebuf, chunklen);
+			stream.next_in = stablebuf;
+			stream.avail_in = chunklen;
+			do {
+				ret = deflate(&stream, Z_NO_FLUSH);
+			} while (ret == Z_OK);
+			bufptr += chunklen;
+			remainder -= chunklen;
+		}
+		if (ret != Z_STREAM_END)
+			ret = deflate(&stream, Z_FINISH);
+	} else {
+		stream.next_in = buf;
+		stream.avail_in = len;
+		ret = deflate(&stream, Z_FINISH);
+	}
+
 	if (ret != Z_STREAM_END)
 		die("unable to deflate new object %s (%d)", sha1_to_hex(sha1), ret);
 
@@ -2381,6 +2414,12 @@ static int write_loose_object(const unsigned char *sha1, char *hdr, int hdrlen,
 
 	size = stream.total_out;
 
+	if (paranoid) {
+		unsigned char paranoid_sha1[20];
+		git_SHA1_Final(paranoid_sha1, &ctx);
+		if (hashcmp(paranoid_sha1, sha1))
+			die("hashed file is volatile");
+	}
 	if (write_buffer(fd, compressed, size) < 0)
 		die("unable to write sha1 file");
 	close_sha1_file(fd);
@@ -2398,7 +2437,7 @@ static int write_loose_object(const unsigned char *sha1, char *hdr, int hdrlen,
 	return move_temp_to_file(tmpfile, filename);
 }
 
-int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *returnsha1)
+static int write_sha1_file_paranoid(void *buf, unsigned long len, const char *type, unsigned char *returnsha1, int paranoid)
 {
 	unsigned char sha1[20];
 	char hdr[32];
@@ -2412,7 +2451,12 @@ int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned cha
 		hashcpy(returnsha1, sha1);
 	if (has_sha1_file(sha1))
 		return 0;
-	return write_loose_object(sha1, hdr, hdrlen, buf, len, 0);
+	return write_loose_object(sha1, hdr, hdrlen, buf, len, 0, paranoid);
+}
+
+int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *returnsha1)
+{
+	return write_sha1_file_paranoid(buf, len, type, returnsha1, 0);
 }
 
 int force_object_loose(const unsigned char *sha1, time_t mtime)
@@ -2430,7 +2474,7 @@ int force_object_loose(const unsigned char *sha1, time_t mtime)
 	if (!buf)
 		return error("cannot read sha1_file for %s", sha1_to_hex(sha1));
 	hdrlen = sprintf(hdr, "%s %lu", typename(type), len) + 1;
-	ret = write_loose_object(sha1, hdr, hdrlen, buf, len, mtime);
+	ret = write_loose_object(sha1, hdr, hdrlen, buf, len, mtime, 0);
 	free(buf);
 
 	return ret;
@@ -2467,10 +2511,15 @@ int has_sha1_file(const unsigned char *sha1)
 	return has_loose_object(sha1);
 }
 
+#define INDEX_MEM_WRITE_OBJECT  01
+#define INDEX_MEM_PARANOID      02
+
 static int index_mem(unsigned char *sha1, void *buf, size_t size,
-		     int write_object, enum object_type type, const char *path)
+		     enum object_type type, const char *path, int flag)
 {
 	int ret, re_allocated = 0;
+	int write_object = flag & INDEX_MEM_WRITE_OBJECT;
+	int paranoid = flag & INDEX_MEM_PARANOID;
 
 	if (!type)
 		type = OBJ_BLOB;
@@ -2488,9 +2537,11 @@ static int index_mem(unsigned char *sha1, void *buf, size_t size,
 	}
 
 	if (write_object)
-		ret = write_sha1_file(buf, size, typename(type), sha1);
+		ret = write_sha1_file_paranoid(buf, size, typename(type),
+					       sha1, paranoid);
 	else
 		ret = hash_sha1_file(buf, size, typename(type), sha1);
+
 	if (re_allocated)
 		free(buf);
 	return ret;
@@ -2499,23 +2550,25 @@ static int index_mem(unsigned char *sha1, void *buf, size_t size,
 int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object,
 	     enum object_type type, const char *path)
 {
-	int ret;
+	int ret, flag;
 	size_t size = xsize_t(st->st_size);
 
+	flag = write_object ? INDEX_MEM_WRITE_OBJECT : 0;
 	if (!S_ISREG(st->st_mode)) {
 		struct strbuf sbuf = STRBUF_INIT;
 		if (strbuf_read(&sbuf, fd, 4096) >= 0)
-			ret = index_mem(sha1, sbuf.buf, sbuf.len, write_object,
-					type, path);
+			ret = index_mem(sha1, sbuf.buf, sbuf.len,
+					type, path, flag);
 		else
 			ret = -1;
 		strbuf_release(&sbuf);
 	} else if (size) {
 		void *buf = xmmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
-		ret = index_mem(sha1, buf, size, write_object, type, path);
+		flag |= INDEX_MEM_PARANOID;
+		ret = index_mem(sha1, buf, size, type, path, flag);
 		munmap(buf, size);
 	} else
-		ret = index_mem(sha1, NULL, size, write_object, type, path);
+		ret = index_mem(sha1, NULL, size, type, path, flag);
 	close(fd);
 	return ret;
 }
-- 
1.7.0.81.g58679
Previous: Nicolas PitreNext: Wincent Colaiuta
Message 44 of 84 in “Re: Bug#569505: git-core: 'git add' corrupts repository if the working directory is modified as it runs”
  1. Jonathan NiederFeb 12, 2010
  2. Zygo BlaxellFeb 12, 2010
  3. Jonathan NiederFeb 13, 2010
  4. Ilari LiusvaaraFeb 13, 2010
  5. Thomas RastFeb 13, 2010
  6. Ilari LiusvaaraFeb 13, 2010
  7. Dmitry PotapovFeb 13, 2010
  8. Zygo BlaxellFeb 13, 2010
  9. don't use mmap() to hash filesDmitry Potapov, Feb 14, 2010
  10. Junio C HamanoFeb 14, 2010
  11. Dmitry PotapovFeb 14, 2010
  12. Junio C HamanoFeb 14, 2010
  13. Thomas RastFeb 14, 2010
  14. Junio C HamanoFeb 14, 2010
  15. Johannes SchindelinFeb 14, 2010
  16. Junio C HamanoFeb 14, 2010
  17. Dmitry PotapovFeb 14, 2010
  18. Jakub NarebskiFeb 14, 2010
  19. Paolo BonziniFeb 14, 2010
  20. Johannes SchindelinFeb 14, 2010
  21. Dmitry PotapovFeb 14, 2010
  22. Johannes SchindelinFeb 14, 2010
  23. Johannes SchindelinFeb 14, 2010
  24. Dmitry PotapovFeb 14, 2010
  25. Zygo BlaxellFeb 14, 2010
  26. Nicolas PitreFeb 15, 2010
  27. Dmitry PotapovFeb 15, 2010
  28. Paolo BonziniFeb 15, 2010
  29. Dmitry PotapovFeb 15, 2010
  30. Dmitry PotapovFeb 14, 2010
  31. Avery PennarunFeb 14, 2010
  32. Nicolas PitreFeb 15, 2010
  33. Avery PennarunFeb 15, 2010
  34. Nicolas PitreFeb 15, 2010
  35. Avery PennarunFeb 15, 2010
  36. Nicolas PitreFeb 15, 2010
  37. don't use mmap() to hash filesDmitry Potapov, Feb 14, 2010
  38. Teach "git add" and friends to be paranoidJunio C Hamano, Feb 18, 2010
  39. Junio C HamanoFeb 18, 2010
  40. Zygo BlaxellFeb 18, 2010
  41. Junio C HamanoFeb 19, 2010
  42. Jeff KingFeb 18, 2010
  43. Nicolas PitreFeb 18, 2010
  44. Junio C HamanoFeb 18, 2010
  45. Wincent ColaiutaFeb 18, 2010
  46. Zygo BlaxellFeb 18, 2010
  47. Jonathan NiederFeb 18, 2010
  48. Junio C HamanoFeb 18, 2010
  49. Paolo BonziniFeb 22, 2010
  50. Dmitry PotapovFeb 22, 2010
  51. Thomas RastFeb 18, 2010
  52. Junio C HamanoFeb 18, 2010
  53. Nicolas PitreFeb 18, 2010
  54. 16 gig, 350,000 file repositoryBill Lear, Feb 18, 2010
  55. Nicolas PitreFeb 18, 2010
  56. Erik Faye-LundFeb 19, 2010
  57. Bill LearFeb 22, 2010
  58. Nicolas PitreFeb 22, 2010
  59. Peter HarrisFeb 18, 2010
  60. Junio C HamanoFeb 18, 2010
  61. Nicolas PitreFeb 18, 2010
  62. Jonathan NiederFeb 19, 2010
  63. Zygo BlaxellFeb 19, 2010
  64. Junio C HamanoFeb 19, 2010
  65. Zygo BlaxellFeb 19, 2010
  66. Dmitry PotapovFeb 19, 2010
  67. Junio C HamanoFeb 19, 2010
  68. Junio C HamanoFeb 20, 2010
  69. Dmitry PotapovFeb 21, 2010
  70. Junio C HamanoFeb 21, 2010
  71. Dmitry PotapovFeb 22, 2010
  72. Junio C HamanoFeb 22, 2010
  73. Dmitry PotapovFeb 22, 2010
  74. Nicolas PitreFeb 22, 2010
  75. Dmitry PotapovFeb 22, 2010
  76. Zygo BlaxellFeb 22, 2010
  77. Nicolas PitreFeb 22, 2010
  78. Junio C HamanoFeb 22, 2010
  79. Nicolas PitreFeb 22, 2010
  80. Dmitry PotapovFeb 22, 2010
  81. Nicolas PitreFeb 22, 2010
  82. mmap with MAP_PRIVATE is useless (was Re: Bug#569505: git-core: 'git add' corrupts repository if the working directory is modified as it runs)Paolo Bonzini, Feb 14, 2010
  83. Junio C HamanoFeb 14, 2010
  84. Paolo BonziniFeb 14, 2010

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.