This is xnu-12377.1.9. See this file in:
/*
 * Copyright (c) 2025 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 <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>

#include <darwintest.h>
#include <darwintest/utils.h>

T_GLOBAL_META(
	T_META_NAMESPACE("xnu.vfs"),
	T_META_RADAR_COMPONENT_NAME("xnu"),
	T_META_RADAR_COMPONENT_VERSION("vfs"),
	T_META_ASROOT(false),
	T_META_CHECK_LEAKS(false),
	T_META_TAG_VM_PREFERRED,
	T_META_OWNER("m_staveleytaylor"));

static char lstat_testdir[PATH_MAX];
static char access_testdir[PATH_MAX];

static void
cleanup_lstat()
{
	rmdir("c");
	unlink("b");
	unlink("a");
	rmdir(lstat_testdir);
}

static void
cleanup_access()
{
	unlink("test.f");
	unlink("test.lnk");
	unlink("test.d/test.df");
	rmdir("test.d");
	rmdir(access_testdir);
}

T_DECL(
	lstat_symlink_trailing_slash,
	"Check symlinks-to-symlinks are resolved correctly when trailing slashes are involved"
	) {
	struct stat st;

	T_ATEND(cleanup_lstat);
	T_SETUPBEGIN;

	// Create test root dir
	snprintf(lstat_testdir, sizeof(lstat_testdir), "%s/symlink_trailing_slash-lstat-XXXXXX", dt_tmpdir());
	T_ASSERT_POSIX_NOTNULL(mkdtemp(lstat_testdir), "setup: create test root dir");

	// CD into test root dir
	T_ASSERT_POSIX_SUCCESS(chdir(lstat_testdir), "setup: cd testdir");

	// Setup a scenario with 'a -> b -> c' (where -> means 'is a symlink to').
	T_ASSERT_POSIX_SUCCESS(mkdir("c", 0755), "setup: mkdir c");
	T_ASSERT_POSIX_SUCCESS(symlink("c", "b"), "setup: ln c b");
	T_ASSERT_POSIX_SUCCESS(symlink("b", "a"), "setup: ln b a");

	T_SETUPEND;

	// stat

	T_ASSERT_POSIX_SUCCESS(stat("a", &st), "stat a succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "stat thinks a is directory");

	T_ASSERT_POSIX_SUCCESS(stat("b", &st), "stat b succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "stat thinks b is directory");

	T_ASSERT_POSIX_SUCCESS(stat("b/", &st), "stat b/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "stat thinks b/ is directory");

	T_ASSERT_POSIX_SUCCESS(stat("a/.", &st), "stat a/. succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "stat thinks a/. is directory");

	T_ASSERT_POSIX_SUCCESS(stat("a/", &st), "stat a/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "stat thinks a/ is directory");

	// lstat

	T_ASSERT_POSIX_SUCCESS(lstat("a", &st), "lstat a succeeds");
	T_ASSERT_TRUE(S_ISLNK(st.st_mode), "lstat thinks a is symlink");

	T_ASSERT_POSIX_SUCCESS(lstat("b", &st), "lstat b succeeds");
	T_ASSERT_TRUE(S_ISLNK(st.st_mode), "lstat thinks b is symlink");

	T_ASSERT_POSIX_SUCCESS(lstat("b/", &st), "lstat b/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks b/ is directory");

	T_ASSERT_POSIX_SUCCESS(lstat("a/.", &st), "lstat a/. succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks a/. is directory");

	// rdar://142559105 (lstat() of a name with trailing '/' is handled differently than other platforms)
	T_ASSERT_POSIX_SUCCESS(lstat("a/", &st), "lstat a/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks a/ is directory");

	// Now modify a such that it has a trailing slash in the link itself.
	T_ASSERT_POSIX_SUCCESS(unlink("a"), "unlink a");
	T_ASSERT_POSIX_SUCCESS(symlink("b/", "a"), "symlink a -> b/");

	T_ASSERT_POSIX_SUCCESS(lstat("a", &st), "lstat a succeeds");
	T_ASSERT_TRUE(S_ISLNK(st.st_mode), "lstat thinks a is symlink");

	T_ASSERT_POSIX_SUCCESS(lstat("a/", &st), "lstat a/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks a/ is directory");

	// Do the same for b.
	T_ASSERT_POSIX_SUCCESS(unlink("b"), "unlink b");
	T_ASSERT_POSIX_SUCCESS(symlink("c/", "b"), "symlink b -> c/");

	T_ASSERT_POSIX_SUCCESS(lstat("a", &st), "lstat a succeeds");
	T_ASSERT_TRUE(S_ISLNK(st.st_mode), "lstat thinks a is symlink");

	T_ASSERT_POSIX_SUCCESS(lstat("a/", &st), "lstat a/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks a/ is directory");

	T_ASSERT_POSIX_SUCCESS(lstat("b", &st), "lstat b succeeds");
	T_ASSERT_TRUE(S_ISLNK(st.st_mode), "lstat thinks b is symlink");

	T_ASSERT_POSIX_SUCCESS(lstat("b/", &st), "lstat b/ succeeds");
	T_ASSERT_TRUE(S_ISDIR(st.st_mode), "lstat thinks b/ is directory");
}

T_DECL(
	access_symlink_trailing_slash,
	"Check access returns ENOTDIR when symlink points to a file and trailing slash was used"
	) {
	T_ATEND(cleanup_access);
	T_SETUPBEGIN;

	// Create test root dir
	snprintf(access_testdir, sizeof(access_testdir), "%s/symlink_trailing_slash-access-XXXXXX", dt_tmpdir());
	T_ASSERT_POSIX_NOTNULL(mkdtemp(access_testdir), "setup: create test root dir");
	printf("testdir is %s\n", access_testdir);

	// CD into test root dir
	T_ASSERT_POSIX_SUCCESS(chdir(access_testdir), "setup: cd testdir");

	T_ASSERT_POSIX_SUCCESS(creat("test.f", 0755), "setup: touch test.f");
	T_ASSERT_POSIX_SUCCESS(symlink("test.f", "test.lnk"), "setup: ln test.f test.lnk");
	T_ASSERT_POSIX_SUCCESS(mkdir("test.d", 0755), "setup: mkdir test.d");
	T_ASSERT_POSIX_SUCCESS(creat("test.d/test.df", 0755), "setup: touch test.d/test.df");

	T_SETUPEND;

	T_ASSERT_POSIX_SUCCESS(access("test.lnk", R_OK), "access test.lnk suceeds");

	T_ASSERT_EQ(access("test.lnk/", R_OK), -1, "access test.lnk/ returns -1");
	T_ASSERT_POSIX_ERROR(errno, ENOTDIR, "access sets errno to ENOTDIR");

	// Now modify test.lnk to contain a trailing slash in the link itself
	T_ASSERT_POSIX_SUCCESS(unlink("test.lnk"), "rm test.lnk");
	T_ASSERT_POSIX_SUCCESS(symlink("test.f/", "test.lnk"), "ln -s test.f/ test.lnk");

	T_ASSERT_EQ(access("test.lnk", R_OK), -1, "access test.lnk returns -1");
	T_ASSERT_POSIX_ERROR(errno, ENOTDIR, "access sets errno to ENOTDIR");

	T_ASSERT_EQ(access("test.lnk/", R_OK), -1, "access test.lnk/ returns -1");
	T_ASSERT_POSIX_ERROR(errno, ENOTDIR, "access sets errno to ENOTDIR");

	// Now introduce a directory so that we have:
	// test.lnk -> test.d/ which contains test.f
	// The trailing slash in test.lnk should not cause access("test.lnk/test.f") to fail
	T_ASSERT_POSIX_SUCCESS(unlink("test.lnk"), "rm test.lnk");
	T_ASSERT_POSIX_SUCCESS(symlink("test.d/", "test.lnk"), "ln -s test.d/ test.lnk");
	T_ASSERT_POSIX_SUCCESS(access("test.lnk/test.df", R_OK), "access test.lnk/test.df");
}