"Backup::Error: Restore operation failed: gtar: .: Cannot unlink: Invalid argument" errors during restore on XFS filesystems containing "./"

Summary

GitLab backup restore fails on XFS filesystems with the error gtar: .: Cannot unlink: Invalid argument when extracting tar archives that contain a ./ (current directory) entry. This is because XFS handles directory unlinking differently than ext4, and the --unlink-first tar option used during restore attempts to unlink the current directory, which XFS rejects with EINVAL.

There have been at least 3 customer reports. I was working on this one

Steps to reproduce

  1. Set up a GitLab Omnibus installation on a system using XFS filesystem for /var/opt/gitlab
  2. Create a GitLab backup:
    /opt/gitlab/bin/gitlab-backup create
  3. Attempt to restore the backup:
    BACKUP="<backup_id>" force=yes /opt/gitlab/bin/gitlab-backup restore

What is the current bug behavior?

The restore process fails during the uploads restoration phase with:

rake aborted!
Backup::Error: Restore operation failed: gtar: .: Cannot unlink: Invalid argument
gtar: Exiting with failure status due to previous errors
/opt/gitlab/embedded/service/gitlab-rails/lib/backup/targets/files.rb:104:in `restore'

The backup tar archives contain a ./ entry:

$ tar -tvf uploads.tar.gz
drwxr-xr-x git/git           0 2026-01-20 20:35 ./
...

When tar uses the --unlink-first option and encounters this entry on XFS, it attempts to call unlink(".") which XFS rejects with EINVAL (Invalid argument). This is valid filesystem behavior - XFS is stricter about this operation than ext4.

What is the expected correct behavior?

The restore process should complete successfully on XFS filesystems by treating the gtar: .: Cannot unlink: Invalid argument warning as non-critical, similar to how other non-critical tar warnings are already handled.

Relevant logs and/or screenshots

2026-01-14 11:22:48 +0100 -- Restoring repositories ... done
2026-01-14 11:22:48 +0100 -- Restoring uploads ...
rake aborted!
Backup::Error: Restore operation failed: gtar: .: Cannot unlink: Invalid argument
gtar: Exiting with failure status due to previous errors
/opt/gitlab/embedded/service/gitlab-rails/lib/backup/targets/files.rb:104:in `restore'
/opt/gitlab/embedded/service/gitlab-rails/lib/backup/tasks/task.rb:31:in `restore!'
/opt/gitlab/embedded/service/gitlab-rails/lib/backup/restore/process.rb:30:in `execute!'
/opt/gitlab/embedded/service/gitlab-rails/lib/backup/manager.rb:101:in `run_restore_task'
...

Environment

  • GitLab Omnibus installation
  • AlmaLinux 9.7
  • XFS filesystem on /var/opt/gitlab
  • SELinux enabled (confirmed not the cause)

Output of checks

This bug happens on GitLab Self-Managed (Omnibus).

Results of GitLab environment info

Filesystem     Type  1K-blocks       Used Available Use% Mounted on
/dev/sdb1      xfs  2146696176 1380857340 765838836  65% /var/opt/gitlab

Proposed fix

Add the XFS-specific unlink error to the noncritical_warnings list in lib/backup/targets/files.rb:

def noncritical_warning?(warning)
  noncritical_warnings = [
    /^g?tar: \.: Cannot mkdir: No such file or directory$/,
    /^g?tar: \.: Cannot unlink: Invalid argument$/  # XFS filesystem behavior
  ]

  noncritical_warnings.map { |w| warning =~ w }.any?
end

This approach is consistent with the existing handling of the Cannot mkdir warning (see #22442 (closed)) and allows the restore to complete successfully since the ./ entry doesn't need to be unlinked for the restore to work correctly.

Workaround

Users can manually patch lib/backup/targets/files.rb to add the warning pattern to the noncritical_warnings array, or skip the affected restore targets and manually extract them:

BACKUP="<backup_id>" SKIP=uploads force=yes /opt/gitlab/bin/gitlab-backup restore

# Then manually extract uploads
cd /var/opt/gitlab/backups
sudo -u git tar -xzf uploads.tar.gz -C /var/opt/gitlab/gitlab-rails/uploads --no-overwrite-dir

Note: The --no-overwrite-dir flag cannot be used with --unlink-first, so the workaround requires manual extraction.

Edited by 🤖 GitLab Bot 🤖