* [pseudo][PATCH RFC 1/2] ports/linux: Pass an empty openat() path to the kernel
2026-11-12 18:47 [pseudo][PATCH RFC 0/2] Implement new openat2 flags Mark Hatle
@ 2026-11-12 18:47 ` Mark Hatle
2026-11-12 18:47 ` [pseudo][PATCH RFC 2/2] openat2: Look at dirfd for an empty path Mark Hatle
1 sibling, 0 replies; 3+ messages in thread
From: Mark Hatle @ 2026-11-12 18:47 UTC (permalink / raw)
To: yocto-patches
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
^ permalink raw reply related [flat|nested] 3+ messages in thread* [pseudo][PATCH RFC 2/2] openat2: Look at dirfd for an empty path
2026-11-12 18:47 [pseudo][PATCH RFC 0/2] Implement new openat2 flags Mark Hatle
2026-11-12 18:47 ` [pseudo][PATCH RFC 1/2] ports/linux: Pass an empty openat() path to the kernel Mark Hatle
@ 2026-11-12 18:47 ` Mark Hatle
1 sibling, 0 replies; 3+ messages in thread
From: Mark Hatle @ 2026-11-12 18:47 UTC (permalink / raw)
To: yocto-patches
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 --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;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread