# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Tests that the seccomp filters don't let denied syscalls through."""

import json as json_lib
import os
import platform
import tempfile

from framework import utils
from host_tools.cargo_build import run_seccompiler_bin


def _get_basic_syscall_list():
    """Return the JSON list of syscalls that the demo jailer needs."""
    if platform.machine() == "x86_64":
        sys_list = [
            "rt_sigprocmask",
            "rt_sigaction",
            "execve",
            "mmap",
            "mprotect",
            "arch_prctl",
            "set_tid_address",
            "readlink",
            "open",
            "read",
            "close",
            "brk",
            "sched_getaffinity",
            "sigaltstack",
            "munmap",
            "exit_group",
            "poll",
        ]
    else:
        # platform.machine() == "aarch64"
        sys_list = [
            "rt_sigprocmask",
            "rt_sigaction",
            "execve",
            "mmap",
            "mprotect",
            "set_tid_address",
            "read",
            "close",
            "brk",
            "sched_getaffinity",
            "sigaltstack",
            "munmap",
            "exit_group",
            "ppoll",
        ]

    json = ""
    for syscall in sys_list[0:-1]:
        json += """
            {{
                "syscall": \"{}\"
            }},
        """.format(
            syscall
        )

    json += """
        {{
            "syscall": \"{}\"
        }}
    """.format(
        sys_list[-1]
    )

    return json


def _run_seccompiler_bin(json_data, basic=False):
    json_temp = tempfile.NamedTemporaryFile(delete=False)
    json_temp.write(json_data.encode("utf-8"))
    json_temp.flush()

    bpf_temp = tempfile.NamedTemporaryFile(delete=False)

    run_seccompiler_bin(bpf_path=bpf_temp.name, json_path=json_temp.name, basic=basic)

    os.unlink(json_temp.name)
    return bpf_temp.name


def test_seccomp_ls(bin_seccomp_paths):
    """
    Assert that the seccomp filter denies an unallowed syscall.
    """
    # pylint: disable=redefined-outer-name
    # pylint: disable=subprocess-run-check
    # The fixture pattern causes a pylint false positive for that rule.

    # Path to the `ls` binary, which attempts to execute the forbidden
    # `SYS_access`.
    ls_command_path = "/bin/ls"
    demo_jailer = bin_seccomp_paths["demo_jailer"]
    assert os.path.exists(demo_jailer)

    json_filter = """{{
        "main": {{
            "default_action": "trap",
            "filter_action": "allow",
            "filter": [
                {}
            ]
        }}
    }}""".format(
        _get_basic_syscall_list()
    )

    # Run seccompiler-bin.
    bpf_path = _run_seccompiler_bin(json_filter)

    # Run the mini jailer.
    outcome = utils.run_cmd(
        [demo_jailer, ls_command_path, bpf_path], no_shell=True, ignore_return_code=True
    )

    os.unlink(bpf_path)

    # The seccomp filters should send SIGSYS (31) to the binary. `ls` doesn't
    # handle it, so it will exit with error.
    assert outcome.returncode != 0


