diff mbox series

[pseudo,RFC,1/2] ports/linux: Pass an empty openat() path to the kernel

Message ID 20261112184710.549482-2-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
openat() wrappers resolved an empty path to dirfd's recorded path, so
openat(dirfd, "", ...) opened dirfd's path instead of failing with
ENOENT.

Linux 7.2 adds O_EMPTYPATH, with which an empty path reopens
dirfd itself, e.g. to turn an O_PATH descriptor into a usable one;
resolving it to a path opens the wrong file if dirfd's file was renamed
or removed.

Add a makewrappers option, keep_empty_path, to leave an empty path
unchanged, and use it for the openat() wrappers so the kernel decides
what an empty path means.  The guts look at dirfd with AT_EMPTY_PATH
and record dirfd's path for the new descriptor.

[YOCTO #16327]

Reported-by: Paul Barker <paul@pbarker.dev>
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
 makewrappers                  | 13 ++++--
 ports/linux/guts/openat.c     | 21 +++++++---
 ports/linux/wrapfuncs.in      |  8 ++--
 test/test-openat-emptypath.c  | 79 +++++++++++++++++++++++++++++++++++
 test/test-openat-emptypath.sh | 12 ++++++
 5 files changed, 121 insertions(+), 12 deletions(-)
 create mode 100644 test/test-openat-emptypath.c
 create mode 100755 test/test-openat-emptypath.sh
diff mbox series

Patch

diff --git a/makewrappers b/makewrappers
index 303ae32..84dc563 100755
--- a/makewrappers
+++ b/makewrappers
@@ -262,6 +262,10 @@  class Function:
         # In those cases, use a suffix ('_int') for the path variable names instead of
         # overwriting them. This means the wrapper needs to handle paths itself.
         self.preserve_paths = False
+        # Leave an empty path unchanged instead of resolving it to dirfd's
+        # path, for functions where only a flag (e.g. O_EMPTYPATH) makes
+        # an empty path name dirfd.
+        self.keep_empty_path = False
     
         function, comments = line.split(';')
         comment = re.search(r'/\* *(.*) *\*/', comments)
@@ -427,9 +431,12 @@  class Function:
             if self.preserve_paths and self.noignore_path:
                 # path_int is only read by the ignore_paths() check
                 continue
-            fix_paths.append(
-                "%s = pseudo_root_path(__func__, __LINE__, %s%s, %s, %s);" %
-                (pathvar, prefix, self.dirfd, path, self.flags))
+            munge = "%s = pseudo_root_path(__func__, __LINE__, %s%s, %s, %s);" % \
+                (pathvar, prefix, self.dirfd, path, self.flags)
+            if self.keep_empty_path:
+                # see above for the asm()
+                munge = "asm(\"\" : \"+r\"(%s));\n\t\tif (!%s || *%s)\n\t\t\t%s" % (path, path, path, munge)
+            fix_paths.append(munge)
         return "\n\t\t".join(fix_paths)
 
     def ignore_paths(self):
diff --git a/ports/linux/guts/openat.c b/ports/linux/guts/openat.c
index 9d32283..e0075fe 100644
--- a/ports/linux/guts/openat.c
+++ b/ports/linux/guts/openat.c
@@ -13,6 +13,17 @@ 
 	int existed = 1;
 	int save_errno;
 	sigset_t local_saved_sigmask;
+	const char *record_path = path;
+	int stat_flags = (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0;
+
+	/* An empty path is left for the kernel, which only accepts it with
+	 * O_EMPTYPATH, to reopen dirfd.  Look at and record dirfd instead.
+	 */
+	if (path && !*path) {
+		stat_flags |= AT_EMPTY_PATH;
+		record_path = pseudo_root_path(__func__, __LINE__, dirfd,
+			dirfd == AT_FDCWD ? "." : "", 0);
+	}
 
 	/* mask out mode bits appropriately */
 	mode = mode & ~pseudo_umask;
@@ -47,7 +58,7 @@ 
 	/* note "else" in #ifdef O_TMPFILE above */
 	if (flags & O_CREAT) {
 		save_errno = errno;
-		rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		rc = base_fstatat(dirfd, path, &buf, stat_flags);
 		existed = (rc != -1);
 		if (!existed)
 			pseudo_debug(PDBGF_FILE, "openat_creat: %s -> 0%o\n", path, mode);
@@ -60,7 +71,7 @@ 
 	 */
 	if (!(flags & O_NONBLOCK) && ((flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
 		save_errno = errno;
-		rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		rc = base_fstatat(dirfd, path, &buf, stat_flags);
 		if (rc != -1 && S_ISFIFO(buf.st_mode)) {
 			overly_magic_nonblocking = 1;
 		}
@@ -105,7 +116,7 @@ 
 			return rc;
 		}
 #endif
-		stat_rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+		stat_rc = base_fstatat(dirfd, path, &buf, stat_flags);
 
 		pseudo_debug(PDBGF_FILE, "openat(path %s), flags %o, stat rc %d, stat mode %o\n",
 			path, flags, stat_rc, buf.st_mode);
@@ -122,11 +133,11 @@ 
 				}
 				pseudo_client_path_unlock(path_lock_fd);
 			}
-				pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(flags), rc, dirfd, path, &buf);
+				pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(flags), rc, dirfd, record_path, &buf);
 		} else {
 			pseudo_debug(PDBGF_FILE, "openat (fd %d, path %d/%s, flags %d) succeeded, but stat failed (%s).\n",
 				rc, dirfd, path, flags, strerror(errno));
-			pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(flags), rc, dirfd, path, 0);
+			pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(flags), rc, dirfd, record_path, 0);
 		}
 		errno = save_errno;
 	}
