resolve_test.c 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. // SPDX-License-Identifier: GPL-2.0-or-later
  2. /*
  3. * Author: Aleksa Sarai <cyphar@cyphar.com>
  4. * Copyright (C) 2018-2019 SUSE LLC.
  5. */
  6. #define _GNU_SOURCE
  7. #include <fcntl.h>
  8. #include <sched.h>
  9. #include <sys/stat.h>
  10. #include <sys/types.h>
  11. #include <sys/mount.h>
  12. #include <stdlib.h>
  13. #include <stdbool.h>
  14. #include <string.h>
  15. #include "../kselftest.h"
  16. #include "helpers.h"
  17. /*
  18. * Construct a test directory with the following structure:
  19. *
  20. * root/
  21. * |-- procexe -> /proc/self/exe
  22. * |-- procroot -> /proc/self/root
  23. * |-- root/
  24. * |-- mnt/ [mountpoint]
  25. * | |-- self -> ../mnt/
  26. * | `-- absself -> /mnt/
  27. * |-- etc/
  28. * | `-- passwd
  29. * |-- creatlink -> /newfile3
  30. * |-- reletc -> etc/
  31. * |-- relsym -> etc/passwd
  32. * |-- absetc -> /etc/
  33. * |-- abssym -> /etc/passwd
  34. * |-- abscheeky -> /cheeky
  35. * `-- cheeky/
  36. * |-- absself -> /
  37. * |-- self -> ../../root/
  38. * |-- garbageself -> /../../root/
  39. * |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd
  40. * |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd
  41. * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
  42. * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
  43. */
  44. int setup_testdir(void)
  45. {
  46. int dfd, tmpfd;
  47. char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
  48. /* Unshare and make /tmp a new directory. */
  49. E_unshare(CLONE_NEWNS);
  50. E_mount("", "/tmp", "", MS_PRIVATE, "");
  51. /* Make the top-level directory. */
  52. if (!mkdtemp(dirname))
  53. ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
  54. dfd = open(dirname, O_PATH | O_DIRECTORY);
  55. if (dfd < 0)
  56. ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
  57. /* A sub-directory which is actually used for tests. */
  58. E_mkdirat(dfd, "root", 0755);
  59. tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
  60. if (tmpfd < 0)
  61. ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
  62. close(dfd);
  63. dfd = tmpfd;
  64. E_symlinkat("/proc/self/exe", dfd, "procexe");
  65. E_symlinkat("/proc/self/root", dfd, "procroot");
  66. E_mkdirat(dfd, "root", 0755);
  67. /* There is no mountat(2), so use chdir. */
  68. E_mkdirat(dfd, "mnt", 0755);
  69. E_fchdir(dfd);
  70. E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");
  71. E_symlinkat("../mnt/", dfd, "mnt/self");
  72. E_symlinkat("/mnt/", dfd, "mnt/absself");
  73. E_mkdirat(dfd, "etc", 0755);
  74. E_touchat(dfd, "etc/passwd");
  75. E_symlinkat("/newfile3", dfd, "creatlink");
  76. E_symlinkat("etc/", dfd, "reletc");
  77. E_symlinkat("etc/passwd", dfd, "relsym");
  78. E_symlinkat("/etc/", dfd, "absetc");
  79. E_symlinkat("/etc/passwd", dfd, "abssym");
  80. E_symlinkat("/cheeky", dfd, "abscheeky");
  81. E_mkdirat(dfd, "cheeky", 0755);
  82. E_symlinkat("/", dfd, "cheeky/absself");
  83. E_symlinkat("../../root/", dfd, "cheeky/self");
  84. E_symlinkat("/../../root/", dfd, "cheeky/garbageself");
  85. E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");
  86. E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");
  87. E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
  88. dfd, "cheeky/dotdotlink");
  89. E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
  90. dfd, "cheeky/garbagelink");
  91. return dfd;
  92. }
  93. struct basic_test {
  94. const char *name;
  95. const char *dir;
  96. const char *path;
  97. struct open_how how;
  98. bool pass;
  99. union {
  100. int err;
  101. const char *path;
  102. } out;
  103. };
  104. #define NUM_OPENAT2_OPATH_TESTS 88
  105. void test_openat2_opath_tests(void)
  106. {
  107. int rootfd, hardcoded_fd;
  108. char *procselfexe, *hardcoded_fdpath;
  109. E_asprintf(&procselfexe, "/proc/%d/exe", getpid());
  110. rootfd = setup_testdir();
  111. hardcoded_fd = open("/dev/null", O_RDONLY);
  112. E_assert(hardcoded_fd >= 0, "open fd to hardcode");
  113. E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd);
  114. struct basic_test tests[] = {
  115. /** RESOLVE_BENEATH **/
  116. /* Attempts to cross dirfd should be blocked. */
  117. { .name = "[beneath] jump to /",
  118. .path = "/", .how.resolve = RESOLVE_BENEATH,
  119. .out.err = -EXDEV, .pass = false },
  120. { .name = "[beneath] absolute link to $root",
  121. .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
  122. .out.err = -EXDEV, .pass = false },
  123. { .name = "[beneath] chained absolute links to $root",
  124. .path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH,
  125. .out.err = -EXDEV, .pass = false },
  126. { .name = "[beneath] jump outside $root",
  127. .path = "..", .how.resolve = RESOLVE_BENEATH,
  128. .out.err = -EXDEV, .pass = false },
  129. { .name = "[beneath] temporary jump outside $root",
  130. .path = "../root/", .how.resolve = RESOLVE_BENEATH,
  131. .out.err = -EXDEV, .pass = false },
  132. { .name = "[beneath] symlink temporary jump outside $root",
  133. .path = "cheeky/self", .how.resolve = RESOLVE_BENEATH,
  134. .out.err = -EXDEV, .pass = false },
  135. { .name = "[beneath] chained symlink temporary jump outside $root",
  136. .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
  137. .out.err = -EXDEV, .pass = false },
  138. { .name = "[beneath] garbage links to $root",
  139. .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
  140. .out.err = -EXDEV, .pass = false },
  141. { .name = "[beneath] chained garbage links to $root",
  142. .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
  143. .out.err = -EXDEV, .pass = false },
  144. /* Only relative paths that stay inside dirfd should work. */
  145. { .name = "[beneath] ordinary path to 'root'",
  146. .path = "root", .how.resolve = RESOLVE_BENEATH,
  147. .out.path = "root", .pass = true },
  148. { .name = "[beneath] ordinary path to 'etc'",
  149. .path = "etc", .how.resolve = RESOLVE_BENEATH,
  150. .out.path = "etc", .pass = true },
  151. { .name = "[beneath] ordinary path to 'etc/passwd'",
  152. .path = "etc/passwd", .how.resolve = RESOLVE_BENEATH,
  153. .out.path = "etc/passwd", .pass = true },
  154. { .name = "[beneath] relative symlink inside $root",
  155. .path = "relsym", .how.resolve = RESOLVE_BENEATH,
  156. .out.path = "etc/passwd", .pass = true },
  157. { .name = "[beneath] chained-'..' relative symlink inside $root",
  158. .path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH,
  159. .out.path = "etc/passwd", .pass = true },
  160. { .name = "[beneath] absolute symlink component outside $root",
  161. .path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH,
  162. .out.err = -EXDEV, .pass = false },
  163. { .name = "[beneath] absolute symlink target outside $root",
  164. .path = "abssym", .how.resolve = RESOLVE_BENEATH,
  165. .out.err = -EXDEV, .pass = false },
  166. { .name = "[beneath] absolute path outside $root",
  167. .path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH,
  168. .out.err = -EXDEV, .pass = false },
  169. { .name = "[beneath] cheeky absolute path outside $root",
  170. .path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
  171. .out.err = -EXDEV, .pass = false },
  172. { .name = "[beneath] chained cheeky absolute path outside $root",
  173. .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
  174. .out.err = -EXDEV, .pass = false },
  175. /* Tricky paths should fail. */
  176. { .name = "[beneath] tricky '..'-chained symlink outside $root",
  177. .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
  178. .out.err = -EXDEV, .pass = false },
  179. { .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
  180. .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
  181. .out.err = -EXDEV, .pass = false },
  182. { .name = "[beneath] tricky garbage link outside $root",
  183. .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
  184. .out.err = -EXDEV, .pass = false },
  185. { .name = "[beneath] tricky absolute + garbage link outside $root",
  186. .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
  187. .out.err = -EXDEV, .pass = false },
  188. /** RESOLVE_IN_ROOT **/
  189. /* All attempts to cross the dirfd will be scoped-to-root. */
  190. { .name = "[in_root] jump to /",
  191. .path = "/", .how.resolve = RESOLVE_IN_ROOT,
  192. .out.path = NULL, .pass = true },
  193. { .name = "[in_root] absolute symlink to /root",
  194. .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
  195. .out.path = NULL, .pass = true },
  196. { .name = "[in_root] chained absolute symlinks to /root",
  197. .path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
  198. .out.path = NULL, .pass = true },
  199. { .name = "[in_root] '..' at root",
  200. .path = "..", .how.resolve = RESOLVE_IN_ROOT,
  201. .out.path = NULL, .pass = true },
  202. { .name = "[in_root] '../root' at root",
  203. .path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
  204. .out.path = "root", .pass = true },
  205. { .name = "[in_root] relative symlink containing '..' above root",
  206. .path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT,
  207. .out.path = "root", .pass = true },
  208. { .name = "[in_root] garbage link to /root",
  209. .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
  210. .out.path = "root", .pass = true },
  211. { .name = "[in_root] chained garbage links to /root",
  212. .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
  213. .out.path = "root", .pass = true },
  214. { .name = "[in_root] relative path to 'root'",
  215. .path = "root", .how.resolve = RESOLVE_IN_ROOT,
  216. .out.path = "root", .pass = true },
  217. { .name = "[in_root] relative path to 'etc'",
  218. .path = "etc", .how.resolve = RESOLVE_IN_ROOT,
  219. .out.path = "etc", .pass = true },
  220. { .name = "[in_root] relative path to 'etc/passwd'",
  221. .path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
  222. .out.path = "etc/passwd", .pass = true },
  223. { .name = "[in_root] relative symlink to 'etc/passwd'",
  224. .path = "relsym", .how.resolve = RESOLVE_IN_ROOT,
  225. .out.path = "etc/passwd", .pass = true },
  226. { .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
  227. .path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
  228. .out.path = "etc/passwd", .pass = true },
  229. { .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
  230. .path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
  231. .out.path = "etc/passwd", .pass = true },
  232. { .name = "[in_root] absolute symlink to 'etc/passwd'",
  233. .path = "abssym", .how.resolve = RESOLVE_IN_ROOT,
  234. .out.path = "etc/passwd", .pass = true },
  235. { .name = "[in_root] absolute path 'etc/passwd'",
  236. .path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
  237. .out.path = "etc/passwd", .pass = true },
  238. { .name = "[in_root] cheeky absolute path 'etc/passwd'",
  239. .path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
  240. .out.path = "etc/passwd", .pass = true },
  241. { .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
  242. .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
  243. .out.path = "etc/passwd", .pass = true },
  244. { .name = "[in_root] tricky '..'-chained symlink outside $root",
  245. .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
  246. .out.path = "etc/passwd", .pass = true },
  247. { .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
  248. .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
  249. .out.path = "etc/passwd", .pass = true },
  250. { .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
  251. .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
  252. .out.path = "etc/passwd", .pass = true },
  253. { .name = "[in_root] tricky garbage link outside $root",
  254. .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
  255. .out.path = "etc/passwd", .pass = true },
  256. { .name = "[in_root] tricky absolute + garbage link outside $root",
  257. .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
  258. .out.path = "etc/passwd", .pass = true },
  259. { .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
  260. .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
  261. .out.path = "etc/passwd", .pass = true },
  262. /* O_CREAT should handle trailing symlinks correctly. */
  263. { .name = "[in_root] O_CREAT of relative path inside $root",
  264. .path = "newfile1", .how.flags = O_CREAT,
  265. .how.mode = 0700,
  266. .how.resolve = RESOLVE_IN_ROOT,
  267. .out.path = "newfile1", .pass = true },
  268. { .name = "[in_root] O_CREAT of absolute path",
  269. .path = "/newfile2", .how.flags = O_CREAT,
  270. .how.mode = 0700,
  271. .how.resolve = RESOLVE_IN_ROOT,
  272. .out.path = "newfile2", .pass = true },
  273. { .name = "[in_root] O_CREAT of tricky symlink outside root",
  274. .path = "/creatlink", .how.flags = O_CREAT,
  275. .how.mode = 0700,
  276. .how.resolve = RESOLVE_IN_ROOT,
  277. .out.path = "newfile3", .pass = true },
  278. /** RESOLVE_NO_XDEV **/
  279. /* Crossing *down* into a mountpoint is disallowed. */
  280. { .name = "[no_xdev] cross into $mnt",
  281. .path = "mnt", .how.resolve = RESOLVE_NO_XDEV,
  282. .out.err = -EXDEV, .pass = false },
  283. { .name = "[no_xdev] cross into $mnt/",
  284. .path = "mnt/", .how.resolve = RESOLVE_NO_XDEV,
  285. .out.err = -EXDEV, .pass = false },
  286. { .name = "[no_xdev] cross into $mnt/.",
  287. .path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV,
  288. .out.err = -EXDEV, .pass = false },
  289. /* Crossing *up* out of a mountpoint is disallowed. */
  290. { .name = "[no_xdev] goto mountpoint root",
  291. .dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV,
  292. .out.path = "mnt", .pass = true },
  293. { .name = "[no_xdev] cross up through '..'",
  294. .dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV,
  295. .out.err = -EXDEV, .pass = false },
  296. { .name = "[no_xdev] temporary cross up through '..'",
  297. .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
  298. .out.err = -EXDEV, .pass = false },
  299. { .name = "[no_xdev] temporary relative symlink cross up",
  300. .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
  301. .out.err = -EXDEV, .pass = false },
  302. { .name = "[no_xdev] temporary absolute symlink cross up",
  303. .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
  304. .out.err = -EXDEV, .pass = false },
  305. /* Jumping to "/" is ok, but later components cannot cross. */
  306. { .name = "[no_xdev] jump to / directly",
  307. .dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
  308. .out.path = "/", .pass = true },
  309. { .name = "[no_xdev] jump to / (from /) directly",
  310. .dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
  311. .out.path = "/", .pass = true },
  312. { .name = "[no_xdev] jump to / then proc",
  313. .path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV,
  314. .out.err = -EXDEV, .pass = false },
  315. { .name = "[no_xdev] jump to / then tmp",
  316. .path = "/tmp", .how.resolve = RESOLVE_NO_XDEV,
  317. .out.err = -EXDEV, .pass = false },
  318. /* Magic-links are blocked since they can switch vfsmounts. */
  319. { .name = "[no_xdev] cross through magic-link to self/root",
  320. .dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV,
  321. .out.err = -EXDEV, .pass = false },
  322. { .name = "[no_xdev] cross through magic-link to self/cwd",
  323. .dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV,
  324. .out.err = -EXDEV, .pass = false },
  325. /* Except magic-link jumps inside the same vfsmount. */
  326. { .name = "[no_xdev] jump through magic-link to same procfs",
  327. .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
  328. .out.path = "/proc", .pass = true, },
  329. /** RESOLVE_NO_MAGICLINKS **/
  330. /* Regular symlinks should work. */
  331. { .name = "[no_magiclinks] ordinary relative symlink",
  332. .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
  333. .out.path = "etc/passwd", .pass = true },
  334. /* Magic-links should not work. */
  335. { .name = "[no_magiclinks] symlink to magic-link",
  336. .path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS,
  337. .out.err = -ELOOP, .pass = false },
  338. { .name = "[no_magiclinks] normal path to magic-link",
  339. .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
  340. .out.err = -ELOOP, .pass = false },
  341. { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
  342. .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
  343. .how.resolve = RESOLVE_NO_MAGICLINKS,
  344. .out.path = procselfexe, .pass = true },
  345. { .name = "[no_magiclinks] symlink to magic-link path component",
  346. .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
  347. .out.err = -ELOOP, .pass = false },
  348. { .name = "[no_magiclinks] magic-link path component",
  349. .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
  350. .out.err = -ELOOP, .pass = false },
  351. { .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
  352. .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
  353. .how.resolve = RESOLVE_NO_MAGICLINKS,
  354. .out.err = -ELOOP, .pass = false },
  355. /** RESOLVE_NO_SYMLINKS **/
  356. /* Normal paths should work. */
  357. { .name = "[no_symlinks] ordinary path to '.'",
  358. .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
  359. .out.path = NULL, .pass = true },
  360. { .name = "[no_symlinks] ordinary path to 'root'",
  361. .path = "root", .how.resolve = RESOLVE_NO_SYMLINKS,
  362. .out.path = "root", .pass = true },
  363. { .name = "[no_symlinks] ordinary path to 'etc'",
  364. .path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS,
  365. .out.path = "etc", .pass = true },
  366. { .name = "[no_symlinks] ordinary path to 'etc/passwd'",
  367. .path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
  368. .out.path = "etc/passwd", .pass = true },
  369. /* Regular symlinks are blocked. */
  370. { .name = "[no_symlinks] relative symlink target",
  371. .path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS,
  372. .out.err = -ELOOP, .pass = false },
  373. { .name = "[no_symlinks] relative symlink component",
  374. .path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
  375. .out.err = -ELOOP, .pass = false },
  376. { .name = "[no_symlinks] absolute symlink target",
  377. .path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS,
  378. .out.err = -ELOOP, .pass = false },
  379. { .name = "[no_symlinks] absolute symlink component",
  380. .path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
  381. .out.err = -ELOOP, .pass = false },
  382. { .name = "[no_symlinks] cheeky garbage link",
  383. .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
  384. .out.err = -ELOOP, .pass = false },
  385. { .name = "[no_symlinks] cheeky absolute + garbage link",
  386. .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
  387. .out.err = -ELOOP, .pass = false },
  388. { .name = "[no_symlinks] cheeky absolute + absolute symlink",
  389. .path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS,
  390. .out.err = -ELOOP, .pass = false },
  391. /* Trailing symlinks with NO_FOLLOW. */
  392. { .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
  393. .path = "relsym", .how.flags = O_NOFOLLOW,
  394. .how.resolve = RESOLVE_NO_SYMLINKS,
  395. .out.path = "relsym", .pass = true },
  396. { .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
  397. .path = "abssym", .how.flags = O_NOFOLLOW,
  398. .how.resolve = RESOLVE_NO_SYMLINKS,
  399. .out.path = "abssym", .pass = true },
  400. { .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
  401. .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
  402. .how.resolve = RESOLVE_NO_SYMLINKS,
  403. .out.path = "cheeky/garbagelink", .pass = true },
  404. { .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
  405. .path = "abscheeky/absself", .how.flags = O_NOFOLLOW,
  406. .how.resolve = RESOLVE_NO_SYMLINKS,
  407. .out.err = -ELOOP, .pass = false },
  408. { .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
  409. .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
  410. .how.resolve = RESOLVE_NO_SYMLINKS,
  411. .out.err = -ELOOP, .pass = false },
  412. };
  413. BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
  414. for (int i = 0; i < ARRAY_LEN(tests); i++) {
  415. int dfd, fd;
  416. char *fdpath = NULL;
  417. bool failed;
  418. void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
  419. struct basic_test *test = &tests[i];
  420. if (!openat2_supported) {
  421. ksft_print_msg("openat2(2) unsupported\n");
  422. resultfn = ksft_test_result_skip;
  423. goto skip;
  424. }
  425. /* Auto-set O_PATH. */
  426. if (!(test->how.flags & O_CREAT))
  427. test->how.flags |= O_PATH;
  428. if (test->dir)
  429. dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
  430. else
  431. dfd = dup(rootfd);
  432. E_assert(dfd, "failed to openat root '%s': %m", test->dir);
  433. E_dup2(dfd, hardcoded_fd);
  434. fd = sys_openat2(dfd, test->path, &test->how);
  435. if (test->pass)
  436. failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
  437. else
  438. failed = (fd != test->out.err);
  439. if (fd >= 0) {
  440. fdpath = fdreadlink(fd);
  441. close(fd);
  442. }
  443. close(dfd);
  444. if (failed) {
  445. resultfn = ksft_test_result_fail;
  446. ksft_print_msg("openat2 unexpectedly returned ");
  447. if (fdpath)
  448. ksft_print_msg("%d['%s']\n", fd, fdpath);
  449. else
  450. ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
  451. }
  452. skip:
  453. if (test->pass)
  454. resultfn("%s gives path '%s'\n", test->name,
  455. test->out.path ?: ".");
  456. else
  457. resultfn("%s fails with %d (%s)\n", test->name,
  458. test->out.err, strerror(-test->out.err));
  459. fflush(stdout);
  460. free(fdpath);
  461. }
  462. free(procselfexe);
  463. close(rootfd);
  464. free(hardcoded_fdpath);
  465. close(hardcoded_fd);
  466. }
  467. #define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
  468. int main(int argc, char **argv)
  469. {
  470. ksft_print_header();
  471. ksft_set_plan(NUM_TESTS);
  472. /* NOTE: We should be checking for CAP_SYS_ADMIN here... */
  473. if (geteuid() != 0)
  474. ksft_exit_skip("all tests require euid == 0\n");
  475. test_openat2_opath_tests();
  476. if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
  477. ksft_exit_fail();
  478. else
  479. ksft_exit_pass();
  480. }