def test_advanced_seccomp(bin_seccomp_paths):
    """
    Test seccompiler-bin with `demo_jailer`.

    Test that the demo jailer (with advanced seccomp) allows the harmless demo
    binary, denies the malicious demo binary and that an empty allowlist
    denies everything.
    """
    # pylint: disable=redefined-outer-name
    # pylint: disable=subprocess-run-check
    # The fixture pattern causes a pylint false positive for that rule.

    demo_jailer = bin_seccomp_paths["demo_jailer"]
    demo_harmless = bin_seccomp_paths["demo_harmless"]
    demo_malicious = bin_seccomp_paths["demo_malicious"]

    assert os.path.exists(demo_jailer)
    assert os.path.exists(demo_harmless)
    assert os.path.exists(demo_malicious)

    json_filter = """{{
        "main": {{
            "default_action": "trap",
            "filter_action": "allow",
            "filter": [
                {},
                {{
                    "syscall": "write",
                    "args": [
                        {{
                            "index": 0,
                            "type": "dword",
                            "op": "eq",
                            "val": 1,
                            "comment": "stdout fd"
                        }},
                        {{
                            "index": 2,
                            "type": "qword",
                            "op": "eq",
                            "val": 14,
                            "comment": "nr of bytes"
                        }}
                    ]
                }}
            ]
        }}
    }}""".format(
        _get_basic_syscall_list()
    )

    # Run seccompiler-bin.
    bpf_path = _run_seccompiler_bin(json_filter)

    # Run the mini jailer for harmless binary.
    outcome = utils.run_cmd(
        [demo_jailer, demo_harmless, bpf_path], no_shell=True, ignore_return_code=True
    )

    # The demo harmless binary should have terminated gracefully.
    assert outcome.returncode == 0

    # Run the mini jailer for malicious binary.
    outcome = utils.run_cmd(
        [demo_jailer, demo_malicious, bpf_path], no_shell=True, ignore_return_code=True
    )

    # The demo malicious binary should have received `SIGSYS`.
    assert outcome.returncode == -31

    os.unlink(bpf_path)

    # Run seccompiler-bin with `--basic` flag.
    bpf_path = _run_seccompiler_bin(json_filter, basic=True)

    # Run the mini jailer for malicious binary.
    outcome = utils.run_cmd(
        [demo_jailer, demo_malicious, bpf_path], no_shell=True, ignore_return_code=True
    )

    # The malicious binary also terminates gracefully, since the --basic option
    # disables all argument checks.
    assert outcome.returncode == 0

    os.unlink(bpf_path)

    # Run the mini jailer with an empty allowlist. It should trap on any
    # syscall.
    json_filter = """{
        "main": {
            "default_action": "trap",
            "filter_action": "allow",
            "filter": []
        }
    }"""

    # Run seccompiler-bin.
    bpf_path = _run_seccompiler_bin(json_filter)

    outcome = utils.run_cmd(
        [demo_jailer, demo_harmless, bpf_path], no_shell=True, ignore_return_code=True
    )

    # The demo binary should have received `SIGSYS`.
    assert outcome.returncode == -31

    os.unlink(bpf_path)


def test_no_seccomp(test_microvm_with_api):
    """
    Test that Firecracker --no-seccomp installs no filter.
    """
    test_microvm = test_microvm_with_api
    test_microvm.jailer.extra_args.update({"no-seccomp": None})
    test_microvm.spawn()

    test_microvm.basic_config()

    test_microvm.start()

    utils.assert_seccomp_level(test_microvm.jailer_clone_pid, "0")


def test_default_seccomp_level(test_microvm_with_api):
    """
    Test that Firecracker installs a seccomp filter by default.
    """
    test_microvm = test_microvm_with_api
    test_microvm.spawn()

    test_microvm.basic_config()

    test_microvm.start()

    utils.assert_seccomp_level(test_microvm.jailer_clone_pid, "2")


def test_seccomp_rust_panic(bin_seccomp_paths):
    """
    Test seccompiler-bin with `demo_panic`.

    Test that the Firecracker filters allow a Rust panic to run its
    course without triggering a seccomp violation.
    """
    # pylint: disable=redefined-outer-name
    # pylint: disable=subprocess-run-check
    # The fixture pattern causes a pylint false positive for that rule.

    demo_panic = bin_seccomp_paths["demo_panic"]
    assert os.path.exists(demo_panic)

    fc_filters_path = "../resources/seccomp/{}-unknown-linux-musl.json".format(
        platform.machine()
    )
    with open(fc_filters_path, "r", encoding="utf-8") as fc_filters:
        filter_threads = list(json_lib.loads(fc_filters.read()))

    bpf_temp = tempfile.NamedTemporaryFile(delete=False)
    run_seccompiler_bin(bpf_path=bpf_temp.name, json_path=fc_filters_path)
    bpf_path = bpf_temp.name

    # Run the panic binary with all filters.
    for thread in filter_threads:
        code, _, _ = utils.run_cmd(
            [demo_panic, bpf_path, thread], no_shell=True, ignore_return_code=True
        )
        # The demo panic binary should have terminated with SIGABRT
        # and not with a seccomp violation.
        # On a seccomp violation, the program exits with code -31 for
        # SIGSYS. Here, we make sure the program exits with -6, which
        # is for SIGABRT.
        assert (
            code == -6
        ), "Panic binary failed with exit code {} on {} " "filters.".format(
            code, thread
        )

    os.unlink(bpf_path)