Heray-Was-Here
Server : Apache
System : Linux dal-shared-66.hostwindsdns.com 4.18.0-513.24.1.lve.1.el8.x86_64 #1 SMP Thu May 9 15:10:09 UTC 2024 x86_64
User : krnuyqrm ( 1183)
PHP Version : 7.4.33
Disable Function : NONE
Directory :  /proc/self/root/opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : //proc/self/root/opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/php.py
#!/opt/cloudlinux/venv/bin/python3 -sbb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
import os
import signal
import logging
from pathlib import Path

from clcommon.clpwd import drop_privileges
import psutil

from clcagefslib.webisolation import jail_utils


def _term_process(pid, sig):
    try:
        os.kill(pid, sig)
    except OSError as e:
        logging.error("Failed to terminate process PID %s: %s", pid, str(e))
        return False
    return True


def force_process_kill(pids):
    # F-37: block on psutil.wait_procs() instead of busy-polling
    # psutil.pid_exists(). The old tight-spin burned a full CPU core for up
    # to 5s per invocation and, because the CAGEFSCTL_USER proxyexec alias
    # is nolve=root, that burn is charged to shared host CPU rather than
    # the tenant's LVE — a tenant-triggerable DoS primitive.
    procs = []
    for pid in list(pids):
        try:
            procs.append(psutil.Process(pid))
        except psutil.NoSuchProcess:
            pids.discard(pid)

    if procs:
        _, alive = psutil.wait_procs(procs, timeout=5.0)
        alive_pids = {p.pid for p in alive}
        pids &= alive_pids

    for pid in list(pids):
        _term_process(pid, signal.SIGKILL)


def _get_website_isolation_docroot(pid: int) -> str | None:
    """
    Checks if the process is in the isolated environment.
    Returns path to process document root or None
    """
    website_isolation_marker = Path(f"/proc/{pid}/root/var/.cagefs/.cagefs.website")

    try:
        return website_isolation_marker.read_text().strip()
    except (PermissionError, OSError, FileNotFoundError):
        return None


def reload_processes_with_docroots(username: str, filter_by_docroots: list[str]) -> None:
    """
    If filter_by_docroots is not empty - reload only processes with DOCUMENT_ROOT
    environment variable matching one of the given docroots.
    Sends SIGTERM/SIGKILL to all matching processes owned by the given username.
    If filter_by_docroots is empty - reload all processes owned by the given username with DOCUMENT_ROOT set.
    """
    # CLOS-4642: normalize trailing slashes. Dedicated LiteSpeed pools spawn
    # with DOCUMENT_ROOT from $DOC_ROOT (trailing slash); panel docroots have
    # none. Compare slash-insensitively so dedicated lsphp workers are matched.
    docroots = set(dr.rstrip("/") for dr in filter_by_docroots if dr)
    signaled_pids = set()
    logging.debug(f"Requested to reload processes for user '{username}' and docroots: {docroots}")

    # first of all invalidate the existing cache
    for document_root in docroots:
        jail_utils.invalidate_ns_cache(username, document_root)

    # Iterate through all processes and filter by username first
    for proc in psutil.process_iter(attrs=["pid", "name", "username"]):
        current_process = proc
        if current_process.info.get("username") != username:
            continue
        try:
            env = current_process.environ()
        except psutil.AccessDenied:
            logging.warning(
                "Access denied to process PID %s; skipping", current_process.info.get("pid")
            )
            continue

        # _get_website_isolation_docroot returns document root for processes
        # which are already isolated, while DOCUMENT_ROOT in env gives us
        # processes which should be inside isolation (e.g. to kill during site-isolation-enable)
        document_root = _get_website_isolation_docroot(proc.pid) or env.get("DOCUMENT_ROOT")
        normalized_docroot = document_root.rstrip("/") if document_root else document_root
        # F-48 / CLOS-5435: enforce the docstring's "with DOCUMENT_ROOT set"
        # contract in BOTH branches. The prior guard was
        # `if docroots and normalized_docroot not in docroots: continue`,
        # which short-circuits on an empty `docroots` set and lets every
        # user-owned process (bash, sshd, cron, php-cli, background workers)
        # reach SIGTERM/SIGKILL below. Require the isolation marker (or
        # DOCUMENT_ROOT env) to be present before signaling in either
        # branch: filter-empty = "reload all isolated processes for this
        # user"; filter non-empty = "reload only processes whose docroot is
        # in the filter set". Under no code path may a process without an
        # isolation marker be signaled.
        if normalized_docroot is None:
            continue
        if docroots and normalized_docroot not in docroots:
            continue

        pid = current_process.info.get("pid")
        if pid is None or pid in signaled_pids:
            continue

        with drop_privileges(username):
            logging.info(f"Terminating process PID {pid} with DOCUMENT_ROOT={document_root}")
            if not _term_process(pid, signal.SIGTERM):
                continue
        signaled_pids.add(pid)

    with drop_privileges(username):
        force_process_kill(signaled_pids)

Hry