diff --git a/borg/archive.py b/borg/archive.py index 167cc4711..b10bac07a 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -372,17 +372,6 @@ Number of files: {0.stats.nfiles}'''.format( raise Exception('Unknown archive item type %r' % item[b'mode']) def restore_attrs(self, path, item, symlink=False, fd=None): - xattrs = item.get(b'xattrs', {}) - for k, v in xattrs.items(): - try: - xattr.setxattr(fd or path, k, v, follow_symlinks=False) - except OSError as e: - if e.errno not in (errno.ENOTSUP, errno.EACCES, ): - # only raise if the errno is not on our ignore list: - # ENOTSUP == xattrs not supported here - # EACCES == permission denied to set this specific xattr - # (this may happen related to security.* keys) - raise uid = gid = None if not self.numeric_owner: uid = user2uid(item[b'user']) @@ -420,6 +409,19 @@ Number of files: {0.stats.nfiles}'''.format( os.lchflags(path, item[b'bsdflags']) except OSError: pass + # chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include + # the Linux capabilities in the "security.capability" attribute. + xattrs = item.get(b'xattrs', {}) + for k, v in xattrs.items(): + try: + xattr.setxattr(fd or path, k, v, follow_symlinks=False) + except OSError as e: + if e.errno not in (errno.ENOTSUP, errno.EACCES): + # only raise if the errno is not on our ignore list: + # ENOTSUP == xattrs not supported here + # EACCES == permission denied to set this specific xattr + # (this may happen related to security.* keys) + raise def rename(self, name): if name in self.manifest.archives: diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 1723bd7af..7ee13a77f 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -639,6 +639,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input/taggedall')), ['.NOBACKUP1', '.NOBACKUP2', 'CACHEDIR.TAG', ]) + @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2') + def test_extract_capabilities(self): + fchown = os.fchown + + # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not + # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them. + def patched_fchown(fd, uid, gid): + xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False) + fchown(fd, uid, gid) + + # The capability descriptor used here is valid and taken from a /usr/bin/ping + capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + self.create_regular_file('file') + xattr.setxattr('input/file', 'security.capability', capabilities) + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + with changedir('output'): + with patch.object(os, 'fchown', patched_fchown): + self.cmd('extract', self.repository_location + '::test') + assert xattr.getxattr('input/file', 'security.capability') == capabilities + def test_path_normalization(self): self.cmd('init', self.repository_location) self.create_regular_file('dir1/dir2/file', size=1024 * 80) diff --git a/borg/xattr.py b/borg/xattr.py index 27a18df6d..f2f764737 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -2,10 +2,12 @@ """ import errno import os +import subprocess import sys import tempfile from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_int, c_uint32, get_errno from ctypes.util import find_library +from distutils.version import LooseVersion from .logger import create_logger logger = create_logger() @@ -46,6 +48,22 @@ if libc_name is None: logger.error(msg) raise Exception(msg) +# If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by +# the 'test_extract_capabilities' test, but also allows xattrs to work with fakeroot on Linux in normal use. +# TODO: Check whether fakeroot supports xattrs on all platforms supported below. +# TODO: If that's the case then we can make Borg fakeroot-xattr-compatible on these as well. +LD_PRELOAD = os.environ.get('LD_PRELOAD', '') +XATTR_FAKEROOT = False +if sys.platform.startswith('linux') and 'fakeroot' in LD_PRELOAD: + fakeroot_version = LooseVersion(subprocess.check_output(['fakeroot', '-v']).decode('ascii').split()[-1]) + if fakeroot_version >= LooseVersion("1.20.2"): + # 1.20.2 has been confirmed to have xattr support + # 1.18.2 has been confirmed not to have xattr support + # Versions in-between are unknown + libc_name = LD_PRELOAD + XATTR_FAKEROOT = True + + try: libc = CDLL(libc_name, use_errno=True) except OSError as e: