diff mbox series

[pseudo,RFC,2/2] openat2: Look at dirfd for an empty path

Message ID 20261112184710.549482-3-mark.hatle@amd.com
State New
Headers show
Series Implement new openat2 flags | expand

Commit Message

Mark Hatle Nov. 12, 2026, 6:47 p.m. UTC
openat2() passes an empty path to the kernel, which with Linux 7.2's
O_EMPTYPATH reopens dirfd, but pseudo then looked at and recorded
dirfd's recorded path.  With AT_FDCWD that path is empty, so *at() calls
relative to the new descriptor failed, and for a descriptor on a symlink
it described the target instead.

Look at dirfd itself with AT_EMPTY_PATH, as openat() now does, and record
dirfd's path, or the working directory's for AT_FDCWD.

[YOCTO #16327]

Reported-by: Paul Barker <paul@pbarker.dev>
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
 ports/linux/openat2/guts/openat2.c | 24 +++++++++++++++++++-----
 test/test-openat-emptypath.c       | 30 ++++++++++++++++++++++++++++--
 2 files changed, 47 insertions(+), 7 deletions(-)
diff mbox series

Patch

diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c
index bb4a125..30a51ab 100644
--- a/ports/linux/openat2/guts/openat2.c
+++ b/ports/linux/openat2/guts/openat2.c
@@ -15,6 +15,8 @@ 
 	sigset_t local_saved_sigmask;
 	struct open_how my_how;
 	char *pseudo_path;
+	const char *stat_path, *record_path;
+	int stat_flags;
 
 	/* Validate parameters */
 	if (!how || size < sizeof(struct open_how)) {
@@ -41,6 +43,18 @@ 
 
 	memcpy(&my_how, how, size);
 
+	stat_path = record_path = pseudo_path;
+	stat_flags = (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0;
+	/* An empty path is passed to the kernel, which only accepts it with
+	 * O_EMPTYPATH, to reopen dirfd.  Look at and record dirfd instead.
+	 */
+	if (path && !*path) {
+		stat_path = "";
+		stat_flags |= AT_EMPTY_PATH;
+		record_path = pseudo_root_path(__func__, __LINE__, dirfd,
+			dirfd == AT_FDCWD ? "." : "", 0);
+	}
+
 	/* mask out mode bits appropriately */
 	my_how.mode = my_how.mode & ~pseudo_umask;
 
@@ -81,7 +95,7 @@ 
 	/* note "else" in #ifdef O_TMPFILE above */
 	if (my_how.flags & O_CREAT) {
 		save_errno = errno;
-		rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		rc = base_fstatat(dirfd, stat_path, &buf, stat_flags);
 		existed = (rc != -1);
 		if (!existed)
 			pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", pseudo_path, my_how.mode);
@@ -94,7 +108,7 @@ 
 	 */
 	if (!(my_how.flags & O_NONBLOCK) && ((my_how.flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
 		save_errno = errno;
-		rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		rc = base_fstatat(dirfd, stat_path, &buf, stat_flags);
 		if (rc != -1 && S_ISFIFO(buf.st_mode)) {
 			overly_magic_nonblocking = 1;
 		}
@@ -151,7 +165,7 @@ 
 			return rc;
 		}
 #endif
-		stat_rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		stat_rc = base_fstatat(dirfd, stat_path, &buf, stat_flags);
 
 		pseudo_debug(PDBGF_FILE, "openat2(path %s), flags %lld, stat rc %d, stat mode %o\n",
 			pseudo_path, my_how.flags, stat_rc, buf.st_mode);
@@ -168,11 +182,11 @@ 
 				}
 				pseudo_client_path_unlock(path_lock_fd);
 			}
-				pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, pseudo_path, &buf);
+				pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, record_path, &buf);
 		} else {
 			pseudo_debug(PDBGF_FILE, "openat2 (fd %d, path %d/%s, flags %lld) succeeded, but stat failed (%s).\n",
 				rc, dirfd, pseudo_path, my_how.flags, strerror(errno));
-			pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, pseudo_path, 0);
+			pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, record_path, 0);
 		}
 		errno = save_errno;
 	}
diff --git a/test/test-openat-emptypath.c b/test/test-openat-emptypath.c
index 076f1fd..7ebec12 100644
--- a/test/test-openat-emptypath.c
+++ b/test/test-openat-emptypath.c
@@ -1,6 +1,6 @@ 
 /*
- * Test opening an empty path, which only names dirfd with O_EMPTYPATH
- * (Linux 7.2), the way the kernel does.
+ * Test opening an empty path with openat() and openat2(), which only
+ * names dirfd with O_EMPTYPATH (Linux 7.2), the way the kernel does.
  * SPDX-License-Identifier: LGPL-2.1-only
  */
 #define _GNU_SOURCE
@@ -10,8 +10,19 @@ 
 #include <stdio.h>
 #include <string.h>
 #include <sys/stat.h>
+#include <sys/syscall.h>
 #include <unistd.h>
 
+#if __has_include (<linux/openat2.h>)
+# include <linux/openat2.h>
+#else
+struct open_how {
+	unsigned long long flags;
+	unsigned long long mode;
+	unsigned long long resolve;
+};
+#endif
+
 #ifndef O_EMPTYPATH
 #define O_EMPTYPATH (1 << 26)
 #endif
@@ -20,6 +31,13 @@  static int failed;
 
 #define FAIL(...) do { printf(__VA_ARGS__); failed = 1; } while (0)
 
+static int
+do_openat2(int dirfd, const char *path, unsigned long long flags) {
+	struct open_how how = { .flags = flags };
+
+	return syscall(SYS_openat2, dirfd, path, &how, sizeof(how));
+}
+
 /* fd should be dir, with pseudo knowing its path */
 static void
 check_reopened(const char *how, int fd) {
@@ -67,13 +85,21 @@  main(int argc, char *argv[]) {
 			fd, strerror(errno));
 	if (fd != -1)
 		close(fd);
+	fd = do_openat2(dfd, "", O_RDONLY);
+	if (fd != -1 || errno != ENOENT)
+		FAIL("openat2 without O_EMPTYPATH: expected ENOENT, got %d (%s)\n",
+			fd, strerror(errno));
+	if (fd != -1)
+		close(fd);
 
 	check_reopened("openat", openat(pfd, "", O_RDONLY | O_DIRECTORY | O_EMPTYPATH));
+	check_reopened("openat2", do_openat2(pfd, "", O_RDONLY | O_DIRECTORY | O_EMPTYPATH));
 	if (fchdir(dfd) == -1) {
 		perror("fchdir");
 		return 1;
 	}
 	check_reopened("openat AT_FDCWD", openat(AT_FDCWD, "", O_RDONLY | O_DIRECTORY | O_EMPTYPATH));
+	check_reopened("openat2 AT_FDCWD", do_openat2(AT_FDCWD, "", O_RDONLY | O_DIRECTORY | O_EMPTYPATH));
 
 	return failed;
 }