"""check_kernel.py - a first look: could this computer run an OpenShell sandbox?

    python3 check_kernel.py

OpenShell builds its sandbox from two parts of the Linux kernel. Landlock
fences off files. seccomp watches what a program asks the kernel to do, and
OpenShell asks seccomp for a "listener", so that it can answer those requests.

This file asks the kernel three things: which Landlock it has, what seccomp
can do, and whether a program here may have a listener. It changes nothing.
The one request that could change something is made in a child process, and
the child ends at once.

It is only a first look. OpenShell runs its own, stricter tests every time it
starts a sandbox, and refuses to start one that fails them.
"""
import ctypes
import errno
import os
import platform

LANDLOCK_NEEDED = 3               # OpenShell asks for Landlock version 3 or newer
LANDLOCK_CREATE_RULESET = 444     # the kernel's number for this request
ONLY_THE_VERSION = 1              # "make nothing, just say your version"
SECCOMP = {"x86_64": 317, "aarch64": 277}     # the number of seccomp() on each chip
SET_A_FILTER = 1
WITH_A_LISTENER = 1 << 3
NO_NEW_PRIVILEGES = 38            # a program must promise this before it sets a filter
ALLOW_EVERYTHING = (0x06, 0, 0, 0x7FFF0000)   # a filter of one line
SECCOMP_ACTIONS = "/proc/sys/kernel/seccomp/actions_avail"


class FilterLine(ctypes.Structure):
    """One line of a seccomp filter, laid out the way the kernel reads it."""
    _fields_ = [("code", ctypes.c_ushort), ("jump_true", ctypes.c_ubyte),
                ("jump_false", ctypes.c_ubyte), ("value", ctypes.c_uint)]


class Filter(ctypes.Structure):
    """A whole filter: how many lines it has, and where they are."""
    _fields_ = [("length", ctypes.c_ushort), ("lines", ctypes.POINTER(FilterLine))]


def landlock_version():
    """The Landlock version this kernel offers, or 0 if it has none."""
    c_library = ctypes.CDLL(None, use_errno=True)
    version = c_library.syscall(LANDLOCK_CREATE_RULESET, None, 0, ONLY_THE_VERSION)
    return max(version, 0)        # the kernel answers -1 when it has no Landlock


def seccomp_actions():
    """What seccomp can do on this kernel, as a list of words."""
    if not os.path.exists(SECCOMP_ACTIONS):
        return []
    with open(SECCOMP_ACTIONS) as file:
        return file.read().split()


def filters_already():
    """How many seccomp filters this program was started with. Usually none."""
    with open("/proc/self/status") as file:
        for line in file:
            if line.startswith("Seccomp_filters:"):
                return int(line.split()[1])
    return 0


def ask_for_listener():
    """Ask the kernel for a listener. Returns 0, or the kernel's error number."""
    c_library = ctypes.CDLL(None, use_errno=True)
    c_library.prctl(NO_NEW_PRIVILEGES, 1, 0, 0, 0)
    lines = (FilterLine * 1)(FilterLine(*ALLOW_EVERYTHING))
    answer = c_library.syscall(SECCOMP[platform.machine()], SET_A_FILTER,
                               WITH_A_LISTENER, ctypes.byref(Filter(1, lines)))
    return 0 if answer >= 0 else ctypes.get_errno()


def listener_error():
    """What a child process was told when it asked for a listener: 0 means yes."""
    read_end, write_end = os.pipe()
    child = os.fork()
    if child == 0:                # this is the child: ask, report, end
        os.write(write_end, str(ask_for_listener()).encode())
        os._exit(0)
    os.waitpid(child, 0)
    return int(os.read(read_end, 16))


def main():
    print("system  :", platform.system(), platform.release())
    print("chip    :", platform.machine())
    if platform.system() != "Linux":
        print("This check needs a Linux kernel. On Windows, run it inside WSL 2.")
        print("On a Mac, the sandbox uses the Linux that Docker Desktop runs.")
        return
    version = landlock_version()
    actions = seccomp_actions()
    error = listener_error()
    print("Landlock:", f"version {version}" if version else "none")
    print("seccomp :", " ".join(actions) if actions else "none")
    print("filters :", filters_already(), "already on this program before it asked")
    if error == 0:
        print("listener: yes")
    else:
        print("listener: no, the kernel answered", errno.errorcode[error],
              f"({os.strerror(error)})")
    ready = version >= LANDLOCK_NEEDED and "user_notif" in actions and error == 0
    print("a container sandbox looks possible:", "yes" if ready else "no")
    if error == errno.EBUSY:
        print("Something else already holds the one listener a program may have.")
        print("OpenShell's VM runtime still works: each sandbox gets its own kernel.")


if __name__ == "__main__":
    main()
