Server IP : 184.154.167.98 / Your IP : 3.141.201.92 Web Server : Apache System : Linux pink.dnsnetservice.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 User : puertode ( 1767) PHP Version : 7.2.34 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /usr/share/cagefs/ |
Upload File : |
#!/opt/cloudlinux/venv/bin/python3 -bb # -*- coding: utf-8 -*- # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2022 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT # pylint: disable=no-absolute-import import os import time import subprocess import signal from typing import Optional, List, Dict import traceback import fcntl import secureio from pathlib import Path import glob import psutil from clcommon.lib import cledition from clcommon.utils import run_command, ExternalProgramFailed, get_file_lines, write_file_lines from cldetectlib import is_da from clsentry import CLLIB_DSN, init_sentry_client from clsentry.utils import get_pkg_version from secureio import write_file_via_tempfile, logging _CAGEFS_MOUNT_BIN_FILE = '/usr/sbin/cagefs-mount' _LSNS_BIN_FILE = '/usr/bin/lsns' _MOUNT_BIN_FILE = '/bin/mount' _UMOUNT_BIN_FILE = '/bin/umount' _NSENTER_BIN = '/bin/nsenter' _GREP_BIN = '/bin/grep' _LOCK_FILE_NAME_PATTERN = '/var/cagefs/%s/%s.lock' _CAGEFS_SKELETON_DIR = '/usr/share/cagefs-skeleton' # /var/cagefs.uid/$PREFIX/$UID/ns.mnt _NS_MNT_FILE_NAME_PATTERN = '/var/cagefs.uid/%s/%d/ns.mnt' # /var/cagefs.uid/$PREFIX/$UID/ns.id _NS_ID_FILE_NAME_PATTERN = '/var/cagefs.uid/%s/%d/ns.id' sentry_client = init_sentry_client( 'cagefs', release=get_pkg_version('cagefs'), dsn=CLLIB_DSN, handle=False, ) class LockFailedException(Exception): pass class NsNotFoundException(Exception): pass class MountCommandFailedException(Exception): pass class CagefsMountInvalidUserException(Exception): pass class CagefsMountPIDWriteFailed(Exception): pass class CagefsMountJailCallFailed(Exception): pass _EXIT_VALIDATION_ERROR = 1 _EXIT_JAIL_MOUNT_ERROR = 2 _EXIT_PID_WRITE_ERROR = 3 class CagefsMountNotStartedException(Exception): def __init__(self, msg=''): self.msg = f'{msg}' if cledition.is_container(): self.msg += '\nThe Virtuozzo host mounting limit may have been reached.\n' \ 'Check for the presence of the kernel error "reached the limit on mounts" on VZ host.\n' \ 'More info at https://docs.cloudlinux.com/cloudlinux_installation/#known-restrictions-and-issues' def __str__(self): return self.msg def _find_command_in_ns(ns_id: str, cmd_to_search: str) -> bool: """ Find proposed command in proposed NS id :param ns_id: NS id to search command :param cmd_to_search: Command to search :return: True - command found in NS, False - not found """ # Example: # # lsns --type mnt --list --output command <ns_id> # COMMAND # /bin/sh /usr/bin/mysqld_safe --basedir=/usr # /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql .... try: cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'command', ns_id] stdout = run_command(cmd) for line in stdout.split('\n'): line = line.strip() if line == '' or 'COMMAND' in line: # Skip header line and empty line continue # line example: '/bin/sh /usr/bin/mysqld_safe --basedir=/usr' if line == cmd_to_search: return True except (ExternalProgramFailed, ): pass return False def _find_save_ns_id_for_user(username: str, filename_to_write: str): """ Find user's NS id and write it to file :param username: User name :param filename_to_write: File name to write """ cmd_to_search = f'{_CAGEFS_MOUNT_BIN_FILE} {username}' # 1. Get all user's NS id # lsns --type mnt --list --output ns,command cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'ns,command'] stdout = run_command(cmd) ns_id_list = [] for line in stdout.split('\n'): line = line.strip() if line == '' or 'NS' in line: # Skip header line and empty line continue # line example: # '4026532195 /usr/sbin/pdns_server --socket-dir=/run/pdns --guardian=no --daemon=no ...' ns_id, ns_command = line.split(' ', 1) if ns_command.strip() == cmd_to_search: # Command found, write NS id write_file_via_tempfile(ns_id, filename_to_write, 0o600) return ns_id_list.append(ns_id) # Command not found in lsns output, search it in each NS for ns_id in ns_id_list: # try to find user's NS by process '/usr/sbin/cagefs-mount <username>' if _find_command_in_ns(ns_id, cmd_to_search): # Command found, write NS id write_file_via_tempfile(ns_id, filename_to_write, 0x600) return raise NsNotFoundException(f"NS not found for user {username} when save NS id to file") def _get_all_ns() -> Dict[str, str]: """ Get all NS with processes in system :return: Dict: {'some_ns_id', 'some_pid_from_ns'} """ ns_dict = {} try: cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'ns,pid'] stdout = run_command(cmd) for line in stdout.split('\n'): line = line.strip() if line == '' or 'PID' in line: # Skip header line and empty line continue # line example: '4026532195 1582' line_parts = line.split() ns_dict[line_parts[0].strip()] = line_parts[1].strip() except (ExternalProgramFailed, ): pass return ns_dict def _get_pid_list_by_ns_id(ns_id: str, user_homedir: str) -> List[int]: """ Retrieves PID list for user in proposed NS id :param ns_id: NS id to retrieve PID list :param user_homedir: User homedir :return: list PIDs in NS, [] - NS not found/has no processes """ # Get all NS id in system with some PID in NS as dict {ns_id: pid} all_ns_id_dict = _get_all_ns() if ns_id not in all_ns_id_dict: return [] ns_pid = all_ns_id_dict[ns_id] try: # Check that NS with ns_id owned by user with proposed homedir # /bin/nsenter -m -t PID /bin/grep /usr/share/cagefs-skeleton/$USERHOME /proc/mounts user_home_is_skeleton = os.path.join(_CAGEFS_SKELETON_DIR, user_homedir) proc = subprocess.run([_NSENTER_BIN, '-m', '-t', ns_pid, _GREP_BIN, user_home_is_skeleton, '/proc/mounts'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) if proc.returncode != 0: # user home mount not found in NS return [] pid_list = [] # NS valid, get PID list from it cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'pid', ns_id] stdout = run_command(cmd) for line in stdout.split('\n'): line = line.strip() if line == '' or 'PID' in line: # Skip header line and empty line continue pid_list.append(int(line)) return pid_list except (ExternalProgramFailed, OSError, IOError, ): pass return [] def _get_pid_list_for_user(user_uid: int, cagefs_user_prefix: str, user_homedir: str) -> Optional[List[int]]: """ Retrieve pid list from user's NS :param user_uid: User's uid :param cagefs_user_prefix: User's cagefs prefix :param user_homedir: User's homedir :return: List of user's PIDs or None if user has no NS """ try: # Read NS id from file lines = get_file_lines(_NS_ID_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid)) ns_id = lines[0].strip() return _get_pid_list_by_ns_id(ns_id, user_homedir) except (OSError, IOError, IndexError, ): # No NS for this user pass return None def _kill_processes_in_list(pid_list: List[int]): """ Kill processes from list :param pid_list: PID list to kill """ for pid in pid_list: try: parent = psutil.Process(pid) except psutil.NoSuchProcess: logging(f'Process with PID {pid} is already dead, nothing to kill') continue children = parent.children(recursive=True) children.append(parent) for p in children: try: p.terminate() except psutil.NoSuchProcess: pass _, alive = psutil.wait_procs(children, timeout=3) if not alive: continue for p in alive: try: p.kill() except psutil.NoSuchProcess: pass _, still_alive = psutil.wait_procs(alive, timeout=3) if still_alive: logging("Some processes are still alive after sending " f"the SIGKILL signal: {still_alive}") def _acquire_lock(filename: str) -> int: """ Creates a lock file and acquire lock on it :return: File descriptor of created file """ try: os.makedirs(os.path.dirname(filename), mode=0o700, exist_ok=True) lock_fd = os.open(filename, os.O_CREAT, 0o600) fcntl.flock(lock_fd, fcntl.LOCK_EX) return lock_fd except IOError: raise LockFailedException("IO error happened while getting lock") def _release_lock(lock_fd: int) -> None: """ Release lock and close lock file :param lock_fd: Lock file descriptor """ fcntl.flock(lock_fd, fcntl.LOCK_UN) os.close(lock_fd) def _is_pid_file_created_successfully( pid_filename: str, process: subprocess.Popen ) -> bool: """ Waits for cagefs-mount pid file appears up to 60 seconds :param pid_filename: PID filename :param process: subprocess that creates the PID file :return: True - appears; False - Not """ for i in range(60000): try: os.stat(pid_filename) return True except (OSError, IOError): # Error, PID file absent # Check if subprocess is still alive if process.poll() is not None: return False time.sleep(0.001) return False def _create_namespace_user(username: str) -> bool: """ Create namespace for single user :param username: User name to create namespace """ from cagefsctl import get_user_prefix error = False background_processes = [] pid_file_created = False cagefs_mount_time_taken = 0 cagefs_mount_timeout = 20 # seconds try: cagefs_user_prefix = get_user_prefix(username) user_uid = secureio.clpwd.get_uid(username) # To create namespace for USER, use: # if /var/cagefs.uid/$PREFIX/$UID/ns.mnt bind mount exists already - do nothing # mkdir -p /var/cagefs.uid/$PREFIX/$UID # touch /var/cagefs.uid/$PREFIX/$UID/ns.mnt # /usr/sbin/cagefs-mount $USER # mount --bind /proc/$PID/ns/mnt /var/cagefs.uid/$PREFIX/$UID/ns.mnt # release lockfile /var/cagefs/$PREFIX/$USER.lock # _NS_MNT_FILE_NAME_PATTERN = '/var/cagefs.uid/$PREFIX/$UID/ns.mnt' # kill $PID (where $PID = pid of cagefs-mount process) ns_mnt_filename = _NS_MNT_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid) if os.path.exists(ns_mnt_filename): return False os.makedirs(os.path.dirname(ns_mnt_filename), mode=0o700, exist_ok=True) # touch /var/cagefs.uid/$PREFIX/$UID/ns.mnt Path(ns_mnt_filename).touch() cagefs_pid_file = f'/var/cagefs.uid/{cagefs_user_prefix}/{user_uid}/cagefs-mount.pid' try: os.unlink(cagefs_pid_file) except (OSError, IOError,): pass proc = subprocess.Popen( [_CAGEFS_MOUNT_BIN_FILE, username], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, text=True, ) cagefs_mount_pid = proc.pid background_processes.append(cagefs_mount_pid) # We should wait while binary creates cagefs mounts start_time = time.time() pid_file_created = _is_pid_file_created_successfully(cagefs_pid_file, proc) cagefs_mount_time_taken = time.time() - start_time if not pid_file_created: _kill_processes_in_list(background_processes) stdout, stderr = proc.communicate() stdout, stderr = stdout.strip(), stderr.strip() if proc.returncode == _EXIT_VALIDATION_ERROR: raise CagefsMountInvalidUserException( f"Input arguments validation failed in cagefs-mount for user={username}.\n" f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"' ) elif proc.returncode == _EXIT_JAIL_MOUNT_ERROR: raise CagefsMountJailCallFailed( f"Can't create namespace {ns_mnt_filename} for user {username}.\n" f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"' ) elif proc.returncode == _EXIT_PID_WRITE_ERROR: raise CagefsMountPIDWriteFailed( f"cagefs-mount created namespace for user={username}, but was unable to write pidfile.\n" f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"' ) raise CagefsMountNotStartedException( f'{cagefs_pid_file} was not created within the expected time frame.\n' f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"' ) # /bin/mount --bind /proc/$PID/ns/mnt /var/cagefs.uid/$PREFIX/$UID/ns.mnt res = subprocess.run( [_MOUNT_BIN_FILE, '--bind', f'/proc/{cagefs_mount_pid}/ns/mnt', ns_mnt_filename], capture_output=True, text=True, shell=False, ) if res.returncode != 0: raise MountCommandFailedException( f"Can't mount {ns_mnt_filename} for user {username}.\n" f'STDOUT: "{res.stdout}"\nSTDERR: "{res.stderr}"' ) # Find user's NS id and save it to file /var/cagefs.uid/$PREFIX/$UID/ns.id ns_id_filename = _NS_ID_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid) _find_save_ns_id_for_user(username, ns_id_filename) except ( CagefsMountNotStartedException, MountCommandFailedException, CagefsMountInvalidUserException, CagefsMountJailCallFailed, CagefsMountPIDWriteFailed, ) as e: error = True logging(str(e)) sentry_client.captureException() except Exception: error = True msg = traceback.format_exc() logging(f"General error while attempting to create a namespace for user {username}. Error is: {msg}") sentry_client.captureException() finally: # Kill cagefs-mount process _kill_processes_in_list(background_processes) if pid_file_created and cagefs_mount_time_taken > cagefs_mount_timeout: stdout, stderr = proc.communicate() sentry_client.captureMessage( f'{cagefs_pid_file} file was created after {cagefs_mount_time_taken:.2f}.\n' f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"' ) return error def _delete_namespace_user(username: str) -> bool: """ Delete namespace for single user :param username: User name to delete namespace """ from cagefsctl import get_user_prefix lock_obj = None error = False try: cagefs_user_prefix = get_user_prefix(username) user_uid = secureio.clpwd.get_uid(username) user_homedir = secureio.clpwd.get_homedir(username) ns_mnt_filename = _NS_MNT_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid) if not os.path.exists(ns_mnt_filename): # User has no NS return False lock_obj = _acquire_lock(_LOCK_FILE_NAME_PATTERN % (cagefs_user_prefix, username)) if not os.path.exists(ns_mnt_filename): # Check if User has no NS again after acquiring lock return False user_pids = _get_pid_list_for_user(user_uid, cagefs_user_prefix, user_homedir) if user_pids: _kill_processes_in_list(user_pids) # umount /var/cagefs.uid/$PREFIX/$UID/ns.mnt res = subprocess.run( [_UMOUNT_BIN_FILE, ns_mnt_filename], capture_output=True, text=True, shell=False, env={**os.environ, **{'LC_ALL': 'C'}}, ) if res.returncode != 0 and 'not mounted' not in res.stderr: raise MountCommandFailedException( f"Can't umount {ns_mnt_filename} for user {username}.\n" f'STDOUT: "{res.stdout}"\nSTDERR: "{res.stderr}"' ) # rf -f /var/cagefs.uid/$PREFIX/$UID/ns.mnt os.unlink(ns_mnt_filename) except MountCommandFailedException as e: # NOTE: Don't set `error` to `True` to preserve the previous behavior, # but send errors to Sentry to determine if there are any actual issues # with this command and decide if something needs to be fixed logging(str(e)) sentry_client.captureException() except (LockFailedException, ) as e: logging(f"Can't acqure lock for user {username}. Error is: {str(e)}") error = True sentry_client.captureException() except Exception: msg = traceback.format_exc() logging(f"Error during delete namespace for user {username}. Error is:\n{msg}") error = True sentry_client.captureException() if lock_obj: _release_lock(lock_obj) return error def create_namespace_user_list(username_list: list[str], verbose = False) -> int: """ Create namespace for users from list :param username_list: username list for prosess :param verbose: prints log in stdout if set """ errors = 0 for username in username_list: if verbose: print("Creating NS for user:", username) if _create_namespace_user(username): errors += 1 return errors def delete_namespace_user_list(username_list: list[str], verbose = False) -> bool: """ Delete namespace for users from list :param username_list: username list for prosess :param verbose: If True, print messages to stdout """ error = False for username in username_list: if verbose: print("Deleting NS for user:", username) if _delete_namespace_user(username): error = True return error def _get_httpd_php_fpm_service_override_files() -> Dict[str, str]: """ Get list of all php-fpm services override files on server :return Dict. Example: {'ea-php74-php-fpm.service': '/etc/systemd/system/ea-php74-php-fpm.service.d/override.conf', 'ea-php56-php-fpm.service': '/etc/systemd/system/ea-php56-php-fpm.service.d/override.conf' } """ systemd_dir = '/usr/lib/systemd/system' # Scan available ea-php fpm services mask_to_search = os.path.join(systemd_dir, 'ea-php*-fpm.service') service_names = [os.path.basename(x) for x in glob.glob(mask_to_search)] # Scan available alt-php fpm services mask_to_search = os.path.join(systemd_dir, 'alt-php*-fpm.service') service_names.extend([os.path.basename(x) for x in glob.glob(mask_to_search)]) # Add additional services - native php-fpm and httpd # DA doesn't register service in /usr/lib/systemd/service, so we need to check /etc/systemd/system add_services_names = ['php-fpm.service', 'httpd.service'] for service_name in add_services_names: if os.path.exists(os.path.join(systemd_dir, service_name)) or ( is_da() and os.path.exists(os.path.join('/etc/systemd/system', service_name)) ): service_names.append(service_name) # Create override configs list override_file_dict = {service_name: '/etc/systemd/system/%s.d/zzz-cagefs.conf' % service_name for service_name in service_names} return override_file_dict def fix_httpd_php_fpm_services(): """ Reconfigure httpd and ea-php-fpm services to work in without LVE Write to each systemd service file directives: PrivateDevices=false PrivateMounts=false PrivateTmp=false """ try: override_files_dict = _get_httpd_php_fpm_service_override_files() lines_to_write = ['[Service]\n', 'PrivateDevices=false\n', 'PrivateMounts=false\n', 'PrivateTmp=false\n', 'ProtectSystem=false\n', 'ReadOnlyDirectories=\n', 'ReadWriteDirectories=\n', 'InaccessibleDirectories=\n', 'ProtectHome=false\n' ] for override_file in override_files_dict.values(): # Create /etc/systemd/system/%s.d directory if need os.makedirs(os.path.dirname(override_file), mode=0o700, exist_ok=True) write_file_lines(override_file, lines_to_write, 'w') os.system('/bin/systemctl daemon-reload 2> /dev/null') # Restart all need services for service_name in override_files_dict.keys(): os.system(f'/sbin/service {service_name} restart 2> /dev/null') except (OSError, IOError,): pass def restore_httpd_php_fpm_services(): try: override_files_dict = _get_httpd_php_fpm_service_override_files() # override_files_dict example: # {'ea-php74-php-fpm.service': '/etc/systemd/system/ea-php74-php-fpm.service.d/zzz-cagefs.conf', # 'ea-php56-php-fpm.service': '/etc/systemd/system/ea-php56-php-fpm.service.d/zzz-cagefs.conf'} for override_file in override_files_dict.values(): # Remove override file try: os.unlink(override_file) except (OSError, IOError, ): pass # Remove override dir if it empty try: os.rmdir(os.path.dirname(override_file)) except (OSError, IOError, ): pass os.system('/bin/systemctl daemon-reload 2> /dev/null') # Restart all need services for service_name in override_files_dict.keys(): os.system(f'/sbin/service {service_name} restart 2> /dev/null') except (OSError, IOError,): pass