All of lore.kernel.org
 help / color / mirror / Atom feed
From: Mark Hatle <mark.hatle@amd.com>
To: <yocto-patches@lists.yoctoproject.org>
Subject: [pseudo][PATCH RFC 1/2] ports/linux: Pass an empty openat() path to the kernel
Date: Thu, 12 Nov 2026 11:47:09 -0700	[thread overview]
Message-ID: <20261112184710.549482-2-mark.hatle@amd.com> (raw)
In-Reply-To: <20261112184710.549482-1-mark.hatle@amd.com>

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 --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"
-- 
2.34.1


  reply	other threads:[~2026-11-12 22:54 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12 18:47 [pseudo][PATCH RFC 0/2] Implement new openat2 flags Mark Hatle
2026-11-12 18:47 ` Mark Hatle [this message]
2026-11-12 18:47 ` [pseudo][PATCH RFC 2/2] openat2: Look at dirfd for an empty path Mark Hatle

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261112184710.549482-2-mark.hatle@amd.com \
    --to=mark.hatle@amd.com \
    --cc=yocto-patches@lists.yoctoproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.