162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-or-later
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * Author: Aleksa Sarai <cyphar@cyphar.com>
462306a36Sopenharmony_ci * Copyright (C) 2018-2019 SUSE LLC.
562306a36Sopenharmony_ci */
662306a36Sopenharmony_ci
762306a36Sopenharmony_ci#define _GNU_SOURCE
862306a36Sopenharmony_ci#include <fcntl.h>
962306a36Sopenharmony_ci#include <sched.h>
1062306a36Sopenharmony_ci#include <sys/stat.h>
1162306a36Sopenharmony_ci#include <sys/types.h>
1262306a36Sopenharmony_ci#include <sys/mount.h>
1362306a36Sopenharmony_ci#include <stdlib.h>
1462306a36Sopenharmony_ci#include <stdbool.h>
1562306a36Sopenharmony_ci#include <string.h>
1662306a36Sopenharmony_ci
1762306a36Sopenharmony_ci#include "../kselftest.h"
1862306a36Sopenharmony_ci#include "helpers.h"
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_ci/*
2162306a36Sopenharmony_ci * Construct a test directory with the following structure:
2262306a36Sopenharmony_ci *
2362306a36Sopenharmony_ci * root/
2462306a36Sopenharmony_ci * |-- procexe -> /proc/self/exe
2562306a36Sopenharmony_ci * |-- procroot -> /proc/self/root
2662306a36Sopenharmony_ci * |-- root/
2762306a36Sopenharmony_ci * |-- mnt/ [mountpoint]
2862306a36Sopenharmony_ci * |   |-- self -> ../mnt/
2962306a36Sopenharmony_ci * |   `-- absself -> /mnt/
3062306a36Sopenharmony_ci * |-- etc/
3162306a36Sopenharmony_ci * |   `-- passwd
3262306a36Sopenharmony_ci * |-- creatlink -> /newfile3
3362306a36Sopenharmony_ci * |-- reletc -> etc/
3462306a36Sopenharmony_ci * |-- relsym -> etc/passwd
3562306a36Sopenharmony_ci * |-- absetc -> /etc/
3662306a36Sopenharmony_ci * |-- abssym -> /etc/passwd
3762306a36Sopenharmony_ci * |-- abscheeky -> /cheeky
3862306a36Sopenharmony_ci * `-- cheeky/
3962306a36Sopenharmony_ci *     |-- absself -> /
4062306a36Sopenharmony_ci *     |-- self -> ../../root/
4162306a36Sopenharmony_ci *     |-- garbageself -> /../../root/
4262306a36Sopenharmony_ci *     |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd
4362306a36Sopenharmony_ci *     |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd
4462306a36Sopenharmony_ci *     |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
4562306a36Sopenharmony_ci *     `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
4662306a36Sopenharmony_ci */
4762306a36Sopenharmony_ciint setup_testdir(void)
4862306a36Sopenharmony_ci{
4962306a36Sopenharmony_ci	int dfd, tmpfd;
5062306a36Sopenharmony_ci	char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
5162306a36Sopenharmony_ci
5262306a36Sopenharmony_ci	/* Unshare and make /tmp a new directory. */
5362306a36Sopenharmony_ci	E_unshare(CLONE_NEWNS);
5462306a36Sopenharmony_ci	E_mount("", "/tmp", "", MS_PRIVATE, "");
5562306a36Sopenharmony_ci
5662306a36Sopenharmony_ci	/* Make the top-level directory. */
5762306a36Sopenharmony_ci	if (!mkdtemp(dirname))
5862306a36Sopenharmony_ci		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
5962306a36Sopenharmony_ci	dfd = open(dirname, O_PATH | O_DIRECTORY);
6062306a36Sopenharmony_ci	if (dfd < 0)
6162306a36Sopenharmony_ci		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
6262306a36Sopenharmony_ci
6362306a36Sopenharmony_ci	/* A sub-directory which is actually used for tests. */
6462306a36Sopenharmony_ci	E_mkdirat(dfd, "root", 0755);
6562306a36Sopenharmony_ci	tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
6662306a36Sopenharmony_ci	if (tmpfd < 0)
6762306a36Sopenharmony_ci		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
6862306a36Sopenharmony_ci	close(dfd);
6962306a36Sopenharmony_ci	dfd = tmpfd;
7062306a36Sopenharmony_ci
7162306a36Sopenharmony_ci	E_symlinkat("/proc/self/exe", dfd, "procexe");
7262306a36Sopenharmony_ci	E_symlinkat("/proc/self/root", dfd, "procroot");
7362306a36Sopenharmony_ci	E_mkdirat(dfd, "root", 0755);
7462306a36Sopenharmony_ci
7562306a36Sopenharmony_ci	/* There is no mountat(2), so use chdir. */
7662306a36Sopenharmony_ci	E_mkdirat(dfd, "mnt", 0755);
7762306a36Sopenharmony_ci	E_fchdir(dfd);
7862306a36Sopenharmony_ci	E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");
7962306a36Sopenharmony_ci	E_symlinkat("../mnt/", dfd, "mnt/self");
8062306a36Sopenharmony_ci	E_symlinkat("/mnt/", dfd, "mnt/absself");
8162306a36Sopenharmony_ci
8262306a36Sopenharmony_ci	E_mkdirat(dfd, "etc", 0755);
8362306a36Sopenharmony_ci	E_touchat(dfd, "etc/passwd");
8462306a36Sopenharmony_ci
8562306a36Sopenharmony_ci	E_symlinkat("/newfile3", dfd, "creatlink");
8662306a36Sopenharmony_ci	E_symlinkat("etc/", dfd, "reletc");
8762306a36Sopenharmony_ci	E_symlinkat("etc/passwd", dfd, "relsym");
8862306a36Sopenharmony_ci	E_symlinkat("/etc/", dfd, "absetc");
8962306a36Sopenharmony_ci	E_symlinkat("/etc/passwd", dfd, "abssym");
9062306a36Sopenharmony_ci	E_symlinkat("/cheeky", dfd, "abscheeky");
9162306a36Sopenharmony_ci
9262306a36Sopenharmony_ci	E_mkdirat(dfd, "cheeky", 0755);
9362306a36Sopenharmony_ci
9462306a36Sopenharmony_ci	E_symlinkat("/", dfd, "cheeky/absself");
9562306a36Sopenharmony_ci	E_symlinkat("../../root/", dfd, "cheeky/self");
9662306a36Sopenharmony_ci	E_symlinkat("/../../root/", dfd, "cheeky/garbageself");
9762306a36Sopenharmony_ci
9862306a36Sopenharmony_ci	E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");
9962306a36Sopenharmony_ci	E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");
10062306a36Sopenharmony_ci
10162306a36Sopenharmony_ci	E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
10262306a36Sopenharmony_ci		    dfd, "cheeky/dotdotlink");
10362306a36Sopenharmony_ci	E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
10462306a36Sopenharmony_ci		    dfd, "cheeky/garbagelink");
10562306a36Sopenharmony_ci
10662306a36Sopenharmony_ci	return dfd;
10762306a36Sopenharmony_ci}
10862306a36Sopenharmony_ci
10962306a36Sopenharmony_cistruct basic_test {
11062306a36Sopenharmony_ci	const char *name;
11162306a36Sopenharmony_ci	const char *dir;
11262306a36Sopenharmony_ci	const char *path;
11362306a36Sopenharmony_ci	struct open_how how;
11462306a36Sopenharmony_ci	bool pass;
11562306a36Sopenharmony_ci	union {
11662306a36Sopenharmony_ci		int err;
11762306a36Sopenharmony_ci		const char *path;
11862306a36Sopenharmony_ci	} out;
11962306a36Sopenharmony_ci};
12062306a36Sopenharmony_ci
12162306a36Sopenharmony_ci#define NUM_OPENAT2_OPATH_TESTS 88
12262306a36Sopenharmony_ci
12362306a36Sopenharmony_civoid test_openat2_opath_tests(void)
12462306a36Sopenharmony_ci{
12562306a36Sopenharmony_ci	int rootfd, hardcoded_fd;
12662306a36Sopenharmony_ci	char *procselfexe, *hardcoded_fdpath;
12762306a36Sopenharmony_ci
12862306a36Sopenharmony_ci	E_asprintf(&procselfexe, "/proc/%d/exe", getpid());
12962306a36Sopenharmony_ci	rootfd = setup_testdir();
13062306a36Sopenharmony_ci
13162306a36Sopenharmony_ci	hardcoded_fd = open("/dev/null", O_RDONLY);
13262306a36Sopenharmony_ci	E_assert(hardcoded_fd >= 0, "open fd to hardcode");
13362306a36Sopenharmony_ci	E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd);
13462306a36Sopenharmony_ci
13562306a36Sopenharmony_ci	struct basic_test tests[] = {
13662306a36Sopenharmony_ci		/** RESOLVE_BENEATH **/
13762306a36Sopenharmony_ci		/* Attempts to cross dirfd should be blocked. */
13862306a36Sopenharmony_ci		{ .name = "[beneath] jump to /",
13962306a36Sopenharmony_ci		  .path = "/",			.how.resolve = RESOLVE_BENEATH,
14062306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
14162306a36Sopenharmony_ci		{ .name = "[beneath] absolute link to $root",
14262306a36Sopenharmony_ci		  .path = "cheeky/absself",	.how.resolve = RESOLVE_BENEATH,
14362306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
14462306a36Sopenharmony_ci		{ .name = "[beneath] chained absolute links to $root",
14562306a36Sopenharmony_ci		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_BENEATH,
14662306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
14762306a36Sopenharmony_ci		{ .name = "[beneath] jump outside $root",
14862306a36Sopenharmony_ci		  .path = "..",			.how.resolve = RESOLVE_BENEATH,
14962306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
15062306a36Sopenharmony_ci		{ .name = "[beneath] temporary jump outside $root",
15162306a36Sopenharmony_ci		  .path = "../root/",		.how.resolve = RESOLVE_BENEATH,
15262306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
15362306a36Sopenharmony_ci		{ .name = "[beneath] symlink temporary jump outside $root",
15462306a36Sopenharmony_ci		  .path = "cheeky/self",	.how.resolve = RESOLVE_BENEATH,
15562306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
15662306a36Sopenharmony_ci		{ .name = "[beneath] chained symlink temporary jump outside $root",
15762306a36Sopenharmony_ci		  .path = "abscheeky/self",	.how.resolve = RESOLVE_BENEATH,
15862306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
15962306a36Sopenharmony_ci		{ .name = "[beneath] garbage links to $root",
16062306a36Sopenharmony_ci		  .path = "cheeky/garbageself",	.how.resolve = RESOLVE_BENEATH,
16162306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
16262306a36Sopenharmony_ci		{ .name = "[beneath] chained garbage links to $root",
16362306a36Sopenharmony_ci		  .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
16462306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
16562306a36Sopenharmony_ci		/* Only relative paths that stay inside dirfd should work. */
16662306a36Sopenharmony_ci		{ .name = "[beneath] ordinary path to 'root'",
16762306a36Sopenharmony_ci		  .path = "root",		.how.resolve = RESOLVE_BENEATH,
16862306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
16962306a36Sopenharmony_ci		{ .name = "[beneath] ordinary path to 'etc'",
17062306a36Sopenharmony_ci		  .path = "etc",		.how.resolve = RESOLVE_BENEATH,
17162306a36Sopenharmony_ci		  .out.path = "etc",		.pass = true },
17262306a36Sopenharmony_ci		{ .name = "[beneath] ordinary path to 'etc/passwd'",
17362306a36Sopenharmony_ci		  .path = "etc/passwd",		.how.resolve = RESOLVE_BENEATH,
17462306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
17562306a36Sopenharmony_ci		{ .name = "[beneath] relative symlink inside $root",
17662306a36Sopenharmony_ci		  .path = "relsym",		.how.resolve = RESOLVE_BENEATH,
17762306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
17862306a36Sopenharmony_ci		{ .name = "[beneath] chained-'..' relative symlink inside $root",
17962306a36Sopenharmony_ci		  .path = "cheeky/passwd",	.how.resolve = RESOLVE_BENEATH,
18062306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
18162306a36Sopenharmony_ci		{ .name = "[beneath] absolute symlink component outside $root",
18262306a36Sopenharmony_ci		  .path = "abscheeky/passwd",	.how.resolve = RESOLVE_BENEATH,
18362306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
18462306a36Sopenharmony_ci		{ .name = "[beneath] absolute symlink target outside $root",
18562306a36Sopenharmony_ci		  .path = "abssym",		.how.resolve = RESOLVE_BENEATH,
18662306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
18762306a36Sopenharmony_ci		{ .name = "[beneath] absolute path outside $root",
18862306a36Sopenharmony_ci		  .path = "/etc/passwd",	.how.resolve = RESOLVE_BENEATH,
18962306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
19062306a36Sopenharmony_ci		{ .name = "[beneath] cheeky absolute path outside $root",
19162306a36Sopenharmony_ci		  .path = "cheeky/abspasswd",	.how.resolve = RESOLVE_BENEATH,
19262306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
19362306a36Sopenharmony_ci		{ .name = "[beneath] chained cheeky absolute path outside $root",
19462306a36Sopenharmony_ci		  .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
19562306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
19662306a36Sopenharmony_ci		/* Tricky paths should fail. */
19762306a36Sopenharmony_ci		{ .name = "[beneath] tricky '..'-chained symlink outside $root",
19862306a36Sopenharmony_ci		  .path = "cheeky/dotdotlink",	.how.resolve = RESOLVE_BENEATH,
19962306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
20062306a36Sopenharmony_ci		{ .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
20162306a36Sopenharmony_ci		  .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
20262306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
20362306a36Sopenharmony_ci		{ .name = "[beneath] tricky garbage link outside $root",
20462306a36Sopenharmony_ci		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_BENEATH,
20562306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
20662306a36Sopenharmony_ci		{ .name = "[beneath] tricky absolute + garbage link outside $root",
20762306a36Sopenharmony_ci		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
20862306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
20962306a36Sopenharmony_ci
21062306a36Sopenharmony_ci		/** RESOLVE_IN_ROOT **/
21162306a36Sopenharmony_ci		/* All attempts to cross the dirfd will be scoped-to-root. */
21262306a36Sopenharmony_ci		{ .name = "[in_root] jump to /",
21362306a36Sopenharmony_ci		  .path = "/",			.how.resolve = RESOLVE_IN_ROOT,
21462306a36Sopenharmony_ci		  .out.path = NULL,		.pass = true },
21562306a36Sopenharmony_ci		{ .name = "[in_root] absolute symlink to /root",
21662306a36Sopenharmony_ci		  .path = "cheeky/absself",	.how.resolve = RESOLVE_IN_ROOT,
21762306a36Sopenharmony_ci		  .out.path = NULL,		.pass = true },
21862306a36Sopenharmony_ci		{ .name = "[in_root] chained absolute symlinks to /root",
21962306a36Sopenharmony_ci		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_IN_ROOT,
22062306a36Sopenharmony_ci		  .out.path = NULL,		.pass = true },
22162306a36Sopenharmony_ci		{ .name = "[in_root] '..' at root",
22262306a36Sopenharmony_ci		  .path = "..",			.how.resolve = RESOLVE_IN_ROOT,
22362306a36Sopenharmony_ci		  .out.path = NULL,		.pass = true },
22462306a36Sopenharmony_ci		{ .name = "[in_root] '../root' at root",
22562306a36Sopenharmony_ci		  .path = "../root/",		.how.resolve = RESOLVE_IN_ROOT,
22662306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
22762306a36Sopenharmony_ci		{ .name = "[in_root] relative symlink containing '..' above root",
22862306a36Sopenharmony_ci		  .path = "cheeky/self",	.how.resolve = RESOLVE_IN_ROOT,
22962306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
23062306a36Sopenharmony_ci		{ .name = "[in_root] garbage link to /root",
23162306a36Sopenharmony_ci		  .path = "cheeky/garbageself",	.how.resolve = RESOLVE_IN_ROOT,
23262306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
23362306a36Sopenharmony_ci		{ .name = "[in_root] chained garbage links to /root",
23462306a36Sopenharmony_ci		  .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
23562306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
23662306a36Sopenharmony_ci		{ .name = "[in_root] relative path to 'root'",
23762306a36Sopenharmony_ci		  .path = "root",		.how.resolve = RESOLVE_IN_ROOT,
23862306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
23962306a36Sopenharmony_ci		{ .name = "[in_root] relative path to 'etc'",
24062306a36Sopenharmony_ci		  .path = "etc",		.how.resolve = RESOLVE_IN_ROOT,
24162306a36Sopenharmony_ci		  .out.path = "etc",		.pass = true },
24262306a36Sopenharmony_ci		{ .name = "[in_root] relative path to 'etc/passwd'",
24362306a36Sopenharmony_ci		  .path = "etc/passwd",		.how.resolve = RESOLVE_IN_ROOT,
24462306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
24562306a36Sopenharmony_ci		{ .name = "[in_root] relative symlink to 'etc/passwd'",
24662306a36Sopenharmony_ci		  .path = "relsym",		.how.resolve = RESOLVE_IN_ROOT,
24762306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
24862306a36Sopenharmony_ci		{ .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
24962306a36Sopenharmony_ci		  .path = "cheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
25062306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
25162306a36Sopenharmony_ci		{ .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
25262306a36Sopenharmony_ci		  .path = "abscheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
25362306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
25462306a36Sopenharmony_ci		{ .name = "[in_root] absolute symlink to 'etc/passwd'",
25562306a36Sopenharmony_ci		  .path = "abssym",		.how.resolve = RESOLVE_IN_ROOT,
25662306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
25762306a36Sopenharmony_ci		{ .name = "[in_root] absolute path 'etc/passwd'",
25862306a36Sopenharmony_ci		  .path = "/etc/passwd",	.how.resolve = RESOLVE_IN_ROOT,
25962306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
26062306a36Sopenharmony_ci		{ .name = "[in_root] cheeky absolute path 'etc/passwd'",
26162306a36Sopenharmony_ci		  .path = "cheeky/abspasswd",	.how.resolve = RESOLVE_IN_ROOT,
26262306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
26362306a36Sopenharmony_ci		{ .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
26462306a36Sopenharmony_ci		  .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
26562306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
26662306a36Sopenharmony_ci		{ .name = "[in_root] tricky '..'-chained symlink outside $root",
26762306a36Sopenharmony_ci		  .path = "cheeky/dotdotlink",	.how.resolve = RESOLVE_IN_ROOT,
26862306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
26962306a36Sopenharmony_ci		{ .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
27062306a36Sopenharmony_ci		  .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
27162306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
27262306a36Sopenharmony_ci		{ .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
27362306a36Sopenharmony_ci		  .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
27462306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
27562306a36Sopenharmony_ci		{ .name = "[in_root] tricky garbage link outside $root",
27662306a36Sopenharmony_ci		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_IN_ROOT,
27762306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
27862306a36Sopenharmony_ci		{ .name = "[in_root] tricky absolute + garbage link outside $root",
27962306a36Sopenharmony_ci		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
28062306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
28162306a36Sopenharmony_ci		{ .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
28262306a36Sopenharmony_ci		  .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
28362306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
28462306a36Sopenharmony_ci		/* O_CREAT should handle trailing symlinks correctly. */
28562306a36Sopenharmony_ci		{ .name = "[in_root] O_CREAT of relative path inside $root",
28662306a36Sopenharmony_ci		  .path = "newfile1",		.how.flags = O_CREAT,
28762306a36Sopenharmony_ci						.how.mode = 0700,
28862306a36Sopenharmony_ci						.how.resolve = RESOLVE_IN_ROOT,
28962306a36Sopenharmony_ci		  .out.path = "newfile1",	.pass = true },
29062306a36Sopenharmony_ci		{ .name = "[in_root] O_CREAT of absolute path",
29162306a36Sopenharmony_ci		  .path = "/newfile2",		.how.flags = O_CREAT,
29262306a36Sopenharmony_ci						.how.mode = 0700,
29362306a36Sopenharmony_ci						.how.resolve = RESOLVE_IN_ROOT,
29462306a36Sopenharmony_ci		  .out.path = "newfile2",	.pass = true },
29562306a36Sopenharmony_ci		{ .name = "[in_root] O_CREAT of tricky symlink outside root",
29662306a36Sopenharmony_ci		  .path = "/creatlink",		.how.flags = O_CREAT,
29762306a36Sopenharmony_ci						.how.mode = 0700,
29862306a36Sopenharmony_ci						.how.resolve = RESOLVE_IN_ROOT,
29962306a36Sopenharmony_ci		  .out.path = "newfile3",	.pass = true },
30062306a36Sopenharmony_ci
30162306a36Sopenharmony_ci		/** RESOLVE_NO_XDEV **/
30262306a36Sopenharmony_ci		/* Crossing *down* into a mountpoint is disallowed. */
30362306a36Sopenharmony_ci		{ .name = "[no_xdev] cross into $mnt",
30462306a36Sopenharmony_ci		  .path = "mnt",		.how.resolve = RESOLVE_NO_XDEV,
30562306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
30662306a36Sopenharmony_ci		{ .name = "[no_xdev] cross into $mnt/",
30762306a36Sopenharmony_ci		  .path = "mnt/",		.how.resolve = RESOLVE_NO_XDEV,
30862306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
30962306a36Sopenharmony_ci		{ .name = "[no_xdev] cross into $mnt/.",
31062306a36Sopenharmony_ci		  .path = "mnt/.",		.how.resolve = RESOLVE_NO_XDEV,
31162306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
31262306a36Sopenharmony_ci		/* Crossing *up* out of a mountpoint is disallowed. */
31362306a36Sopenharmony_ci		{ .name = "[no_xdev] goto mountpoint root",
31462306a36Sopenharmony_ci		  .dir = "mnt", .path = ".",	.how.resolve = RESOLVE_NO_XDEV,
31562306a36Sopenharmony_ci		  .out.path = "mnt",		.pass = true },
31662306a36Sopenharmony_ci		{ .name = "[no_xdev] cross up through '..'",
31762306a36Sopenharmony_ci		  .dir = "mnt", .path = "..",	.how.resolve = RESOLVE_NO_XDEV,
31862306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
31962306a36Sopenharmony_ci		{ .name = "[no_xdev] temporary cross up through '..'",
32062306a36Sopenharmony_ci		  .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
32162306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
32262306a36Sopenharmony_ci		{ .name = "[no_xdev] temporary relative symlink cross up",
32362306a36Sopenharmony_ci		  .dir = "mnt", .path = "self",	.how.resolve = RESOLVE_NO_XDEV,
32462306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
32562306a36Sopenharmony_ci		{ .name = "[no_xdev] temporary absolute symlink cross up",
32662306a36Sopenharmony_ci		  .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
32762306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
32862306a36Sopenharmony_ci		/* Jumping to "/" is ok, but later components cannot cross. */
32962306a36Sopenharmony_ci		{ .name = "[no_xdev] jump to / directly",
33062306a36Sopenharmony_ci		  .dir = "mnt", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
33162306a36Sopenharmony_ci		  .out.path = "/",		.pass = true },
33262306a36Sopenharmony_ci		{ .name = "[no_xdev] jump to / (from /) directly",
33362306a36Sopenharmony_ci		  .dir = "/", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
33462306a36Sopenharmony_ci		  .out.path = "/",		.pass = true },
33562306a36Sopenharmony_ci		{ .name = "[no_xdev] jump to / then proc",
33662306a36Sopenharmony_ci		  .path = "/proc/1",		.how.resolve = RESOLVE_NO_XDEV,
33762306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
33862306a36Sopenharmony_ci		{ .name = "[no_xdev] jump to / then tmp",
33962306a36Sopenharmony_ci		  .path = "/tmp",		.how.resolve = RESOLVE_NO_XDEV,
34062306a36Sopenharmony_ci		  .out.err = -EXDEV,		.pass = false },
34162306a36Sopenharmony_ci		/* Magic-links are blocked since they can switch vfsmounts. */
34262306a36Sopenharmony_ci		{ .name = "[no_xdev] cross through magic-link to self/root",
34362306a36Sopenharmony_ci		  .dir = "/proc", .path = "self/root", 	.how.resolve = RESOLVE_NO_XDEV,
34462306a36Sopenharmony_ci		  .out.err = -EXDEV,			.pass = false },
34562306a36Sopenharmony_ci		{ .name = "[no_xdev] cross through magic-link to self/cwd",
34662306a36Sopenharmony_ci		  .dir = "/proc", .path = "self/cwd",	.how.resolve = RESOLVE_NO_XDEV,
34762306a36Sopenharmony_ci		  .out.err = -EXDEV,			.pass = false },
34862306a36Sopenharmony_ci		/* Except magic-link jumps inside the same vfsmount. */
34962306a36Sopenharmony_ci		{ .name = "[no_xdev] jump through magic-link to same procfs",
35062306a36Sopenharmony_ci		  .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
35162306a36Sopenharmony_ci		  .out.path = "/proc",			    .pass = true, },
35262306a36Sopenharmony_ci
35362306a36Sopenharmony_ci		/** RESOLVE_NO_MAGICLINKS **/
35462306a36Sopenharmony_ci		/* Regular symlinks should work. */
35562306a36Sopenharmony_ci		{ .name = "[no_magiclinks] ordinary relative symlink",
35662306a36Sopenharmony_ci		  .path = "relsym",		.how.resolve = RESOLVE_NO_MAGICLINKS,
35762306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
35862306a36Sopenharmony_ci		/* Magic-links should not work. */
35962306a36Sopenharmony_ci		{ .name = "[no_magiclinks] symlink to magic-link",
36062306a36Sopenharmony_ci		  .path = "procexe",		.how.resolve = RESOLVE_NO_MAGICLINKS,
36162306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
36262306a36Sopenharmony_ci		{ .name = "[no_magiclinks] normal path to magic-link",
36362306a36Sopenharmony_ci		  .path = "/proc/self/exe",	.how.resolve = RESOLVE_NO_MAGICLINKS,
36462306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
36562306a36Sopenharmony_ci		{ .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
36662306a36Sopenharmony_ci		  .path = "/proc/self/exe",	.how.flags = O_NOFOLLOW,
36762306a36Sopenharmony_ci						.how.resolve = RESOLVE_NO_MAGICLINKS,
36862306a36Sopenharmony_ci		  .out.path = procselfexe,	.pass = true },
36962306a36Sopenharmony_ci		{ .name = "[no_magiclinks] symlink to magic-link path component",
37062306a36Sopenharmony_ci		  .path = "procroot/etc",	.how.resolve = RESOLVE_NO_MAGICLINKS,
37162306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
37262306a36Sopenharmony_ci		{ .name = "[no_magiclinks] magic-link path component",
37362306a36Sopenharmony_ci		  .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
37462306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
37562306a36Sopenharmony_ci		{ .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
37662306a36Sopenharmony_ci		  .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
37762306a36Sopenharmony_ci						 .how.resolve = RESOLVE_NO_MAGICLINKS,
37862306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
37962306a36Sopenharmony_ci
38062306a36Sopenharmony_ci		/** RESOLVE_NO_SYMLINKS **/
38162306a36Sopenharmony_ci		/* Normal paths should work. */
38262306a36Sopenharmony_ci		{ .name = "[no_symlinks] ordinary path to '.'",
38362306a36Sopenharmony_ci		  .path = ".",			.how.resolve = RESOLVE_NO_SYMLINKS,
38462306a36Sopenharmony_ci		  .out.path = NULL,		.pass = true },
38562306a36Sopenharmony_ci		{ .name = "[no_symlinks] ordinary path to 'root'",
38662306a36Sopenharmony_ci		  .path = "root",		.how.resolve = RESOLVE_NO_SYMLINKS,
38762306a36Sopenharmony_ci		  .out.path = "root",		.pass = true },
38862306a36Sopenharmony_ci		{ .name = "[no_symlinks] ordinary path to 'etc'",
38962306a36Sopenharmony_ci		  .path = "etc",		.how.resolve = RESOLVE_NO_SYMLINKS,
39062306a36Sopenharmony_ci		  .out.path = "etc",		.pass = true },
39162306a36Sopenharmony_ci		{ .name = "[no_symlinks] ordinary path to 'etc/passwd'",
39262306a36Sopenharmony_ci		  .path = "etc/passwd",		.how.resolve = RESOLVE_NO_SYMLINKS,
39362306a36Sopenharmony_ci		  .out.path = "etc/passwd",	.pass = true },
39462306a36Sopenharmony_ci		/* Regular symlinks are blocked. */
39562306a36Sopenharmony_ci		{ .name = "[no_symlinks] relative symlink target",
39662306a36Sopenharmony_ci		  .path = "relsym",		.how.resolve = RESOLVE_NO_SYMLINKS,
39762306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
39862306a36Sopenharmony_ci		{ .name = "[no_symlinks] relative symlink component",
39962306a36Sopenharmony_ci		  .path = "reletc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
40062306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
40162306a36Sopenharmony_ci		{ .name = "[no_symlinks] absolute symlink target",
40262306a36Sopenharmony_ci		  .path = "abssym",		.how.resolve = RESOLVE_NO_SYMLINKS,
40362306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
40462306a36Sopenharmony_ci		{ .name = "[no_symlinks] absolute symlink component",
40562306a36Sopenharmony_ci		  .path = "absetc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
40662306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
40762306a36Sopenharmony_ci		{ .name = "[no_symlinks] cheeky garbage link",
40862306a36Sopenharmony_ci		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_NO_SYMLINKS,
40962306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
41062306a36Sopenharmony_ci		{ .name = "[no_symlinks] cheeky absolute + garbage link",
41162306a36Sopenharmony_ci		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
41262306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
41362306a36Sopenharmony_ci		{ .name = "[no_symlinks] cheeky absolute + absolute symlink",
41462306a36Sopenharmony_ci		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_NO_SYMLINKS,
41562306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
41662306a36Sopenharmony_ci		/* Trailing symlinks with NO_FOLLOW. */
41762306a36Sopenharmony_ci		{ .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
41862306a36Sopenharmony_ci		  .path = "relsym",		.how.flags = O_NOFOLLOW,
41962306a36Sopenharmony_ci						.how.resolve = RESOLVE_NO_SYMLINKS,
42062306a36Sopenharmony_ci		  .out.path = "relsym",		.pass = true },
42162306a36Sopenharmony_ci		{ .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
42262306a36Sopenharmony_ci		  .path = "abssym",		.how.flags = O_NOFOLLOW,
42362306a36Sopenharmony_ci						.how.resolve = RESOLVE_NO_SYMLINKS,
42462306a36Sopenharmony_ci		  .out.path = "abssym",		.pass = true },
42562306a36Sopenharmony_ci		{ .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
42662306a36Sopenharmony_ci		  .path = "cheeky/garbagelink",	.how.flags = O_NOFOLLOW,
42762306a36Sopenharmony_ci						.how.resolve = RESOLVE_NO_SYMLINKS,
42862306a36Sopenharmony_ci		  .out.path = "cheeky/garbagelink", .pass = true },
42962306a36Sopenharmony_ci		{ .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
43062306a36Sopenharmony_ci		  .path = "abscheeky/absself",	.how.flags = O_NOFOLLOW,
43162306a36Sopenharmony_ci						.how.resolve = RESOLVE_NO_SYMLINKS,
43262306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
43362306a36Sopenharmony_ci		{ .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
43462306a36Sopenharmony_ci		  .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
43562306a36Sopenharmony_ci						   .how.resolve = RESOLVE_NO_SYMLINKS,
43662306a36Sopenharmony_ci		  .out.err = -ELOOP,		.pass = false },
43762306a36Sopenharmony_ci	};
43862306a36Sopenharmony_ci
43962306a36Sopenharmony_ci	BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
44062306a36Sopenharmony_ci
44162306a36Sopenharmony_ci	for (int i = 0; i < ARRAY_LEN(tests); i++) {
44262306a36Sopenharmony_ci		int dfd, fd;
44362306a36Sopenharmony_ci		char *fdpath = NULL;
44462306a36Sopenharmony_ci		bool failed;
44562306a36Sopenharmony_ci		void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
44662306a36Sopenharmony_ci		struct basic_test *test = &tests[i];
44762306a36Sopenharmony_ci
44862306a36Sopenharmony_ci		if (!openat2_supported) {
44962306a36Sopenharmony_ci			ksft_print_msg("openat2(2) unsupported\n");
45062306a36Sopenharmony_ci			resultfn = ksft_test_result_skip;
45162306a36Sopenharmony_ci			goto skip;
45262306a36Sopenharmony_ci		}
45362306a36Sopenharmony_ci
45462306a36Sopenharmony_ci		/* Auto-set O_PATH. */
45562306a36Sopenharmony_ci		if (!(test->how.flags & O_CREAT))
45662306a36Sopenharmony_ci			test->how.flags |= O_PATH;
45762306a36Sopenharmony_ci
45862306a36Sopenharmony_ci		if (test->dir)
45962306a36Sopenharmony_ci			dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
46062306a36Sopenharmony_ci		else
46162306a36Sopenharmony_ci			dfd = dup(rootfd);
46262306a36Sopenharmony_ci		E_assert(dfd, "failed to openat root '%s': %m", test->dir);
46362306a36Sopenharmony_ci
46462306a36Sopenharmony_ci		E_dup2(dfd, hardcoded_fd);
46562306a36Sopenharmony_ci
46662306a36Sopenharmony_ci		fd = sys_openat2(dfd, test->path, &test->how);
46762306a36Sopenharmony_ci		if (test->pass)
46862306a36Sopenharmony_ci			failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
46962306a36Sopenharmony_ci		else
47062306a36Sopenharmony_ci			failed = (fd != test->out.err);
47162306a36Sopenharmony_ci		if (fd >= 0) {
47262306a36Sopenharmony_ci			fdpath = fdreadlink(fd);
47362306a36Sopenharmony_ci			close(fd);
47462306a36Sopenharmony_ci		}
47562306a36Sopenharmony_ci		close(dfd);
47662306a36Sopenharmony_ci
47762306a36Sopenharmony_ci		if (failed) {
47862306a36Sopenharmony_ci			resultfn = ksft_test_result_fail;
47962306a36Sopenharmony_ci
48062306a36Sopenharmony_ci			ksft_print_msg("openat2 unexpectedly returned ");
48162306a36Sopenharmony_ci			if (fdpath)
48262306a36Sopenharmony_ci				ksft_print_msg("%d['%s']\n", fd, fdpath);
48362306a36Sopenharmony_ci			else
48462306a36Sopenharmony_ci				ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
48562306a36Sopenharmony_ci		}
48662306a36Sopenharmony_ci
48762306a36Sopenharmony_ciskip:
48862306a36Sopenharmony_ci		if (test->pass)
48962306a36Sopenharmony_ci			resultfn("%s gives path '%s'\n", test->name,
49062306a36Sopenharmony_ci				 test->out.path ?: ".");
49162306a36Sopenharmony_ci		else
49262306a36Sopenharmony_ci			resultfn("%s fails with %d (%s)\n", test->name,
49362306a36Sopenharmony_ci				 test->out.err, strerror(-test->out.err));
49462306a36Sopenharmony_ci
49562306a36Sopenharmony_ci		fflush(stdout);
49662306a36Sopenharmony_ci		free(fdpath);
49762306a36Sopenharmony_ci	}
49862306a36Sopenharmony_ci
49962306a36Sopenharmony_ci	free(procselfexe);
50062306a36Sopenharmony_ci	close(rootfd);
50162306a36Sopenharmony_ci
50262306a36Sopenharmony_ci	free(hardcoded_fdpath);
50362306a36Sopenharmony_ci	close(hardcoded_fd);
50462306a36Sopenharmony_ci}
50562306a36Sopenharmony_ci
50662306a36Sopenharmony_ci#define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
50762306a36Sopenharmony_ci
50862306a36Sopenharmony_ciint main(int argc, char **argv)
50962306a36Sopenharmony_ci{
51062306a36Sopenharmony_ci	ksft_print_header();
51162306a36Sopenharmony_ci	ksft_set_plan(NUM_TESTS);
51262306a36Sopenharmony_ci
51362306a36Sopenharmony_ci	/* NOTE: We should be checking for CAP_SYS_ADMIN here... */
51462306a36Sopenharmony_ci	if (geteuid() != 0)
51562306a36Sopenharmony_ci		ksft_exit_skip("all tests require euid == 0\n");
51662306a36Sopenharmony_ci
51762306a36Sopenharmony_ci	test_openat2_opath_tests();
51862306a36Sopenharmony_ci
51962306a36Sopenharmony_ci	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
52062306a36Sopenharmony_ci		ksft_exit_fail();
52162306a36Sopenharmony_ci	else
52262306a36Sopenharmony_ci		ksft_exit_pass();
52362306a36Sopenharmony_ci}
524