From: Justin Tobler <jltobler@gmail.com>
To: git@vger.kernel.org
Cc: Justin Tobler <jltobler@gmail.com>, Qin ShiCheng <qeesung@live.com>
Subject: [PATCH 3/3] object-file: write ".keep" lockfiles during transaction commit
Date: Thu, 15 Oct 2026 21:29:01 -0500 [thread overview]
Message-ID: <20261016022901.3838402-4-jltobler@gmail.com> (raw)
In-Reply-To: <20261016022901.3838402-1-jltobler@gmail.com>
The "files" ODB backend writes a ".keep" lockfile into the transaction
quarantine directory via `odb_transaction_files_write_pack()`. When the
ODB transaction is committed, the ".keep" lockfile is migrated to the
primary ODB alongside its packfile. Writing the temporary lockfile in
this manner has a couple of downsides:
- Failing to migrate the lockfile during transaction commit due to a
concurrent process already acquiring the same lock still allows the
rest of the transaction to be migrated into the primary ODB. This
dirties the primary ODB with new objects even though no references
were updated and the operation failed.
- Even more concerning, the ".keep" tempfile atexit(3) handler tries
to remove any lockfile of the same name regardless of whether it was
created by the same process. If the ".keep" lockfile of the same
name for a concurrent process gets deleted prematurely, its packfile
becomes at risk of being deleted by concurrent housekeeping.
Stop creating/registering the temporary lockfiles when writing the
packfile to the transaction and instead do so when committing the
transaction. This ensures the lockfile is properly acquired prior to
migrating objects and that only the owning process can delete its own
lockfiles.
Reported-by: Qin ShiCheng <qeesung@live.com>
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
object-file.c | 76 +++++++++++++++++++++-----------------
t/t5547-push-quarantine.sh | 29 +++++++++++++--
2 files changed, 69 insertions(+), 36 deletions(-)
diff --git a/object-file.c b/object-file.c
index ebe46d35de..39e6299be1 100644
--- a/object-file.c
+++ b/object-file.c
@@ -492,6 +492,7 @@ struct odb_transaction_files {
struct transaction_packfile packfile;
const char *prefix;
+ struct strvec pack_names;
struct tempfile **pack_lockfiles;
size_t pack_lockfiles_nr;
size_t pack_lockfiles_alloc;
@@ -1257,7 +1258,7 @@ int read_loose_object(struct repository *repo,
}
static int write_pack_lock(struct odb_source *source, const char *pack_name,
- const char *keep_msg)
+ const char *keep_msg, struct tempfile **out)
{
struct strbuf lock_path = STRBUF_INIT;
struct strbuf contents = STRBUF_INIT;
@@ -1284,6 +1285,7 @@ static int write_pack_lock(struct odb_source *source, const char *pack_name,
goto out;
}
+ *out = register_tempfile(lock_path.buf);
ret = 0;
out:
@@ -1293,6 +1295,40 @@ static int write_pack_lock(struct odb_source *source, const char *pack_name,
return ret;
}
+static int write_pack_locks(struct odb_transaction_files *transaction)
+{
+ struct strbuf keep_msg = STRBUF_INIT;
+ char hostname[HOST_NAME_MAX + 1];
+ int ret = 0;
+
+ if (!transaction->pack_names.nr)
+ return 0;
+
+ if (xgethostname(hostname, sizeof(hostname)))
+ xsnprintf(hostname, sizeof(hostname), "localhost");
+ strbuf_addf(&keep_msg, "receive-pack %"PRIuMAX" on %s",
+ (uintmax_t)getpid(), hostname);
+
+ for (size_t i = 0; i < transaction->pack_names.nr; i++) {
+ struct tempfile *lock;
+
+ ret = write_pack_lock(transaction->base.source,
+ transaction->pack_names.v[i],
+ keep_msg.buf, &lock);
+ if (ret < 0)
+ break;
+
+ ALLOC_GROW(transaction->pack_lockfiles,
+ transaction->pack_lockfiles_nr + 1,
+ transaction->pack_lockfiles_alloc);
+ transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] = lock;
+ }
+
+ strbuf_release(&keep_msg);
+
+ return ret;
+}
+
static int odb_transaction_files_commit(struct odb_transaction *base)
{
struct odb_transaction_files *transaction =
@@ -1321,6 +1357,9 @@ static int odb_transaction_files_commit(struct odb_transaction *base)
delete_tempfile(&temp);
strbuf_release(&temp_path);
+ if (write_pack_locks(transaction))
+ return error(_("unable to lock packfiles"));
+
/*
* Make the object files visible in the primary ODB after their data is
* fully durable.
@@ -1426,18 +1465,12 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,
return -1;
}
} else {
- struct strbuf final_path = STRBUF_INIT;
struct strbuf pack_name = STRBUF_INIT;
- struct strbuf keep_msg = STRBUF_INIT;
- char hostname[HOST_NAME_MAX + 1];
int ret;
strvec_pushl(&child.args, "index-pack", "--stdin", NULL);
push_header_arg(&child.args, &hdr);
- if (xgethostname(hostname, sizeof(hostname)))
- xsnprintf(hostname, sizeof(hostname), "localhost");
-
if (!opts->quiet && err_fd)
strvec_push(&child.args, "--show-resolving-progress");
if (err_fd)
@@ -1477,37 +1510,12 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,
return -1;
}
- strbuf_addf(&keep_msg, "receive-pack %"PRIuMAX" on %s",
- (uintmax_t)getpid(), hostname);
-
- if (write_pack_lock(transaction->quarantine, pack_name.buf, keep_msg.buf)) {
- strbuf_release(&pack_name);
- strbuf_release(&keep_msg);
- strbuf_addstr(err_msg, "lockfile write failed");
- return -1;
- }
-
- ALLOC_GROW(transaction->pack_lockfiles,
- transaction->pack_lockfiles_nr + 1,
- transaction->pack_lockfiles_alloc);
-
- /*
- * The lockfile filepath is expected to be the final location of
- * the ".keep" file after being migrated to the main ODB source.
- * This ensures the lockfile can be found and removed later
- * after the ODB transaction has been committed.
- */
- strbuf_addf(&final_path, "%s/pack/pack-%s.keep",
- base->source->path, pack_name.buf);
- transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =
- register_tempfile(final_path.buf);
+ strvec_push(&transaction->pack_names, pack_name.buf);
odb_source_prepare(transaction->quarantine,
ODB_PREPARE_FLUSH_CACHES);
- strbuf_release(&final_path);
strbuf_release(&pack_name);
- strbuf_release(&keep_msg);
}
return 0;
@@ -1523,6 +1531,7 @@ static int odb_transaction_files_finalize(struct odb_transaction *base)
ret |= delete_tempfile(&transaction->pack_lockfiles[i]);
free(transaction->pack_lockfiles);
+ strvec_clear(&transaction->pack_names);
return ret;
}
@@ -1555,6 +1564,7 @@ int odb_transaction_files_begin(struct odb_source *source,
transaction->base.write_pack = odb_transaction_files_write_pack;
transaction->base.env = odb_transaction_files_env;
transaction->flags = flags;
+ strvec_init(&transaction->pack_names);
transaction->prefix = "bulk-fsync";
if (flags & ODB_TRANSACTION_RECEIVE) {
diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh
index 1b7097179e..cf349eee2c 100755
--- a/t/t5547-push-quarantine.sh
+++ b/t/t5547-push-quarantine.sh
@@ -77,10 +77,9 @@ test_expect_success '.keep file is removed after push' '
git -C keep.git config set receive.unpackLimit 0 &&
# While incoming objects are still quarantined, validate that the
- # ".keep" lockfile is present in the quarantine directory.
+ # ".keep" lockfile is not present in the quarantine directory.
test_hook -C keep.git pre-receive <<-\EOF &&
- keep="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.keep)" &&
- test -f "$keep"
+ test ! -e "$GIT_QUARANTINE_PATH"/pack/pack-*.keep
EOF
# After quarantined objects are migrated, validate that the ".keep"
@@ -101,4 +100,28 @@ test_expect_success '.keep file is removed after push' '
test_path_is_missing "$keep"
'
+test_expect_success '.keep file created outside of process is not removed' '
+ test_when_finished rm -rf keep.git &&
+ git init --bare keep.git &&
+
+ git -C keep.git config set receive.unpackLimit 0 &&
+
+ # Create a ".keep" lockfile matching the packfile to simulate a
+ # concurrent git-receive-pack(1) that was already written its lockfile.
+ test_hook -C keep.git pre-receive <<-\EOF &&
+ pack="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.pack)" &&
+ pack="${pack##*/}" &&
+ echo "baz" >"objects/pack/${pack%.pack}.keep"
+ EOF
+
+ test_commit bar &&
+ test_must_fail git push keep.git HEAD &&
+
+ # The ".keep" lockfile of the concurrent git-receive-pack(1) should be
+ # left in its place.
+ keep="$(ls keep.git/objects/pack/pack-*.keep)" &&
+ test_path_is_file "$keep" &&
+ test_grep "baz" "$keep"
+'
+
test_done
--
2.55.0.424.g13c7afec21
prev parent reply other threads:[~2026-10-16 2:29 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-16 2:28 [PATCH 0/3] object-file: fix ".keep" lockfile ownership in ODB transactions Justin Tobler
2026-10-16 2:28 ` [PATCH 1/3] pack-write: lift and expose index-pack report parsing Justin Tobler
2026-10-16 2:29 ` [PATCH 2/3] object-file: explicitly write ".keep" lockfile Justin Tobler
2026-10-16 2:29 ` Justin Tobler [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261016022901.3838402-4-jltobler@gmail.com \
--to=jltobler@gmail.com \
--cc=git@vger.kernel.org \
--cc=qeesung@live.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox