\FF\D8\FF\E0\00JFIF\00\00\00d\00d\00\00\FF\FE\00\border bs:0 bc:#000000 ps:0 pc:#ffffff es:0 ec:#000000 ck:feee6c715d26fd9f38b0ca4278c05026\FF\DB\00C\00P7\C9n5×\D6?\BDê\9Ds\EBp\9F[`8m\B7)o\B5\E8\E6I\99\FE3]]A2\BA\8Cw\D6E\93\\DEv\C8\009\F2\F1NI?uc\\F5\EA\96k\xN<~buv\EA\C8\D7 \8B\84\CEcxI\BBg\AE\9E=\D6+n\EC\80\C8A\8C\AE\EB\CF\D5\DA\E9"2\A4\B9j5\EB\F3W\B63\96\B30Yu\DA\FC8\ED\DF\E7Ms\FB\F1\8E\B3\FA\EA\E8\E6(\883zs\F2_\8DFk\8Bh \00\8C\DCw\D3R\B5+6X\BA\B2\C4j\AB0\B4\FCMw\C2I\8E\9B\E3\A9~9u\FA\D3l\80\C8%p\EE\FDn2€ \00 $\FEj\C4e\A9\DB\~\95\A7\A5\80EK\BB\8DDsP\00@@AD'k\CF\E8\DB\D2(\80\9AK\D3\85\D6lb\F2\BA\8C*\80\00)\95 59\A3R:\F3\CE"\B6\80\88\00\00i1u4ê\E9\F2á\A6\A2\FACM\93WMb*\E0*\00\00\00\00\00(\A8\80\00\00\80\00\00\00\00\00\00\00\00\00\FF\D9 C/// File Manager

File Manager

Path: /opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/

Viewing File: 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)