repo-commit: preserve existing object inode when staging to objects/
authorelectricface <songwentai@uniontech.com>
Mon, 29 Jun 2026 08:03:23 +0000 (16:03 +0800)
committerelectricface <songwentai@uniontech.com>
Wed, 8 Jul 2026 02:01:58 +0000 (10:01 +0800)
When ostree commit --consume is used and an object with the same
checksum already exists in objects/, rename_pending_loose_objects() was
unconditionally renaming the staging copy over it.  On Linux, renameat(2)
atomically replaces the destination for two regular files, silently
changing the inode of the existing repo object.

Fix this by checking whether the object already exists in objects/
before renaming.  If it does, the content is identical by definition
(the object store is content-addressed by SHA256), so we can simply
unlink the staging copy and keep the existing object with its original
inode.

Exception: .commitmeta objects are keyed by commit checksum rather than
their own content, so they can be updated in place (e.g. when GPG
signatures are added or deleted via ostree gpg-sign).  These are always
renamed unconditionally.

src/libostree/ostree-repo-commit.c

index 36240faea7d647a6a4cd983aa0655cdccee17ede..b7773e1db1480c6d2af4a04e1d97250ebdcfec03 100644 (file)
@@ -1801,9 +1801,34 @@ rename_pending_loose_objects (OstreeRepo *self, GCancellable *cancellable, GErro
                                                     cancellable, error))
             return FALSE;
 
-          if (!glnx_renameat (child_dfd_iter.fd, loose_objpath + 3, self->objects_dir_fd,
-                              loose_objpath, error))
-            return FALSE;
+          /* For content-addressed objects, use RENAME_NOREPLACE so that an
+           * existing object keeps its inode.  If the destination already
+           * exists (EEXIST) the content is identical by definition, so just
+           * drop the staging copy.
+           *
+           * Exception: .commitmeta objects are keyed by commit checksum, not
+           * by their own content, so they can be updated in place (e.g. when
+           * GPG signatures are added or deleted).  Always overwrite those.
+           */
+          if (g_str_has_suffix (child_dent->d_name, ".commitmeta"))
+            {
+              if (!glnx_renameat (child_dfd_iter.fd, loose_objpath + 3, self->objects_dir_fd,
+                                  loose_objpath, error))
+                return FALSE;
+            }
+          else if (glnx_renameat2_noreplace (child_dfd_iter.fd, loose_objpath + 3,
+                                             self->objects_dir_fd, loose_objpath)
+                   < 0)
+            {
+              if (errno == EEXIST)
+                {
+                  /* Object already present with same content; drop staging copy */
+                  if (!glnx_unlinkat (child_dfd_iter.fd, loose_objpath + 3, 0, error))
+                    return FALSE;
+                }
+              else
+                return glnx_throw_errno_prefix (error, "renameat2(noreplace, %s)", loose_objpath);
+            }
         }
     }