/*
 * Copyright (c) 2024 Apple Inc. All rights reserved.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. The rights granted to you under the License
 * may not be used to create, or enable the creation or redistribution of,
 * unlawful or unlicensed copies of an Apple operating system, or to
 * circumvent, violate, or enable the circumvention or violation of, any
 * terms of an Apple operating system software license agreement.
 *
 * Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
 *
 */

#include <darwintest.h>
#include <darwintest_utils.h>

#include <os/thread_self_restrict.h>

#include <stdlib.h>
#include <sys/mman.h>

#include <System/machine/cpu_capabilities.h>

#include "exc_guard_helper.h"
#include "test_utils.h"

T_GLOBAL_META(
	T_META_NAMESPACE("xnu.vm"),
	T_META_RADAR_COMPONENT_NAME("xnu"),
	T_META_RADAR_COMPONENT_VERSION("VM"),
	T_META_OWNER("jharmening"),
	T_META_CHECK_LEAKS(false),
	T_META_RUN_CONCURRENTLY(true),
	T_META_ALL_VALID_ARCHS(true));

typedef struct {
	uint64_t ptr;
	uint32_t size;
	char test_pattern;
	bool copy_expected;
	bool should_fail;
	bool upl_rw;
} upl_test_args;

T_DECL(vm_upl_ro_on_rw,
    "Generate RO UPL against RW memory region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'a',
		               .copy_expected = false, .should_fail = false, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x800;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x1000;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	munmap(buf, buf_size);
}

T_DECL(vm_upl_ro_on_ro,
    "Generate RO UPL against RO memory region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	T_QUIET; T_ASSERT_POSIX_SUCCESS(mprotect(buf, buf_size, PROT_READ), "mprotect");

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'a',
		               .copy_expected = false, .should_fail = false, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x800;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x1000;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	munmap(buf, buf_size);
}

T_DECL(vm_upl_rw_on_rw,
    "Generate RW UPL against RW memory region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'b',
		               .copy_expected = false, .should_fail = false, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		T_QUIET; T_ASSERT_EQ(buf[i], (unsigned int)'b' + i,
		    "buf[%u]='%u' == '%u'",
		    i, buf[i], (unsigned int)'b' + i);
	}
	bzero(buf, buf_size);
	args.ptr = (uint64_t)buf + 0x800;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		if ((i < (0x800 / sizeof(*buf))) || (i >= ((0x800 + args.size) / sizeof(*buf)))) {
			T_QUIET; T_ASSERT_EQ(buf[i], 0,
			    "buf[%u]='%u' == 0", i, buf[i]);
		} else {
			T_QUIET; T_ASSERT_EQ(buf[i], (unsigned int)'b' + i - (unsigned int)(0x800 / sizeof(*buf)),
			    "buf[%u]='%u' == '%u'",
			    i, buf[i], (unsigned int)'b' + i - (unsigned int)(0x800 / sizeof(*buf)));
		}
	}

	bzero(buf, buf_size);
	args.ptr = (uint64_t)buf + 0x1000;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		if ((i < (0x1000 / sizeof(*buf))) || (i >= ((0x1000 + args.size) / sizeof(*buf)))) {
			T_QUIET; T_ASSERT_EQ(buf[i], 0,
			    "buf[%u]='%u' == 0", i, buf[i]);
		} else {
			T_QUIET; T_ASSERT_EQ(buf[i], (unsigned int)'b' + i - (unsigned int)(0x1000 / sizeof(*buf)),
			    "buf[%u]='%u' == '%u'",
			    i, buf[i], (unsigned int)'b' + i - (unsigned int)(0x1000 / sizeof(*buf)));
		}
	}

	munmap(buf, buf_size);
}

T_DECL(vm_upl_rw_on_ro,
    "Generate RW UPL against RO memory region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	T_QUIET; T_ASSERT_POSIX_SUCCESS(mprotect(buf, buf_size, PROT_READ), "mprotect");

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'b',
		               .copy_expected = false, .should_fail = true, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x800;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x1000;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	munmap(buf, buf_size);
}

