[fix] Make read-only mount bind actually read-only (#343)

* [fix] Mount bind readonly not really readonly
* Attempt to clarify and fix some issues with the readonly mount
* Fix some missing messages and exception handling
* Get rid of horrible bash command
* Use subproces.check_call to avoid security hazard
* Revert comment about hard link
* Add test that mount binds are readonly
This commit is contained in:
ljf (zamentur) 2017-08-13 22:28:36 +02:00 committed by Alexandre Aubin
parent 3ede5fc39d
commit bb4af396d8
3 changed files with 60 additions and 18 deletions

View file

@ -69,11 +69,12 @@
"backup_archive_open_failed": "Unable to open the backup archive", "backup_archive_open_failed": "Unable to open the backup archive",
"backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup",
"backup_archive_writing_error": "Unable to add files to backup into the compressed archive", "backup_archive_writing_error": "Unable to add files to backup into the compressed archive",
"backup_ask_for_copying_if_needed": "Your system don't support completely the quick method to organize files in the archive, do you want to organize its by copying {size:s}MB?", "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?",
"backup_borg_not_implemented": "Borg backup method is not yet implemented", "backup_borg_not_implemented": "Borg backup method is not yet implemented",
"backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory",
"backup_cleaning_failed": "Unable to clean-up the temporary backup directory", "backup_cleaning_failed": "Unable to clean-up the temporary backup directory",
"backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive",
"backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.",
"backup_created": "Backup created", "backup_created": "Backup created",
"backup_creating_archive": "Creating the backup archive...", "backup_creating_archive": "Creating the backup archive...",
"backup_creation_failed": "Backup creation failed", "backup_creation_failed": "Backup creation failed",

View file

@ -40,6 +40,7 @@ from moulinette import msignals, m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils import filesystem from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
from yunohost.app import ( from yunohost.app import (
app_info, _is_installed, _parse_app_instance_name app_info, _is_installed, _parse_app_instance_name
@ -1543,23 +1544,37 @@ class BackupMethod(object):
if not os.path.isdir(dest_dir): if not os.path.isdir(dest_dir):
filesystem.mkdir(dest_dir, parents=True) filesystem.mkdir(dest_dir, parents=True)
# Try to bind files # For directory, attempt to mount bind
if os.path.isdir(src): if os.path.isdir(src):
filesystem.mkdir(dest, parents=True, force=True) filesystem.mkdir(dest, parents=True, force=True)
ret = subprocess.call(["mount", "-r", "--rbind", src, dest])
if ret == 0: try:
continue subprocess.check_call(["mount", "--rbind", src, dest])
subprocess.check_call(["mount", "-o", "remount,ro,bind", dest])
except Exception as e:
logger.warning(m18n.n("backup_couldnt_bind", src=src, dest=dest))
# To check if dest is mounted, use /proc/mounts that
# escape spaces as \040
raw_mounts = read_file("/proc/mounts").strip().split('\n')
mounts = [ m.split()[1] for m in raw_mounts ]
mounts = [ m.replace("\\040", " ") for m in mounts ]
if dest in mounts:
subprocess.check_call(["umount", "-R", dest])
else: else:
logger.warning(m18n.n("bind_mouting_disable")) # Success, go to next file to organize
subprocess.call(["mountpoint", "-q", dest,
"&&", "umount", "-R", dest])
elif os.path.isfile(src) or os.path.islink(src):
# Create a hardlink if src and dest are on the filesystem
if os.stat(src).st_dev == os.stat(dest_dir).st_dev:
os.link(src, dest)
continue continue
# Add to the list to copy # For files, create a hardlink
elif os.path.isfile(src) or os.path.islink(src):
# Can create a hard link only if files are on the same fs
# (i.e. we can't if it's on a different fs)
if os.stat(src).st_dev == os.stat(dest_dir).st_dev:
os.link(src, dest)
# Success, go to next file to organize
continue
# If mountbind or hardlink couldnt be created,
# prepare a list of files that need to be copied
paths_needed_to_be_copied.append(path) paths_needed_to_be_copied.append(path)
if len(paths_needed_to_be_copied) == 0: if len(paths_needed_to_be_copied) == 0:
@ -1576,15 +1591,18 @@ class BackupMethod(object):
if size > MB_ALLOWED_TO_ORGANIZE: if size > MB_ALLOWED_TO_ORGANIZE:
try: try:
i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed',
answers='y/N', size=size)) answers='y/N', size=str(size)))
except NotImplemented: except NotImplemented:
logger.error(m18n.n('backup_unable_to_organize_files')) raise MoulinetteError(errno.EIO,
m18n.n('backup_unable_to_organize_files'))
else: else:
if i != 'y' and i != 'Y': if i != 'y' and i != 'Y':
logger.error(m18n.n('backup_unable_to_organize_files')) raise MoulinetteError(errno.EIO,
m18n.n('backup_unable_to_organize_files'))
# Copy unbinded path # Copy unbinded path
logger.info(m18n.n('backup_copying_to_organize_the_archive', size=size)) logger.info(m18n.n('backup_copying_to_organize_the_archive',
size=str(size)))
for path in paths_needed_to_be_copied: for path in paths_needed_to_be_copied:
dest = os.path.join(self.work_dir, path['dest']) dest = os.path.join(self.work_dir, path['dest'])
if os.path.isdir(path['source']): if os.path.isdir(path['source']):

View file

@ -652,3 +652,26 @@ def test_restore_archive_with_no_json(mocker):
ignore_system=False, ignore_apps=False) ignore_system=False, ignore_apps=False)
m18n.n.assert_any_call('backup_invalid_archive') m18n.n.assert_any_call('backup_invalid_archive')
def test_backup_binds_are_readonly(monkeypatch):
def custom_mount_and_backup(self, backup_manager):
self.manager = backup_manager
self._organize_files()
confssh = os.path.join(self.work_dir, "conf/ssh")
output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh,
shell=True)
assert "Read-only file system" in output
if self._recursive_umount(self.work_dir) > 0:
raise Exception("Backup cleaning failed !")
self.clean()
monkeypatch.setattr("yunohost.backup.BackupMethod.mount_and_backup",
custom_mount_and_backup)
# Create the backup
backup_create(ignore_system=False, ignore_apps=True)