# SPDX-License-Identifier: AGPL-3.0-or-later import contextlib import fcntl import logging import os import pwd import re import subprocess import threading from . import interproc 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_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): raise RuntimeError('Could not acquire thread lock') try: yield finally: self._thread_mutex.release() @contextlib.contextmanager def lock_all(self): """Acquire both the thread lock and the file lock""" with self.lock_threads_only(): # Set up fd = self._open_lock_file() fcntl.lockf(fd, fcntl.LOCK_EX) # Enter context try: yield finally: # Clean up fcntl.lockf(fd, fcntl.LOCK_UN) fd.close() def _open_lock_file(self): """Attempt to open lock file for R&W. Raises OSError on failure""" 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(og_euid) 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, your_euid): # Don't change the current processes umask # Do create a new process args = [] if your_euid == 0: args.extend(['sudo', '-n', '-u', 'plinth']) args.extend(['/bin/sh', '-c']) args.append('umask 177 && > ' + self.lock_path) completed = subprocess.run(args, capture_output=True, check=False) if completed.returncode != 0: interproc.log_subprocess(completed) raise OSError('Could not create ' + self.lock_path) 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)