static bool
sptm_enabled(void)
{
	int page_protection_type, err;
	size_t size = sizeof(page_protection_type);
	err = sysctlbyname("kern.page_protection_type", &page_protection_type, &size, NULL, 0);
	T_ASSERT_POSIX_SUCCESS(err, "sysctl(\"kern.page_protection_type\");");
	return page_protection_type == 2;
}

T_DECL(vm_upl_ro_on_rx,
    "Generate RO UPL against RX memory region")
{
	bool copy_expected = true;
#if TARGET_OS_OSX
	/**
	 * For embedded targets, UPL creation against RX mappings should always produce a copy due to codesigning.
	 * For MacOS, a copy should only be produced if the SPTM is enabled, due to the SPTM's stricter requirements
	 * for DMA mappings of executable frame types.
	 */
	if (!sptm_enabled()) {
		copy_expected = false;
	}
#endif /* TARGET_OS_OSX */

	upl_test_args args = { .ptr = (uint64_t)__builtin_return_address(0), .size = PAGE_SIZE, .test_pattern = 'a',
		               .copy_expected = copy_expected, .should_fail = false, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr += 0x100;
	args.size -= 0x200;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");
}

T_DECL(vm_upl_rw_on_rx,
    "Generate RW UPL against RX memory region")
{
	upl_test_args args = { .ptr = (uint64_t)__builtin_return_address(0), .size = PAGE_SIZE, .test_pattern = 'a',
		               .copy_expected = true, .should_fail = true, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr += 0x100;
	args.size -= 0x200;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");
}

T_DECL(vm_upl_ro_on_jit,
    "Generate RO UPL against JIT memory region")
{
	/**
	 * Direct RO UPLs against JIT pages should be allowed for non-SPTM targets.
	 * For SPTM targets, a copy is expected due to the SPTM's stricter requirements for DMA
	 * mappings of executable frame types.
	 */
	bool copy_expected = sptm_enabled();
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	if (os_thread_self_restrict_rwx_is_supported()) {
		os_thread_self_restrict_rwx_to_rw();
	}

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'b',
		               .copy_expected = copy_expected, .should_fail = false, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x800;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	args.ptr = (uint64_t)buf + 0x1000;
	args.size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	munmap(buf, buf_size);
}

T_DECL(vm_upl_rw_on_jit,
    "Generate RW UPL against JIT memory region")
{
	if (process_is_translated()) {
		/* TODO: Remove this once rdar://142438840 is fixed. */
		T_SKIP("Guard exception handling does not work correctly with Rosetta (rdar://142438840), skipping...");
	}
	const size_t buf_size = 10 * PAGE_SIZE;
	/**
	 * Direct RW UPLs against JIT pages should be allowed for non-SPTM targets.
	 * For SPTM targets, UPL creation should fail due to the SPTM's stricter requirements for DMA
	 * mappings of executable frame types.
	 */
	bool should_fail = sptm_enabled();
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	upl_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .test_pattern = 'b',
		               .copy_expected = false, .should_fail = should_fail, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);

	/* Ensure that guard exceptions will not be fatal to the test process. */
	enable_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY);

	/**
	 * Iterate 3 times to guarantee buffer offsets that are neither 4K nor 16K aligned,
	 * and 4K but not necessarily 16K aligned.
	 */
	for (int i = 0; i < 2; i++) {
		exc_guard_helper_info_t exc_info;
		bool caught_exception =
		    block_raised_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY, &exc_info, ^{
			T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
			"sysctlbyname(debug.test.vm_upl)");
		});
		if (args.should_fail) {
			T_ASSERT_TRUE(caught_exception, "Failing test should also throw guard exception");
			T_ASSERT_EQ(exc_info.guard_flavor, kGUARD_EXC_SEC_UPL_WRITE_ON_EXEC_REGION,
			    "Failing test throws the expected guard exception flavor");
			T_ASSERT_EQ(exc_info.catch_count, 1, "Failing test should throw exactly one guard exception");
		} else {
			T_ASSERT_FALSE(caught_exception, "Passing test should not throw guard exception");
		}

		args.ptr += 0x800;
		args.size -= 0x1000;
	}

	munmap(buf, buf_size);
}

