From patchwork Mon Apr 20 18:30:25 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Mark Hatle X-Patchwork-Id: 86507 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id D142BF5A8AD for ; Mon, 20 Apr 2026 18:30:40 +0000 (UTC) Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.205.1776709832953681953 for ; Mon, 20 Apr 2026 11:30:33 -0700 Authentication-Results: mx.groups.io; dkim=none (message not signed); spf=pass (domain: kernel.crashing.org, ip: 63.228.1.57, mailfrom: mark.hatle@kernel.crashing.org) Received: from kernel.crashing.org.net (70-99-78-136.nuveramail.net [70.99.78.136] (may be forged)) by gate.crashing.org (8.18.1/8.18.1/Debian-2) with ESMTP id 63KIUUPu3563308; Mon, 20 Apr 2026 13:30:30 -0500 From: Mark Hatle To: yocto-patches@lists.yoctoproject.org, richard.purdie@linuxfoundation.org Subject: [PATCH 1/5] openat2: Implement openat2 wrapper Date: Mon, 20 Apr 2026 13:30:25 -0500 Message-Id: <1776709829-2754-2-git-send-email-mark.hatle@kernel.crashing.org> X-Mailer: git-send-email 1.8.3.1 In-Reply-To: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> References: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 20 Apr 2026 18:30:40 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/3741 From: Mark Hatle This wrapper is based on ports/linux/guts/openat.c wrapper. The flag and mode semantics have been replaced with 'open_how'. The new resolve items should work, but are not processed by pseudo. The input value is simply passed to the openat2 syscall. Signed-off-by: Mark Hatle --- ports/linux/openat2/guts/openat2.c | 184 +++++++++++++++++++++++++++++++++++-- ports/linux/pseudo_wrappers.c | 3 + test/test-syscall.c | 9 +- 3 files changed, 182 insertions(+), 14 deletions(-) diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c index 366fb95..946a4e4 100644 --- a/ports/linux/openat2/guts/openat2.c +++ b/ports/linux/openat2/guts/openat2.c @@ -1,22 +1,186 @@ /* - * Copyright (c) 2026 Mark Hatle ; see - * guts/COPYRIGHT for information. + * Copyright (c) 2008-2010, 2013 Wind River Systems + * Copyright (c) 2026 Yocto Project + * see guts/COPYRIGHT for information. * * SPDX-License-Identifier: LGPL-2.1-only * * int openat2(int dirfd, const char *path, const struct open_how *how, size_t size) * int rc = -1; */ + struct stat64 buf; + int overly_magic_nonblocking = 0; + int existed = 1; + int save_errno; + sigset_t local_saved_sigmask; + struct open_how my_how; - (void) dirfd; - (void) path; - (void) how; - (void) size; - /* for now, let's try just failing out hard, and hope things retry with a - * different syscall. + /* Validate parameters */ + if (!how || size < sizeof(struct open_how)) { + errno = EINVAL; + return -1; + } + if (size > sizeof(struct open_how)) { + errno = E2BIG; + return -1; + } + + memcpy(&my_how, how, size); + + /* mask out mode bits appropriately */ + my_how.mode = my_how.mode & ~pseudo_umask; + +#if defined(PSEUDO_NO_REAL_AT_FUNCTIONS) || ! defined(SYS_openat2) + if (dirfd != AT_FDCWD) { + errno = ENOSYS; + return -1; + } +#endif + +#ifdef PSEUDO_FORCE_ASYNC + /* Yes, I'm aware that every Linux system I've seen has + * DSYNC and RSYNC being the same value as SYNC. + */ + + my_how.flags &= ~(O_SYNC +#ifdef O_DIRECT + | O_DIRECT +#endif +#ifdef O_DSYNC + | O_DSYNC +#endif +#ifdef O_RSYNC + | O_RSYNC +#endif + ); +#endif + +#ifdef O_TMPFILE + /* don't handle O_CREAT the same way if O_TMPFILE exists + * and is set. */ - errno = ENOSYS; - rc = -1; + if ((my_how.flags & O_TMPFILE) == O_TMPFILE) { + existed = 0; + } else +#endif + /* if a creation has been requested, check whether file exists */ + /* note "else" in #ifdef O_TMPFILE above */ + if (my_how.flags & O_CREAT) { + save_errno = errno; +#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS + if (my_how.flags & O_NOFOLLOW) { + rc = real___lxstat64(_STAT_VER, path, &buf); + } else { + rc = real___xstat64(_STAT_VER, path, &buf); + } +#else + rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); +#endif + existed = (rc != -1); + if (!existed) + pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", path, my_how.mode); + errno = save_errno; + } + + /* if a pipe is opened without O_NONBLOCK, for only reading or + * only writing, it can block forever. We need to do extra magic + * in that case... + */ + if (!(my_how.flags & O_NONBLOCK) && ((my_how.flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) { + save_errno = errno; +#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS + if (my_how.flags & O_NOFOLLOW) { + rc = real___lxstat64(_STAT_VER, path, &buf); + } else { + rc = real___xstat64(_STAT_VER, path, &buf); + } +#else + rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); +#endif + if (rc != -1 && S_ISFIFO(buf.st_mode)) { + overly_magic_nonblocking = 1; + } + } + + /* this is a horrible special case and i do not know whether it will work */ + if (overly_magic_nonblocking) { + pseudo_droplock(); + sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, &local_saved_sigmask); + } + /* because we are not actually root, secretly mask in 0600 to the + * underlying mode. The ", 0" is because the only time mode matters + * is if a file is going to be created, in which case it's + * not a directory. + */ +#if defined(PSEUDO_NO_REAL_AT_FUNCTIONS) || ! defined(SYS_openat2) + pseudo_debug(PDBGF_SYSCALL, "openat2, calling open.\n"); + rc = real_open(path, my_how.flags, PSEUDO_FS_MODE(my_how.mode, 0)); +#else + /* openat2 in glibc is still rare, so directly call the syscall if required */ + if (real_openat2) { + pseudo_debug(PDBGF_SYSCALL, "openat2, calling openat2.\n"); + rc = real_openat2(dirfd, path, how, size); + } else { + pseudo_debug(PDBGF_SYSCALL, "openat2, calling syscall.\n"); + rc = real_syscall(SYS_openat2, dirfd, path, how, size); + } +#endif + if (overly_magic_nonblocking) { + save_errno = errno; + sigprocmask(SIG_SETMASK, &local_saved_sigmask, NULL); + /* well this is a problem. we can't NOT proceed; we may have + * already opened the file! we can't even return up the call + * stack to stuff that's going to try to drop the lock. + */ + if (pseudo_getlock()) { + pseudo_diag("PANIC: after opening a readonly/writeonly FIFO (path '%s', fd %d, errno %d, saved errno %d), could not regain lock. unrecoverable. sorry. bye.\n", + path, rc, errno, save_errno); + abort(); + } + errno = save_errno; + } + + if (rc != -1) { + save_errno = errno; + int stat_rc; +#ifdef O_TMPFILE + /* in O_TMPFILE case, nothing gets put in the + * database, because there's no directory entries for + * the file yet. + */ + if ((my_how.flags & O_TMPFILE) == O_TMPFILE) { + real_fchmod(rc, PSEUDO_FS_MODE(my_how.mode, 0)); + errno = save_errno; + return rc; + } +#endif +#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS + if (my_how.flags & O_NOFOLLOW) { + stat_rc = real___lxstat64(_STAT_VER, path, &buf); + } else { + stat_rc = real___xstat64(_STAT_VER, path, &buf); + } +#else + stat_rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); +#endif + + pseudo_debug(PDBGF_FILE, "openat2(path %s), flags %lld, stat rc %d, stat mode %o\n", + path, my_how.flags, stat_rc, buf.st_mode); + if (stat_rc != -1) { + buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, my_how.mode); + if (!existed) { + real_fchmod(rc, PSEUDO_FS_MODE(my_how.mode, 0)); + // file has no path, but has been created + pseudo_client_op(OP_CREAT, 0, -1, dirfd, path, &buf); + } + pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, path, &buf); + } else { + pseudo_debug(PDBGF_FILE, "openat2 (fd %d, path %d/%s, flags %lld) succeeded, but stat failed (%s).\n", + rc, dirfd, path, my_how.flags, strerror(errno)); + pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, path, 0); + } + errno = save_errno; + } /* return rc; * } diff --git a/ports/linux/pseudo_wrappers.c b/ports/linux/pseudo_wrappers.c index 300b5a5..b920cb2 100644 --- a/ports/linux/pseudo_wrappers.c +++ b/ports/linux/pseudo_wrappers.c @@ -69,6 +69,7 @@ syscall(long number, ...) { /* pseudo and seccomp are incompatible as pseudo uses different syscalls * so pretend to enable seccomp but really do nothing */ if (number == SYS_seccomp) { + pseudo_debug(PDBGF_SYSCALL, "syscall, faking seccomp.\n"); unsigned long cmd; va_start(ap, number); cmd = va_arg(ap, unsigned long); @@ -88,6 +89,7 @@ syscall(long number, ...) { * uses syscall to access openat2() and breaks builds if we don't redirect. */ if (number == SYS_openat2) { + pseudo_debug(PDBGF_SYSCALL, "syscall, faking openat2.\n"); va_start(ap, number); int dirfd = va_arg(ap, int); const char * path = va_arg(ap, const char *); @@ -103,6 +105,7 @@ syscall(long number, ...) { #ifdef SYS_renameat2 /* Call out wrapper, expanding the variable arguments first */ if (number == SYS_renameat2) { + pseudo_debug(PDBGF_SYSCALL, "syscall, faking renameat2.\n"); va_start(ap, number); int olddirfd = va_arg(ap, int); const char * oldpath = va_arg(ap, const char *); diff --git a/test/test-syscall.c b/test/test-syscall.c index 9031766..170fd31 100644 --- a/test/test-syscall.c +++ b/test/test-syscall.c @@ -72,12 +72,13 @@ int main() { printf("openat2: fail: function implemented: %s\n", strerror(errno)); rc++; } - else - printf("openat2: pass\n"); + else { + printf("openat2: fail: %s", strerror(errno)); + rc++; + } } else { - printf("openat2: fail: function implemented\n"); - rc++; + printf("openat2: pass\n"); } close(fd); From patchwork Mon Apr 20 18:30:26 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Mark Hatle X-Patchwork-Id: 86511 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 0CB70F5A8B1 for ; Mon, 20 Apr 2026 18:30:41 +0000 (UTC) Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.212.1776709833130689080 for ; Mon, 20 Apr 2026 11:30:33 -0700 Authentication-Results: mx.groups.io; dkim=none (message not signed); spf=pass (domain: kernel.crashing.org, ip: 63.228.1.57, mailfrom: mark.hatle@kernel.crashing.org) Received: from kernel.crashing.org.net (70-99-78-136.nuveramail.net [70.99.78.136] (may be forged)) by gate.crashing.org (8.18.1/8.18.1/Debian-2) with ESMTP id 63KIUUPv3563308; Mon, 20 Apr 2026 13:30:31 -0500 From: Mark Hatle To: yocto-patches@lists.yoctoproject.org, richard.purdie@linuxfoundation.org Subject: [PATCH 2/5] makewrappers/openat2: Add preserve_path option Date: Mon, 20 Apr 2026 13:30:26 -0500 Message-Id: <1776709829-2754-3-git-send-email-mark.hatle@kernel.crashing.org> X-Mailer: git-send-email 1.8.3.1 In-Reply-To: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> References: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 20 Apr 2026 18:30:41 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/3744 From: Richard Purdie openat2 needs the relative path preserved as a call argument, we can't pass in the absolute path pseudo converts to. The difference in behaviour is clear in this example: openat2(AT_FDCWD, "/tmp/testdir/dir1/dir2/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 EXDEV (Invalid cross-device link) vs. openat2(AT_FDCWD, "dir1/dir2/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = 4 Add a makewrappers option to preserve paths to the underlying function call and use it for openat2. This code attempts to handle a chroot for absolute paths but if the path is relative, chroot behaviour would be tricky. Signed-off-by: Richard Purdie --- makewrappers | 18 ++++++++++++++- ports/linux/openat2/guts/openat2.c | 46 +++++++++++++++++++++++++------------- ports/linux/openat2/wrapfuncs.in | 2 +- templates/wrapfuncs.c | 1 + 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/makewrappers b/makewrappers index 326f70e..df405fc 100755 --- a/makewrappers +++ b/makewrappers @@ -248,6 +248,11 @@ class Function: self.date = datetime.date.today().year # Used to define pointers that should EFAULT if null self.efault = None + + # Used for functions which need to called with the original unconverted paths, e.g. openat2 + # 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 function, comments = line.split(';') comment = re.search(r'/\* *(.*) *\*/', comments) @@ -383,6 +388,11 @@ class Function: """present argument list for a function call""" return self.args.call() + def paths_decl(self): + if self.preserve_paths: + return "\n\t".join("char *" + path + "_int;" for path in self.paths_to_munge) + return "" + def fix_paths(self): """create/allocate canonical paths""" fix_paths = [] @@ -390,6 +400,9 @@ class Function: prefix = path[:-4] if prefix not in self.specific_dirfds: prefix = '' + pathvar = path + if self.preserve_paths: + pathvar = path + "_int" if self.dirfd != "AT_FDCWD" and "flags" in self.flags \ and "AT_SYMLINK_NOFOLLOW" in self.flags: fix_paths.append( @@ -409,7 +422,7 @@ class Function: "\t\t}\n" % (path, self.rc_assign())) fix_paths.append( "%s = pseudo_root_path(__func__, __LINE__, %s%s, %s, %s);" % - (path, prefix, self.dirfd, path, self.flags)) + (pathvar, prefix, self.dirfd, path, self.flags)) return "\n\t\t".join(fix_paths) def ignore_paths(self): @@ -424,6 +437,9 @@ class Function: elif "path" in self.paths_to_munge: mainpath = "path" + if mainpath and self.preserve_paths: + mainpath += "_int" + if mainpath: return "pseudo_client_ignore_path(%s)" % mainpath if self.fd_arg: diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c index 946a4e4..a0acbe7 100644 --- a/ports/linux/openat2/guts/openat2.c +++ b/ports/linux/openat2/guts/openat2.c @@ -14,6 +14,7 @@ int save_errno; sigset_t local_saved_sigmask; struct open_how my_how; + char *pseudo_path; /* Validate parameters */ if (!how || size < sizeof(struct open_how)) { @@ -25,6 +26,19 @@ return -1; } + /* This wrapper is not called with modified paths, therefore we have to handle this ourselves. + We can't use the standard mappings since the function needs relative paths to be preserved, + they can't be our usual absolute paths. An example difference: + openat2(AT_FDCWD, "/tmp/testdir/dir1/dir2/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = -1 EXDEV (Invalid cross-device link) + vs. + openat2(AT_FDCWD, "dir1/dir2/", {flags=O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_DIRECTORY, resolve=RESOLVE_BENEATH}, 24) = 4 + FIXME - we don't handle chroot for relative paths + */ + pseudo_path = pseudo_root_path(__func__, __LINE__, dirfd, path, 0); + if (path && path[0] == '/') + /* If the path is absolute, we should account for a potential chroot */ + path = pseudo_path; + memcpy(&my_how, how, size); /* mask out mode bits appropriately */ @@ -69,16 +83,16 @@ save_errno = errno; #ifdef PSEUDO_NO_REAL_AT_FUNCTIONS if (my_how.flags & O_NOFOLLOW) { - rc = real___lxstat64(_STAT_VER, path, &buf); + rc = real___lxstat64(_STAT_VER, pseudo_path, &buf); } else { - rc = real___xstat64(_STAT_VER, path, &buf); + rc = real___xstat64(_STAT_VER, pseudo_path, &buf); } #else - rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); + rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); #endif existed = (rc != -1); if (!existed) - pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", path, my_how.mode); + pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", pseudo_path, my_how.mode); errno = save_errno; } @@ -90,12 +104,12 @@ save_errno = errno; #ifdef PSEUDO_NO_REAL_AT_FUNCTIONS if (my_how.flags & O_NOFOLLOW) { - rc = real___lxstat64(_STAT_VER, path, &buf); + rc = real___lxstat64(_STAT_VER, pseudo_path, &buf); } else { - rc = real___xstat64(_STAT_VER, path, &buf); + rc = real___xstat64(_STAT_VER, pseudo_path, &buf); } #else - rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); + rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); #endif if (rc != -1 && S_ISFIFO(buf.st_mode)) { overly_magic_nonblocking = 1; @@ -134,7 +148,7 @@ */ if (pseudo_getlock()) { pseudo_diag("PANIC: after opening a readonly/writeonly FIFO (path '%s', fd %d, errno %d, saved errno %d), could not regain lock. unrecoverable. sorry. bye.\n", - path, rc, errno, save_errno); + pseudo_path, rc, errno, save_errno); abort(); } errno = save_errno; @@ -156,28 +170,28 @@ #endif #ifdef PSEUDO_NO_REAL_AT_FUNCTIONS if (my_how.flags & O_NOFOLLOW) { - stat_rc = real___lxstat64(_STAT_VER, path, &buf); + stat_rc = real___lxstat64(_STAT_VER, pseudo_path, &buf); } else { - stat_rc = real___xstat64(_STAT_VER, path, &buf); + stat_rc = real___xstat64(_STAT_VER, pseudo_path, &buf); } #else - stat_rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); + stat_rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0); #endif pseudo_debug(PDBGF_FILE, "openat2(path %s), flags %lld, stat rc %d, stat mode %o\n", - path, my_how.flags, stat_rc, buf.st_mode); + pseudo_path, my_how.flags, stat_rc, buf.st_mode); if (stat_rc != -1) { buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, my_how.mode); if (!existed) { real_fchmod(rc, PSEUDO_FS_MODE(my_how.mode, 0)); // file has no path, but has been created - pseudo_client_op(OP_CREAT, 0, -1, dirfd, path, &buf); + pseudo_client_op(OP_CREAT, 0, -1, dirfd, pseudo_path, &buf); } - pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, path, &buf); + pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, pseudo_path, &buf); } else { pseudo_debug(PDBGF_FILE, "openat2 (fd %d, path %d/%s, flags %lld) succeeded, but stat failed (%s).\n", - rc, dirfd, path, my_how.flags, strerror(errno)); - pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, path, 0); + rc, dirfd, pseudo_path, my_how.flags, strerror(errno)); + pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, pseudo_path, 0); } errno = save_errno; } diff --git a/ports/linux/openat2/wrapfuncs.in b/ports/linux/openat2/wrapfuncs.in index 2f1e716..2995646 100644 --- a/ports/linux/openat2/wrapfuncs.in +++ b/ports/linux/openat2/wrapfuncs.in @@ -1 +1 @@ -int openat2(int dirfd, const char *path, const struct open_how *how, size_t size); +int openat2(int dirfd, const char *path, const struct open_how *how, size_t size); /* preserve_paths=1 */ diff --git a/templates/wrapfuncs.c b/templates/wrapfuncs.c index e925b6e..9385a40 100644 --- a/templates/wrapfuncs.c +++ b/templates/wrapfuncs.c @@ -22,6 +22,7 @@ ${name}(${decl_args}) { sigset_t saved; ${variadic_decl} ${rc_decl} + ${paths_decl} PROFILE_START; ${maybe_async_skip} From patchwork Mon Apr 20 18:30:27 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Mark Hatle X-Patchwork-Id: 86508 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id CAFE2F5A8AB for ; Mon, 20 Apr 2026 18:30:40 +0000 (UTC) Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.213.1776709833130889982 for ; Mon, 20 Apr 2026 11:30:33 -0700 Authentication-Results: mx.groups.io; dkim=none (message not signed); spf=pass (domain: kernel.crashing.org, ip: 63.228.1.57, mailfrom: mark.hatle@kernel.crashing.org) Received: from kernel.crashing.org.net (70-99-78-136.nuveramail.net [70.99.78.136] (may be forged)) by gate.crashing.org (8.18.1/8.18.1/Debian-2) with ESMTP id 63KIUUPw3563308; Mon, 20 Apr 2026 13:30:31 -0500 From: Mark Hatle To: yocto-patches@lists.yoctoproject.org, richard.purdie@linuxfoundation.org Subject: [PATCH 3/5] test: Add openat2 test cases Date: Mon, 20 Apr 2026 13:30:27 -0500 Message-Id: <1776709829-2754-4-git-send-email-mark.hatle@kernel.crashing.org> X-Mailer: git-send-email 1.8.3.1 In-Reply-To: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> References: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> MIME-Version: 1.0 X-MIME-Autoconverted: from 8bit to quoted-printable by gate.crashing.org id 63KIUUPw3563308 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 20 Apr 2026 18:30:40 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/3745 From: Mark Hatle Add two primary test cases, openat2 via syscall and openat2 via function call. Add two secondary tests calling each of these from a chroot environment. Since the function may or may not exist in different versions of glibc, this may return skipped if the glibc used for testing does not contain the function. Similarly if the running kernel does not define the openat2 syscall, the test may also be skipped. Otherwise the two test cases are virtually identical in nature, but were broken into two different .c files to make it easier to capture the difference between the syscall and function in the future. Note, we're not checking for the openat2 escaping the chroot. We expect it is able to escape the chroot via relative paths. AI-Generated: Test cases written using GitHub Copilot (Claude Sonnet 4.6) Signed-off-by: Mark Hatle --- test/test-openat2-func-chroot.sh | 18 +++ test/test-openat2-func.c | 274 +++++++++++++++++++++++++++++++++ test/test-openat2-func.sh | 18 +++ test/test-openat2-syscall-chroot.sh | 18 +++ test/test-openat2-syscall.c | 299 ++++++++++++++++++++++++++++++++++++ test/test-openat2-syscall.sh | 18 +++ 6 files changed, 645 insertions(+) create mode 100755 test/test-openat2-func-chroot.sh create mode 100644 test/test-openat2-func.c create mode 100755 test/test-openat2-func.sh create mode 100755 test/test-openat2-syscall-chroot.sh create mode 100644 test/test-openat2-syscall.c create mode 100755 test/test-openat2-syscall.sh diff --git a/test/test-openat2-func-chroot.sh b/test/test-openat2-func-chroot.sh new file mode 100755 index 0000000..b20b4ff --- /dev/null +++ b/test/test-openat2-func-chroot.sh @@ -0,0 +1,18 @@ +#! /bin/sh + +# rc 77 indicates an ENOSYS, so return back we're skipped +# This usually indicates that glibc does not have the openat2 function +run_test() { + $@ + rc=$? + if [ "$rc" -eq 77 ]; then + exit 255 + fi + return "$rc" +} + +# Run with and without ignored paths, matching test-openat coverage. +run_test chroot ./test ./test-openat2-func || exit $? +run_test env PSEUDO_IGNORE_PATHS=/ chroot ./test ./test-openat2-func || exit $? + +exit 0 diff --git a/test/test-openat2-func.c b/test/test-openat2-func.c new file mode 100644 index 0000000..00c3e12 --- /dev/null +++ b/test/test-openat2-func.c @@ -0,0 +1,274 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include () +# include +#else +/* Kernel too old for SYS_openat2 — define the necessary bits ourselves. */ + struct open_how { + unsigned long long flags; + unsigned long long mode; + unsigned long long resolve; + }; + #ifndef RESOLVE_NO_XDEV + #define RESOLVE_NO_XDEV 0x01 + #endif + #ifndef RESOLVE_NO_MAGICLINKS + #define RESOLVE_NO_MAGICLINKS 0x02 + #endif + #ifndef RESOLVE_NO_SYMLINKS + #define RESOLVE_NO_SYMLINKS 0x04 + #endif + #ifndef RESOLVE_BENEATH + #define RESOLVE_BENEATH 0x08 + #endif + #ifndef RESOLVE_IN_ROOT + #define RESOLVE_IN_ROOT 0x10 + #endif + #ifndef RESOLVE_CACHED + #define RESOLVE_CACHED 0x20 + #endif +#endif /* has linux/openat2.h */ + +#ifndef O_PATH +#define O_PATH 0 +#endif + +/* + * Pseudo intercepts openat2 via two separate code paths: + * + * 1) syscall(SYS_openat2, ...) — intercepted by wrap_syscall() in + * pseudo_wrappers.c, which extracts the varargs and calls + * wrap_openat2() directly. The path is NOT converted. + * + * 2) openat2(...) as a direct function call — intercepted by pseudo's + * openat2() symbol in libpseudo.so. This wrapper runs + * pseudo_root_path() to convert the path to an absolute form, + * then calls wrap_openat2(). + * + * The fix in f18abb3 (preserve_paths) only affects path #2: without it, + * the pseudo_root_path conversion turns a relative path into an absolute + * one, which then fails with EXDEV when RESOLVE_BENEATH is set. + * + * We test both paths to ensure neither regresses. + */ + +/* + * Declare openat2 as an extern — glibc doesn't provide it, but pseudo's + * libpseudo.so exports it. Calling this exercises the glibc-level + * interceptor path where pseudo_root_path() is applied to the path arg. + * When running without pseudo, this resolves to a weak alias for the + * syscall wrapper below. + */ +extern int openat2(int dirfd, const char *path, + const struct open_how *how, size_t size) __attribute__((weak)); + +/* Call openat2 as a function if available, fall back to syscall. */ +static int do_openat2_func(int dirfd, const char *path, + struct open_how *how, size_t size) { + if (openat2) + return openat2(dirfd, path, how, size); + errno = ENOSYS; + return -1; +} + +static int touch_file(const char *path) { + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd == -1) { + perror("open"); + return -1; + } + if (write(fd, "x", 1) != 1) { + perror("write"); + close(fd); + return -1; + } + if (close(fd) == -1) { + perror("close"); + return -1; + } + return 0; +} + +int main(void) { + char template[] = "test-openat2.XXXXXX"; + char subdir[PATH_MAX]; + char leafdir[PATH_MAX]; + char marker[PATH_MAX]; + char marker_relative[] = "../marker"; + char *base; + int dirfd = -1; + int fd = -1; + int rc = 1; + int saved_errno; + struct open_how how; + char cwd_save[PATH_MAX]; + int cwd_saved = 0; + + base = mkdtemp(template); + if (!base) { + perror("mkdtemp"); + return 1; + } + + if (snprintf(subdir, sizeof(subdir), "%s/sub", base) >= (int) sizeof(subdir)) { + fprintf(stderr, "path too long\n"); + goto out; + } + if (snprintf(leafdir, sizeof(leafdir), "%s/sub/leaf", base) >= (int) sizeof(leafdir)) { + fprintf(stderr, "path too long\n"); + goto out; + } + if (snprintf(marker, sizeof(marker), "%s/marker", base) >= (int) sizeof(marker)) { + fprintf(stderr, "path too long\n"); + goto out; + } + + if (mkdir(subdir, 0755) == -1) { + perror("mkdir sub"); + goto out; + } + if (mkdir(leafdir, 0755) == -1) { + perror("mkdir leaf"); + goto out; + } + if (touch_file(marker) == -1) { + goto out; + } + + dirfd = open(subdir, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) { + perror("open subdir"); + goto out; + } + + /* openat semantics: resolving ".." from a dirfd should escape to parent. */ + fd = openat(dirfd, marker_relative, O_RDONLY); + if (fd == -1) { + perror("openat ../marker"); + goto out; + } + close(fd); + fd = -1; + + memset(&how, 0, sizeof(how)); + how.flags = O_RDONLY; + + /* + * Verify fix from f18abb3: openat2 must preserve relative paths. + * + * Before the fix, pseudo's openat2() interceptor converted relative + * paths to absolute via pseudo_root_path(), so a call like: + * openat2(AT_FDCWD, "dir1/dir2/", + * {O_DIRECTORY, resolve=RESOLVE_BENEATH}) + * was turned into: + * openat2(AT_FDCWD, "/full/path/to/dir1/dir2/", + * {O_DIRECTORY, resolve=RESOLVE_BENEATH}) + * which failed with EXDEV because the absolute path escapes the + * directory tree rooted at AT_FDCWD. + * + * This bug only affects the direct openat2() function call path + * (pseudo's glibc-level interceptor), NOT the syscall(SYS_openat2) + * path (which goes through wrap_syscall and never converts paths). + * + * We test both paths to ensure neither regresses. + * + * Test: chdir into base, then open a relative sub-path with + * RESOLVE_BENEATH via AT_FDCWD. This must succeed via both paths. + */ + { + int beneath_fd; + char abspath[PATH_MAX]; + + if (!getcwd(cwd_save, sizeof(cwd_save))) { + perror("getcwd"); + goto out; + } + cwd_saved = 1; + + /* chdir into base so "sub/leaf" is a valid relative path */ + if (chdir(base) == -1) { + perror("chdir base"); + goto out; + } + + memset(&how, 0, sizeof(how)); + how.flags = O_RDONLY | O_DIRECTORY; + how.resolve = RESOLVE_BENEATH; + + + /* --- Test via openat2() direct function call (f18abb3 path) --- */ + + /* Positive: relative path + RESOLVE_BENEATH via direct call. + * This is the case that failed before f18abb3 because pseudo + * converted the relative path to absolute. + */ + beneath_fd = do_openat2_func(AT_FDCWD, "sub/leaf", &how, + sizeof(how)); + if (beneath_fd == -1) { + if (errno == ENOSYS) { + rc = 77; + goto out_restore_cwd; + } + fprintf(stderr, "openat2(): RESOLVE_BENEATH + relative " + "\"sub/leaf\" failed: %s (bug f18abb3)\n", + strerror(errno)); + goto out_restore_cwd; + } + close(beneath_fd); + + /* Negative: absolute path + RESOLVE_BENEATH via direct call */ + if (snprintf(abspath, sizeof(abspath), "%s/%s/sub/leaf", + cwd_save, base) < (int) sizeof(abspath)) { + beneath_fd = do_openat2_func(AT_FDCWD, abspath, + &how, sizeof(how)); + if (beneath_fd != -1) { + fprintf(stderr, "openat2(): RESOLVE_BENEATH + " + "absolute path should have failed\n"); + close(beneath_fd); + goto out_restore_cwd; + } + if (errno != EXDEV) { + fprintf(stderr, "openat2(): RESOLVE_BENEATH + " + "absolute: expected EXDEV, got %s\n", + strerror(errno)); + goto out_restore_cwd; + } + } + + if (chdir(cwd_save) == -1) { + perror("chdir restore"); + cwd_saved = 0; + goto out; + } + } + + rc = 0; + goto out; + +out_restore_cwd: + if (cwd_saved) + if (chdir(cwd_save) == -1) + perror("chdir restore (cleanup)"); + +out: + if (fd != -1) + close(fd); + if (dirfd != -1) + close(dirfd); + unlink(marker); + rmdir(leafdir); + rmdir(subdir); + rmdir(base); + + return rc; +} diff --git a/test/test-openat2-func.sh b/test/test-openat2-func.sh new file mode 100755 index 0000000..727b377 --- /dev/null +++ b/test/test-openat2-func.sh @@ -0,0 +1,18 @@ +#! /bin/sh + +# rc 77 indicates an ENOSYS, so return back we're skipped +# This usually indicates that glibc does not have the openat2 function +run_test() { + $@ + rc=$? + if [ "$rc" -eq 77 ]; then + exit 255 + fi + return "$rc" +} + +# Run with and without ignored paths, matching test-openat coverage. +run_test ./test/test-openat2-func || exit $? +run_test env PSEUDO_IGNORE_PATHS=/ ./test/test-openat2-func || exit $? + +exit 0 diff --git a/test/test-openat2-syscall-chroot.sh b/test/test-openat2-syscall-chroot.sh new file mode 100755 index 0000000..c3aef7a --- /dev/null +++ b/test/test-openat2-syscall-chroot.sh @@ -0,0 +1,18 @@ +#! /bin/sh + +# rc 77 indicates an ENOSYS, so return back we're skipped +# This shouldn't happen, but the user could disable openat2 syscall emulation +run_test() { + $@ + rc=$? + if [ "$rc" -eq 77 ]; then + exit 255 + fi + return "$rc" +} + +# Run with and without ignored paths, matching test-openat coverage. +run_test chroot ./test ./test-openat2-syscall || exit $? +run_test env PSEUDO_IGNORE_PATHS=/ chroot ./test ./test-openat2-syscall || exit $? + +exit 0 diff --git a/test/test-openat2-syscall.c b/test/test-openat2-syscall.c new file mode 100644 index 0000000..4dbb279 --- /dev/null +++ b/test/test-openat2-syscall.c @@ -0,0 +1,299 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include () +# include +#else +/* Kernel too old for SYS_openat2 — define the necessary bits ourselves. */ + struct open_how { + unsigned long long flags; + unsigned long long mode; + unsigned long long resolve; + }; + #ifndef RESOLVE_NO_XDEV + #define RESOLVE_NO_XDEV 0x01 + #endif + #ifndef RESOLVE_NO_MAGICLINKS + #define RESOLVE_NO_MAGICLINKS 0x02 + #endif + #ifndef RESOLVE_NO_SYMLINKS + #define RESOLVE_NO_SYMLINKS 0x04 + #endif + #ifndef RESOLVE_BENEATH + #define RESOLVE_BENEATH 0x08 + #endif + #ifndef RESOLVE_IN_ROOT + #define RESOLVE_IN_ROOT 0x10 + #endif + #ifndef RESOLVE_CACHED + #define RESOLVE_CACHED 0x20 + #endif +#endif /* has linux/openat2.h */ + +#ifndef O_PATH +#define O_PATH 0 +#endif + +/* + * Pseudo intercepts openat2 via two separate code paths: + * + * 1) syscall(SYS_openat2, ...) — intercepted by wrap_syscall() in + * pseudo_wrappers.c, which extracts the varargs and calls + * wrap_openat2() directly. The path is NOT converted. + * + * 2) openat2(...) as a direct function call — intercepted by pseudo's + * openat2() symbol in libpseudo.so. This wrapper runs + * pseudo_root_path() to convert the path to an absolute form, + * then calls wrap_openat2(). + * + * The fix in f18abb3 (preserve_paths) only affects path #2: without it, + * the pseudo_root_path conversion turns a relative path into an absolute + * one, which then fails with EXDEV when RESOLVE_BENEATH is set. + * + * We test both paths to ensure neither regresses. + */ + +/* Call openat2 via syscall() — exercises the wrap_syscall dispatch path. */ +static int do_openat2_syscall(int dirfd, const char *path, + struct open_how *how, size_t size) { +#ifdef SYS_openat2 + return syscall(SYS_openat2, dirfd, path, how, size); +#elif defined(__NR_openat2) + return syscall(__NR_openat2, dirfd, path, how, size); +#else + errno = ENOSYS; + return -1; +#endif +} + +static int touch_file(const char *path) { + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd == -1) { + perror("open"); + return -1; + } + if (write(fd, "x", 1) != 1) { + perror("write"); + close(fd); + return -1; + } + if (close(fd) == -1) { + perror("close"); + return -1; + } + return 0; +} + +int main(void) { + char template[] = "test-openat2.XXXXXX"; + char subdir[PATH_MAX]; + char leafdir[PATH_MAX]; + char marker[PATH_MAX]; + char marker_relative[] = "../marker"; + char *base; + int dirfd = -1; + int fd = -1; + int rc = 1; + int saved_errno; + struct open_how how; + char cwd_save[PATH_MAX]; + int cwd_saved = 0; + + base = mkdtemp(template); + if (!base) { + perror("mkdtemp"); + return 1; + } + + if (snprintf(subdir, sizeof(subdir), "%s/sub", base) >= (int) sizeof(subdir)) { + fprintf(stderr, "path too long\n"); + goto out; + } + if (snprintf(leafdir, sizeof(leafdir), "%s/sub/leaf", base) >= (int) sizeof(leafdir)) { + fprintf(stderr, "path too long\n"); + goto out; + } + if (snprintf(marker, sizeof(marker), "%s/marker", base) >= (int) sizeof(marker)) { + fprintf(stderr, "path too long\n"); + goto out; + } + + if (mkdir(subdir, 0755) == -1) { + perror("mkdir sub"); + goto out; + } + if (mkdir(leafdir, 0755) == -1) { + perror("mkdir leaf"); + goto out; + } + if (touch_file(marker) == -1) { + goto out; + } + + dirfd = open(subdir, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) { + perror("open subdir"); + goto out; + } + + /* openat semantics: resolving ".." from a dirfd should escape to parent. */ + fd = openat(dirfd, marker_relative, O_RDONLY); + if (fd == -1) { + perror("openat ../marker"); + goto out; + } + close(fd); + fd = -1; + + memset(&how, 0, sizeof(how)); + how.flags = O_RDONLY; + + /* openat2 via syscall() with no resolve flags should match openat-style lookup. */ + fd = do_openat2_syscall(dirfd, marker_relative, &how, sizeof(how)); + if (fd == -1) { + if (errno == ENOSYS) { + rc = 77; + goto out; + } + perror("openat2 ../marker without resolve"); + goto out; + } + close(fd); + fd = -1; + + /* openat2-specific semantics: RESOLVE_BENEATH must block parent escape. */ + how.resolve = RESOLVE_BENEATH; + fd = do_openat2_syscall(dirfd, marker_relative, &how, sizeof(how)); + if (fd != -1) { + fprintf(stderr, "openat2 RESOLVE_BENEATH unexpectedly allowed ../ escape\n"); + close(fd); + fd = -1; + goto out; + } + saved_errno = errno; + if (saved_errno != EXDEV) { + fprintf(stderr, "openat2 RESOLVE_BENEATH returned errno %d, expected %d\n", + saved_errno, EXDEV); + goto out; + } + + /* Positive RESOLVE_BENEATH case should still allow a path under dirfd. */ + how.flags = O_RDONLY | O_DIRECTORY; + fd = do_openat2_syscall(dirfd, "leaf", &how, sizeof(how)); + if (fd == -1) { + perror("openat2 RESOLVE_BENEATH leaf"); + goto out; + } + close(fd); + fd = -1; + + /* + * Verify fix from f18abb3: openat2 must preserve relative paths. + * + * Before the fix, pseudo's openat2() interceptor converted relative + * paths to absolute via pseudo_root_path(), so a call like: + * openat2(AT_FDCWD, "dir1/dir2/", + * {O_DIRECTORY, resolve=RESOLVE_BENEATH}) + * was turned into: + * openat2(AT_FDCWD, "/full/path/to/dir1/dir2/", + * {O_DIRECTORY, resolve=RESOLVE_BENEATH}) + * which failed with EXDEV because the absolute path escapes the + * directory tree rooted at AT_FDCWD. + * + * This bug only affects the direct openat2() function call path + * (pseudo's glibc-level interceptor), NOT the syscall(SYS_openat2) + * path (which goes through wrap_syscall and never converts paths). + * + * We test both paths to ensure neither regresses. + * + * Test: chdir into base, then open a relative sub-path with + * RESOLVE_BENEATH via AT_FDCWD. This must succeed via both paths. + */ + { + int beneath_fd; + char abspath[PATH_MAX]; + + if (!getcwd(cwd_save, sizeof(cwd_save))) { + perror("getcwd"); + goto out; + } + cwd_saved = 1; + + /* chdir into base so "sub/leaf" is a valid relative path */ + if (chdir(base) == -1) { + perror("chdir base"); + goto out; + } + + memset(&how, 0, sizeof(how)); + how.flags = O_RDONLY | O_DIRECTORY; + how.resolve = RESOLVE_BENEATH; + + /* --- Test via syscall(SYS_openat2) path --- */ + + /* Positive: relative path + RESOLVE_BENEATH via syscall */ + beneath_fd = do_openat2_syscall(AT_FDCWD, "sub/leaf", &how, + sizeof(how)); + if (beneath_fd == -1) { + fprintf(stderr, "syscall: RESOLVE_BENEATH + relative " + "\"sub/leaf\" failed: %s\n", strerror(errno)); + goto out_restore_cwd; + } + close(beneath_fd); + + /* Negative: absolute path + RESOLVE_BENEATH via syscall must fail */ + if (snprintf(abspath, sizeof(abspath), "%s/%s/sub/leaf", + cwd_save, base) < (int) sizeof(abspath)) { + beneath_fd = do_openat2_syscall(AT_FDCWD, abspath, + &how, sizeof(how)); + if (beneath_fd != -1) { + fprintf(stderr, "syscall: RESOLVE_BENEATH + " + "absolute path should have failed\n"); + close(beneath_fd); + goto out_restore_cwd; + } + if (errno != EXDEV) { + fprintf(stderr, "syscall: RESOLVE_BENEATH + " + "absolute: expected EXDEV, got %s\n", + strerror(errno)); + goto out_restore_cwd; + } + } + + if (chdir(cwd_save) == -1) { + perror("chdir restore"); + cwd_saved = 0; + goto out; + } + } + + rc = 0; + goto out; + +out_restore_cwd: + if (cwd_saved) + if (chdir(cwd_save) == -1) + perror("chdir restore (cleanup)"); + +out: + if (fd != -1) + close(fd); + if (dirfd != -1) + close(dirfd); + unlink(marker); + rmdir(leafdir); + rmdir(subdir); + rmdir(base); + + return rc; +} diff --git a/test/test-openat2-syscall.sh b/test/test-openat2-syscall.sh new file mode 100755 index 0000000..0a26c0c --- /dev/null +++ b/test/test-openat2-syscall.sh @@ -0,0 +1,18 @@ +#! /bin/sh + +# rc 77 indicates an ENOSYS, so return back we're skipped +# This shouldn't happen, but the user could disable openat2 syscall emulation +run_test() { + $@ + rc=$? + if [ "$rc" -eq 77 ]; then + exit 255 + fi + return "$rc" +} + +# Run with and without ignored paths, matching test-openat coverage. +run_test ./test/test-openat2-syscall || exit $? +run_test env PSEUDO_IGNORE_PATHS=/ ./test/test-openat2-syscall || exit $? + +exit 0 From patchwork Mon Apr 20 18:30:28 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Mark Hatle X-Patchwork-Id: 86509 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id E1115F5A8AC for ; Mon, 20 Apr 2026 18:30:40 +0000 (UTC) Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.208.1776709833150896232 for ; Mon, 20 Apr 2026 11:30:33 -0700 Authentication-Results: mx.groups.io; dkim=none (message not signed); spf=pass (domain: kernel.crashing.org, ip: 63.228.1.57, mailfrom: mark.hatle@kernel.crashing.org) Received: from kernel.crashing.org.net (70-99-78-136.nuveramail.net [70.99.78.136] (may be forged)) by gate.crashing.org (8.18.1/8.18.1/Debian-2) with ESMTP id 63KIUUPx3563308; Mon, 20 Apr 2026 13:30:31 -0500 From: Mark Hatle To: yocto-patches@lists.yoctoproject.org, richard.purdie@linuxfoundation.org Subject: [PATCH 4/5] test: Add renameat2 test cases Date: Mon, 20 Apr 2026 13:30:28 -0500 Message-Id: <1776709829-2754-5-git-send-email-mark.hatle@kernel.crashing.org> X-Mailer: git-send-email 1.8.3.1 In-Reply-To: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> References: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> MIME-Version: 1.0 X-MIME-Autoconverted: from 8bit to quoted-printable by gate.crashing.org id 63KIUUPx3563308 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 20 Apr 2026 18:30:40 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/3743 From: Mark Hatle Add two test cases for renameat2 semantics. A primary test and a chroot test case. AI-Generated: Test cases written using GitHub Copilot (Claude Sonnet 4.6) Signed-off-by: Mark Hatle --- test/test-renameat2-chroot.sh | 11 ++ test/test-renameat2.c | 263 ++++++++++++++++++++++++++++++++++++++++++ test/test-renameat2.sh | 11 ++ 3 files changed, 285 insertions(+) create mode 100755 test/test-renameat2-chroot.sh create mode 100644 test/test-renameat2.c create mode 100755 test/test-renameat2.sh diff --git a/test/test-renameat2-chroot.sh b/test/test-renameat2-chroot.sh new file mode 100755 index 0000000..3e44033 --- /dev/null +++ b/test/test-renameat2-chroot.sh @@ -0,0 +1,11 @@ +#! /bin/sh + +# Run with and without ignored paths, matching test-openat coverage. + +chroot ./test ./test-renameat2 +rc=$? +if [ "$rc" -ne 0 ]; then + exit "$rc" +fi + +PSEUDO_IGNORE_PATHS=/ chroot ./test ./test-renameat2 diff --git a/test/test-renameat2.c b/test/test-renameat2.c new file mode 100644 index 0000000..286045f --- /dev/null +++ b/test/test-renameat2.c @@ -0,0 +1,263 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef RENAME_NOREPLACE +#define RENAME_NOREPLACE (1 << 0) +#endif +#ifndef RENAME_EXCHANGE +#define RENAME_EXCHANGE (1 << 1) +#endif + +static int do_renameat2(int olddirfd, const char *oldpath, + int newdirfd, const char *newpath, + unsigned int flags) { +#ifdef SYS_renameat2 + return syscall(SYS_renameat2, olddirfd, oldpath, newdirfd, newpath, flags); +#else + errno = ENOSYS; + return -1; +#endif +} + +static int touch_file(const char *path) { + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd == -1) { + perror("open"); + return -1; + } + if (write(fd, "x", 1) != 1) { + perror("write"); + close(fd); + return -1; + } + if (close(fd) == -1) { + perror("close"); + return -1; + } + return 0; +} + +static int file_exists(int dirfd, const char *name) { + struct stat st; + return fstatat(dirfd, name, &st, AT_SYMLINK_NOFOLLOW) == 0; +} + +int main(void) { + char template[] = "test-renameat2.XXXXXX"; + char *base; + int dirfd = -1; + int rc = 1; + int ret; + int renameat2_works = 1; + + base = mkdtemp(template); + if (!base) { + perror("mkdtemp"); + return 1; + } + + if (chdir(base) == -1) { + perror("chdir to base"); + rmdir(base); + return 1; + } + + dirfd = open(".", O_RDONLY | O_DIRECTORY); + if (dirfd == -1) { + perror("open base dir"); + goto out; + } + + /* ---------------------------------------------------------- */ + /* Part 1: renameat (classic) semantics via dirfd */ + /* ---------------------------------------------------------- */ + + if (touch_file("fileA") == -1) + goto out_in_base; + + /* Rename fileA -> fileB using renameat with dirfd */ + if (renameat(AT_FDCWD, "fileA", AT_FDCWD, "fileB") == -1) { + perror("renameat fileA -> fileB"); + goto out_in_base; + } + if (file_exists(AT_FDCWD, "fileA")) { + fprintf(stderr, "renameat: source still exists after rename\n"); + goto out_in_base; + } + if (!file_exists(AT_FDCWD, "fileB")) { + fprintf(stderr, "renameat: destination missing after rename\n"); + goto out_in_base; + } + unlink("fileB"); + + /* Same test but with actual dirfd instead of AT_FDCWD */ + if (touch_file("fileC") == -1) + goto out_in_base; + + if (renameat(dirfd, "fileC", dirfd, "fileD") == -1) { + perror("renameat dirfd fileC -> fileD"); + goto out_in_base; + } + if (file_exists(dirfd, "fileC")) { + fprintf(stderr, "renameat dirfd: source still exists\n"); + goto out_in_base; + } + if (!file_exists(dirfd, "fileD")) { + fprintf(stderr, "renameat dirfd: destination missing\n"); + goto out_in_base; + } + unlinkat(dirfd, "fileD", 0); + + /* ---------------------------------------------------------- */ + /* Part 2: renameat2 with flags=0 (should match renameat) */ + /* ---------------------------------------------------------- */ + + if (touch_file("fileE") == -1) + goto out_in_base; + + ret = do_renameat2(AT_FDCWD, "fileE", AT_FDCWD, "fileF", 0); + if (ret == -1) { + if (errno == ENOSYS) { + /* renameat2 not available (kernel too old or pseudo stub) */ + renameat2_works = 0; + unlink("fileE"); + } else { + perror("renameat2 flags=0 fileE -> fileF"); + goto out_in_base; + } + } else { + if (file_exists(AT_FDCWD, "fileE")) { + fprintf(stderr, "renameat2 flags=0: source still exists\n"); + goto out_in_base; + } + if (!file_exists(AT_FDCWD, "fileF")) { + fprintf(stderr, "renameat2 flags=0: destination missing\n"); + goto out_in_base; + } + unlink("fileF"); + } + + if (!renameat2_works) { + /* renameat2 returned ENOSYS; skip the remaining renameat2-specific tests */ + rc = 0; + goto out; + } + + /* ---------------------------------------------------------- */ + /* Part 3: RENAME_NOREPLACE — must fail when target exists */ + /* ---------------------------------------------------------- */ + + if (touch_file("src_noreplace") == -1) + goto out_in_base; + if (touch_file("dst_noreplace") == -1) + goto out_in_base; + + ret = do_renameat2(AT_FDCWD, "src_noreplace", + AT_FDCWD, "dst_noreplace", RENAME_NOREPLACE); + if (ret != -1) { + fprintf(stderr, "RENAME_NOREPLACE: should have failed when target exists\n"); + goto out_in_base; + } + if (errno != EEXIST) { + fprintf(stderr, "RENAME_NOREPLACE: expected EEXIST, got %s\n", + strerror(errno)); + goto out_in_base; + } + /* Both files must still exist */ + if (!file_exists(AT_FDCWD, "src_noreplace") || + !file_exists(AT_FDCWD, "dst_noreplace")) { + fprintf(stderr, "RENAME_NOREPLACE: files disappeared\n"); + goto out_in_base; + } + + /* Positive case: RENAME_NOREPLACE succeeds when target does not exist */ + unlink("dst_noreplace"); + ret = do_renameat2(AT_FDCWD, "src_noreplace", + AT_FDCWD, "dst_noreplace", RENAME_NOREPLACE); + if (ret == -1) { + perror("RENAME_NOREPLACE (no target)"); + goto out_in_base; + } + if (file_exists(AT_FDCWD, "src_noreplace")) { + fprintf(stderr, "RENAME_NOREPLACE: source still exists\n"); + goto out_in_base; + } + if (!file_exists(AT_FDCWD, "dst_noreplace")) { + fprintf(stderr, "RENAME_NOREPLACE: destination missing\n"); + goto out_in_base; + } + unlink("dst_noreplace"); + + /* ---------------------------------------------------------- */ + /* Part 4: RENAME_EXCHANGE — atomically swap two paths */ + /* ---------------------------------------------------------- */ + + if (touch_file("swap_a") == -1) + goto out_in_base; + if (touch_file("swap_b") == -1) + goto out_in_base; + + /* Write distinct content so we can verify the swap */ + { + struct stat sa, sb; + ino_t ino_a, ino_b; + + if (stat("swap_a", &sa) == -1 || stat("swap_b", &sb) == -1) { + perror("stat before exchange"); + goto out_in_base; + } + ino_a = sa.st_ino; + ino_b = sb.st_ino; + + ret = do_renameat2(AT_FDCWD, "swap_a", + AT_FDCWD, "swap_b", RENAME_EXCHANGE); + if (ret == -1) { + perror("RENAME_EXCHANGE"); + goto out_in_base; + } + + if (stat("swap_a", &sa) == -1 || stat("swap_b", &sb) == -1) { + perror("stat after exchange"); + goto out_in_base; + } + + /* After exchange, inodes should be swapped */ + if (sa.st_ino != ino_b || sb.st_ino != ino_a) { + fprintf(stderr, "RENAME_EXCHANGE: inodes not swapped " + "(a: %lu->%lu, b: %lu->%lu)\n", + (unsigned long) ino_a, (unsigned long) sa.st_ino, + (unsigned long) ino_b, (unsigned long) sb.st_ino); + goto out_in_base; + } + } + unlink("swap_a"); + unlink("swap_b"); + + rc = 0; + goto out; + +out_in_base: + /* Best-effort cleanup of any leftover files inside the temp dir */ + unlink("fileA"); unlink("fileB"); unlink("fileC"); unlink("fileD"); + unlink("fileE"); unlink("fileF"); + unlink("src_noreplace"); unlink("dst_noreplace"); + unlink("swap_a"); unlink("swap_b"); + +out: + if (dirfd != -1) + close(dirfd); + if (chdir("..") == 0) + rmdir(base); + + return rc; +} diff --git a/test/test-renameat2.sh b/test/test-renameat2.sh new file mode 100755 index 0000000..6aa7fbc --- /dev/null +++ b/test/test-renameat2.sh @@ -0,0 +1,11 @@ +#! /bin/sh + +# Run with and without ignored paths, matching test-openat coverage. + +./test/test-renameat2 +rc=$? +if [ "$rc" -ne 0 ]; then + exit "$rc" +fi + +PSEUDO_IGNORE_PATHS=/ ./test/test-renameat2 From patchwork Mon Apr 20 18:30:29 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Mark Hatle X-Patchwork-Id: 86510 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 40ABDF5A8B5 for ; Mon, 20 Apr 2026 18:30:41 +0000 (UTC) Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.207.1776709833130191179 for ; Mon, 20 Apr 2026 11:30:33 -0700 Authentication-Results: mx.groups.io; dkim=none (message not signed); spf=pass (domain: kernel.crashing.org, ip: 63.228.1.57, mailfrom: mark.hatle@kernel.crashing.org) Received: from kernel.crashing.org.net (70-99-78-136.nuveramail.net [70.99.78.136] (may be forged)) by gate.crashing.org (8.18.1/8.18.1/Debian-2) with ESMTP id 63KIUUQ03563308; Mon, 20 Apr 2026 13:30:31 -0500 From: Mark Hatle To: yocto-patches@lists.yoctoproject.org, richard.purdie@linuxfoundation.org Subject: [PATCH 5/5] Makefile.in: Bump version to 1.9.4 Date: Mon, 20 Apr 2026 13:30:29 -0500 Message-Id: <1776709829-2754-6-git-send-email-mark.hatle@kernel.crashing.org> X-Mailer: git-send-email 1.8.3.1 In-Reply-To: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> References: <1776709829-2754-1-git-send-email-mark.hatle@kernel.crashing.org> List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 20 Apr 2026 18:30:41 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/3742 Signed-off-by: Mark Hatle --- Makefile.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.in b/Makefile.in index 8bf3f48..a5aba38 100644 --- a/Makefile.in +++ b/Makefile.in @@ -34,7 +34,7 @@ BITS=@BITS@ ARCH_FLAGS=@ARCH_FLAGS@ MARK64=@MARK64@ RPATH=@RPATH@ -VERSION=1.9.3 +VERSION=1.9.4 LIB=@LIB@ BIN=bin