new file mode 100644
@@ -0,0 +1,48 @@
+From 21b5e5b9d394612f44e384c8fa024aea5e8dac68 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 12 Jun 2026 17:32:40 +0200
+Subject: [PATCH] Add visibility attribute header
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=21b5e5b9d394612f44e384c8fa024aea5e8dac68]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 21b5e5b9d394612f44e384c8fa024aea5e8dac68)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ include/Makemodule.am | 1 +
+ include/visibility-hidden.h | 11 +++++++++++
+ 2 files changed, 12 insertions(+)
+ create mode 100644 include/visibility-hidden.h
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index 3531fa1..f300edb 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -4,6 +4,7 @@ noinst_HEADERS += \
+ include/libattr.h \
+ include/misc.h \
+ include/nls.h \
++ include/visibility-hidden.h \
+ include/walk_tree.h
+
+
+diff --git a/include/visibility-hidden.h b/include/visibility-hidden.h
+new file mode 100644
+index 0000000..2d02731
+--- /dev/null
++++ b/include/visibility-hidden.h
+@@ -0,0 +1,11 @@
++#ifndef VISIBILITY_HIDDEN_H
++#define VISIBILITY_HIDDEN_H
++
++/* Mark library internal functions as hidden */
++#if defined(HAVE_VISIBILITY_ATTRIBUTE)
++# define hidden __attribute__((visibility("hidden")))
++#else
++# define hidden /* hidden */
++#endif
++
++#endif /* VISIBILITY_HIDDEN_H */
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,197 @@
+From c62e9b55edd512ba3f87eaaaf5b822bd2a76bfe9 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 12 Jun 2026 13:18:23 +0200
+Subject: [PATCH] Add xattrat syscall wrappers
+
+The getxattrat(), setxattrat(), listxattrat(), and removexattrat() system calls
+don't have shims provided by glibc, yet. For each of those system calls, test
+if glibc defines it and add a local shim using syscall() if not.
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=c62e9b55edd512ba3f87eaaaf5b822bd2a76bfe9]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit c62e9b55edd512ba3f87eaaaf5b822bd2a76bfe9)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ configure.ac | 7 ++++
+ include/Makemodule.am | 4 +-
+ include/xattrat.h | 25 +++++++++++++
+ libmisc/Makemodule.am | 5 +++
+ libmisc/xattrat.c | 87 +++++++++++++++++++++++++++++++++++++++++++
+ 5 files changed, 126 insertions(+), 2 deletions(-)
+ create mode 100644 include/xattrat.h
+ create mode 100644 libmisc/xattrat.c
+
+diff --git a/configure.ac b/configure.ac
+index 02322c3..bf32dce 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -36,6 +36,13 @@ AC_SYS_LARGEFILE
+ AM_PROG_AR
+ LT_INIT
+
++AC_CHECK_FUNCS(getxattrat setxattrat listxattrat removexattrat)
++AM_CONDITIONAL([NEED_XATTRAT],
++ [test "x$ac_cv_func_getxattrat" = xno -o \
++ "x$ac_cv_func_setxattrat" = xno -o \
++ "x$ac_cv_func_listxattrat" = xno -o \
++ "x$ac_cv_func_removexattrat" = xno])
++
+ AM_GNU_GETTEXT_VERSION([0.18.2])
+ AM_GNU_GETTEXT([external])
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index f300edb..bd1a4a8 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -5,8 +5,8 @@ noinst_HEADERS += \
+ include/misc.h \
+ include/nls.h \
+ include/visibility-hidden.h \
+- include/walk_tree.h
+-
++ include/walk_tree.h \
++ include/xattrat.h
+
+ SUBST_INSTALL_HEADER = \
+ subst_install_header() { \
+diff --git a/include/xattrat.h b/include/xattrat.h
+new file mode 100644
+index 0000000..e4ba0c0
+--- /dev/null
++++ b/include/xattrat.h
+@@ -0,0 +1,25 @@
++#ifndef MISC_XATTRAT_H
++#define MISC_XATTRAT_H
++
++#include "config.h"
++
++#ifndef HAVE_GETXATTRAT
++ssize_t getxattrat(int dirfd, const char *path, int at_flags, const char *name,
++ void *value, size_t size);
++#endif
++
++#ifndef HAVE_SETXATTRAT
++int setxattrat(int dirfd, const char *path, int at_flags, const char *name,
++ const void *value, size_t size, int flags);
++#endif
++
++#ifndef HAVE_LISTXATTRAT
++ssize_t listxattrat(int dirfd, const char *path, int at_flags, char *list,
++ size_t size);
++#endif
++
++#ifndef HAVE_REMOVEXATTRAT
++int removexattrat(int dirfd, const char *path, int at_flags, const char *name);
++#endif
++
++#endif /* MISC_XATTRAT_H */
+diff --git a/libmisc/Makemodule.am b/libmisc/Makemodule.am
+index d784622..9349959 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -6,3 +6,8 @@ libmisc_la_SOURCES = \
+ libmisc/quote.c \
+ libmisc/unquote.c \
+ libmisc/walk_tree.c
++
++if NEED_XATTRAT
++libmisc_la_SOURCES += \
++ libmisc/xattrat.c
++endif
+diff --git a/libmisc/xattrat.c b/libmisc/xattrat.c
+new file mode 100644
+index 0000000..5f6212c
+--- /dev/null
++++ b/libmisc/xattrat.c
+@@ -0,0 +1,87 @@
++/*
++ File: xattrat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <linux/xattr.h>
++#include <sys/syscall.h>
++#include <unistd.h>
++#include <errno.h>
++
++#include "xattrat.h"
++
++#pragma GCC diagnostic ignored "-Wunused-parameter"
++
++ssize_t
++getxattrat(int dirfd, const char *path, int at_flags, const char *name,
++ void *value, size_t size)
++{
++#ifdef __NR_getxattrat
++ struct xattr_args uargs = {
++ .value = (unsigned long)value,
++ .size = size,
++ };
++
++ return syscall(__NR_getxattrat, dirfd, path, at_flags, name,
++ &uargs, sizeof(uargs));
++#else
++ errno = ENOSYS;
++ return -1;
++#endif
++}
++
++int
++setxattrat(int dirfd, const char *path, int at_flags, const char *name,
++ const void *value, size_t size, int flags)
++{
++#ifdef __NR_setxattrat
++ struct xattr_args uargs = {
++ .value = (unsigned long)value,
++ .size = size,
++ .flags = flags,
++ };
++
++ return syscall(__NR_setxattrat, dirfd, path, at_flags, name,
++ &uargs, sizeof(uargs));
++#else
++ errno = ENOSYS;
++ return -1;
++#endif
++}
++
++ssize_t listxattrat(int dirfd, const char *path, int at_flags, char *list,
++ size_t size)
++{
++#ifdef __NR_listxattrat
++ return syscall(__NR_listxattrat, dirfd, path, at_flags, list, size);
++#else
++ errno = ENOSYS;
++ return -1;
++#endif
++}
++
++int
++removexattrat(int dirfd, const char *path, int at_flags, const char *name)
++{
++#ifdef __NR_removexattrat
++ return syscall(__NR_removexattrat, dirfd, path, at_flags, name);
++#else
++ errno = ENOSYS;
++ return -1;
++#endif
++}
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,710 @@
+From c462283577f1a6479229c43184ad6af5921f439b Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 12 Jun 2026 15:45:25 +0200
+Subject: [PATCH] Add xattrat syscall backwards compatibility code
+
+The getxattrat(), setxattrat(), listxattrat(), and removexattrat() system calls
+have only been added in kernel version 6.13 which was released in January 2025.
+On older systems, those system calls will fail with errno == ENOSYS. So we use
+the old system calls (for example, getxattr() and lgetxattr()) when they are
+sufficient, and the new xattrat() system calls when they are needed.
+
+In addition, when the new system calls are not available, we resort to the
+following hacks for achieving the desired behavior: when dirfd is not AT_FDCWD,
+the pathname can usually be resolved relative to dirfd as
+"/proc/self/fd/<dirfd>/pathname". And when that also fails because the
+resulting pathname is too long, the original pathname can be resolved using
+openat(dirfd, pathname, O_PATH) and then accessed as "/proc/self/fd/<fd>".
+
+Also, due to a current kernel limitation, the xattrat system calls will not
+accept dirfd file descriptors of type O_PATH with an empty pathname (and with
+the AT_EMPTY_PATH flag set in at_flags) [*]. When running into this case, we
+also fall back to accessing the file via "/proc/self/fd/<fd>".
+
+When those hacks fail because the /proc filesystem is not available, these
+compatibility wrappers will fail with errno == ENOSYS.
+
+[*] https://lore.kernel.org/linux-fsdevel/20260607164136.2948550-2-agruenba@redhat.com/
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=c462283577f1a6479229c43184ad6af5921f439b]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit c462283577f1a6479229c43184ad6af5921f439b)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ include/Makemodule.am | 3 +-
+ include/xattrat_compat.h | 24 ++++++++
+ libmisc/Makemodule.am | 8 +++
+ libmisc/getxattrat_compat.c | 108 +++++++++++++++++++++++++++++++++
+ libmisc/listxattrat_compat.c | 107 ++++++++++++++++++++++++++++++++
+ libmisc/proc-self-fd.c | 92 ++++++++++++++++++++++++++++
+ libmisc/proc-self-fd.h | 31 ++++++++++
+ libmisc/removexattrat_compat.c | 107 ++++++++++++++++++++++++++++++++
+ libmisc/setxattrat_compat.c | 108 +++++++++++++++++++++++++++++++++
+ 9 files changed, 587 insertions(+), 1 deletion(-)
+ create mode 100644 include/xattrat_compat.h
+ create mode 100644 libmisc/getxattrat_compat.c
+ create mode 100644 libmisc/listxattrat_compat.c
+ create mode 100644 libmisc/proc-self-fd.c
+ create mode 100644 libmisc/proc-self-fd.h
+ create mode 100644 libmisc/removexattrat_compat.c
+ create mode 100644 libmisc/setxattrat_compat.c
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index bd1a4a8..54f8ee0 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -6,7 +6,8 @@ noinst_HEADERS += \
+ include/nls.h \
+ include/visibility-hidden.h \
+ include/walk_tree.h \
+- include/xattrat.h
++ include/xattrat.h \
++ include/xattrat_compat.h
+
+ SUBST_INSTALL_HEADER = \
+ subst_install_header() { \
+diff --git a/include/xattrat_compat.h b/include/xattrat_compat.h
+new file mode 100644
+index 0000000..0016e9e
+--- /dev/null
++++ b/include/xattrat_compat.h
+@@ -0,0 +1,24 @@
++#ifndef XATTRAT_COMPAT_H
++#define XATTRAT_COMPAT_H
++
++ssize_t getxattrat_compat(int dirfd, const char *path, int at_flags,
++ const char *name, void *value, size_t size);
++
++int setxattrat_compat(int dirfd, const char *path, int at_flags,
++ const char *name, const void *value, size_t size,
++ int flags);
++
++ssize_t listxattrat_compat(int dirfd, const char *path, int at_flags,
++ char *list, size_t size);
++
++int removexattrat_compat(int dirfd, const char *path, int at_flags,
++ const char *name);
++
++#ifndef DECLARATIONS_ONLY
++# define getxattrat getxattrat_compat
++# define setxattrat setxattrat_compat
++# define listxattrat listxattrat_compat
++# define removexattrat removexattrat_compat
++#endif
++
++#endif /* XATTRAT_COMPAT_H */
+diff --git a/libmisc/Makemodule.am b/libmisc/Makemodule.am
+index 9349959..a42ae32 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -1,9 +1,17 @@
+ noinst_LTLIBRARIES += libmisc.la
+
++noinst_HEADERS += \
++ libmisc/proc-self-fd.h
++
+ libmisc_la_SOURCES = \
++ libmisc/getxattrat_compat.c \
+ libmisc/high_water_alloc.c \
++ libmisc/listxattrat_compat.c \
+ libmisc/next_line.c \
++ libmisc/proc-self-fd.c \
+ libmisc/quote.c \
++ libmisc/removexattrat_compat.c \
++ libmisc/setxattrat_compat.c \
+ libmisc/unquote.c \
+ libmisc/walk_tree.c
+
+diff --git a/libmisc/getxattrat_compat.c b/libmisc/getxattrat_compat.c
+new file mode 100644
+index 0000000..8c642b6
+--- /dev/null
++++ b/libmisc/getxattrat_compat.c
+@@ -0,0 +1,108 @@
++/*
++ File: getxattrat_compat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <sys/xattr.h>
++#include <unistd.h>
++#include <fcntl.h>
++#include <errno.h>
++#include <stdlib.h>
++#include "xattrat.h"
++#include "libmisc/proc-self-fd.h"
++
++#define DECLARATIONS_ONLY
++#include "xattrat_compat.h"
++
++/* Used in {set,list,remove}xattrat_compat.c */
++int slow_empty_paths;
++
++static ssize_t
++xgetxattr(const char *path, int at_flags, const char *name, void *value,
++ size_t size)
++{
++ if (at_flags & AT_SYMLINK_NOFOLLOW)
++ return lgetxattr(path, name, value, size);
++ return getxattr(path, name, value, size);
++}
++
++ssize_t
++getxattrat_compat(int dirfd, const char *path, int at_flags, const char *name,
++ void *value, size_t size)
++{
++ struct proc_self_fd_buffer buffer;
++ static int no_getxattrat;
++ char *newpath;
++ ssize_t ret;
++ int fd;
++
++ if (at_flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) {
++ errno = EINVAL;
++ return -1;
++ }
++ if (!*path) {
++ if (!(at_flags & AT_EMPTY_PATH)) {
++ errno = ENOENT;
++ return -1;
++ }
++ }
++
++ if (dirfd == AT_FDCWD || *path == '/')
++ return xgetxattr(path, at_flags, name, value, size);
++
++ if (!no_getxattrat) {
++ if (!*path && slow_empty_paths)
++ goto proc_self_fd_path;
++ ret = getxattrat(dirfd, path, at_flags, name, value, size);
++ if (ret != -1)
++ return ret;
++ if (need_slow_empty_paths(dirfd, path)) {
++ slow_empty_paths = 1;
++ goto proc_self_fd_path;
++ }
++ if (errno != ENOSYS)
++ return -1;
++ no_getxattrat = 1;
++ }
++
++proc_self_fd_path:
++ newpath = proc_self_fd_realpath(dirfd, path, &at_flags, &buffer);
++ if (!newpath)
++ return -1;
++ ret = xgetxattr(newpath, at_flags, name, value, size);
++ proc_self_fd_free(newpath, &buffer);
++ if (ret != -1)
++ return ret;
++ if (errno == ENOENT) {
++ if (!*path) {
++ errno = ENOSYS;
++ return -1;
++ }
++ if (fcntl(dirfd, F_GETFL) != -1)
++ errno = ENOENT;
++ return -1;
++ } else if (errno != ENAMETOOLONG)
++ return ret;
++
++ fd = openat_with_flags(dirfd, path, at_flags);
++ if (fd == -1)
++ return -1;
++ ret = getxattr(proc_self_fd_path(fd, &buffer), name, value, size);
++ close(fd);
++ return ret;
++}
+diff --git a/libmisc/listxattrat_compat.c b/libmisc/listxattrat_compat.c
+new file mode 100644
+index 0000000..17640cc
+--- /dev/null
++++ b/libmisc/listxattrat_compat.c
+@@ -0,0 +1,107 @@
++/*
++ File: listxattrat_compat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <sys/xattr.h>
++#include <unistd.h>
++#include <fcntl.h>
++#include <errno.h>
++#include <stdlib.h>
++#include "xattrat.h"
++#include "libmisc/proc-self-fd.h"
++
++#define DECLARATIONS_ONLY
++#include "xattrat_compat.h"
++
++/* Defined in getxattrat_compat.c */
++extern int slow_empty_paths;
++
++static ssize_t
++xlistxattr(const char *path, int at_flags, char *list, size_t size)
++{
++ if (at_flags & AT_SYMLINK_NOFOLLOW)
++ return llistxattr(path, list, size);
++ return listxattr(path, list, size);
++}
++
++ssize_t
++listxattrat_compat(int dirfd, const char *path, int at_flags, char *list,
++ size_t size)
++{
++ struct proc_self_fd_buffer buffer;
++ static int no_listxattrat;
++ char *newpath;
++ ssize_t ret;
++ int fd;
++
++ if (at_flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) {
++ errno = EINVAL;
++ return -1;
++ }
++ if (!*path) {
++ if (!(at_flags & AT_EMPTY_PATH)) {
++ errno = ENOENT;
++ return -1;
++ }
++ }
++
++ if (dirfd == AT_FDCWD || *path == '/')
++ return xlistxattr(path, at_flags, list, size);
++
++ if (!no_listxattrat) {
++ if (!*path && slow_empty_paths)
++ goto proc_self_fd_path;
++ ret = listxattrat(dirfd, path, at_flags, list, size);
++ if (ret != -1)
++ return ret;
++ if (need_slow_empty_paths(dirfd, path)) {
++ slow_empty_paths = 1;
++ goto proc_self_fd_path;
++ }
++ if (errno != ENOSYS)
++ return -1;
++ no_listxattrat = 1;
++ }
++
++proc_self_fd_path:
++ newpath = proc_self_fd_realpath(dirfd, path, &at_flags, &buffer);
++ if (!newpath)
++ return -1;
++ ret = xlistxattr(newpath, at_flags, list, size);
++ proc_self_fd_free(newpath, &buffer);
++ if (ret != -1)
++ return ret;
++ if (errno == ENOENT) {
++ if (!*path) {
++ errno = ENOSYS;
++ return -1;
++ }
++ if (fcntl(dirfd, F_GETFL) != -1)
++ errno = ENOENT;
++ return -1;
++ } else if (errno != ENAMETOOLONG)
++ return ret;
++
++ fd = openat_with_flags(dirfd, path, at_flags);
++ if (fd == -1)
++ return -1;
++ ret = listxattr(proc_self_fd_path(fd, &buffer), list, size);
++ close(fd);
++ return ret;
++}
+diff --git a/libmisc/proc-self-fd.c b/libmisc/proc-self-fd.c
+new file mode 100644
+index 0000000..0ae5cf2
+--- /dev/null
++++ b/libmisc/proc-self-fd.c
+@@ -0,0 +1,92 @@
++/*
++ File: xattrat_compat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <unistd.h>
++#include <fcntl.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <errno.h>
++#include "libmisc/proc-self-fd.h"
++
++char *
++proc_self_fd_path(int fd, struct proc_self_fd_buffer *buffer)
++{
++ sprintf(buffer->buffer, "/proc/self/fd/%d", fd);
++ return buffer->buffer;
++}
++
++void
++proc_self_fd_free(char *path, struct proc_self_fd_buffer *buffer)
++{
++ if (path != buffer->buffer)
++ free(path);
++}
++
++char *
++proc_self_fd_realpath(int dirfd, const char *path, int *at_flags,
++ struct proc_self_fd_buffer *buffer)
++{
++ char *newpath;
++ int ret;
++
++ if (!*path) {
++ *at_flags &= ~AT_SYMLINK_NOFOLLOW;
++ return proc_self_fd_path(dirfd, buffer);
++ }
++ ret = asprintf(&newpath, "/proc/self/fd/%d/%s", dirfd, path);
++ if (ret == -1)
++ return NULL;
++ return newpath;
++}
++
++int
++openat_with_flags(int dirfd, const char *path, int at_flags)
++{
++ int flags = O_PATH | O_CLOEXEC;
++
++ if (at_flags & AT_SYMLINK_NOFOLLOW)
++ flags |= O_NOFOLLOW;
++ return openat(dirfd, path, flags);
++}
++
++/*
++ * getxattrat(), setxattrat(), listxattrat(), and removexattrat() up to at
++ * least kernel version 7.2 fail with EBADF when the dirfd is an O_PATH file
++ * descriptor and the pathname is NULL or empty.
++ */
++int
++need_slow_empty_paths(int dirfd, const char *path)
++{
++ int saved_errno = errno;
++ int open_flags;
++
++ if (errno != EBADF)
++ return 0;
++ if (*path)
++ return 0;
++ open_flags = fcntl(dirfd, F_GETFL);
++ if (open_flags == -1) {
++ errno = saved_errno;
++ return 0;
++ }
++ if (!(open_flags & O_PATH))
++ return 0;
++ return 1;
++}
+diff --git a/libmisc/proc-self-fd.h b/libmisc/proc-self-fd.h
+new file mode 100644
+index 0000000..368fd1e
+--- /dev/null
++++ b/libmisc/proc-self-fd.h
+@@ -0,0 +1,31 @@
++#ifndef MISC_XATTRAT_COMPAT_H
++#define MISC_XATTRAT_COMPAT_H
++
++#include <limits.h>
++#include "include/visibility-hidden.h"
++
++/*
++ * The maximum number of decimal digits of an N-bit number is
++ * ceil(N * log10(2)), with log10(2) being approx. 0.30103. We
++ * use 31/100 as a safe and close enough approximation; the
++ * overshoot even for 128-bit numbers is only one byte.
++ */
++#define TYPE_BITS(type) (sizeof(type) * CHAR_BIT)
++#define DECIMAL_DIGITS(type) ((TYPE_BITS(type) * 31 + 99) / 100)
++
++struct proc_self_fd_buffer {
++ char buffer[sizeof("/proc/self/fd/") +
++ /* sign */ 1 + DECIMAL_DIGITS(int)];
++};
++
++hidden char *proc_self_fd_path(int fd, struct proc_self_fd_buffer *buffer);
++hidden void proc_self_fd_free(char *path, struct proc_self_fd_buffer *buffer);
++
++hidden extern int slow_empty_paths;
++
++hidden char *proc_self_fd_realpath(int dirfd, const char *path, int *at_flags,
++ struct proc_self_fd_buffer *buffer);
++hidden int openat_with_flags(int dirfd, const char *path, int at_flags);
++hidden int need_slow_empty_paths(int dirfd, const char *path);
++
++#endif /* MISC_XATTRAT_COMPAT_H */
+diff --git a/libmisc/removexattrat_compat.c b/libmisc/removexattrat_compat.c
+new file mode 100644
+index 0000000..32e37cd
+--- /dev/null
++++ b/libmisc/removexattrat_compat.c
+@@ -0,0 +1,107 @@
++/*
++ File: getxattrat_compat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <sys/xattr.h>
++#include <unistd.h>
++#include <fcntl.h>
++#include <errno.h>
++#include <stdlib.h>
++#include "xattrat.h"
++#include "libmisc/proc-self-fd.h"
++
++#define DECLARATIONS_ONLY
++#include "xattrat_compat.h"
++
++/* Defined in getxattrat_compat.c */
++extern int slow_empty_paths;
++
++static int
++xremovexattr(const char *path, int at_flags, const char *name)
++{
++ if (at_flags & AT_SYMLINK_NOFOLLOW)
++ return lremovexattr(path, name);
++ return removexattr(path, name);
++}
++
++
++int
++removexattrat_compat(int dirfd, const char *path, int at_flags,
++ const char *name)
++{
++ struct proc_self_fd_buffer buffer;
++ static int no_removexattrat;
++ char *newpath;
++ int fd, ret;
++
++ if (at_flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) {
++ errno = EINVAL;
++ return -1;
++ }
++ if (!*path) {
++ if (!(at_flags & AT_EMPTY_PATH)) {
++ errno = ENOENT;
++ return -1;
++ }
++ }
++
++ if (dirfd == AT_FDCWD || *path == '/')
++ return xremovexattr(path, at_flags, name);
++
++ if (!no_removexattrat) {
++ if (!*path && slow_empty_paths)
++ goto proc_self_fd_path;
++ ret = removexattrat(dirfd, path, at_flags, name);
++ if (ret != -1)
++ return ret;
++ if (need_slow_empty_paths(dirfd, path)) {
++ slow_empty_paths = 1;
++ goto proc_self_fd_path;
++ }
++ if (errno != ENOSYS)
++ return ret;
++ no_removexattrat = 1;
++ }
++
++proc_self_fd_path:
++ newpath = proc_self_fd_realpath(dirfd, path, &at_flags, &buffer);
++ if (!newpath)
++ return -1;
++ ret = xremovexattr(newpath, at_flags, name);
++ proc_self_fd_free(newpath, &buffer);
++ if (ret != -1)
++ return ret;
++ if (errno == ENOENT) {
++ if (!*path) {
++ errno = ENOSYS;
++ return -1;
++ }
++ if (fcntl(dirfd, F_GETFL) != -1)
++ errno = ENOENT;
++ return -1;
++ } else if (errno != ENAMETOOLONG)
++ return ret;
++
++ fd = openat_with_flags(dirfd, path, at_flags);
++ if (fd == -1)
++ return -1;
++ ret = removexattr(proc_self_fd_path(fd, &buffer), name);
++ close(fd);
++ return ret;
++}
+diff --git a/libmisc/setxattrat_compat.c b/libmisc/setxattrat_compat.c
+new file mode 100644
+index 0000000..b8340b8
+--- /dev/null
++++ b/libmisc/setxattrat_compat.c
+@@ -0,0 +1,108 @@
++/*
++ File: getxattrat_compat.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <sys/xattr.h>
++#include <unistd.h>
++#include <fcntl.h>
++#include <errno.h>
++#include <stdlib.h>
++#include "xattrat.h"
++#include "libmisc/proc-self-fd.h"
++
++#define DECLARATIONS_ONLY
++#include "xattrat_compat.h"
++
++/* Defined in getxattrat_compat.c */
++extern int slow_empty_paths;
++
++static int
++xsetxattr(const char *path, int at_flags, const char *name, const void *value,
++ size_t size, int flags)
++{
++ if (at_flags & AT_SYMLINK_NOFOLLOW)
++ return lsetxattr(path, name, value, size, flags);
++ return setxattr(path, name, value, size, flags);
++}
++
++int
++setxattrat_compat(int dirfd, const char *path, int at_flags, const char *name,
++ const void *value, size_t size, int flags)
++{
++ struct proc_self_fd_buffer buffer;
++ static int no_setxattrat;
++ char *newpath;
++ int fd, ret;
++
++ if (at_flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) {
++ errno = EINVAL;
++ return -1;
++ }
++ if (!*path) {
++ if (!(at_flags & AT_EMPTY_PATH)) {
++ errno = ENOENT;
++ return -1;
++ }
++ }
++
++ if (dirfd == AT_FDCWD || *path == '/')
++ return xsetxattr(path, at_flags, name, value, size, flags);
++
++ if (!no_setxattrat) {
++ if (!*path && slow_empty_paths)
++ goto proc_self_fd_path;
++ ret = setxattrat(dirfd, path, at_flags, name, value, size,
++ flags);
++ if (ret != -1)
++ return ret;
++ if (need_slow_empty_paths(dirfd, path)) {
++ slow_empty_paths = 1;
++ goto proc_self_fd_path;
++ }
++ if (errno != ENOSYS)
++ return ret;
++ no_setxattrat = 1;
++ }
++
++proc_self_fd_path:
++ newpath = proc_self_fd_realpath(dirfd, path, &at_flags, &buffer);
++ if (!newpath)
++ return -1;
++ ret = xsetxattr(newpath, at_flags, name, value, size, flags);
++ proc_self_fd_free(newpath, &buffer);
++ if (ret != -1)
++ return ret;
++ if (errno == ENOENT) {
++ if (!*path) {
++ errno = ENOSYS;
++ return -1;
++ }
++ if (fcntl(dirfd, F_GETFL) != -1)
++ errno = ENOENT;
++ return -1;
++ } else if (errno != ENAMETOOLONG)
++ return ret;
++
++ fd = openat_with_flags(dirfd, path, at_flags);
++ if (fd == -1)
++ return -1;
++ ret = setxattr(proc_self_fd_path(fd, &buffer), name, value, size, flags);
++ close(fd);
++ return ret;
++}
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,638 @@
+From 9b37b117c5d5703aa7481f4c34f73b24c010ad25 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 12 Jun 2026 17:42:42 +0200
+Subject: [PATCH] Add the new walk_tree helper
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=9b37b117c5d5703aa7481f4c34f73b24c010ad25]
+
+Backport Changes:
+- Replaced attr 2.5.2's existing walk_tree.h and walk_tree.c in place
+ with the upstream walker. After the getfattr hardening, no caller uses
+ the old walker, so the separate old_walk_tree files and prerequisite
+ rename commit c21f3004ef0ad55c11ac0297ad3d0f82dfb4f768 are omitted.
+- Kept the existing walker entries in the Automake source lists; no
+ second walker implementation needs to be built.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 9b37b117c5d5703aa7481f4c34f73b24c010ad25)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ include/walk_tree.h | 44 ++--
+ libmisc/walk_tree.c | 479 ++++++++++++++++++++++++++------------------
+ 2 files changed, 302 insertions(+), 221 deletions(-)
+
+diff --git a/include/walk_tree.h b/include/walk_tree.h
+index b19ec23..60df8da 100644
+--- a/include/walk_tree.h
++++ b/include/walk_tree.h
+@@ -1,40 +1,40 @@
+ /*
+ File: walk_tree.h
+
+- Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
++ Copyright (C) 2007-2026 Andreas Gruenbacher <a.gruenbacher@computer.org>
+
+- This program is free software; you can redistribute it and/or modify it under
+- the terms of the GNU Lesser General Public License as published by the
+- Free Software Foundation; either version 2.1 of the License, or (at
+- your option) any later version.
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
+
+- This program is distributed in the hope that it will be useful, but WITHOUT
+- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+- License for more details.
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+- License along with this program. If not, see <http://www.gnu.org/licenses/>.
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+ #ifndef __WALK_TREE_H
+ #define __WALK_TREE_H
+
+-#define WALK_TREE_RECURSIVE 0x01
+-#define WALK_TREE_PHYSICAL 0x02
+-#define WALK_TREE_LOGICAL 0x04
+-#define WALK_TREE_DEREFERENCE 0x08
+-#define WALK_TREE_DEREFERENCE_TOPLEVEL 0x10
+-#define WALK_TREE_ONE_FILESYSTEM 0x20
++enum walk_flags {
++ WALK_TREE_RECURSIVE = 0x01,
++ WALK_TREE_PHYSICAL = 0x02,
++ WALK_TREE_LOGICAL = 0x04,
++ WALK_TREE_ONE_FILESYSTEM = 0x08,
+
+-#define WALK_TREE_TOPLEVEL 0x100
+-#define WALK_TREE_SYMLINK 0x200
+-#define WALK_TREE_FAILED 0x400
++ WALK_TREE_TOPLEVEL = 0x100,
++ WALK_TREE_FAILED = 0x200,
++};
+
+ struct stat;
+
+-extern int walk_tree(const char *path, int walk_flags, unsigned int num,
+- int (*func)(const char *, const struct stat *, int,
+- void *), void *arg);
++extern int walk_tree(const char *pathname, enum walk_flags walk_flags,
++ int (*func)(int, const char *, const char *,
++ unsigned char, enum walk_flags, void *),
++ void *arg);
+
+ #endif
+diff --git a/libmisc/walk_tree.c b/libmisc/walk_tree.c
+index e9a942f..5b1b576 100644
+--- a/libmisc/walk_tree.c
++++ b/libmisc/walk_tree.c
+@@ -1,259 +1,340 @@
+ /*
+ File: walk_tree.c
+
+- Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
++ Copyright (C) 2007-2026 Andreas Gruenbacher <a.gruenbacher@computer.org>
+
+- This program is free software; you can redistribute it and/or modify it under
+- the terms of the GNU Lesser General Public License as published by the
+- Free Software Foundation; either version 2.1 of the License, or (at
+- your option) any later version.
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
+
+- This program is distributed in the hope that it will be useful, but WITHOUT
+- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+- License for more details.
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+- License along with this program. If not, see <http://www.gnu.org/licenses/>.
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+ #include "config.h"
+-
+ #include <sys/types.h>
+ #include <sys/stat.h>
+ #include <unistd.h>
++#include <fcntl.h>
+ #include <sys/time.h>
+ #include <sys/resource.h>
+ #include <dirent.h>
+ #include <stdio.h>
++#include <stdlib.h>
+ #include <string.h>
+ #include <errno.h>
+
+ #include "walk_tree.h"
+
+-struct entry_handle {
+- struct entry_handle *prev, *next;
+- dev_t dev;
+- ino_t ino;
++struct dir_stack_entry {
++ struct dir_stack_entry *parent;
+ DIR *stream;
+ long pos;
++ dev_t dev;
++ ino_t ino;
+ };
+
++struct dir_hash_entry {
++ struct dir_hash_entry *next;
++ dev_t dev;
++ ino_t ino;
++};
++
++#define DIR_HASH_SIZE 128
++
+ struct walk_tree_args {
+- char path[FILENAME_MAX];
+- int walk_flags;
+- int (*func)(const char *, const struct stat *, int, void *);
++ enum walk_flags walk_flags;
++ int (*func)(int, const char *, const char *, unsigned char,
++ enum walk_flags, void *);
+ void *arg;
+ int depth;
+- struct entry_handle dirs;
+- struct entry_handle *closed;
+ unsigned int num_dir_handles;
+- struct stat st;
+ dev_t dev;
++ struct dir_hash_entry **dir_hash;
+ };
+
+-static int walk_tree_visited(struct entry_handle *dirs, dev_t dev, ino_t ino)
++static int dir_visited(struct dir_stack_entry *entry, dev_t dev, ino_t ino)
+ {
+- struct entry_handle *i;
++ while (entry) {
++ if (entry->dev == dev && entry->ino == ino)
++ return 1;
++ entry = entry->parent;
++ }
++ return 0;
++}
+
+- for (i = dirs->next; i != dirs; i = i->next)
+- if (i->dev == dev && i->ino == ino)
++static unsigned int dir_hash(dev_t dev, ino_t ino)
++{
++ return (dev ^ ino) % DIR_HASH_SIZE;
++}
++
++static int dir_hash_insert_unique(struct walk_tree_args *args, struct stat *st)
++{
++ unsigned int bucket = dir_hash(st->st_dev, st->st_ino);
++ struct dir_hash_entry *entry;
++
++ if (!args->dir_hash) {
++ /* allocate the directory hash table */
++ args->dir_hash = calloc(DIR_HASH_SIZE, sizeof(*args->dir_hash));
++ if (!args->dir_hash)
++ return -1;
++ }
++
++ entry = args->dir_hash[bucket];
++ while (entry) {
++ if (entry->dev == st->st_dev && entry->ino == st->st_ino)
+ return 1;
++ entry = entry->next;
++ }
++
++ entry = malloc(sizeof(*entry));
++ if (!entry)
++ return -1;
++
++ entry->dev = st->st_dev;
++ entry->ino = st->st_ino;
++ entry->next = args->dir_hash[bucket];
++ args->dir_hash[bucket] = entry;
+ return 0;
+ }
+
+-static int walk_tree_rec(struct walk_tree_args *args)
++static char *new_dirname(const char *dirname, const char *pathname)
+ {
+- int follow_symlinks = (args->walk_flags & WALK_TREE_LOGICAL) ||
+- ((args->walk_flags & WALK_TREE_DEREFERENCE) &&
+- !(args->walk_flags & WALK_TREE_PHYSICAL) &&
+- args->depth == 0);
+- int have_dir_stat = 0, flags = args->walk_flags, err;
+- struct entry_handle dir;
+-
+- /*
+- * If (walk_flags & WALK_TREE_PHYSICAL), do not traverse symlinks.
+- * If (walk_flags & WALK_TREE_LOGICAL), traverse all symlinks.
+- * Otherwise, traverse only top-level symlinks.
+- */
+- if (args->depth == 0)
+- flags |= WALK_TREE_TOPLEVEL;
++ size_t dirlen, pathlen;
++ char *newname, *n;
++ int need_slash;
++
++ if (*dirname == '\0' && *pathname == '.' &&
++ (*(pathname+1) == '/' || *(pathname+1) == '\0')) {
++ pathname++;
++ while (*pathname == '/')
++ pathname++;
++ }
+
+- if (lstat(args->path, &args->st) != 0)
+- return args->func(args->path, NULL, flags | WALK_TREE_FAILED,
+- args->arg);
++ dirlen = strlen(dirname);
++ pathlen = strlen(pathname);
++ need_slash = (pathlen > 0 && pathname[pathlen - 1] != '/');
++ n = newname = malloc(dirlen + pathlen + need_slash + 1);
++ if (newname == NULL)
++ return NULL;
++ memcpy(n, dirname, dirlen);
++ n += dirlen;
++ memcpy(n, pathname, pathlen);
++ n += pathlen;
++ if (need_slash)
++ *n++ = '/';
++ *n++ = '\0';
++ return newname;
++}
+
+- if (flags & WALK_TREE_ONE_FILESYSTEM) {
+- if (args->dev == 0)
+- args->dev = args->st.st_dev;
+- else if (args->st.st_dev != args->dev)
+- return 0;
+- }
++static int walk_tree_rec(int dirfd, const char *dirname, const char *pathname,
++ unsigned char dirtype, struct walk_tree_args *args,
++ struct dir_stack_entry *parent_dir)
++{
++ enum walk_flags walk_flags = args->walk_flags;
++ struct dir_stack_entry this_dir = {
++ .parent = parent_dir,
++ };
++ int follow;
++ struct stat st;
++ char *mydirname = NULL;
++ int dir_open_flags;
++ int fd = -1;
++ struct dirent *entry;
++ int err = 0;
+
+- if (S_ISLNK(args->st.st_mode)) {
+- flags |= WALK_TREE_SYMLINK;
+- if ((flags & WALK_TREE_DEREFERENCE) ||
+- ((flags & WALK_TREE_TOPLEVEL) &&
+- (flags & WALK_TREE_DEREFERENCE_TOPLEVEL))) {
+- if (stat(args->path, &args->st) != 0)
+- return args->func(args->path, NULL,
+- flags | WALK_TREE_FAILED,
+- args->arg);
+- dir.dev = args->st.st_dev;
+- dir.ino = args->st.st_ino;
+- have_dir_stat = 1;
++ if (args->depth == 0)
++ walk_flags |= WALK_TREE_TOPLEVEL;
++
++ follow = (walk_flags & WALK_TREE_LOGICAL) ||
++ (!(walk_flags & WALK_TREE_PHYSICAL) && !args->depth);
++
++ if (!(walk_flags & WALK_TREE_RECURSIVE) ||
++ (dirtype != DT_UNKNOWN &&
++ dirtype != DT_DIR &&
++ (dirtype != DT_LNK ||
++ ((walk_flags & WALK_TREE_PHYSICAL) &&
++ !(walk_flags & WALK_TREE_TOPLEVEL)))))
++ goto visit_this;
++
++ if (args->num_dir_handles == 0) {
++ struct dir_stack_entry *top;
++
++close_another_parent:
++ /* Close the topmost parent directory handle still open. */
++ top = parent_dir;
++ if (!top || !top->stream) {
++ errno = ENFILE;
++ goto fail;
+ }
+- } else if (S_ISDIR(args->st.st_mode)) {
+- dir.dev = args->st.st_dev;
+- dir.ino = args->st.st_ino;
+- have_dir_stat = 1;
++ while (top->parent && top->parent->stream)
++ top = top->parent;
++ top->pos = telldir(top->stream);
++ closedir(top->stream);
++ top->stream = NULL;
++ args->num_dir_handles++;
+ }
+- err = args->func(args->path, &args->st, flags, args->arg);
+-
+- /*
+- * Recurse if WALK_TREE_RECURSIVE and the path is:
+- * a dir not from a symlink
+- * a link and follow_symlinks
+- */
+- if ((flags & WALK_TREE_RECURSIVE) &&
+- ((!(flags & WALK_TREE_SYMLINK) && S_ISDIR(args->st.st_mode)) ||
+- ((flags & WALK_TREE_SYMLINK) && follow_symlinks))) {
+- struct dirent *entry;
+-
+- /*
+- * Check if we have already visited this directory to break
+- * endless loops.
+- *
+- * If we haven't stat()ed the file yet, do an opendir() for
+- * figuring out whether we have a directory, and check whether
+- * the directory has been visited afterwards. This saves a
+- * system call for each non-directory found.
+- */
+- if (have_dir_stat && walk_tree_visited(&args->dirs, dir.dev, dir.ino))
+- return err;
+-
+- if (args->num_dir_handles == 0 && args->closed->prev != &args->dirs) {
+-close_another_dir:
+- /* Close the topmost directory handle still open. */
+- args->closed = args->closed->prev;
+- args->closed->pos = telldir(args->closed->stream);
+- closedir(args->closed->stream);
+- args->closed->stream = NULL;
+- args->num_dir_handles++;
+- }
+
+- dir.stream = opendir(args->path);
+- if (!dir.stream) {
+- if (errno == ENFILE && args->closed->prev != &args->dirs) {
+- /* Ran out of file descriptors. */
+- args->num_dir_handles = 0;
+- goto close_another_dir;
+- }
++ dir_open_flags = O_RDONLY | O_DIRECTORY | (follow ? 0 : O_NOFOLLOW);
++ fd = openat(dirfd, pathname, dir_open_flags);
++ if (fd == -1) {
++ if (errno == ENOTDIR || errno == ENOENT || errno == ELOOP)
++ goto visit_this;
++ if (errno != ENFILE)
++ goto fail;
++ /* Ran out of file descriptors. */
++ args->num_dir_handles = 0;
++ goto close_another_parent;
++ }
++ args->num_dir_handles--;
++ if (dirtype == DT_UNKNOWN && !follow)
++ dirtype = DT_DIR;
+
+- /*
+- * PATH may be a symlink to a regular file, or a dead
+- * symlink which we didn't follow above.
+- */
+- if (errno != ENOTDIR && errno != ENOENT)
+- err += args->func(args->path, NULL, flags |
+- WALK_TREE_FAILED, args->arg);
+- return err;
+- }
++ if (fstat(fd, &st) == -1)
++ goto fail;
++ if (walk_flags & WALK_TREE_ONE_FILESYSTEM) {
++ if (args->dev == 0)
++ args->dev = st.st_dev;
++ else if (st.st_dev != args->dev)
++ goto dir_visited;
++ }
+
+- /* See walk_tree_visited() comment above... */
+- if (!have_dir_stat) {
+- if (stat(args->path, &args->st) != 0)
+- goto skip_dir;
+- dir.dev = args->st.st_dev;
+- dir.ino = args->st.st_ino;
+- if (walk_tree_visited(&args->dirs, dir.dev, dir.ino))
+- goto skip_dir;
++ if (walk_flags & WALK_TREE_LOGICAL) {
++ if (dir_visited(parent_dir, st.st_dev, st.st_ino))
++ goto dir_visited;
++ this_dir.dev = st.st_dev;
++ this_dir.ino = st.st_ino;
++ } else {
++ int ret = dir_hash_insert_unique(args, &st);
++ if (ret == 1)
++ goto dir_visited;
++ if (ret == -1)
++ goto fail;
++ }
++ goto visit_this;
++
++dir_visited:
++ close(fd);
++ fd = -1;
++ if (dirtype == DT_UNKNOWN) {
++ if (fstatat(dirfd, pathname, &st, AT_SYMLINK_NOFOLLOW) == -1)
++ goto fail;
++ dirtype = IFTODT(st.st_mode);
++ }
++ if (dirtype == DT_DIR)
++ goto out;
++
++visit_this:
++ err += args->func(dirfd, dirname, pathname, dirtype,
++ walk_flags, args->arg);
++
++ if (fd == -1)
++ goto out;
++ this_dir.stream = fdopendir(fd);
++ if (this_dir.stream == NULL)
++ goto fail;
++
++ mydirname = new_dirname(dirname, pathname);
++ if (mydirname == NULL)
++ goto fail;
++
++ args->depth++;
++ while ((entry = readdir(this_dir.stream)) != NULL) {
++ if (!strcmp(entry->d_name, ".") ||
++ !strcmp(entry->d_name, ".."))
++ continue;
++
++ err += walk_tree_rec(fd, mydirname, entry->d_name,
++ entry->d_type, args, &this_dir);
++
++ if (!this_dir.stream) {
++ /* Reopen the directory handle. */
++ fd = openat(dirfd, pathname, dir_open_flags);
++ if (fd == -1)
++ goto fail_depth;
++ args->num_dir_handles--;
++ this_dir.stream = fdopendir(fd);
++ if (!this_dir.stream)
++ goto fail_depth;
++ seekdir(this_dir.stream, this_dir.pos);
+ }
++ }
++ args->depth--;
+
+- /* Insert into the list of handles. */
+- dir.next = args->dirs.next;
+- dir.prev = &args->dirs;
+- dir.prev->next = &dir;
+- dir.next->prev = &dir;
+- args->num_dir_handles--;
+-
+- while ((entry = readdir(dir.stream)) != NULL) {
+- char *path_end;
+-
+- if (!strcmp(entry->d_name, ".") ||
+- !strcmp(entry->d_name, ".."))
+- continue;
+- path_end = strchr(args->path, 0);
+- if ((path_end - args->path) + strlen(entry->d_name) + 1 >=
+- FILENAME_MAX) {
+- errno = ENAMETOOLONG;
+- err += args->func(args->path, NULL,
+- flags | WALK_TREE_FAILED,
+- args->arg);
+- continue;
+- }
+- *path_end++ = '/';
+- strcpy(path_end, entry->d_name);
+- args->depth++;
+- err += walk_tree_rec(args);
+- args->depth--;
+- *--path_end = 0;
+- if (!dir.stream) {
+- /* Reopen the directory handle. */
+- dir.stream = opendir(args->path);
+- if (!dir.stream)
+- return err + args->func(args->path,
+- NULL,
+- flags | WALK_TREE_FAILED,
+- args->arg);
+- seekdir(dir.stream, dir.pos);
+-
+- args->closed = args->closed->next;
+- args->num_dir_handles--;
+- }
+- }
++ if (closedir(this_dir.stream) != 0) {
++ this_dir.stream = NULL;
++ goto fail;
++ }
++ this_dir.stream = NULL;
+
+- /* Remove from the list of handles. */
+- dir.prev->next = dir.next;
+- dir.next->prev = dir.prev;
++out:
++ free(mydirname);
++ if (this_dir.stream) {
++ closedir(this_dir.stream);
++ args->num_dir_handles++;
++ fd = -1;
++ }
++ if (fd != -1) {
++ close(fd);
+ args->num_dir_handles++;
+-
+- skip_dir:
+- if (closedir(dir.stream) != 0)
+- err += args->func(args->path, NULL,
+- flags | WALK_TREE_FAILED, args->arg);
+ }
+ return err;
++
++fail:
++ err += args->func(dirfd, dirname, pathname, dirtype,
++ walk_flags | WALK_TREE_FAILED, args->arg);
++ goto out;
++
++fail_depth:
++ args->depth--;
++ goto fail;
+ }
+
+-int walk_tree(const char *path, int walk_flags, unsigned int num,
+- int (*func)(const char *, const struct stat *, int, void *),
++/*
++ * If (walk_flags & WALK_TREE_PHYSICAL), do not traverse symlinks.
++ * If (walk_flags & WALK_TREE_LOGICAL), traverse all symlinks.
++ * Otherwise, traverse only top-level symlinks.
++ */
++int walk_tree(const char *pathname, enum walk_flags walk_flags,
++ int (*func)(int, const char *, const char *, unsigned char,
++ enum walk_flags, void *),
+ void *arg)
+ {
+- struct walk_tree_args args;
+-
+- args.num_dir_handles = num;
+- if (args.num_dir_handles < 1) {
+- struct rlimit rlimit;
+-
+- args.num_dir_handles = 1;
+- if (getrlimit(RLIMIT_NOFILE, &rlimit) == 0 &&
+- rlimit.rlim_cur >= 2)
+- args.num_dir_handles = rlimit.rlim_cur / 2;
+- }
+- args.dirs.next = &args.dirs;
+- args.dirs.prev = &args.dirs;
+- args.closed = &args.dirs;
+- if (strlen(path) >= FILENAME_MAX) {
+- errno = ENAMETOOLONG;
+- return func(path, NULL, WALK_TREE_FAILED, arg);
++ struct walk_tree_args args = {
++ .walk_flags = walk_flags,
++ .func = func,
++ .arg = arg,
++ };
++ struct rlimit rlimit;
++ int err;
++
++ /* number of directory file descriptors to keep open at a time */
++ if (getrlimit(RLIMIT_NOFILE, &rlimit) != 0)
++ rlimit.rlim_cur = 0;
++ args.num_dir_handles = rlimit.rlim_cur / 4 + 1;
++
++ err = walk_tree_rec(AT_FDCWD, "", pathname, DT_UNKNOWN, &args, NULL);
++
++ if (args.dir_hash) {
++ /* destroy the directory hash table */
++ int n;
++
++ for (n = 0; n < DIR_HASH_SIZE; n++) {
++ struct dir_hash_entry *entry = args.dir_hash[n];
++ while (entry) {
++ struct dir_hash_entry *next = entry->next;
++ free(entry);
++ entry = next;
++ }
++ }
++ free(args.dir_hash);
+ }
+- strcpy(args.path, path);
+- args.walk_flags = walk_flags;
+- args.func = func;
+- args.arg = arg;
+- args.depth = 0;
+- args.dev = 0;
+-
+- return walk_tree_rec(&args);
++
++ return err;
+ }
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,63 @@
+From 4fce17388e014a06cca16914ab3b7e4a0d117856 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 19 Jun 2026 19:35:09 +0200
+Subject: [PATCH] setfattr: Do not ignore --no-dereference after --restore
+
+Process the entire command line before starting to restore all files specified
+by --restore=file options. That way, --no-dereference will take effect even
+when it occurs at the end of the command line.
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=4fce17388e014a06cca16914ab3b7e4a0d117856]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 4fce17388e014a06cca16914ab3b7e4a0d117856)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ tools/setfattr.c | 17 +++++++++++++++--
+ 1 file changed, 15 insertions(+), 2 deletions(-)
+
+diff --git a/tools/setfattr.c b/tools/setfattr.c
+index 7906fe9..7b30159 100644
+--- a/tools/setfattr.c
++++ b/tools/setfattr.c
+@@ -193,6 +193,7 @@ void help(void)
+
+ int main(int argc, char *argv[])
+ {
++ char **restore_args = NULL;
+ int opt;
+
+ progname = basename(argv[0]);
+@@ -234,8 +235,14 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'B': /* restore */
+- opt_restore = 1;
+- restore(optarg);
++ opt_restore++;
++ restore_args = realloc(restore_args,
++ opt_restore * sizeof(*restore_args));
++ if (!restore_args) {
++ perror(progname);
++ exit(1);
++ }
++ restore_args[opt_restore - 1] = optarg;
+ break;
+
+ case 'V':
+@@ -253,6 +260,12 @@ int main(int argc, char *argv[])
+ if (!(((opt_remove || opt_set) && optind < argc) || opt_restore))
+ goto synopsis;
+
++ if (opt_restore) {
++ for (opt = 0; opt < opt_restore; opt++)
++ restore(restore_args[opt]);
++ free(restore_args);
++ }
++
+ while (optind < argc) {
+ do_set(argv[optind], unquote(opt_name), opt_value);
+ optind++;
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,140 @@
+From 1cea2fc5e8b5c8a5f29abf1141399e01a57a3154 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Sat, 20 Jun 2026 16:17:02 +0200
+Subject: [PATCH] Add openat2 syscall wrapper
+
+Add a syscall wrapper for the openat2() syscall added in kernel version v5.6
+from March 2020.
+
+On systems where the openat2() syscall is available but shouldn't be used, use
+the --disable-openat2 configure option.
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=1cea2fc5e8b5c8a5f29abf1141399e01a57a3154]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 1cea2fc5e8b5c8a5f29abf1141399e01a57a3154)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ configure.ac | 14 ++++++++++++++
+ include/Makemodule.am | 1 +
+ include/openat2.h | 16 ++++++++++++++++
+ libmisc/Makemodule.am | 7 +++++++
+ libmisc/openat2.c | 30 ++++++++++++++++++++++++++++++
+ 5 files changed, 68 insertions(+)
+ create mode 100644 include/openat2.h
+ create mode 100644 libmisc/openat2.c
+
+diff --git a/configure.ac b/configure.ac
+index 0139fe6..7d13edf 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -37,6 +37,20 @@ AC_SYS_LARGEFILE
+ AM_PROG_AR
+ LT_INIT
+
++AC_CHECK_FUNCS(openat2)
++AC_CHECK_HEADERS([linux/openat2.h])
++AC_CHECK_DECL([__NR_openat2], [], [], [[#include <sys/syscall.h>]])
++if test "x$ac_cv_have_decl___NR_openat2" = xyes -a \
++ "x$ac_cv_header_linux_openat2_h" = xyes; then
++ use_openat2=yes
++else
++ use_openat2=no
++fi
++AM_CONDITIONAL([USE_OPENAT2], [test "x$use_openat2" = xyes])
++AM_CONDITIONAL([HAVE_OPENAT2], [test "x$ac_cv_func_openat2" = xyes])
++AS_IF([test "x$use_openat2" = xyes],
++ [AC_DEFINE([USE_OPENAT2], [1], [Define to use openat2 syscall])])
++
+ AC_CHECK_FUNCS(getxattrat setxattrat listxattrat removexattrat)
+ AM_CONDITIONAL([NEED_XATTRAT],
+ [test "x$ac_cv_func_getxattrat" = xno -o \
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index 54f8ee0..3a0b50d 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -4,6 +4,7 @@ noinst_HEADERS += \
+ include/libattr.h \
+ include/misc.h \
+ include/nls.h \
++ include/openat2.h \
+ include/visibility-hidden.h \
+ include/walk_tree.h \
+ include/xattrat.h \
+diff --git a/include/openat2.h b/include/openat2.h
+new file mode 100644
+index 0000000..d267895
+--- /dev/null
++++ b/include/openat2.h
+@@ -0,0 +1,16 @@
++#ifndef MISC_OPENAT2_H
++#define MISC_OPENAT2_H
++
++#include "config.h"
++#include <stdint.h>
++
++#ifdef USE_OPENAT2
++#include <linux/openat2.h>
++
++#ifndef HAVE_OPENAT2
++int openat2(int dirfd, const char *pathname, const struct open_how *how,
++ size_t size);
++#endif
++#endif
++
++#endif /* MISC_OPENAT2_H */
+diff --git a/libmisc/Makemodule.am b/libmisc/Makemodule.am
+index a42ae32..99b1a49 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -19,3 +19,10 @@ if NEED_XATTRAT
+ libmisc_la_SOURCES += \
+ libmisc/xattrat.c
+ endif
++
++if USE_OPENAT2
++if !HAVE_OPENAT2
++libmisc_la_SOURCES += \
++ libmisc/openat2.c
++endif
++endif
+diff --git a/libmisc/openat2.c b/libmisc/openat2.c
+new file mode 100644
+index 0000000..bdc78e1
+--- /dev/null
++++ b/libmisc/openat2.c
+@@ -0,0 +1,30 @@
++/*
++ File: openat2.c
++
++ Copyright (C) 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
++
++ This library is free software; you can redistribute it and/or
++ modify it under the terms of the GNU Lesser General Public
++ License as published by the Free Software Foundation; either
++ version 2.1 of the License, or (at your option) any later version.
++
++ This library is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
++*/
++
++#include "config.h"
++#include <sys/syscall.h>
++#include <unistd.h>
++
++#include "openat2.h"
++
++int openat2(int dirfd, const char *pathname, const struct open_how *how,
++ size_t size)
++{
++ return syscall(__NR_openat2, dirfd, pathname, how, size);
++}
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,390 @@
+From 641ea6fcc556c1f34b77efb9cd3f876dff0a0a07 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Fri, 12 Jun 2026 17:30:22 +0200
+Subject: [PATCH] harden getfattr
+
+With this commit, the behavior of option -R (--recursive) changes as follows
+when used together with -h (--no-dereference): previously, after setting things
+up as follows:
+
+ $ mkdir d
+ $ touch d/f
+ $ setfattr -n user.name -v value d/f
+ $ ln -s d l
+
+The following command:
+
+ $ getfattr -Rdh l
+
+would produce no output because the -h (--no-dereference) option prevented
+getfattr from following l and resursing into d. This was wrong: the -h
+(--no-dereference) option is supposed to control whether to dereference the
+files visited, but not which files are visited. So with this commit, the
+following output will be produced instead:
+
+ $ getfattr -Rdh l
+ > # file: l/f
+ > user.name="value"
+ >
+
+This is because by default, getfattr follows symbolic links specified on the
+command line, but not symbolic links encountered while traversing the directory
+tree. The -L (--logical) option changes the behavior to always follow symbolic
+links, while the -P (--physical) option changes the behavior to never follow
+them.
+
+Fixes: CVE-2026-54371 ("attr < 2.6.0 Symlink Traversal Privilege Escalation via getfattr")
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=641ea6fcc556c1f34b77efb9cd3f876dff0a0a07]
+
+Backport Changes:
+- Retained the attr 2.5.2 non-static declarations because the unrelated
+ tools cleanup commit is not required by the security fix.
+- Kept the existing getfattr.test filename used by the 2.5.2 ptest harness.
+- Included seven sort-getfattr-output pipelines in the root test and
+ the helper's security.selinux filter from upstream commit
+ 303335494a326026bc37e8d83ca47fa1a4d8dde6 so output remains
+ stable on SELinux hosts.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 641ea6fcc556c1f34b77efb9cd3f876dff0a0a07)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ man/man1/getfattr.1 | 3 +-
+ test/root/getfattr.test | 20 +++++---
+ test/sort-getfattr-output | 6 +++++-
+ tools/getfattr.c | 100 ++++++++++++++++++++++------------------
+ 4 files changed, 73 insertions(+), 56 deletions(-)
+
+diff --git a/man/man1/getfattr.1 b/man/man1/getfattr.1
+index df8bb8a..c527271 100644
+--- a/man/man1/getfattr.1
++++ b/man/man1/getfattr.1
+@@ -79,8 +79,7 @@ while strings encoded as hexadecimal and base64 are prefixed with
+ .TP
+ .BR \-h ", " \-\-no-dereference
+ Do not dereference symlinks. Instead of the file a symlink refers to, the
+-symlink itself is examined. Unless doing a logical (\-L) traversal, do not
+-traverse symlinks to directories.
++symlink itself is examined.
+ .TP
+ .BR \-m " \f2pattern\f1, " \-\-match "=\f2pattern\f1"
+ Only include attributes with names matching the regular expression
+diff --git a/test/root/getfattr.test b/test/root/getfattr.test
+index 8319de7..4fb0754 100644
+--- a/test/root/getfattr.test
++++ b/test/root/getfattr.test
+@@ -11,19 +11,19 @@ Cry immediately if we are not running as root.
+ $ setfattr -h -n trusted.test -v test l
+
+ This case should be obvious:
+- $ getfattr -m- -d f
++ $ getfattr -m- -d f | sort-getfattr-output
+ > # file: f
+ > user.test="test"
+ >
+
+ Without -h, we dereference symlinks:
+- $ getfattr -m- -d l
++ $ getfattr -m- -d l | sort-getfattr-output
+ > # file: l
+ > user.test="test"
+ >
+
+ With -h, we do not dereference symlinks:
+- $ getfattr -m- -hd l
++ $ getfattr -m- -hd l | sort-getfattr-output
+ > # file: l
+ > trusted.test="test"
+ >
+@@ -55,7 +55,13 @@ Make sure we follow symlinks on the command line only when we should:
+ > user.test="test"
+ >
+
+- $ getfattr -m- -Rhd here
++ $ getfattr -m- -Rhd here | sort-getfattr-output
++ > # file: here/f
++ > user.test="test"
++ >
++ > # file: here/l
++ > trusted.test="test"
++ >
+
+ $ getfattr -m- -RLhd here | sort-getfattr-output
+ > # file: here/f
+@@ -65,7 +71,7 @@ Make sure we follow symlinks on the command line only when we should:
+ > trusted.test="test"
+ >
+
+- $ getfattr -m- -RPhd here
++ $ getfattr -m- -RPhd here | sort-getfattr-output
+
+ Make sure we recurse into sub-directories:
+ $ mkdir sub
+@@ -89,7 +95,7 @@ Make sure we recurse into sub-directories:
+ Make sure we follow symlinks to directories only when we should:
+ $ mkdir sub2
+ $ ln -s ../sub sub2/to-sub
+- $ getfattr -m- -Rhd sub2
++ $ getfattr -m- -Rhd sub2 | sort-getfattr-output
+
+ $ getfattr -m- -RLhd sub2 | sort-getfattr-output
+ > # file: sub2/to-sub/f
+@@ -99,7 +105,7 @@ Make sure we follow symlinks to directories only when we should:
+ > trusted.test="test"
+ >
+
+- $ getfattr -m- -RPhd sub2
++ $ getfattr -m- -RPhd sub2 | sort-getfattr-output
+
+ Symlink loop detection:
+ $ ln -s .. sub/up
+diff --git a/test/sort-getfattr-output b/test/sort-getfattr-output
+index 9901ed3..1d5d135 100755
+--- a/test/sort-getfattr-output
++++ b/test/sort-getfattr-output
+@@ -1,4 +1,8 @@
+ #! /usr/bin/perl -w
+
+ undef $/;
+-print join("\n\n", sort split(/\n\n/, <>)), "\n\n";
++my @blocks = sort split(/\n\n/, <>);
++for my $block (@blocks) {
++ my @lines = grep { !/^security\.selinux/ } split(/\n/, $block);
++ print join("\n", @lines) . "\n\n" if @lines > 1;
++}
+diff --git a/tools/getfattr.c b/tools/getfattr.c
+index c0b733d..2823578 100644
+--- a/tools/getfattr.c
++++ b/tools/getfattr.c
+@@ -24,6 +24,7 @@
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
++#include <fcntl.h>
+ #include <errno.h>
+ #include <ctype.h>
+ #include <getopt.h>
+@@ -32,6 +33,7 @@
+ #include <libgen.h>
+ #include <sys/xattr.h>
+
++#include "xattrat_compat.h"
+ #include "walk_tree.h"
+ #include "misc.h"
+
+@@ -55,7 +57,8 @@ static const struct option long_options[] = {
+ { NULL, 0, 0, 0 }
+ };
+
+-int walk_flags = WALK_TREE_DEREFERENCE;
++enum walk_flags walk_flags = 0;
++static int at_flags = 0;
+ int opt_dump; /* dump attribute values (or only list the names) */
+ char *opt_name; /* dump named attributes */
+ char *opt_name_pattern = "^user\\."; /* include only matching names */
+@@ -80,18 +83,6 @@ static const char *xquote(const char *str, const char *quote_chars)
+ return q;
+ }
+
+-int do_getxattr(const char *path, const char *name, void *value, size_t size)
+-{
+- return ((walk_flags & WALK_TREE_DEREFERENCE) ?
+- getxattr : lgetxattr)(path, name, value, size);
+-}
+-
+-int do_listxattr(const char *path, char *list, size_t size)
+-{
+- return ((walk_flags & WALK_TREE_DEREFERENCE) ?
+- listxattr : llistxattr)(path, list, size);
+-}
+-
+ const char *strerror_ea(int err)
+ {
+ #ifdef __linux__
+@@ -229,7 +220,9 @@ const char *encode(const char *value, size_t *size)
+ return encoded;
+ }
+
+-int print_attribute(const char *path, const char *name, int *header_printed)
++int print_attribute(int dirfd, const char *dirname, const char *pathname,
++ const char *fullname, const char *name,
++ int *header_printed)
+ {
+ static char *value;
+ static size_t value_size;
+@@ -237,9 +230,9 @@ int print_attribute(const char *path, const char *name, int *header_printed)
+ size_t length = 0;
+
+ if (opt_dump || opt_value_only) {
+- rval = do_getxattr(path, name, NULL, 0);
++ rval = getxattrat(dirfd, pathname, at_flags, name, NULL, 0);
+ if (rval < 0) {
+- fprintf(stderr, "%s: ", xquote(path, "\n\r"));
++ fprintf(stderr, "%s: ", xquote(fullname, "\n\r"));
+ fprintf(stderr, "%s: %s\n", xquote(name, "\n\r"),
+ strerror_ea(errno));
+ return 1;
+@@ -249,9 +242,10 @@ int print_attribute(const char *path, const char *name, int *header_printed)
+ had_errors++;
+ return 1;
+ }
+- rval = do_getxattr(path, name, value, value_size);
++ rval = getxattrat(dirfd, pathname, at_flags, name, value,
++ value_size);
+ if (rval < 0) {
+- fprintf(stderr, "%s: ", xquote(path, "\n\r"));
++ fprintf(stderr, "%s: ", xquote(fullname, "\n\r"));
+ fprintf(stderr, "%s: %s\n", xquote(name, "\n\r"),
+ strerror_ea(errno));
+ return 1;
+@@ -260,24 +254,20 @@ int print_attribute(const char *path, const char *name, int *header_printed)
+ }
+
+ if (opt_strip_leading_slash) {
+- if (*path == '/') {
++ if (*fullname == '/') {
+ if (!absolute_warning) {
+- fprintf(stderr, _("%s: Removing leading '/' "
+- "from absolute path names\n"),
++ fprintf(stderr, _("%s: Removing leading "
++ "'/' from absolute path names\n"),
+ progname);
+ absolute_warning = 1;
+ }
+- while (*path == '/')
+- path++;
+- } else if (*path == '.' && *(path+1) == '/')
+- while (*++path == '/')
+- /* nothing */ ;
+- if (*path == '\0')
+- path = ".";
++ while (*fullname == '/')
++ fullname++;
++ }
+ }
+
+ if (!*header_printed && !opt_value_only) {
+- printf("# file: %s\n", xquote(path, "\n\r"));
++ printf("# file: %s\n", xquote(fullname, "\n\r"));
+ *header_printed = 1;
+ }
+
+@@ -294,7 +284,8 @@ int print_attribute(const char *path, const char *name, int *header_printed)
+ return 0;
+ }
+
+-int list_attributes(const char *path, int *header_printed)
++int list_attributes(int dirfd, const char *dirname, const char *pathname,
++ const char *fullname, int *header_printed)
+ {
+ static char *list;
+ static size_t list_size;
+@@ -304,10 +295,10 @@ int list_attributes(const char *path, int *header_printed)
+ ssize_t length;
+ char *l;
+
+- length = do_listxattr(path, NULL, 0);
++ length = listxattrat(dirfd, pathname, at_flags, NULL, 0);
+ if (length < 0) {
+- fprintf(stderr, "%s: %s: %s\n", progname, xquote(path, "\n\r"),
+- strerror_ea(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname,
++ xquote(fullname, "\n\r"), strerror_ea(errno));
+ had_errors++;
+ return 1;
+ } else if (length == 0)
+@@ -319,9 +310,10 @@ int list_attributes(const char *path, int *header_printed)
+ return 1;
+ }
+
+- length = do_listxattr(path, list, list_size);
++ length = listxattrat(dirfd, pathname, at_flags, list, list_size);
+ if (length < 0) {
+- perror(xquote(path, "\n\r"));
++ fprintf(stderr, "%s: %s: %s\n", progname,
++ xquote(fullname, "\n\r"), strerror_ea(errno));
+ had_errors++;
+ return 1;
+ }
+@@ -351,27 +343,44 @@ int list_attributes(const char *path, int *header_printed)
+ int n;
+
+ for (n = 0; n < num_names; n++)
+- print_attribute(path, names[n], header_printed);
++ print_attribute(dirfd, dirname, pathname, fullname,
++ names[n], header_printed);
+ }
+ return 0;
+ }
+
+-int do_print(const char *path, const struct stat *stat, int walk_flags,
+- void *unused)
++int do_print(int dirfd, const char *dirname, const char *pathname,
++ unsigned char dirtype, enum walk_flags walk_flags,
++ void *unused)
+ {
++ static char *__fullname;
++ const char *fullname;
+ int header_printed = 0;
+ int err = 0;
+
++ if (*dirname) {
++ free(__fullname);
++ __fullname = NULL;
++ if (asprintf(&__fullname, "%s%s", dirname, pathname) == -1) {
++ fprintf(stderr, "%s: %s", progname, strerror(errno));
++ return 1;
++ }
++ fullname = __fullname;
++ } else
++ fullname = pathname;
++
+ if (walk_flags & WALK_TREE_FAILED) {
+- fprintf(stderr, "%s: %s: %s\n", progname, xquote(path, "\n\r"),
+- strerror(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname,
++ xquote(fullname, "\n\r"), strerror(errno));
+ return 1;
+ }
+
+ if (opt_name)
+- err = print_attribute(path, opt_name, &header_printed);
++ err = print_attribute(dirfd, dirname, pathname, fullname,
++ opt_name, &header_printed);
+ else
+- err = list_attributes(path, &header_printed);
++ err = list_attributes(dirfd, dirname, pathname, fullname,
++ &header_printed);
+
+ if (header_printed)
+ puts("");
+@@ -435,7 +444,7 @@ int main(int argc, char *argv[])
+ return 0;
+
+ case 'h': /* do not dereference symlinks */
+- walk_flags &= ~WALK_TREE_DEREFERENCE;
++ at_flags |= AT_SYMLINK_NOFOLLOW;
+ break;
+
+ case 'n': /* get named attribute */
+@@ -492,8 +501,8 @@ int main(int argc, char *argv[])
+ }
+
+ while (optind < argc) {
+- had_errors += walk_tree(argv[optind], walk_flags, 0,
+- do_print, NULL);
++ had_errors += walk_tree(argv[optind], walk_flags, do_print,
++ NULL);
+ optind++;
+ }
+
+@@ -505,4 +514,3 @@ synopsis:
+ progname, CMD_LINE_SPEC, progname);
+ return 2;
+ }
+-
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,476 @@
+From 3fb06b9ba314d37035d0877e6de313de754f1ac8 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Sun, 21 Jun 2026 01:46:49 +0200
+Subject: [PATCH] harden setfattr --restore
+
+In setfattr --restore, use functions that support the AT_SYMLINK_NOFOLLOW flag
+([l]setxattr() -> setxattrat(), [l]removexattr() -> removexattrat()). Add a -P
+(--physical) option to use together with --restore.
+
+In physical restore mode, use openat2(RESOLVE_NO_SYMLINKS) for resolving the
+pathname components leading up to the last one. Then, depending on the -h
+(--no-dereference) option, use the AT_SYMLINK_NOFOLLOW flag on the last
+pathname component.
+
+On systems where openat2() *is* available, warn when attempting a restore
+without also using the -P (--physical) and -h (--no-dereference) options as
+that can be dangerous. The purpose of these warnings is to cause users to
+switch to using those options over time.
+
+These warnings can be disabled with the --disable-unsafe-restore-warnings
+configure option.
+
+On systems where openat2() *is not* available, do not warn about these
+conditions; this is to preserve the historic behavior when building this
+package on older systems. On those systems, a physical restore will fail with
+ENOSYS ("Function not implemented").
+
+Fixes: CVE-2026-54371 ("attr < 2.6.0 Symlink Traversal Privilege Escalation via getfattr")
+
+CVE: CVE-2026-54371
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/attr.git/commit/?id=3fb06b9ba314d37035d0877e6de313de754f1ac8]
+
+Backport Changes:
+- Retained the attr 2.5.2 non-static declarations because the unrelated
+ tools cleanup commit is not required by the security fix.
+- Kept the existing .test filename used by the 2.5.2 ptest harness.
+- Replaced the original attr 2.5.2 static restore buffer directly because
+ prerequisite commit dcdab7724265 is not carried as a separate patch.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 3fb06b9ba314d37035d0877e6de313de754f1ac8)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ configure.ac | 9 +++
+ man/man1/setfattr.1 | 9 ++-
+ test/Makemodule.am | 5 ++
+ test/restore.test | 19 +++++
+ tools/setfattr.c | 168 +++++++++++++++++++++++++++++++++++---------
+ 5 files changed, 174 insertions(+), 36 deletions(-)
+ create mode 100644 test/restore.test
+
+diff --git a/configure.ac b/configure.ac
+index 7d13edf..b13ac08 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -51,6 +51,15 @@ AM_CONDITIONAL([HAVE_OPENAT2], [test "x$ac_cv_func_openat2" = xyes])
+ AS_IF([test "x$use_openat2" = xyes],
+ [AC_DEFINE([USE_OPENAT2], [1], [Define to use openat2 syscall])])
+
++AC_ARG_ENABLE([unsafe-restore-warnings],
++ [AS_HELP_STRING([--disable-unsafe-restore-warnings], [Disable warnings about unsafe restore operations])])
++if test "x$use_openat2" = xno; then
++ enable_unsafe_restore_warnings=no
++fi
++AM_CONDITIONAL([UNSAFE_RESTORE_WARNINGS], [test "x$enable_unsafe_restore_warnings" != xno])
++AS_IF([test "x$enable_unsafe_restore_warnings" != xno],
++ [AC_DEFINE([UNSAFE_RESTORE_WARNINGS], [1], [Define to enable unsafe restore warnings])])
++
+ AC_CHECK_FUNCS(getxattrat setxattrat listxattrat removexattrat)
+ AM_CONDITIONAL([NEED_XATTRAT],
+ [test "x$ac_cv_func_getxattrat" = xno -o \
+diff --git a/man/man1/setfattr.1 b/man/man1/setfattr.1
+index e011ca7..d24135c 100644
+--- a/man/man1/setfattr.1
++++ b/man/man1/setfattr.1
+@@ -30,7 +30,7 @@ setfattr \- set extended attributes of filesystem objects
+ .nf
+ \f3setfattr\f1 [\f3\-h\f1] \f3\-n name\f1 [\f3\-v value\f1] \f3pathname\f1...
+ \f3setfattr\f1 [\f3\-h\f1] \f3\-x name\f1 \f3pathname\f1...
+-\f3setfattr\f1 [\f3\-h\f1] \f3\-\-restore=file\f1
++\f3setfattr\f1 [\f3\-hP\f1] \f3\-\-restore=file\f1
+ .fi
+ .SH DESCRIPTION
+ The
+@@ -62,8 +62,11 @@ Remove the named extended attribute entirely.
+ .BR \-h ", " \-\-no-dereference
+ Do not follow symlinks. If
+ .I pathname
+-is a symbolic link, it is not followed, but is instead itself the
+-inode being modified.
++is a symbolic link, it is not followed, but is instead modified itself.
++.TP
++.BR \-P ", " \-\-physical
++Do not traverse symbolic links in pathnames during a restore. Can be combined
++with option \-h.
+ .TP
+ .BR \-\-restore =\f2file\f1
+ Restores extended attributes from file.
+diff --git a/test/Makemodule.am b/test/Makemodule.am
+index 8bfadb9..5279d8c 100644
+--- a/test/Makemodule.am
++++ b/test/Makemodule.am
+@@ -2,6 +2,11 @@ TESTS = \
+ test/attr.test \
+ test/root/getfattr.test
+
++if UNSAFE_RESTORE_WARNINGS
++TESTS += \
++ test/restore.test
++endif
++
+ EXTRA_DIST += \
+ test/README \
+ test/run \
+diff --git a/test/restore.test b/test/restore.test
+new file mode 100644
+index 0000000..f9fb0bc
+--- /dev/null
++++ b/test/restore.test
+@@ -0,0 +1,19 @@
++ $ mkdir d
++ $ touch d/f
++ $ touch d/l
++ $ setfattr -n user.name -v value d/f
++ $ [ "$(id -u)" != 0 ] || setfattr -h -n trusted.name -v value d/l
++
++ $ getfattr -RP d > physical
++
++ $ setfattr --restore=physical
++ > Warning: option --restore=file is unsafe without option -P (--physical) as it traverses symbolic links in pathnames
++ > Warning: option --restore=file is unsafe without option -h (--no-dereference) as it dereferences symbolic link pathnames
++
++ $ setfattr --restore=physical -h
++ > Warning: option --restore=file is unsafe without option -P (--physical) as it traverses symbolic links in pathnames
++
++ $ setfattr --restore=physical --physical
++ > Warning: option --restore=file is unsafe without option -h (--no-dereference) as it dereferences symbolic link pathnames
++
++ $ setfattr --restore=physical -h --physical
+diff --git a/tools/setfattr.c b/tools/setfattr.c
+index 7b30159..1fe3253 100644
+--- a/tools/setfattr.c
++++ b/tools/setfattr.c
+@@ -30,19 +30,25 @@
+ #include <ctype.h>
+ #include <libgen.h>
+ #include <sys/xattr.h>
++#include <unistd.h>
++#include <fcntl.h>
+ #include <errno.h>
++#include "openat2.h"
+
++#include "xattrat_compat.h"
+ #include "misc.h"
+
+-#define CMD_LINE_OPTIONS "n:x:v:h"
++#define CMD_LINE_OPTIONS "n:x:v:hP"
+ #define CMD_LINE_SPEC1 "{-n name} [-v value] [-h] file..."
+ #define CMD_LINE_SPEC2 "{-x name} [-h] file..."
++#define CMD_LINE_SPEC3 "[-hP] --restore=file"
+
+ static const struct option long_options[] = {
+ { "name", 1, 0, 'n' },
+ { "remove", 1, 0, 'x' },
+ { "value", 1, 0, 'v' },
+ { "no-dereference", 0, 0, 'h' },
++ { "physical", 0, 0, 'P' },
+ { "restore", 1, 0, 'B' },
+ { "raw", 0, 0, CHAR_MAX + 1 },
+ { "version", 0, 0, 'V' },
+@@ -57,13 +63,14 @@ int opt_remove; /* remove an attribute */
+ int opt_restore; /* restore has been run */
+ int opt_deref = 1; /* dereference symbolic links */
+ int opt_raw; /* attribute value is not encoded */
++static int opt_physical;
+
+ int had_errors;
+ const char *progname;
+
+-int do_set(const char *path, const char *name, const char *value);
++int do_set(int dirfd, const char *pathname, const char *fullpath,
++ const char *name, const char *value);
+ const char *decode(const char *value, size_t *size);
+-int restore(const char *filename);
+ int hex_digit(char c);
+ int base64_digit(char c);
+
+@@ -87,25 +94,20 @@ static const char *xquote(const char *str, const char *quote_chars)
+ return q;
+ }
+
+-int do_setxattr(const char *path, const char *name,
+- const void *value, size_t size)
+-{
+- return (opt_deref ? setxattr : lsetxattr)(path, name, value, size, 0);
+-}
+-
+-int do_removexattr(const char *path, const char *name)
+-{
+- return (opt_deref ? removexattr : lremovexattr)(path, name);
+-}
+-
+ int restore(const char *filename)
+ {
+- static char *path;
+- static size_t path_size;
++ char *fullpath = NULL;
++ size_t fullpath_size = 0;
+ FILE *file;
+ char *l;
+ int line = 0, backup_line, status = 0;
+-
++ int dirfd = -1;
++ char *pathname, *dirname = NULL;
++#ifdef UNSAFE_RESTORE_WARNINGS
++ static int non_physical_restore_warning;
++ static int deref_restore_warning;
++#endif
++
+ if (strcmp(filename, "-") == 0)
+ file = stdin;
+ else {
+@@ -140,20 +142,85 @@ int restore(const char *filename)
+ } else
+ l += 8;
+ l = unquote(l);
+- if (high_water_alloc((void **)&path, &path_size, strlen(l)+1)) {
++ if (high_water_alloc((void **)&fullpath, &fullpath_size, strlen(l)+1)) {
+ perror(progname);
+ status = 1;
+ goto cleanup;
+ }
+- strcpy(path, l);
++ strcpy(fullpath, l);
++
++#ifdef UNSAFE_RESTORE_WARNINGS
++ if (!opt_physical && !non_physical_restore_warning) {
++ fprintf(stderr,
++ _("Warning: option --restore=file is unsafe "
++ "without option -P (--physical) as it "
++ "traverses symbolic links in pathnames\n"));
++ non_physical_restore_warning = 1;
++ }
++ if (opt_deref && !deref_restore_warning) {
++ fprintf(stderr,
++ _("Warning: option --restore=file is unsafe "
++ "without option -h (--no-dereference) as it "
++ "dereferences symbolic link pathnames\n"));
++ deref_restore_warning = 1;
++ }
++#endif
++
++ /* find the last pathname component */
++ pathname = fullpath + strlen(fullpath);
++ while (pathname > fullpath && pathname[-1] == '/')
++ pathname--;
++ while (pathname > fullpath && pathname[-1] != '/')
++ pathname--;
++
++ if (opt_physical && pathname != fullpath) {
++ dirname = malloc(pathname - fullpath + 1);
++ if (dirname == NULL) {
++ fprintf(stderr, "%s: %s\n", progname,
++ strerror(errno));
++ status = 1;
++ goto cleanup;
++ }
++ memcpy(dirname, fullpath, pathname - fullpath);
++ dirname[pathname - fullpath] = '\0';
++#ifdef USE_OPENAT2
++ struct open_how how = {
++ .flags = O_PATH | O_DIRECTORY,
++ .resolve = RESOLVE_NO_SYMLINKS,
++ };
++
++ dirfd = openat2(AT_FDCWD, dirname, &how, sizeof(how));
++#else
++ errno = ENOSYS;
++ dirfd = -1;
++#endif
++ if (dirfd == -1) {
++ fprintf(stderr,
++ _("%s: lookup of directory %s without "
++ "following symlinks: %s\n"),
++ progname, dirname, strerror(errno));
++ status = 1;
++ goto cleanup;
++ }
++ } else {
++ pathname = fullpath;
++ dirfd = AT_FDCWD;
++ }
+
+ while ((l = next_line(file)) != NULL && *l != '\0') {
+ char *name = l, *value = strchr(l, '=');
+ line++;
+ if (value)
+ *value++ = '\0';
+- status = do_set(path, unquote(name), value);
++ status = do_set(dirfd, pathname, fullpath,
++ unquote(name), value);
+ }
++ if (dirfd != -1 && dirfd != AT_FDCWD) {
++ close(dirfd);
++ dirfd = -1;
++ }
++ free(dirname);
++ dirname = NULL;
+ if (l == NULL)
+ break;
+ line++;
+@@ -166,8 +233,12 @@ int restore(const char *filename)
+ }
+
+ cleanup:
+- if (path)
+- free(path);
++ if (dirfd != -1 && dirfd != AT_FDCWD) {
++ close(dirfd);
++ dirfd = -1;
++ }
++ free(dirname);
++ free(fullpath);
+ if (file != stdin)
+ fclose(file);
+ if (status)
+@@ -180,11 +251,13 @@ void help(void)
+ printf(_("%s %s -- set extended attributes\n"), progname, VERSION);
+ printf(_("Usage: %s %s\n"), progname, CMD_LINE_SPEC1);
+ printf(_(" %s %s\n"), progname, CMD_LINE_SPEC2);
++ printf(_(" %s %s\n"), progname, CMD_LINE_SPEC3);
+ printf(_(
+ " -n, --name=name set the value of the named extended attribute\n"
+ " -x, --remove=name remove the named extended attribute\n"
+ " -v, --value=value use value as the attribute value\n"
+ " -h, --no-dereference do not dereference symbolic links\n"
++" -P, --physical do not traverse symbolic links during a restore\n"
+ " --restore=file restore extended attributes\n"
+ " --raw attribute value is not encoded\n"
+ " --version print version and exit\n"
+@@ -193,6 +266,7 @@ void help(void)
+
+ int main(int argc, char *argv[])
+ {
++ enum { UNDEFINED_MODE, SET_MODE, RESTORE_MODE } mode = UNDEFINED_MODE;
+ char **restore_args = NULL;
+ int opt;
+
+@@ -207,6 +281,9 @@ int main(int argc, char *argv[])
+ long_options, NULL)) != -1) {
+ switch(opt) {
+ case 'n': /* attribute name */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (opt_name || opt_remove)
+ goto synopsis;
+ opt_name = optarg;
+@@ -218,16 +295,25 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'v': /* attribute value */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (opt_value || opt_remove)
+ goto synopsis;
+ opt_value = optarg;
+ break;
+
+ case CHAR_MAX + 1:
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ opt_raw = 1;
+ break;
+
+ case 'x': /* remove attribute */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (opt_name || opt_set)
+ goto synopsis;
+ opt_name = optarg;
+@@ -235,6 +321,9 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'B': /* restore */
++ if (mode == SET_MODE)
++ goto synopsis;
++ mode = RESTORE_MODE;
+ opt_restore++;
+ restore_args = realloc(restore_args,
+ opt_restore * sizeof(*restore_args));
+@@ -245,6 +334,13 @@ int main(int argc, char *argv[])
+ restore_args[opt_restore - 1] = optarg;
+ break;
+
++ case 'P': /* --physical */
++ if (mode == SET_MODE)
++ goto synopsis;
++ mode = RESTORE_MODE;
++ opt_physical = 1;
++ break;
++
+ case 'V':
+ printf("%s " VERSION "\n", progname);
+ return 0;
+@@ -260,29 +356,35 @@ int main(int argc, char *argv[])
+ if (!(((opt_remove || opt_set) && optind < argc) || opt_restore))
+ goto synopsis;
+
+- if (opt_restore) {
++ if (mode == RESTORE_MODE) {
+ for (opt = 0; opt < opt_restore; opt++)
+ restore(restore_args[opt]);
+ free(restore_args);
+ }
+
+ while (optind < argc) {
+- do_set(argv[optind], unquote(opt_name), opt_value);
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
++ do_set(AT_FDCWD, argv[optind], argv[optind], unquote(opt_name),
++ opt_value);
+ optind++;
+ }
+
+ return (had_errors ? 1 : 0);
+
+ synopsis:
+- fprintf(stderr, _("Usage: %s %s\n"
+- " %s %s\n"
+- "Try `%s --help' for more information.\n"),
+- progname, CMD_LINE_SPEC1, progname, CMD_LINE_SPEC2, progname);
++ fprintf(stderr, _("Usage: %s %s\n"), progname, CMD_LINE_SPEC1);
++ fprintf(stderr, _(" %s %s\n"), progname, CMD_LINE_SPEC2);
++ fprintf(stderr, _(" %s %s\n"), progname, CMD_LINE_SPEC3);
++ fprintf(stderr, _("Try `%s --help' for more information.\n"), progname);
+ return 2;
+ }
+
+-int do_set(const char *path, const char *name, const char *value)
++int do_set(int dirfd, const char *pathname, const char *fullpath,
++ const char *name, const char *value)
+ {
++ int at_flags = opt_deref ? 0 : AT_SYMLINK_NOFOLLOW;
+ size_t size = 0;
+ int error;
+
+@@ -294,13 +396,14 @@ int do_set(const char *path, const char *name, const char *value)
+ return 1;
+ }
+ if (opt_set || opt_restore)
+- error = do_setxattr(path, name, value, size);
++ error = setxattrat(dirfd, pathname, at_flags,
++ name, value, size, 0);
+ else
+- error = do_removexattr(path, name);
++ error = removexattrat(dirfd, pathname, at_flags, name);
+
+ if (error < 0) {
+ fprintf(stderr, "%s: %s: %s\n",
+- progname, xquote(path, "\n\r"), strerror_ea(errno));
++ progname, xquote(fullpath, "\n\r"), strerror_ea(errno));
+ had_errors++;
+ return 1;
+ }
+@@ -486,4 +589,3 @@ int base64_digit(char c)
+ else
+ return -1;
+ }
+-
+--
+2.35.6
@@ -17,6 +17,14 @@ LIC_FILES_CHKSUM = "file://doc/COPYING;md5=2d0aa14b3fce4694e4f615e30186335f \
SRC_URI = "${SAVANNAH_GNU_MIRROR}/attr/${BP}.tar.gz \
file://run-ptest \
file://0001-attr.c-Include-libgen.h-for-posix-version-of-basenam.patch \
+ file://CVE-2026-54371-dependent_p1.patch \
+ file://CVE-2026-54371-dependent_p2.patch \
+ file://CVE-2026-54371-dependent_p3.patch \
+ file://CVE-2026-54371-dependent_p4.patch \
+ file://CVE-2026-54371-dependent_p5.patch \
+ file://CVE-2026-54371-dependent_p6.patch \
+ file://CVE-2026-54371_p1.patch \
+ file://CVE-2026-54371_p2.patch \
"
SRC_URI[sha256sum] = "39bf67452fa41d0948c2197601053f48b3d78a029389734332a6309a680c6c87"
@@ -35,7 +43,7 @@ ALTERNATIVE_TARGET[getfattr] = "${bindir}/getfattr"
do_install_ptest() {
install -m755 ${S}/test/run ${S}/test/sort-getfattr-output ${D}${PTEST_PATH}/
- for t in $(makefile-getvar ${S}/test/Makemodule.am TESTS); do
+ for t in $(makefile-getvar ${B}/Makefile TESTS); do
install -m644 ${S}/$t ${D}${PTEST_PATH}/
done
}