email: Open lock file as plinth user

This commit is contained in:
fliu 2021-07-07 21:51:23 +00:00 committed by Sunil Mohan Adapa
parent e2535bad49
commit 91c907f657
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
2 changed files with 54 additions and 31 deletions

View File

@ -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)

View File

@ -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