RefDirectory: fix pack() unlocking held locks prematurely When packing loose refs, if the caller already holds a lock on a ref being packed, pack() releases that lock as a side effect of deleting the loose ref file. This causes the caller to lose exclusive access without being aware of it. We have observed ref rewinds in production that we believe were caused by this bug. The algorithm described in the Javadoc for PackedBatchRefUpdate explicitly requires these locks to prevent concurrent writes. Releasing a caller-held lock from pack() violates that requirement. Preserve locks held by the caller when deleting loose refs during packing. Add a test verifying that a caller-held lock remains locked after pack() returns. The test fails without this fix. Bug: jgit-281 Co-Authored-By: Kaushik Lingarkar <klingarkar@nvidia.com> Signed-off-by: Adithya Chakilam <achakilam@nvidia.com> Change-Id: I7fad28c5fdacc172b5023c066d321f7a2f8b275a (cherry picked from commit 9d7ec6911e3d46a4621eac0958afc4e4e881406c)
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java index 2bafde6..488de5e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -1362,6 +1362,25 @@ public void testCommonRefPrefix() { StringUtils.commonPrefix("refs/heads/", "refs/heads/main")); } + @Test + public void testPackDoesNotUnlockHeldLocks() throws IOException { + String ref = "refs/heads/master"; + writeLooseRef(ref, A); + + File refFile = refdir.fileFor(ref); + LockFile held = new LockFile(refFile); + assertTrue("must acquire lock", held.lock()); + try { + refdir.pack(Map.of(ref, held)); + assertFalse("loose ref must be deleted after packing", + refFile.exists()); + assertTrue("held lock must still be locked after pack()", + held.isLocked()); + } finally { + held.unlock(); + } + } + void writePackedRef(String name, AnyObjectId id) throws IOException { writePackedRefs(id.name() + " " + name + "\n"); }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java index 29630b7..40b456c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -842,8 +842,13 @@ private void pack(Collection<String> refs, } newLoose = curLoose.remove(idx); } while (!looseRefs.compareAndSet(curLoose, newLoose)); - int levels = levelsIn(refName) - 2; - deleteAndUnlock(refFile, levels, rLck); + if (shouldUnlock) { + int levels = levelsIn(refName) - 2; + deleteAndUnlock(refFile, levels, rLck); + shouldUnlock = false; + } else { + delete(refFile); + } } } finally { if (shouldUnlock) {