diff --git a/ports/linux/wrapfuncs.in b/ports/linux/wrapfuncs.in
index 1874b60..dbbf2f7 100644
--- a/ports/linux/wrapfuncs.in
+++ b/ports/linux/wrapfuncs.in
@@ -8,8 +8,8 @@  int __fxstat(int ver, int fd, struct stat *buf);
 int lchmod(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
 int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
 int __fxstatat(int ver, int dirfd, const char *path, struct stat *buf, int flags); /* path_lock=shared */
-int openat(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
-int __openat_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
+int openat(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1, keep_empty_path=1 */
+int __openat_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1, keep_empty_path=1 */
 int open_tree(int dirfd, const char *path, unsigned int flags); /* preserve_paths=1, noignore_path=1 */
 int open_tree_attr(int dirfd, const char *path, unsigned int flags, struct mount_attr *attr, size_t size); /* preserve_paths=1, noignore_path=1*/
 int name_to_handle_at(int dirfd, const char *name, struct file_handle *handle, int *mount_id, int flags);
@@ -24,8 +24,8 @@  int fcntl64(int fd, int cmd, ...{struct flock *lock});  /* noignore_path=1 */
 char *canonicalize_file_name(const char *filename);
 int eaccess(const char *path, int mode);
 int open64(const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
-int openat64(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
-int __openat64_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
+int openat64(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1, keep_empty_path=1 */
+int __openat64_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1, keep_empty_path=1 */
 int creat64(const char *path, mode_t mode);
 int stat(const char *path, struct stat *buf); /* real_func=pseudo_stat, path_lock=shared */
 int lstat(const char *path, struct stat *buf); /* real_func=pseudo_lstat, flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
diff --git a/test/test-openat-emptypath.c b/test/test-openat-emptypath.c
new file mode 100644
index 0000000..076f1fd
--- /dev/null
+++ b/test/test-openat-emptypath.c
@@ -0,0 +1,79 @@ 
+/*
+ * Test opening an empty path, 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
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifndef O_EMPTYPATH
+#define O_EMPTYPATH (1 << 26)
+#endif
+
+static int failed;
+
+#define FAIL(...) do { printf(__VA_ARGS__); failed = 1; } while (0)
+
+/* fd should be dir, with pseudo knowing its path */
+static void
+check_reopened(const char *how, int fd) {
+	struct stat st;
+
+	if (fd == -1) {
+		if (errno == ENOENT || errno == EINVAL)
+			printf("%s: O_EMPTYPATH not supported by the kernel, skipping\n", how);
+		else
+			FAIL("%s: %s\n", how, strerror(errno));
+		return;
+	}
+	if (fstatat(fd, "file", &st, 0) == -1)
+		FAIL("%s: fstatat: %s\n", how, strerror(errno));
+	else if (st.st_uid != 123 || st.st_gid != 456)
+		FAIL("%s: file owned by %d:%d, expected 123:456\n",
+			how, (int) st.st_uid, (int) st.st_gid);
+	close(fd);
+}
+
+int
+main(int argc, char *argv[]) {
+	char path[4096];
+	int dfd, pfd, fd;
+
+	if (argc != 2) {
+		fprintf(stderr, "usage: %s <dir>\n", argv[0]);
+		return 2;
+	}
+	snprintf(path, sizeof(path), "%s/file", argv[1]);
+	if (chown(path, 123, 456) == -1) {
+		perror("chown");
+		return 1;
+	}
+	dfd = open(argv[1], O_RDONLY | O_DIRECTORY);
+	pfd = open(argv[1], O_PATH | O_DIRECTORY);
+	if (dfd == -1 || pfd == -1) {
+		perror("open");
+		return 1;
+	}
+
+	fd = openat(dfd, "", O_RDONLY);
+	if (fd != -1 || errno != ENOENT)
+		FAIL("openat 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));
+	if (fchdir(dfd) == -1) {
+		perror("fchdir");
+		return 1;
+	}
+	check_reopened("openat AT_FDCWD", openat(AT_FDCWD, "", O_RDONLY | O_DIRECTORY | O_EMPTYPATH));
+
+	return failed;
+}
diff --git a/test/test-openat-emptypath.sh b/test/test-openat-emptypath.sh
new file mode 100755
index 0000000..ead5191
--- /dev/null
+++ b/test/test-openat-emptypath.sh
@@ -0,0 +1,12 @@ 
+#!/bin/sh
+#
+# Test opening an empty path, with and without O_EMPTYPATH.
+# SPDX-License-Identifier: LGPL-2.1-only
+
+dir=$(pwd)/openat_emptypath_test
+trap 'rm -rf "$dir"' 0
+rm -rf "$dir"
+mkdir -p "$dir"
+touch "$dir/file"
+
+./test/test-openat-emptypath "$dir"