// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2023 ARM Limited.
 *
 * Tests for GCS mode locking.  These tests rely on both having GCS
 * unconfigured on entry and on the kselftest harness running each
 * test in a fork()ed process which will have it's own mode.
 */

#include <limits.h>

#include <sys/auxv.h>
#include <sys/prctl.h>

#include <asm/hwcap.h>

#include "kselftest_harness.h"

#include "gcs-util.h"

#define my_syscall2(num, arg1, arg2)                                          \
({                                                                            \
	register long _num  __asm__ ("x8") = (num);                           \
	register long _arg1 __asm__ ("x0") = (long)(arg1);                    \
	register long _arg2 __asm__ ("x1") = (long)(arg2);                    \
	register long _arg3 __asm__ ("x2") = 0;                               \
	register long _arg4 __asm__ ("x3") = 0;                               \
	register long _arg5 __asm__ ("x4") = 0;                               \
	                                                                      \
	__asm__  volatile (                                                   \
		"svc #0\n"                                                    \
		: "=r"(_arg1)                                                 \
		: "r"(_arg1), "r"(_arg2),                                     \
		  "r"(_arg3), "r"(_arg4),                                     \
		  "r"(_arg5), "r"(_num)					      \
		: "memory", "cc"                                              \
	);                                                                    \
	_arg1;                                                                \
})

/* No mode bits are rejected for locking */
TEST(lock_all_modes)
{
	int ret;

	ret = prctl(PR_LOCK_SHADOW_STACK_STATUS, ULONG_MAX, 0, 0, 0);
	ASSERT_EQ(ret, 0);
}

FIXTURE(valid_modes)
{
};

FIXTURE_VARIANT(valid_modes)
{
	unsigned long mode;
};

FIXTURE_VARIANT_ADD(valid_modes, enable)
{
	.mode = PR_SHADOW_STACK_ENABLE,
};

FIXTURE_VARIANT_ADD(valid_modes, enable_write)
{
	.mode = PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_WRITE,
};

FIXTURE_VARIANT_ADD(valid_modes, enable_push)
{
	.mode = PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_PUSH,
};

FIXTURE_VARIANT_ADD(valid_modes, enable_write_push)
{
	.mode = PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_WRITE |
		PR_SHADOW_STACK_PUSH,
};

FIXTURE_SETUP(valid_modes)
{
}

FIXTURE_TEARDOWN(valid_modes)
{
}

/* We can set the mode at all */
TEST_F(valid_modes, set)
{
	int ret;

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  variant->mode);
	ASSERT_EQ(ret, 0);

	_exit(0);
}

/* Enabling, locking then disabling is rejected */
TEST_F(valid_modes, enable_lock_disable)
{
	unsigned long mode;
	int ret;

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  variant->mode);
	ASSERT_EQ(ret, 0);

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);
	ASSERT_EQ(mode, variant->mode);

	ret = prctl(PR_LOCK_SHADOW_STACK_STATUS, variant->mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, 0);
	ASSERT_EQ(ret, -EBUSY);

	_exit(0);
}

/* Locking then enabling is rejected */
TEST_F(valid_modes, lock_enable)
{
	unsigned long mode;
	int ret;

	ret = prctl(PR_LOCK_SHADOW_STACK_STATUS, variant->mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  variant->mode);
	ASSERT_EQ(ret, -EBUSY);

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);
	ASSERT_EQ(mode, 0);

	_exit(0);
}

/* Locking then changing other modes is fine */
TEST_F(valid_modes, lock_enable_disable_others)
{
	unsigned long mode;
	int ret;

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  variant->mode);
	ASSERT_EQ(ret, 0);

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);
	ASSERT_EQ(mode, variant->mode);

	ret = prctl(PR_LOCK_SHADOW_STACK_STATUS, variant->mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  PR_SHADOW_STACK_ALL_MODES);
	ASSERT_EQ(ret, 0);

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);
	ASSERT_EQ(mode, PR_SHADOW_STACK_ALL_MODES);

	ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS,
			  variant->mode);
	ASSERT_EQ(ret, 0);

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	ASSERT_EQ(ret, 0);
	ASSERT_EQ(mode, variant->mode);

	_exit(0);
}

int main(int argc, char **argv)
{
	unsigned long mode;
	int ret;

	if (!(getauxval(AT_HWCAP) & HWCAP_GCS))
		ksft_exit_skip("SKIP GCS not supported\n");

	ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0);
	if (ret) {
		ksft_print_msg("Failed to read GCS state: %d\n", ret);
		return EXIT_FAILURE;
	}

	if (mode & PR_SHADOW_STACK_ENABLE) {
		ksft_print_msg("GCS was enabled, test unsupported\n");
		return KSFT_SKIP;
	}

	return test_harness_run(argc, argv);
}
