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
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 4dc40df..79f2013 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
@@ -1533,6 +1533,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 a9211fe..d5ac5aa 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
@@ -876,8 +876,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); + } LOG.debug(JGitText.get().deleteLooseRef, refFile, clr_oid); } } finally {