#!/usr/bin/env bats

load helpers

function setup() {
	setup_busybox
}

function teardown() {
	teardown_bundle
}

@test "runc run [seccomp -ENOSYS handling]" {
	TEST_NAME="seccomp_syscall_test1"

	# Compile the test binary and update the config to run it.
	gcc -static -o rootfs/seccomp_test "${TESTDATA}/${TEST_NAME}.c"
	update_config ".linux.seccomp = $(<"${TESTDATA}/${TEST_NAME}.json")"
	update_config '.process.args = ["/seccomp_test"]'

	runc run test_busybox
	[ "$status" -eq 0 ]
}

@test "runc run [seccomp defaultErrnoRet=ENXIO]" {
	TEST_NAME="seccomp_syscall_test2"

	# Compile the test binary and update the config to run it.
	gcc -static -o rootfs/seccomp_test2 "${TESTDATA}/${TEST_NAME}.c"
	update_config ".linux.seccomp = $(<"${TESTDATA}/${TEST_NAME}.json")"
	update_config '.process.args = ["/seccomp_test2"]'

	runc run test_busybox
	[ "$status" -eq 0 ]
}

# TODO:
# - Test other actions like SCMP_ACT_TRAP, SCMP_ACT_TRACE, SCMP_ACT_LOG.
# - Test args (index, value, valueTwo, etc).

@test "runc run [seccomp] (SCMP_ACT_ERRNO default)" {
	update_config '   .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
			| .process.noNewPrivileges = false
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_ERRNO"}]
			}'

	runc run test_busybox
	[ "$status" -ne 0 ]
	[[ "$output" == *"mkdir:"*"/dev/shm/foo"*"Operation not permitted"* ]]
}

@test "runc run [seccomp] (SCMP_ACT_ERRNO explicit errno)" {
	update_config '   .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
			| .process.noNewPrivileges = false
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_ERRNO", "errnoRet": 100}]
			}'

	runc run test_busybox
	[ "$status" -ne 0 ]
	[[ "$output" == *"Network is down"* ]]
}

# Prints the numeric value of provided seccomp flags combination.
# The parameter is flags string, as supplied in OCI spec, for example
# '"SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_LOG"'.
function flags_value() {
	# Numeric values of seccomp flags.
	declare -A values=(
		['"SECCOMP_FILTER_FLAG_TSYNC"']=0 # Supported but ignored by runc, thus 0.
		['"SECCOMP_FILTER_FLAG_LOG"']=2
		['"SECCOMP_FILTER_FLAG_SPEC_ALLOW"']=4
		# XXX: add new values above this line.
	)
	# Split the flags.
	IFS=',' read -ra flags <<<"$1"

	local flag v sum=0
	for flag in "${flags[@]}"; do
		# This will produce "values[$flag]: unbound variable"
		# error for a new flag yet unknown to the test.
		v=${values[$flag]}
		((sum += v)) || true
	done

	echo $sum
}

@test "runc run [seccomp] (SECCOMP_FILTER_FLAG_*)" {
	update_config '   .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
			| .process.noNewPrivileges = false
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["mkdir", "mkdirat"], "action":"SCMP_ACT_ERRNO"}]
			}'

	# Get the list of flags supported by runc/seccomp/kernel,
	# or "null" if no flags are supported or runc is too old.
	#
	# Filter out WAIT_KILLABLE_RECV as it requires a listener,
	# and thus tested separately in seccomp-notify.bats.
	mapfile -t flags < <(__runc features | jq -c '.linux.seccomp.supportedFlags' |
		tr -d '[]\n' | tr ',' '\n' | grep -v 'WAIT_KILLABLE_RECV')

	# This is a set of all possible flag combinations to test.
	declare -A TEST_CASES=(
		['EMPTY']=0  # Special value: empty set of flags.
		['REMOVE']=0 # Special value: no flags set.
	)

	# If supported, runc should set SPEC_ALLOW if no flags are set.
	if [[ " ${flags[*]} " == *' "SECCOMP_FILTER_FLAG_SPEC_ALLOW" '* ]]; then
		TEST_CASES['REMOVE']=$(flags_value '"SECCOMP_FILTER_FLAG_SPEC_ALLOW"')
	fi

	# Add all possible combinations of seccomp flags
	# and their expected numeric values to TEST_CASES.
	if [ "${flags[0]}" != "null" ]; then
		# Use shell {a,}{b,}{c,} to generate the powerset.
		for fc in $(eval echo "$(printf "{'%s,',}" "${flags[@]}")"); do
			# Remove the last comma.
			fc="${fc/%,/}"
			TEST_CASES[$fc]=$(flags_value "$fc")
		done
	fi

	# Finally, run the tests.
	for key in "${!TEST_CASES[@]}"; do
		case "$key" in
		'REMOVE')
			update_config ' del(.linux.seccomp.flags)'
			;;
		'EMPTY')
			update_config ' .linux.seccomp.flags = []'
			;;
		*)
			update_config ' .linux.seccomp.flags = [ '"${key}"' ]'
			;;
		esac

		runc --debug run test_busybox
		[ "$status" -ne 0 ]
		[[ "$output" == *"mkdir:"*"/dev/shm/foo"*"Operation not permitted"* ]]

		# Check the numeric flags value, as printed in the debug log, is as expected.
		exp="\"seccomp filter flags: ${TEST_CASES[$key]}\""
		echo "flags $key, expecting $exp"
		[[ "$output" == *"$exp"* ]]
	done
}

@test "runc run [seccomp] (SCMP_ACT_KILL)" {
	update_config '  .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
			| .process.noNewPrivileges = false
			| .process.rlimits = [{"type": "RLIMIT_CORE", "soft": 0, "hard": 0}]
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_KILL"}]
			}'

	runc run test_busybox
	[ "$status" -ne 0 ]
}

# check that a startContainer hook is run with the seccomp filters applied
@test "runc run [seccomp] (startContainer hook)" {
	update_config '   .process.args = ["/bin/true"]
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_KILL"}]
			}
			| .process.rlimits = [{"type": "RLIMIT_CORE", "soft": 0, "hard": 0}]
			| .hooks = {
				"startContainer": [ {
						"path": "/bin/sh",
						"args": ["sh", "-c", "mkdir /dev/shm/foo"]
				} ]
			}'

	runc run test_busybox
	[ "$status" -ne 0 ]
	[[ "$output" == *"error running startContainer hook"* ]]
	[[ "$output" == *"bad system call"* ]]
}

@test "runc run [seccomp] (verify syscall compatibility after seccomp enforcement)" {
	update_config '   .process.args = ["true"]
			| .process.noNewPrivileges = false
			| .linux.seccomp = {
				"defaultAction":"SCMP_ACT_ALLOW",
				"architectures":["SCMP_ARCH_X86","SCMP_ARCH_X32","SCMP_ARCH_X86_64","SCMP_ARCH_AARCH64","SCMP_ARCH_ARM"],
				"syscalls":[{"names":["close_range", "fsopen", "fsconfig", "fspick", "openat2", "open_tree", "move_mount", "mount_setattr"], "action":"SCMP_ACT_ERRNO", "errnoRet": 38}]
			}'

	runc run test_busybox
	[ "$status" -eq 0 ]
}