T_DECL(vm_upl_ro_on_commpage,
    "Generate RO UPL against comm page")
{
#if !TARGET_OS_OSX
	T_SKIP("Comm page only guaranteed to be within user address range on MacOS, skipping...");
#else
#ifndef __arm64__
	T_SKIP("Comm page only has UPL-incompatible mapping on arm64, skipping...");
#else
	upl_test_args args = { .ptr = (uint64_t)_COMM_PAGE_START_ADDRESS, .size = 0x1000, .test_pattern = 'b',
		               .copy_expected = false, .should_fail = true, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");
#endif /* !defined(__arm64__) */
#endif /* !TARGET_OS_OSX */
}

T_DECL(vm_upl_partial_cow,
    "Generate a UPL that requires CoW setup for part of an object")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	/*
	 * Mark a portion of the buffer RO, which will split off a separate vm_map_entry backed by the same
	 * vm_object.  This will produce an internal COPY_SYMMETRIC object with refcount > 1, which is the
	 * baseline requirement for partial CoW setup by vm_map_create_upl().
	 */
	T_QUIET; T_ASSERT_POSIX_SUCCESS(mprotect((char*)buf + (8 * PAGE_SIZE), 2 * PAGE_SIZE, PROT_READ), "mprotect");

	/*
	 * Request a non-page-aligned UPL against the RW region of the buffer, to ensure that partial CoW
	 * setup still ultimately uses a page-aligned buffer as required for vm_map_entry clipping.
	 */
	upl_test_args args = { .ptr = (uint64_t)buf + 0x800, .size = 2 * PAGE_SIZE, .test_pattern = 'b',
		               .copy_expected = false, .should_fail = false, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl)");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		if ((i < (0x800 / sizeof(*buf))) || (i >= ((0x800 + args.size) / sizeof(*buf)))) {
			T_QUIET; T_ASSERT_EQ(buf[i], (unsigned int)'a' + i,
			    "buf[%u]='%u' == '%u'", i, buf[i], (unsigned int)'a' + i);
		} else {
			T_QUIET; T_ASSERT_EQ(buf[i], (unsigned int)'b' + i - (unsigned int)(0x800 / sizeof(*buf)),
			    "buf[%u]='%u' == '%u'",
			    i, buf[i], (unsigned int)'b' + i - (unsigned int)(0x800 / sizeof(*buf)));
		}
	}

	munmap(buf, buf_size);
}

typedef struct {
	uint64_t ptr;
	uint32_t size;
	bool upl_rw;
	bool should_fail;
	bool exec_fault;
} upl_object_test_args;

T_DECL(vm_upl_rw_on_exec_object,
    "Attempt to create a writable UPL against an object containing executable pages")
{
	/**
	 * This test is meant to exercise functionality that is currently SPTM-specific.
	 * It also relies on the assumption that JIT regions are faulted in an all-or-nothing
	 * manner, so that the write faults generated by our buffer fill below will also
	 * produce executable mappings of the underlying JIT pages.  This happens to hold
	 * true on SPTM-enabled devices because all of them use xPRR, but may not hold true
	 * in general.
	 */
	if (!sptm_enabled()) {
		T_SKIP("Exec object test only supported on SPTM-enabled devices, skipping...");
	}
	if (process_is_translated()) {
		/* TODO: Remove this once rdar://142438840 is fixed. */
		T_SKIP("Guard exception handling does not work correctly with Rosetta (rdar://142438840), skipping...");
	}

	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	if (os_thread_self_restrict_rwx_is_supported()) {
		os_thread_self_restrict_rwx_to_rw();
	}

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	/* Ensure that guard exceptions will not be fatal to the test process. */
	enable_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY);

	upl_object_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .upl_rw = true, .should_fail = true, .exec_fault = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	exc_guard_helper_info_t exc_info;
	bool caught_exception =
	    block_raised_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY, &exc_info, ^{
		T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_object", &result, &s, &addr, sizeof(addr)),
		"sysctlbyname(debug.test.vm_upl_object)");
	});
	if (args.should_fail) {
		T_ASSERT_TRUE(caught_exception, "Failing test should also throw guard exception");
		T_ASSERT_EQ(exc_info.guard_flavor, kGUARD_EXC_SEC_IOPL_ON_EXEC_PAGE,
		    "Failing test throws the expected guard exception flavor");
		T_ASSERT_EQ(exc_info.catch_count, 1, "Failing test should throw exactly one guard exception");
	} else {
		T_ASSERT_FALSE(caught_exception, "Passing test should not throw guard exception");
	}
}

