Merge pull request #2528 from ThomasWaldmann/follow-symlinks

document follow_symlinks requirements, check libc, fixes #2507
This commit is contained in:
enkore 2017-05-25 09:26:09 +02:00 committed by GitHub
commit 2dcbe02e5a
5 changed files with 36 additions and 12 deletions

View File

@ -54,6 +54,21 @@ manages data.
.. _locking: https://en.wikipedia.org/wiki/File_locking#Lock_files .. _locking: https://en.wikipedia.org/wiki/File_locking#Lock_files
(G)LIBC requirements
--------------------
Borg uses some filesytem functions from Python's `os` standard library module
with `follow_symlinks=False`. These are implemented since quite a while with
the non-symlink-following (g)libc functions like e.g. `lstat` or `lutimes`
(not: `stat` or `utimes`).
Some stoneage systems (like RHEL/CentOS 5) and also Python interpreter binaries
compiled to be able to run on such systems (like Python installed via Anaconda)
might miss these functions and Borg won't be able to work correctly.
This issue will be detected early and Borg will abort with a fatal error.
For the Borg binaries, there are additional (g)libc requirements, see below.
.. _distribution-package: .. _distribution-package:
Distribution Package Distribution Package

View File

@ -569,7 +569,7 @@ Utilization of max. archive size: {csize_max:.0%}
path = os.path.join(dest, item.path) path = os.path.join(dest, item.path)
# Attempt to remove existing files, ignore errors on failure # Attempt to remove existing files, ignore errors on failure
try: try:
st = os.lstat(path) st = os.stat(path, follow_symlinks=False)
if stat.S_ISDIR(st.st_mode): if stat.S_ISDIR(st.st_mode):
os.rmdir(path) os.rmdir(path)
else: else:
@ -674,7 +674,7 @@ Utilization of max. archive size: {csize_max:.0%}
if fd: if fd:
os.fchown(fd, uid, gid) os.fchown(fd, uid, gid)
else: else:
os.lchown(path, uid, gid) os.chown(path, uid, gid, follow_symlinks=False)
except OSError: except OSError:
pass pass
if fd: if fd:

View File

@ -54,7 +54,7 @@ from .helpers import get_cache_dir
from .helpers import Manifest from .helpers import Manifest
from .helpers import hardlinkable from .helpers import hardlinkable
from .helpers import StableDict from .helpers import StableDict
from .helpers import check_extension_modules from .helpers import check_python, check_extension_modules
from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo
from .helpers import log_multi from .helpers import log_multi
from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
@ -436,7 +436,7 @@ class Archiver:
continue continue
path = os.path.normpath(path) path = os.path.normpath(path)
try: try:
st = os.lstat(path) st = os.stat(path, follow_symlinks=False)
except OSError as e: except OSError as e:
self.print_warning('%s: %s', path, e) self.print_warning('%s: %s', path, e)
continue continue
@ -498,7 +498,7 @@ class Archiver:
""" """
if st is None: if st is None:
with backup_io('stat'): with backup_io('stat'):
st = os.lstat(path) st = os.stat(path, follow_symlinks=False)
recurse_excluded_dir = False recurse_excluded_dir = False
if not matcher.match(path): if not matcher.match(path):
@ -3829,6 +3829,7 @@ class Archiver:
return args return args
def prerun_checks(self, logger): def prerun_checks(self, logger):
check_python()
check_extension_modules() check_extension_modules()
selftest(logger) selftest(logger)

View File

@ -114,6 +114,16 @@ class InvalidPlaceholder(PlaceholderError):
"""Invalid placeholder "{}" in string: {}""" """Invalid placeholder "{}" in string: {}"""
class PythonLibcTooOld(Error):
"""FATAL: this Python was compiled for a too old (g)libc and misses required functionality."""
def check_python():
required_funcs = {os.stat, os.utime, os.chown}
if not os.supports_follow_symlinks.issuperset(required_funcs):
raise PythonLibcTooOld
def check_extension_modules(): def check_extension_modules():
from . import platform, compress, item from . import platform, compress, item
if hashindex.API_VERSION != '1.1_01': if hashindex.API_VERSION != '1.1_01':
@ -1754,7 +1764,7 @@ class GenericDirEntry:
def stat(self, follow_symlinks=True): def stat(self, follow_symlinks=True):
assert not follow_symlinks assert not follow_symlinks
return os.lstat(self.path) return os.stat(self.path, follow_symlinks=follow_symlinks)
def _check_type(self, type): def _check_type(self, type):
st = self.stat(False) st = self.stat(False)

View File

@ -30,13 +30,11 @@ except ImportError:
raises = None raises = None
has_lchflags = hasattr(os, 'lchflags') or sys.platform.startswith('linux') has_lchflags = hasattr(os, 'lchflags') or sys.platform.startswith('linux')
no_lchlfags_because = '' if has_lchflags else '(not supported on this platform)'
try: try:
with tempfile.NamedTemporaryFile() as file: with tempfile.NamedTemporaryFile() as file:
platform.set_flags(file.name, stat.UF_NODUMP) platform.set_flags(file.name, stat.UF_NODUMP)
except OSError: except OSError:
has_lchflags = False has_lchflags = False
no_lchlfags_because = '(the file system at %s does not support flags)' % tempfile.gettempdir()
try: try:
import llfuse import llfuse
@ -67,7 +65,7 @@ def are_symlinks_supported():
with unopened_tempfile() as filepath: with unopened_tempfile() as filepath:
try: try:
os.symlink('somewhere', filepath) os.symlink('somewhere', filepath)
if os.lstat(filepath) and os.readlink(filepath) == 'somewhere': if os.stat(filepath, follow_symlinks=False) and os.readlink(filepath) == 'somewhere':
return True return True
except OSError: except OSError:
pass pass
@ -109,7 +107,7 @@ def is_utime_fully_supported():
open(filepath, 'w').close() open(filepath, 'w').close()
try: try:
os.utime(filepath, (1000, 2000), follow_symlinks=False) os.utime(filepath, (1000, 2000), follow_symlinks=False)
new_stats = os.lstat(filepath) new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000: if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:
return True return True
except OSError as err: except OSError as err:
@ -158,8 +156,8 @@ class BaseTestCase(unittest.TestCase):
for filename in diff.common: for filename in diff.common:
path1 = os.path.join(diff.left, filename) path1 = os.path.join(diff.left, filename)
path2 = os.path.join(diff.right, filename) path2 = os.path.join(diff.right, filename)
s1 = os.lstat(path1) s1 = os.stat(path1, follow_symlinks=False)
s2 = os.lstat(path2) s2 = os.stat(path2, follow_symlinks=False)
# Assume path2 is on FUSE if st_dev is different # Assume path2 is on FUSE if st_dev is different
fuse = s1.st_dev != s2.st_dev fuse = s1.st_dev != s2.st_dev
attrs = ['st_uid', 'st_gid', 'st_rdev'] attrs = ['st_uid', 'st_gid', 'st_rdev']