From 91c907f65700b2829fc7c4a192c81b8c4e24df47 Mon Sep 17 00:00:00 2001 From: fliu <10025-fliu@users.noreply.salsa.debian.org> Date: Wed, 7 Jul 2021 21:51:23 +0000 Subject: [PATCH] email: Open lock file as plinth user --- plinth/modules/email_server/lock.py | 83 ++++++++++++++++--------- plinth/modules/email_server/postconf.py | 2 +- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/plinth/modules/email_server/lock.py b/plinth/modules/email_server/lock.py index c3ff204a5..f613db75e 100644 --- a/plinth/modules/email_server/lock.py +++ b/plinth/modules/email_server/lock.py @@ -1,32 +1,43 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import contextlib -import errno import fcntl import logging import os import pwd +import re import subprocess import threading +lock_name_pattern = re.compile('^[0-9a-zA-Z_-]+$') logger = logging.getLogger(__name__) +class RaceCondition(AssertionError): + pass + + class Mutex: """File and pthread lock based resource mutex""" - def __init__(self, lock_file): - self.thread_mutex = threading.Lock() - self.lock_path = '/var/lock/' + lock_file + def __init__(self, lock_name): + if not lock_name_pattern.match(lock_name): + raise ValueError('Bad lock name') + self._lock_path = '/var/lock/plinth-%s.lock' % lock_name + self._thread_mutex = threading.Lock() + + @property + def lock_path(self): + return self._lock_path @contextlib.contextmanager def lock_threads_only(self): """Acquire the thread lock but not the file lock""" - if not self.thread_mutex.acquire(timeout=5): + if not self._thread_mutex.acquire(timeout=5): raise RuntimeError('Could not acquire thread lock') try: yield finally: - self.thread_mutex.release() + self._thread_mutex.release() @contextlib.contextmanager def lock_all(self): @@ -45,36 +56,48 @@ class Mutex: def _open_lock_file(self): """Attempt to open lock file for R&W. Raises OSError on failure""" - if os.getuid() == 0: - if os.path.exists(self.lock_path): - # Check its owner and mode - stat = os.stat(self.lock_path) - owner = pwd.getpwuid(stat.st_uid).pw_name - mode = stat.st_mode & 0o777 - if owner != 'plinth' or mode != 0o600: - logger.warning('Clean up bad file %s', self.lock_path) - os.unlink(self.lock_path) - self._create_lock_file_as_plinth() - else: - self._create_lock_file_as_plinth() + og_ruid, og_euid, og_suid = os.getresuid() + if og_euid == 0 and threading.active_count() > 1: + raise RaceCondition('setuid in a multi-threaded process') + if not os.path.exists(self.lock_path): + self._create_lock_file_as_plinth() - return open(self.lock_path, 'wb+') + fd = None + try: + if og_euid == 0: + # Temporarily run the current process as plinth + plinth_uid = pwd.getpwnam('plinth').pw_uid + self._checked_setresuid(og_ruid, plinth_uid, 0) + fd = open(self.lock_path, 'w+b') + finally: + # Restore resuid + if og_euid == 0: + self._checked_setresuid(og_ruid, 0, 0) + if og_suid != 0: + self._checked_setresuid(og_ruid, 0, og_suid) + + return fd def _create_lock_file_as_plinth(self): - args = ['sudo', '-n', '-u', 'plinth', 'touch', self.lock_path] + # Don't change the current processes umask + # Do create a new process + args = ['sudo', '-n', '-u', 'plinth', '/bin/sh', '-c'] + args.append('umask 177 && > ' + self.lock_path) + completed = subprocess.run(args, capture_output=True) if completed.returncode != 0: - logger.critical('Process returned %d', completed.returncode) + logger.critical('Subprocess returned %d', completed.returncode) logger.critical('Stdout: %r', completed.stdout) logger.critical('Stderr: %r', completed.stderr) raise OSError('Could not create ' + self.lock_path) - os.chmod(self.lock_path, 0o600) - def _try(self, function): - try: - return 0, function() - except OSError as error: - if error.errno in (errno.EACCES, errno.EPERM): - return error.errno, None - else: - raise + def _checked_setresuid(self, ruid, euid, suid): + os.setresuid(ruid, euid, suid) + if os.getresuid() != (ruid, euid, suid): + try: + raise SystemExit('PANIC: setresuid failed') + except SystemExit as e: + # Print stack trace + logger.exception(e) + # Force exit + exit(1) diff --git a/plinth/modules/email_server/postconf.py b/plinth/modules/email_server/postconf.py index 2553f24d3..caf443ea5 100644 --- a/plinth/modules/email_server/postconf.py +++ b/plinth/modules/email_server/postconf.py @@ -6,7 +6,7 @@ import re import subprocess from .lock import Mutex -postconf_mutex = Mutex('plinth-email-postconf.lock') +postconf_mutex = Mutex('email-postconf') @dataclasses.dataclass