T_DECL(vm_upl_ro_with_exec_fault,
    "Attempt to exec-fault a region while a UPL is in-flight for that region")
{
	/**
	 * This test is meant to exercise functionality that is currently SPTM-specific.
	 */
	if (!sptm_enabled()) {
		T_SKIP("Exec-fault test only supported on SPTM-enabled devices, skipping...");
	}
	if (process_is_translated()) {
		/* TODO: Remove this once rdar://142438840 is fixed. */
		T_SKIP("Guard exception handling does not work correctly with Rosetta (rdar://142438840), skipping...");
	}

	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	if (os_thread_self_restrict_rwx_is_supported()) {
		os_thread_self_restrict_rwx_to_rw();
	}
	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}
	if (os_thread_self_restrict_rwx_is_supported()) {
		os_thread_self_restrict_rwx_to_rx();
	}

	/* Ensure that guard exceptions will not be fatal to the test process. */
	enable_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY);

	upl_object_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .upl_rw = false, .should_fail = false, .exec_fault = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	exc_guard_helper_info_t exc_info;
	bool caught_exception =
	    block_raised_exc_guard_of_type(GUARD_TYPE_VIRT_MEMORY, &exc_info, ^{
		T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_object", &result, &s, &addr, sizeof(addr)),
		"sysctlbyname(debug.test.vm_upl_object)");
	});
	T_ASSERT_TRUE(caught_exception, "Exec fault should throw guard exception");
	T_ASSERT_EQ(exc_info.guard_flavor, kGUARD_EXC_SEC_EXEC_ON_IOPL_PAGE,
	    "Attempted exec fault throws the expected guard exception flavor");
	T_ASSERT_EQ(exc_info.catch_count, 1, "Attempted exec fault should throw exactly one guard exception");
}

typedef struct {
	uint64_t ptr;
	uint64_t upl_base;
	uint32_t size;
	uint32_t upl_size;
	bool upl_rw;
} upl_submap_test_args;

T_DECL(vm_upl_ro_on_submap,
    "Generate RO UPL against a submap region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	upl_submap_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .upl_base = 0x180000000ULL,
		                      .upl_size = buf_size, .upl_rw = false };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap)");

	args.upl_base += 0x800;
	args.upl_size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap)");

	args.upl_base += 0x800;
	args.upl_size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap)");

	munmap(buf, buf_size);
}

T_DECL(vm_upl_rw_on_submap,
    "Generate RW UPL against a submap region")
{
	const size_t buf_size = 10 * PAGE_SIZE;
	unsigned int *buf = mmap(NULL, buf_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
	T_QUIET; T_ASSERT_NE_PTR(buf, MAP_FAILED, "map buffer");

	for (unsigned int i = 0; i < (buf_size / sizeof(*buf)); i++) {
		buf[i] = (unsigned int)'a' + i;
	}

	upl_submap_test_args args = { .ptr = (uint64_t)buf, .size = buf_size, .upl_base = 0x180000000ULL,
		                      .upl_size = buf_size, .upl_rw = true };

	int64_t addr = (int64_t)&args;
	int64_t result = 0;
	size_t s = sizeof(result);
	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap)");

	args.upl_base += 0x800;
	args.upl_size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap");

	args.upl_base += 0x800;
	args.upl_size -= 0x1000;

	T_ASSERT_POSIX_SUCCESS(sysctlbyname("debug.test.vm_upl_submap", &result, &s, &addr, sizeof(addr)),
	    "sysctlbyname(debug.test.vm_upl_submap)");

	munmap(buf, buf_size);
}
