new file mode 100644
@@ -0,0 +1,168 @@
+From 9e97fa6569529fe28af7839f0e83e3a01eb631ab Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Tue, 16 Jun 2026 22:44:07 +0200
+Subject: [PATCH] Add fchmodat syscall backwards compatibility code
+
+Support for the AT_SYMLINK_NOFOLLOW flag was added in commit 09da082b07bb
+("fs: Add fchmodat2()") and support for the AT_EMPTY_PATH flag was added in
+commit 5daeb41a6fc9d ("fchmodat2: add support for AT_EMPTY_PATH"), both
+merged in kernel version 6.6 from October 2023.
+
+Add a fchmodat_compat() wrapper that detects when the kernel doesn't support
+those features and fall back to chmod("/proc/self/fd/<dirfd>", mode) when
+necessary.
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=c3492ef76587ce1e652bbb1bdf99f8ea6224b364]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit c3492ef76587ce1e652bbb1bdf99f8ea6224b364)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ include/Makemodule.am | 1 +
+ include/fchmodat_compat.h | 12 ++++++
+ libmisc/Makemodule.am | 1 +
+ libmisc/fchmodat_compat.c | 88 +++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 102 insertions(+)
+ create mode 100644 include/fchmodat_compat.h
+ create mode 100644 libmisc/fchmodat_compat.c
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index 36fc1c3..30d1fda 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -2,6 +2,7 @@ noinst_HEADERS += \
+ include/libacl.h \
+ include/acl.h \
+ include/acl_ea.h \
++ include/fchmodat_compat.h \
+ include/misc.h \
+ include/visibility-hidden.h \
+ include/walk_tree.h \
+diff --git a/include/fchmodat_compat.h b/include/fchmodat_compat.h
+new file mode 100644
+index 0000000..c10f8ad
+--- /dev/null
++++ b/include/fchmodat_compat.h
+@@ -0,0 +1,12 @@
++#ifndef FCHMODAT_COMPAT_H
++#define FCHMODAT_COMPAT_H
++
++#include <fcntl.h>
++
++int fchmodat_compat(int dirfd, const char *pathname, mode_t mode, int flags);
++
++#ifndef DECLARATIONS_ONLY
++# define fchmodat fchmodat_compat
++#endif
++
++#endif /* FCHMODAT_COMPAT_H */
+diff --git a/libmisc/Makemodule.am b/libmisc/Makemodule.am
+index 4099098..07b03c6 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -5,6 +5,7 @@ noinst_HEADERS += \
+
+ libmisc_la_SOURCES = \
+ libmisc/uid_gid_lookup.c \
++ libmisc/fchmodat_compat.c \
+ libmisc/getxattrat_compat.c \
+ libmisc/high_water_alloc.c \
+ libmisc/listxattrat_compat.c \
+diff --git a/libmisc/fchmodat_compat.c b/libmisc/fchmodat_compat.c
+new file mode 100644
+index 0000000..dd2a8b8
+--- /dev/null
++++ b/libmisc/fchmodat_compat.c
+@@ -0,0 +1,88 @@
++/*
++ File: fchmodat_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 <sys/stat.h>
++#include <errno.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include "libmisc/proc-self-fd.h"
++
++#define DECLARATIONS_ONLY
++#include "fchmodat_compat.h"
++
++/*
++ * Support for the AT_SYMLINK_NOFOLLOW flag was added in commit 09da082b07bb
++ * ("fs: Add fchmodat2()") and support for the AT_EMPTY_PATH flag was added in
++ * commit 5daeb41a6fc9d ("fchmodat2: add support for AT_EMPTY_PATH"), both
++ * merged in kernel version 6.6 from October 2023.
++ */
++
++static int no_symlink_nofollow;
++
++int fchmodat_compat(int dirfd, const char *pathname, mode_t mode, int flags)
++{
++ struct proc_self_fd_buffer buffer;
++ int fd = dirfd;
++ int ret;
++
++ if ((flags & AT_SYMLINK_NOFOLLOW) && no_symlink_nofollow)
++ goto no_symlink_nofollow;
++ if ((flags & AT_EMPTY_PATH) && no_symlink_nofollow) {
++ if (!*pathname)
++ goto proc_self_fd_path;
++ flags &= ~AT_EMPTY_PATH;
++ }
++ ret = fchmodat(dirfd, pathname, mode, flags);
++ if (ret != -1)
++ return ret;
++ if (errno == ENOTSUP && (flags & AT_SYMLINK_NOFOLLOW)) {
++ no_symlink_nofollow = 1;
++ goto no_symlink_nofollow;
++ }
++ if (errno == EINVAL && (flags & AT_EMPTY_PATH)) {
++ no_symlink_nofollow = 1;
++ goto proc_self_fd_path;
++ }
++ return ret;
++
++no_symlink_nofollow:
++ if (*pathname) {
++ int open_flags = O_PATH | O_CLOEXEC;
++ if (flags & AT_SYMLINK_NOFOLLOW)
++ open_flags |= O_NOFOLLOW;
++ fd = openat(dirfd, pathname, open_flags);
++ if (fd == -1)
++ return -1;
++ }
++ /* fall through */
++
++proc_self_fd_path:
++ ret = chmod(proc_self_fd_path(fd, &buffer), mode);
++ if (ret == -1 && errno == ENOENT)
++ errno = EBADF;
++ if (fd != dirfd) {
++ int saved_errno = errno;
++ close(fd);
++ errno = saved_errno;
++ }
++ return ret;
++}
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,761 @@
+From 5234b62884727c1ce151cb259954cc78abb6471a Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Tue, 9 Jun 2026 08:15:30 +0200
+Subject: [PATCH] Rename walk_tree to old_walk_tree
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=f7210d140298ccc73b2b1713635e7c39d58a8eeb]
+
+Backport Changes:
+- Retain ACL 2.3.2's libmisc source ordering for uid_gid_lookup.c.
+- Rename the original walker without the later __acl symbol macro.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit f7210d140298ccc73b2b1713635e7c39d58a8eeb)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ include/Makemodule.am | 2 +-
+ include/old_walk_tree.h | 40 +++++++
+ include/walk_tree.h | 40 -------
+ libmisc/Makemodule.am | 4 +-
+ libmisc/old_walk_tree.c | 258 ++++++++++++++++++++++++++++++++++++++++
+ libmisc/walk_tree.c | 258 ----------------------------------------
+ tools/do_set.c | 2 +-
+ tools/getfacl.c | 6 +-
+ tools/setfacl.c | 6 +-
+ 9 files changed, 308 insertions(+), 308 deletions(-)
+ create mode 100644 include/old_walk_tree.h
+ delete mode 100644 include/walk_tree.h
+ create mode 100644 libmisc/old_walk_tree.c
+ delete mode 100644 libmisc/walk_tree.c
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index 30d1fda..3926bda 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -4,8 +4,8 @@ noinst_HEADERS += \
+ include/acl_ea.h \
+ include/fchmodat_compat.h \
+ include/misc.h \
++ include/old_walk_tree.h \
+ include/visibility-hidden.h \
+- include/walk_tree.h \
+ include/xattrat.h \
+ include/xattrat_compat.h
+
+diff --git a/include/old_walk_tree.h b/include/old_walk_tree.h
+new file mode 100644
+index 0000000..fbc469e
+--- /dev/null
++++ b/include/old_walk_tree.h
+@@ -0,0 +1,40 @@
++/*
++ File: old_walk_tree.h
++
++ Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
++
++ 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/>.
++*/
++
++#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
++
++#define WALK_TREE_TOPLEVEL 0x100
++#define WALK_TREE_SYMLINK 0x200
++#define WALK_TREE_FAILED 0x400
++
++struct stat;
++
++extern int old_walk_tree(const char *path, int walk_flags, unsigned int num,
++ int (*func)(const char *, const struct stat *, int,
++ void *), void *arg);
++
++#endif
+diff --git a/include/walk_tree.h b/include/walk_tree.h
+deleted file mode 100644
+index 4773194..0000000
+--- a/include/walk_tree.h
++++ /dev/null
+@@ -1,40 +0,0 @@
+-/*
+- File: walk_tree.h
+-
+- Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
+-
+- 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/>.
+-*/
+-
+-#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
+-
+-#define WALK_TREE_TOPLEVEL 0x100
+-#define WALK_TREE_SYMLINK 0x200
+-#define WALK_TREE_FAILED 0x400
+-
+-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);
+-
+-#endif
+diff --git a/libmisc/Makemodule.am b/libmisc/Makemodule.am
+index 07b03c6..26e802b 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -10,12 +10,12 @@ libmisc_la_SOURCES = \
+ libmisc/high_water_alloc.c \
+ libmisc/listxattrat_compat.c \
+ libmisc/next_line.c \
++ libmisc/old_walk_tree.c \
+ libmisc/proc-self-fd.c \
+ libmisc/quote.c \
+ libmisc/removexattrat_compat.c \
+ libmisc/setxattrat_compat.c \
+- libmisc/unquote.c \
+- libmisc/walk_tree.c
++ libmisc/unquote.c
+
+ if NEED_XATTRAT
+ libmisc_la_SOURCES += \
+diff --git a/libmisc/old_walk_tree.c b/libmisc/old_walk_tree.c
+new file mode 100644
+index 0000000..2202e0c
+--- /dev/null
++++ b/libmisc/old_walk_tree.c
+@@ -0,0 +1,258 @@
++/*
++ File: old_walk_tree.c
++
++ Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
++
++ 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/types.h>
++#include <sys/stat.h>
++#include <unistd.h>
++#include <sys/time.h>
++#include <sys/resource.h>
++#include <dirent.h>
++#include <stdio.h>
++#include <string.h>
++#include <errno.h>
++
++#include "old_walk_tree.h"
++
++struct entry_handle {
++ struct entry_handle *prev, *next;
++ dev_t dev;
++ ino_t ino;
++ DIR *stream;
++ long pos;
++};
++
++struct walk_tree_args {
++ char path[FILENAME_MAX];
++ int walk_flags;
++ int (*func)(const char *, const struct stat *, int, void *);
++ void *arg;
++ int depth;
++ struct entry_handle dirs;
++ struct entry_handle *closed;
++ unsigned int num_dir_handles;
++ struct stat st;
++ dev_t dev;
++};
++
++static int walk_tree_visited(struct entry_handle *dirs, dev_t dev, ino_t ino)
++{
++ struct entry_handle *i;
++
++ for (i = dirs->next; i != dirs; i = i->next)
++ if (i->dev == dev && i->ino == ino)
++ return 1;
++ return 0;
++}
++
++static int walk_tree_rec(struct walk_tree_args *args)
++{
++ 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;
++
++ if (lstat(args->path, &args->st) != 0)
++ return args->func(args->path, NULL, flags | WALK_TREE_FAILED,
++ args->arg);
++
++ 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;
++ }
++
++ 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;
++ }
++ } else if (S_ISDIR(args->st.st_mode)) {
++ dir.dev = args->st.st_dev;
++ dir.ino = args->st.st_ino;
++ have_dir_stat = 1;
++ }
++ 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;
++ }
++
++ /*
++ * 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;
++ }
++
++ /* 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;
++ }
++
++ /* 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--;
++ }
++ }
++
++ /* Remove from the list of handles. */
++ dir.prev->next = dir.next;
++ dir.next->prev = dir.prev;
++ 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;
++}
++
++int old_walk_tree(const char *path, int walk_flags, unsigned int num,
++ int (*func)(const char *, const struct stat *, int, 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);
++ }
++ 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);
++}
+diff --git a/libmisc/walk_tree.c b/libmisc/walk_tree.c
+deleted file mode 100644
+index 0aa5f35..0000000
+--- a/libmisc/walk_tree.c
++++ /dev/null
+@@ -1,258 +0,0 @@
+-/*
+- File: walk_tree.c
+-
+- Copyright (C) 2007 Andreas Gruenbacher <a.gruenbacher@computer.org>
+-
+- 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/types.h>
+-#include <sys/stat.h>
+-#include <unistd.h>
+-#include <sys/time.h>
+-#include <sys/resource.h>
+-#include <dirent.h>
+-#include <stdio.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;
+- DIR *stream;
+- long pos;
+-};
+-
+-struct walk_tree_args {
+- char path[FILENAME_MAX];
+- int walk_flags;
+- int (*func)(const char *, const struct stat *, int, void *);
+- void *arg;
+- int depth;
+- struct entry_handle dirs;
+- struct entry_handle *closed;
+- unsigned int num_dir_handles;
+- struct stat st;
+- dev_t dev;
+-};
+-
+-static int walk_tree_visited(struct entry_handle *dirs, dev_t dev, ino_t ino)
+-{
+- struct entry_handle *i;
+-
+- for (i = dirs->next; i != dirs; i = i->next)
+- if (i->dev == dev && i->ino == ino)
+- return 1;
+- return 0;
+-}
+-
+-static int walk_tree_rec(struct walk_tree_args *args)
+-{
+- 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;
+-
+- if (lstat(args->path, &args->st) != 0)
+- return args->func(args->path, NULL, flags | WALK_TREE_FAILED,
+- args->arg);
+-
+- 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;
+- }
+-
+- 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;
+- }
+- } else if (S_ISDIR(args->st.st_mode)) {
+- dir.dev = args->st.st_dev;
+- dir.ino = args->st.st_ino;
+- have_dir_stat = 1;
+- }
+- 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;
+- }
+-
+- /*
+- * 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;
+- }
+-
+- /* 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;
+- }
+-
+- /* 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--;
+- }
+- }
+-
+- /* Remove from the list of handles. */
+- dir.prev->next = dir.next;
+- dir.next->prev = dir.prev;
+- 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;
+-}
+-
+-int walk_tree(const char *path, int walk_flags, unsigned int num,
+- int (*func)(const char *, const struct stat *, int, 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);
+- }
+- 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);
+-}
+diff --git a/tools/do_set.c b/tools/do_set.c
+index b510032..a958ede 100644
+--- a/tools/do_set.c
++++ b/tools/do_set.c
+@@ -37,7 +37,7 @@
+ #include "sequence.h"
+ #include "do_set.h"
+ #include "parse.h"
+-#include "walk_tree.h"
++#include "old_walk_tree.h"
+
+
+ extern const char *progname;
+diff --git a/tools/getfacl.c b/tools/getfacl.c
+index 5a45c04..8ef774b 100644
+--- a/tools/getfacl.c
++++ b/tools/getfacl.c
+@@ -37,7 +37,7 @@
+ #include <getopt.h>
+ #include "misc.h"
+ #include "user_group.h"
+-#include "walk_tree.h"
++#include "old_walk_tree.h"
+
+ #define POSIXLY_CORRECT_STR "POSIXLY_CORRECT"
+
+@@ -736,7 +736,7 @@ int main(int argc, char *argv[])
+ if (*line == '\0')
+ continue;
+
+- had_errors += walk_tree(line, walk_flags, 0,
++ had_errors += old_walk_tree(line, walk_flags, 0,
+ do_print, NULL);
+ }
+ if (!feof(stdin)) {
+@@ -745,7 +745,7 @@ int main(int argc, char *argv[])
+ had_errors++;
+ }
+ } else
+- had_errors += walk_tree(argv[optind], walk_flags, 0,
++ had_errors += old_walk_tree(argv[optind], walk_flags, 0,
+ do_print, NULL);
+ optind++;
+ } while (optind < argc);
+diff --git a/tools/setfacl.c b/tools/setfacl.c
+index 86ba733..cb05975 100644
+--- a/tools/setfacl.c
++++ b/tools/setfacl.c
+@@ -33,7 +33,7 @@
+ #include "sequence.h"
+ #include "parse.h"
+ #include "do_set.h"
+-#include "walk_tree.h"
++#include "old_walk_tree.h"
+
+ #define POSIXLY_CORRECT_STR "POSIXLY_CORRECT"
+
+@@ -310,14 +310,14 @@ int next_file(const char *arg, seq_t seq)
+
+ if (strcmp(arg, "-") == 0) {
+ while ((line = __acl_next_line(stdin)))
+- errors = walk_tree(line, walk_flags, 0, do_set, &args);
++ errors = old_walk_tree(line, walk_flags, 0, do_set, &args);
+ if (!feof(stdin)) {
+ fprintf(stderr, _("%s: Standard input: %s\n"),
+ progname, strerror(errno));
+ errors = 1;
+ }
+ } else {
+- errors = walk_tree(arg, walk_flags, 0, do_set, &args);
++ errors = old_walk_tree(arg, walk_flags, 0, do_set, &args);
+ }
+ return errors ? 1 : 0;
+ }
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,439 @@
+From faf0010982141d0644280beada5c9ea957960436 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Tue, 9 Jun 2026 08:42:16 +0200
+Subject: [PATCH] Add the new walk_tree helper
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=39b00348f5911e4e5937ddfb079cc456288b3cd3]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 39b00348f5911e4e5937ddfb079cc456288b3cd3)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ include/Makemodule.am | 1 +
+ include/walk_tree.h | 40 +++++
+ libmisc/Makemodule.am | 1 +
+ libmisc/walk_tree.c | 340 ++++++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 382 insertions(+)
+ create mode 100644 include/walk_tree.h
+ create mode 100644 libmisc/walk_tree.c
+
+diff --git a/include/Makemodule.am b/include/Makemodule.am
+index 3926bda..3e2a47b 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -6,6 +6,7 @@ noinst_HEADERS += \
+ include/misc.h \
+ include/old_walk_tree.h \
+ include/visibility-hidden.h \
++ include/walk_tree.h \
+ include/xattrat.h \
+ include/xattrat_compat.h
+
+diff --git a/include/walk_tree.h b/include/walk_tree.h
+new file mode 100644
+index 0000000..60df8da
+--- /dev/null
++++ b/include/walk_tree.h
+@@ -0,0 +1,40 @@
++/*
++ File: walk_tree.h
++
++ Copyright (C) 2007-2026 Andreas Gruenbacher <a.gruenbacher@computer.org>
++
++ 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/>.
++*/
++
++#ifndef __WALK_TREE_H
++#define __WALK_TREE_H
++
++enum walk_flags {
++ WALK_TREE_RECURSIVE = 0x01,
++ WALK_TREE_PHYSICAL = 0x02,
++ WALK_TREE_LOGICAL = 0x04,
++ WALK_TREE_ONE_FILESYSTEM = 0x08,
++
++ WALK_TREE_TOPLEVEL = 0x100,
++ WALK_TREE_FAILED = 0x200,
++};
++
++struct stat;
++
++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/Makemodule.am b/libmisc/Makemodule.am
+index 26e802b..9d656ca 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -13,6 +13,7 @@ libmisc_la_SOURCES = \
+ libmisc/old_walk_tree.c \
+ libmisc/proc-self-fd.c \
+ libmisc/quote.c \
++ libmisc/walk_tree.c \
+ libmisc/removexattrat_compat.c \
+ libmisc/setxattrat_compat.c \
+ libmisc/unquote.c
+diff --git a/libmisc/walk_tree.c b/libmisc/walk_tree.c
+new file mode 100644
+index 0000000..5b1b576
+--- /dev/null
++++ b/libmisc/walk_tree.c
+@@ -0,0 +1,340 @@
++/*
++ File: walk_tree.c
++
++ Copyright (C) 2007-2026 Andreas Gruenbacher <a.gruenbacher@computer.org>
++
++ 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/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 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 {
++ enum walk_flags walk_flags;
++ int (*func)(int, const char *, const char *, unsigned char,
++ enum walk_flags, void *);
++ void *arg;
++ int depth;
++ unsigned int num_dir_handles;
++ dev_t dev;
++ struct dir_hash_entry **dir_hash;
++};
++
++static int dir_visited(struct dir_stack_entry *entry, dev_t dev, ino_t ino)
++{
++ while (entry) {
++ if (entry->dev == dev && entry->ino == ino)
++ return 1;
++ entry = entry->parent;
++ }
++ return 0;
++}
++
++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 char *new_dirname(const char *dirname, const char *pathname)
++{
++ size_t dirlen, pathlen;
++ char *newname, *n;
++ int need_slash;
++
++ if (*dirname == '\0' && *pathname == '.' &&
++ (*(pathname+1) == '/' || *(pathname+1) == '\0')) {
++ pathname++;
++ while (*pathname == '/')
++ pathname++;
++ }
++
++ 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;
++}
++
++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 (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;
++ }
++ while (top->parent && top->parent->stream)
++ top = top->parent;
++ top->pos = telldir(top->stream);
++ closedir(top->stream);
++ top->stream = NULL;
++ args->num_dir_handles++;
++ }
++
++ 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;
++
++ 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;
++ }
++
++ 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--;
++
++ if (closedir(this_dir.stream) != 0) {
++ this_dir.stream = NULL;
++ goto fail;
++ }
++ this_dir.stream = NULL;
++
++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++;
++ }
++ 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;
++}
++
++/*
++ * 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 = {
++ .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);
++ }
++
++ return err;
++}
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,140 @@
+From d3012ad3c4dbe81f7b33eddc02c14d930a0742fc 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-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=7244f425227a23533f0b0b41c717dbd59736b907]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 7244f425227a23533f0b0b41c717dbd59736b907)
+Signed-off-by: Darsh Kelaiya <dkelaiya@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 22d5084..c3353f6 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -34,6 +34,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 3e2a47b..43a136a 100644
+--- a/include/Makemodule.am
++++ b/include/Makemodule.am
+@@ -5,6 +5,7 @@ noinst_HEADERS += \
+ include/fchmodat_compat.h \
+ include/misc.h \
+ include/old_walk_tree.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 9d656ca..e45a82a 100644
+--- a/libmisc/Makemodule.am
++++ b/libmisc/Makemodule.am
+@@ -22,3 +22,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,299 @@
+From b8a5843fafde6563bc1e7a9e9314f7a1075fc6a0 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Tue, 9 Jun 2026 08:43:16 +0200
+Subject: [PATCH] harden getfacl
+
+Switch to the new walk_tree helper that uses directory file descriptors and
+relative paths instead of building full pathnames. On the final pathname
+component, use functions that only follow symlinks when requested (stat() ->
+fstatat(), acl_get_file() -> acl_get_file_at()). This prevents getfacl from
+accidentally following a symlink an attacker may have planted.
+
+A notable change is that commands like 'getfacl -P symlink' will now fail with
+an ELOOP error ("Too many levels of symbolic links"). Previously, command-line
+arguments that refer to symbolic links were silently ignored in -P (--physical)
+mode.
+
+Fixes: CVE-2026-54370 ("acl < 2.4.0 TOCTOU Symlink Traversal via getfacl/setfacl")
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=2ee940139e9bba5005a3d987a6daecdda909089e]
+
+Backport Changes:
+- Retain ACL 2.3.2's FILE argument in the tabular output helper.
+- Update its caller to use the safe walker display name and stat.
+- Retain existing option variable linkage while changing walk_flags.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 2ee940139e9bba5005a3d987a6daecdda909089e)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ tools/getfacl.c | 111 ++++++++++++++++++++++++++++++------------------
+ 1 file changed, 70 insertions(+), 41 deletions(-)
+
+diff --git a/tools/getfacl.c b/tools/getfacl.c
+index 8ef774b..2077820 100644
+--- a/tools/getfacl.c
++++ b/tools/getfacl.c
+@@ -4,6 +4,7 @@
+
+ Copyright (C) 1999-2002
+ Andreas Gruenbacher, <andreas.gruenbacher@gmail.com>
++ Copyright 2026 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+@@ -30,6 +31,7 @@
+ #include <limits.h>
+ #include <stdlib.h>
+ #include <unistd.h>
++#include <fcntl.h>
+ #include <string.h>
+ #include <sys/stat.h>
+ #include <dirent.h>
+@@ -37,7 +39,7 @@
+ #include <getopt.h>
+ #include "misc.h"
+ #include "user_group.h"
+-#include "old_walk_tree.h"
++#include "walk_tree.h"
+
+ #define POSIXLY_CORRECT_STR "POSIXLY_CORRECT"
+
+@@ -70,7 +72,7 @@ static const struct option long_options[] = {
+ const char *progname;
+ const char *cmd_line_options;
+
+-int walk_flags = WALK_TREE_DEREFERENCE_TOPLEVEL;
++enum walk_flags walk_flags = 0;
+ int opt_print_acl;
+ int opt_print_default_acl;
+ int opt_strip_leading_slash = 1;
+@@ -325,7 +327,7 @@ int show_line(FILE *stream, struct name_list **acl_names, acl_t acl,
+ return 0;
+ }
+
+-int do_show(FILE *stream, const char *path_p, const struct stat *st,
++int do_show(FILE *stream, const char *fullname, const struct stat *st,
+ acl_t acl, acl_t dacl)
+ {
+ struct name_list *acl_names = get_list(st, acl),
+@@ -364,7 +366,7 @@ int do_show(FILE *stream, const char *path_p, const struct stat *st,
+ if (ret < 0)
+ return ret;
+ }
+- fprintf(stream, "# file: %s\n", xquote(path_p, "\n\r"));
++ fprintf(stream, "# file: %s\n", xquote(fullname, "\n\r"));
+ while (acl_names != NULL || dacl_names != NULL) {
+ acl_tag_t acl_tag, dacl_tag;
+
+@@ -423,11 +425,11 @@ int do_show(FILE *stream, const char *path_p, const struct stat *st,
+ * of the file PATH_P.
+ */
+ static acl_t
+-acl_get_file_mode(const char *path_p)
++acl_get_from_file_mode_at(int dirfd, const char *pathname, int at_flags)
+ {
+ struct stat st;
+
+- if (stat(path_p, &st) != 0)
++ if (fstatat(dirfd, pathname, &st, at_flags) != 0)
+ return NULL;
+ return acl_from_mode(st.st_mode);
+ }
+@@ -444,15 +446,32 @@ flagstr(mode_t mode)
+ return str;
+ }
+
+-int do_print(const char *path_p, const struct stat *st, int walk_flags, void *unused)
++static 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;
+ const char *default_prefix = NULL;
+ acl_t acl = NULL, default_acl = NULL;
++ struct stat st;
++ int at_flags;
+ int error = 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_p, "\n\r"),
+- strerror(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname,
++ xquote(fullname, "\n\r"), strerror(errno));
+ return 1;
+ }
+
+@@ -461,21 +480,37 @@ int do_print(const char *path_p, const struct stat *st, int walk_flags, void *un
+ * skip symlinks altogether, and when doing a half-logical walk, we
+ * skip all non-toplevel symlinks.
+ */
+- if ((walk_flags & WALK_TREE_SYMLINK) &&
+- ((walk_flags & WALK_TREE_PHYSICAL) ||
+- !(walk_flags & (WALK_TREE_TOPLEVEL | WALK_TREE_LOGICAL))))
+- return 0;
++ at_flags = AT_SYMLINK_NOFOLLOW;
++ if ((walk_flags & WALK_TREE_LOGICAL) ||
++ ((walk_flags & WALK_TREE_TOPLEVEL) &&
++ !(walk_flags & WALK_TREE_PHYSICAL)))
++ at_flags = 0;
++
++ if (dirtype == DT_LNK && (at_flags & AT_SYMLINK_NOFOLLOW))
++ goto cleanup;
++
++ if (fstatat(dirfd, pathname, &st, at_flags) != 0)
++ goto fail;
++ if (S_ISLNK(st.st_mode)) {
++ if (!(walk_flags & WALK_TREE_TOPLEVEL))
++ goto cleanup;
++ errno = ELOOP;
++ goto fail;
++ }
+
+ if (opt_print_acl) {
+- acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
++ acl = acl_get_file_at(dirfd, pathname, at_flags,
++ ACL_TYPE_ACCESS);
+ if (acl == NULL && (errno == ENOSYS || errno == ENOTSUP))
+- acl = acl_get_file_mode(path_p);
++ acl = acl_get_from_file_mode_at(dirfd, pathname,
++ at_flags);
+ if (acl == NULL)
+ goto fail;
+ }
+
+- if (opt_print_default_acl && S_ISDIR(st->st_mode)) {
+- default_acl = acl_get_file(path_p, ACL_TYPE_DEFAULT);
++ if (opt_print_default_acl && S_ISDIR(st.st_mode)) {
++ default_acl = acl_get_file_at(dirfd, pathname, at_flags,
++ ACL_TYPE_DEFAULT);
+ if (default_acl == NULL) {
+ if (errno != ENOSYS && errno != ENOTSUP)
+ goto fail;
+@@ -493,34 +528,30 @@ int do_print(const char *path_p, const struct stat *st, int walk_flags, void *un
+ default_prefix = "default:";
+
+ if (opt_strip_leading_slash) {
+- if (*path_p == '/') {
++ if (*fullname == '/') {
+ if (!absolute_warning) {
+ fprintf(stderr, _("%s: Removing leading "
+ "'/' from absolute path names\n"),
+- progname);
++ progname);
+ absolute_warning = 1;
+ }
+- while (*path_p == '/')
+- path_p++;
+- } else if (*path_p == '.' && *(path_p+1) == '/')
+- while (*++path_p == '/')
+- /* nothing */ ;
+- if (*path_p == '\0')
+- path_p = ".";
++ while (*fullname == '/')
++ fullname++;
++ }
+ }
+
+ if (opt_tabular) {
+- if (do_show(stdout, path_p, st, acl, default_acl) != 0)
++ if (do_show(stdout, fullname, &st, acl, default_acl) != 0)
+ goto fail;
+ } else {
+ if (opt_comments) {
+- printf("# file: %s\n", xquote(path_p, "\n\r"));
++ printf("# file: %s\n", xquote(fullname, "\n\r"));
+ printf("# owner: %s\n",
+- xquote(user_name(st->st_uid, opt_numeric), " \t\n\r"));
++ xquote(user_name(st.st_uid, opt_numeric), " \t\n\r"));
+ printf("# group: %s\n",
+- xquote(group_name(st->st_gid, opt_numeric), " \t\n\r"));
+- if ((st->st_mode & (S_ISVTX | S_ISUID | S_ISGID)) && !posixly_correct)
+- printf("# flags: %s\n", flagstr(st->st_mode));
++ xquote(group_name(st.st_gid, opt_numeric), " \t\n\r"));
++ if ((st.st_mode & (S_ISVTX | S_ISUID | S_ISGID)) && !posixly_correct)
++ printf("# flags: %s\n", flagstr(st.st_mode));
+ }
+ if (acl != NULL) {
+ char *acl_text = acl_to_any_text(acl, NULL, '\n',
+@@ -557,8 +588,7 @@ cleanup:
+ return error;
+
+ fail:
+- fprintf(stderr, "%s: %s: %s\n", progname, xquote(path_p, "\n\r"),
+- strerror(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname, fullname, strerror(errno));
+ error = -1;
+ goto cleanup;
+ }
+@@ -589,7 +619,7 @@ void help(void)
+ " -P, --physical physical walk, do not follow symbolic links\n"
+ " -t, --tabular use tabular output format\n"
+ " -n, --numeric print numeric user/group identifiers\n"
+-" --one-file-system skip files on different filesystems\n"
++" --one-file-system don't descend into directories on other filesystems\n"
+ " -p, --absolute-names don't strip leading '/' in pathnames\n"));
+ }
+ #endif
+@@ -666,7 +696,7 @@ int main(int argc, char *argv[])
+ case 'L': /* follow all symlinks */
+ if (posixly_correct)
+ goto synopsis;
+- walk_flags |= WALK_TREE_LOGICAL | WALK_TREE_DEREFERENCE;
++ walk_flags |= WALK_TREE_LOGICAL;
+ walk_flags &= ~WALK_TREE_PHYSICAL;
+ break;
+
+@@ -674,8 +704,7 @@ int main(int argc, char *argv[])
+ if (posixly_correct)
+ goto synopsis;
+ walk_flags |= WALK_TREE_PHYSICAL;
+- walk_flags &= ~(WALK_TREE_LOGICAL | WALK_TREE_DEREFERENCE |
+- WALK_TREE_DEREFERENCE_TOPLEVEL);
++ walk_flags &= ~WALK_TREE_LOGICAL;
+ break;
+
+ case 's': /* skip files with only base entries */
+@@ -713,7 +742,7 @@ int main(int argc, char *argv[])
+ help();
+ return 0;
+
+- case ':': /* option missing */
++ case ':': /* argument missing */
+ case '?': /* unknown option */
+ default:
+ goto synopsis;
+@@ -736,7 +765,7 @@ int main(int argc, char *argv[])
+ if (*line == '\0')
+ continue;
+
+- had_errors += old_walk_tree(line, walk_flags, 0,
++ had_errors += walk_tree(line, walk_flags,
+ do_print, NULL);
+ }
+ if (!feof(stdin)) {
+@@ -745,7 +774,7 @@ int main(int argc, char *argv[])
+ had_errors++;
+ }
+ } else
+- had_errors += old_walk_tree(argv[optind], walk_flags, 0,
++ had_errors += walk_tree(argv[optind], walk_flags,
+ do_print, NULL);
+ optind++;
+ } while (optind < argc);
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,403 @@
+From 28d24088e44b651394c83440504353ef130c1e3d Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Tue, 9 Jun 2026 08:43:46 +0200
+Subject: [PATCH] harden setfacl
+
+Switch to the new walk_tree helper that uses directory file descriptors and
+relative paths instead of building full pathnames. On the final pathname
+component, use functions that only follow symlinks when requested (stat() ->
+fstatat(), acl_get_file() -> acl_get_file_at(), chmod() -> fchmodat(),
+acl_set_file() -> acl_set_file_at(), acl_delete_def_file() ->
+acl_delete_def_file_at()). This prevents setfacl from accidentally following a
+symlink an attacker may have planted.
+
+A notable change is that commands like 'setfacl -P ... symlink' will now fail
+with an ELOOP error ("Too many levels of symbolic links"). Previously,
+command-line arguments that refer to symbolic links were silently ignored in
+-P (--physical) mode.
+
+(The --restore option is hardened in a separate patch.)
+
+Fixes: CVE-2026-54370 ("acl < 2.4.0 TOCTOU Symlink Traversal via getfacl/setfacl")
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=2c03ceefa0610d551d9cc089ce0a404cbe2d34b4]
+
+Backport Changes:
+- Retain existing walk_flags linkage with the new enum type.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 2c03ceefa0610d551d9cc089ce0a404cbe2d34b4)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ man/man1/setfacl.1 | 1 -
+ tools/do_set.c | 98 +++++++++++++++++++++++++++++++---------------
+ tools/do_set.h | 5 ++-
+ tools/setfacl.c | 18 ++++-----
+ 4 files changed, 79 insertions(+), 43 deletions(-)
+
+diff --git a/man/man1/setfacl.1 b/man/man1/setfacl.1
+index 1ccaa11..cc826df 100644
+--- a/man/man1/setfacl.1
++++ b/man/man1/setfacl.1
+@@ -130,7 +130,6 @@ This option cannot be mixed with `\-\-restore'.
+ .TP 4
+ .I \-P, \-\-physical
+ Physical walk, do not follow symbolic links to directories.
+-This also skips symbolic link arguments.
+ Only effective in combination with \-R.
+ This option cannot be mixed with `\-\-restore'.
+ .TP 4
+diff --git a/tools/do_set.c b/tools/do_set.c
+index a958ede..2ff533c 100644
+--- a/tools/do_set.c
++++ b/tools/do_set.c
+@@ -31,13 +31,14 @@
+ #include <sys/types.h>
+ #include <sys/stat.h>
+ #include <unistd.h>
++#include <fcntl.h>
+ #include <dirent.h>
+ #include <ftw.h>
+ #include "misc.h"
+ #include "sequence.h"
+ #include "do_set.h"
+ #include "parse.h"
+-#include "old_walk_tree.h"
++#include "walk_tree.h"
+
+
+ extern const char *progname;
+@@ -124,7 +125,7 @@ clone_entry(
+ void
+ print_test(
+ FILE *file,
+- const char *path_p,
++ const char *fullname,
+ const struct stat *st,
+ const acl_t acl,
+ const acl_t default_acl)
+@@ -134,7 +135,7 @@ print_test(
+ acl_text = acl_to_any_text(acl, NULL, ',', TEXT_ABBREVIATE);
+ default_acl_text =
+ acl_to_any_text(default_acl, "d:", ',', TEXT_ABBREVIATE);
+- fprintf(file, "%s: %s,%s\n", path_p,
++ fprintf(file, "%s: %s,%s\n", fullname,
+ acl_text ? acl_text : "*",
+ default_acl_text ? default_acl_text : "*");
+ acl_free(acl_text);
+@@ -167,7 +168,9 @@ set_perm(
+
+ static int
+ retrieve_acl(
+- const char *path_p,
++ int dirfd,
++ const char *pathname,
++ int at_flags,
+ acl_type_t type,
+ const struct stat *st,
+ acl_t *old_acl,
+@@ -177,7 +180,7 @@ retrieve_acl(
+ return 0;
+ *acl = NULL;
+ if (type == ACL_TYPE_ACCESS || S_ISDIR(st->st_mode)) {
+- *old_acl = acl_get_file(path_p, type);
++ *old_acl = acl_get_file_at(dirfd, pathname, at_flags, type);
+ if (*old_acl == NULL && (errno == ENOSYS || errno == ENOTSUP)) {
+ if (type == ACL_TYPE_DEFAULT)
+ *old_acl = acl_init(0);
+@@ -251,18 +254,23 @@ remove_extended_entries(
+
+
+ #define RETRIEVE_ACL(type) do { \
+- error = retrieve_acl(path_p, type, st, old_xacl, xacl); \
++ error = retrieve_acl(dirfd, pathname, at_flags, type, &st, \
++ old_xacl, xacl); \
+ if (error) \
+ goto fail; \
+ } while(0)
+
+ int
+ do_set(
+- const char *path_p,
+- const struct stat *st,
+- int walk_flags,
++ int dirfd,
++ const char *dirname,
++ const char *pathname,
++ unsigned char dirtype,
++ enum walk_flags walk_flags,
+ void *arg)
+ {
++ static char *__fullname;
++ const char *fullname;
+ struct do_set_args *args = arg;
+ acl_t old_acl = NULL, old_default_acl = NULL;
+ acl_t acl = NULL, default_acl = NULL;
+@@ -274,9 +282,23 @@ do_set(
+ char *acl_text;
+ int acl_modified = 0, default_acl_modified = 0;
+ int acl_mask_provided = 0, default_acl_mask_provided = 0;
++ struct stat st;
++ int at_flags;
++
++ 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, path_p, strerror(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname, fullname,
++ strerror(errno));
+ return 1;
+ }
+
+@@ -285,11 +307,23 @@ do_set(
+ * skip symlinks altogether, and when doing a half-logical walk, we
+ * skip all non-toplevel symlinks.
+ */
+- if ((walk_flags & WALK_TREE_SYMLINK) &&
+- ((walk_flags & WALK_TREE_PHYSICAL) ||
+- !(walk_flags & (WALK_TREE_TOPLEVEL | WALK_TREE_LOGICAL))))
++
++ at_flags = AT_SYMLINK_NOFOLLOW;
++ if ((walk_flags & WALK_TREE_LOGICAL) ||
++ ((walk_flags & WALK_TREE_TOPLEVEL) &&
++ !(walk_flags & WALK_TREE_PHYSICAL)))
++ at_flags = 0;
++
++ if (dirtype == DT_LNK && (at_flags & AT_SYMLINK_NOFOLLOW))
+ return 0;
+
++ if (fstatat(dirfd, pathname, &st, at_flags) != 0)
++ goto fail;
++ if (S_ISLNK(st.st_mode)) {
++ errno = ELOOP;
++ goto fail;
++ }
++
+ /* Execute the commands in seq (read ACLs on demand) */
+ error = seq_get_cmd(args->seq, SEQ_FIRST_CMD, &cmd);
+ if (error == 0)
+@@ -316,7 +350,7 @@ do_set(
+ /* Check for `X', and replace with `x' as appropriate. */
+ if (perm & CMD_PERM_COND_EXECUTE) {
+ perm &= ~CMD_PERM_COND_EXECUTE;
+- if (S_ISDIR(st->st_mode) || has_execute_perms(*xacl))
++ if (S_ISDIR(st.st_mode) || has_execute_perms(*xacl))
+ perm |= CMD_PERM_EXECUTE;
+ }
+
+@@ -407,8 +441,9 @@ do_set(
+ if (error > 0) {
+ acl_text = acl_to_any_text(acl, NULL, ',', 0);
+ fprintf(stderr, _("%s: %s: Malformed access ACL "
+- "`%s': %s at entry %d\n"), progname, path_p,
+- acl_text, acl_error(error), which_entry+1);
++ "`%s': %s at entry %d\n"), progname,
++ fullname, acl_text, acl_error(error),
++ which_entry+1);
+ acl_free(acl_text);
+ errors++;
+ goto cleanup;
+@@ -435,7 +470,7 @@ do_set(
+ acl_text = acl_to_any_text(default_acl, NULL, ',', 0);
+ fprintf(stderr, _("%s: %s: Malformed default ACL "
+ "`%s': %s at entry %d\n"),
+- progname, path_p, acl_text,
++ progname, fullname, acl_text,
+ acl_error(error), which_entry+1);
+ acl_free(acl_text);
+ errors++;
+@@ -444,7 +479,7 @@ do_set(
+ }
+
+ /* Only directories can have default ACLs */
+- if (default_acl && !S_ISDIR(st->st_mode) && (walk_flags & WALK_TREE_RECURSIVE)) {
++ if (default_acl && !S_ISDIR(st.st_mode) && (walk_flags & WALK_TREE_RECURSIVE)) {
+ /* In recursive mode, ignore default ACLs for files */
+ acl_free(default_acl);
+ default_acl = NULL;
+@@ -463,7 +498,7 @@ do_set(
+
+ /* update the file system */
+ if (opt_test) {
+- print_test(stdout, path_p, st,
++ print_test(stdout, fullname, &st,
+ acl, default_acl);
+ goto cleanup;
+ }
+@@ -473,17 +508,15 @@ do_set(
+
+ equiv_mode = acl_equiv_mode(acl, &mode);
+
+- if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) != 0) {
++ if (acl_set_file_at(dirfd, pathname, at_flags, ACL_TYPE_ACCESS,
++ acl) != 0) {
+ if (errno == ENOSYS || errno == ENOTSUP) {
+ if (equiv_mode != 0)
+ goto fail;
+ else {
+- struct stat st;
+-
+- if (stat(path_p, &st) != 0)
+- goto fail;
+ mode |= st.st_mode & 07000;
+- if (chmod(path_p, mode) != 0)
++ if (fchmodat(dirfd, pathname, mode,
++ at_flags) != 0)
+ goto fail;
+ }
+ } else
+@@ -492,21 +525,23 @@ do_set(
+ args->mode = mode;
+ }
+ if (default_acl) {
+- if (S_ISDIR(st->st_mode)) {
++ if (S_ISDIR(st.st_mode)) {
+ if (acl_entries(default_acl) == 0) {
+- if (acl_delete_def_file(path_p) != 0 &&
++ if (acl_delete_def_file_at(dirfd, pathname,
++ at_flags) != 0 &&
+ errno != ENOSYS && errno != ENOTSUP)
+ goto fail;
+ } else {
+- if (acl_set_file(path_p, ACL_TYPE_DEFAULT,
+- default_acl) != 0)
++ if (acl_set_file_at(dirfd, pathname, at_flags,
++ ACL_TYPE_DEFAULT,
++ default_acl) != 0)
+ goto fail;
+ }
+ } else {
+ if (acl_entries(default_acl) != 0) {
+ fprintf(stderr, _("%s: %s: Only directories "
+ "can have default ACLs\n"),
+- progname, path_p);
++ progname, fullname);
+ errors++;
+ goto cleanup;
+ }
+@@ -527,7 +562,8 @@ cleanup:
+ return errors;
+
+ fail:
+- fprintf(stderr, "%s: %s: %s\n", progname, path_p, strerror(errno));
++ fprintf(stderr, "%s: %s: %s\n", progname, fullname,
++ strerror(errno));
+ errors++;
+ goto cleanup;
+ }
+diff --git a/tools/do_set.h b/tools/do_set.h
+index 15566f3..460e369 100644
+--- a/tools/do_set.h
++++ b/tools/do_set.h
+@@ -23,13 +23,14 @@
+ #define __DO_SET_H
+
+ #include "sequence.h"
++#include "walk_tree.h"
+
+ struct do_set_args {
+ seq_t seq;
+ mode_t mode;
+ };
+
+-extern int do_set(const char *path_p, const struct stat *stat_p, int flags,
+- void *arg);
++extern int do_set(int dirfd, const char *dirname, const char *pathname,
++ unsigned char dirtype, enum walk_flags flags, void *arg);
+
+ #endif /* __DO_SET_H */
+diff --git a/tools/setfacl.c b/tools/setfacl.c
+index cb05975..6c504a7 100644
+--- a/tools/setfacl.c
++++ b/tools/setfacl.c
+@@ -24,6 +24,7 @@
+ #include <stdio.h>
+ #include <string.h>
+ #include <unistd.h>
++#include <fcntl.h>
+ #include <errno.h>
+ #include <sys/stat.h>
+ #include <dirent.h>
+@@ -33,7 +34,7 @@
+ #include "sequence.h"
+ #include "parse.h"
+ #include "do_set.h"
+-#include "old_walk_tree.h"
++#include "walk_tree.h"
+
+ #define POSIXLY_CORRECT_STR "POSIXLY_CORRECT"
+
+@@ -74,7 +75,7 @@ static const struct option long_options[] = {
+ const char *progname;
+ const char *cmd_line_options, *cmd_line_spec;
+
+-int walk_flags = WALK_TREE_DEREFERENCE_TOPLEVEL;
++enum walk_flags walk_flags = 0;
+ int opt_recalculate; /* recalculate mask entry (0=default, 1=yes, -1=no) */
+ int opt_promote; /* promote access ACL to default ACL */
+ int opt_test; /* do not write to the file system.
+@@ -184,7 +185,7 @@ restore(
+ }
+
+ args.mode = 0;
+- error = do_set(path_p, &st, 0, &args);
++ error = do_set(AT_FDCWD, "", path_p, DT_UNKNOWN, WALK_TREE_PHYSICAL, &args);
+ if (error != 0) {
+ status = 1;
+ goto resume;
+@@ -310,14 +311,14 @@ int next_file(const char *arg, seq_t seq)
+
+ if (strcmp(arg, "-") == 0) {
+ while ((line = __acl_next_line(stdin)))
+- errors = old_walk_tree(line, walk_flags, 0, do_set, &args);
++ errors = walk_tree(line, walk_flags, do_set, &args);
+ if (!feof(stdin)) {
+ fprintf(stderr, _("%s: Standard input: %s\n"),
+ progname, strerror(errno));
+ errors = 1;
+ }
+ } else {
+- errors = old_walk_tree(arg, walk_flags, 0, do_set, &args);
++ errors = walk_tree(arg, walk_flags, do_set, &args);
+ }
+ return errors ? 1 : 0;
+ }
+@@ -590,14 +591,13 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'L': /* follow symlinks */
+- walk_flags |= WALK_TREE_LOGICAL | WALK_TREE_DEREFERENCE;
++ walk_flags |= WALK_TREE_LOGICAL;
+ walk_flags &= ~WALK_TREE_PHYSICAL;
+ break;
+
+ case 'P': /* do not follow symlinks */
+ walk_flags |= WALK_TREE_PHYSICAL;
+- walk_flags &= ~(WALK_TREE_LOGICAL | WALK_TREE_DEREFERENCE |
+- WALK_TREE_DEREFERENCE_TOPLEVEL);
++ walk_flags &= ~WALK_TREE_LOGICAL;
+ break;
+
+ case 't': /* test mode */
+@@ -614,7 +614,7 @@ int main(int argc, char *argv[])
+ status = 0;
+ goto cleanup;
+
+- case ':': /* option missing */
++ case ':': /* argument missing */
+ case '?': /* unknown option */
+ default:
+ goto synopsis;
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,603 @@
+From 8949b1ccf74caeef8f8d6134bebd0e2a98117ff0 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Sat, 20 Jun 2026 19:35:48 +0200
+Subject: [PATCH] harden setfacl --restore
+
+In setfacl --restore, use functions that support the AT_SYMLINK_NOFOLLOW flag
+(stat() -> fstatat(), chown() -> fchownat(), chmod() -> fchmodat()). Allow the
+-P (--physical) option to be used together with --restore.
+
+In physical restore mode, use openat2(RESOLVE_NO_SYMLINKS) for resolving the
+pathname components leading up to the last one, and use the AT_SYMLINK_NOFOLLOW
+flag on the last pathname component.
+
+On systems where openat2() *is* available, warn when attempting a non-physical
+restore as that can be dangerous. The purpose of this warning is to cause
+users to switch to physical restore mode over time.
+
+This warning can be disabled with the --disable-unsafe-restore-warnings
+configure option.
+
+On systems where openat2() *is not* available, do not warn when attempting a
+non-physical restore; 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-54370 ("acl < 2.4.0 TOCTOU Symlink Traversal via getfacl/setfacl")
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=54e14e9bc545f505b379d0792a2748d9baf88700]
+
+Backport Changes:
+- Retain .test names used by the ACL 2.3.2 test harness.
+- Include the upstream restore test absent from ACL 2.3.2.
+- Name it restore-symlinks.test to avoid root ptest exclusions.
+- Rebase restore logic onto ACL 2.3.2's existing implementation.
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 54e14e9bc545f505b379d0792a2748d9baf88700)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ configure.ac | 9 ++
+ man/man1/setfacl.1 | 16 +--
+ test/Makemodule.am | 9 +-
+ test/restore-symlinks.test | 26 +++++
+ test/root/restore.test | 2 +-
+ test/sbits-restore.test | 2 +-
+ test/utf8-filenames.test | 2 +-
+ tools/setfacl.c | 211 +++++++++++++++++++++++++++++++------
+ 8 files changed, 231 insertions(+), 46 deletions(-)
+ create mode 100644 test/restore-symlinks.test
+
+diff --git a/configure.ac b/configure.ac
+index c3353f6..3632345 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -48,6 +48,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/setfacl.1 b/man/man1/setfacl.1
+index cc826df..c04eb3a 100644
+--- a/man/man1/setfacl.1
++++ b/man/man1/setfacl.1
+@@ -30,7 +30,7 @@ setfacl \- set file access control lists
+ [\-bkndRLPvh] [{\-m|\-x} acl_spec] [{\-M|\-X} acl_file] file ...
+
+ .B setfacl
+-\-\-restore={file|-}
++[\-P] \-\-restore={file|-}
+
+ .SH DESCRIPTION
+ This utility sets Access Control Lists (ACLs) of files and directories.
+@@ -113,25 +113,27 @@ of a complete directory subtree are restored using this mechanism. If the input
+ contains owner comments or group comments, setfacl attempts to restore the
+ owner and owning group. If the input contains flags comments (which define the setuid,
+ setgid, and sticky bits), setfacl sets those three bits accordingly; otherwise,
+-it clears them. This option cannot be mixed with other options except `\-\-test'.
+-If the file specified is '\fB-\fP', then it will be read from standard input.
++it clears them. This option cannot be combined with any other options except
++`\-P' and `\-\-test'. When combined with `\-P', setfacl will not follow
++symlinks when resolving pathnames. If the file specified is '\fB-\fP', then it
++will be read from standard input.
+ .TP 4
+ .I \-\-test
+ Test mode. Instead of changing the ACLs of any files, the resulting ACLs are listed.
+ .TP 4
+ .I \-R, \-\-recursive
+-Apply operations to all files and directories recursively. This option cannot be mixed with `\-\-restore'.
++Apply operations to all files and directories recursively. This option cannot
++be combined with `\-\-restore'.
+ .TP 4
+ .I \-L, \-\-logical
+ Logical walk, follow symbolic links to directories. The default behavior is to follow
+ symbolic link arguments, and skip symbolic links encountered in subdirectories.
+ Only effective in combination with \-R.
+-This option cannot be mixed with `\-\-restore'.
++This option cannot be combined with `\-\-restore'.
+ .TP 4
+ .I \-P, \-\-physical
+ Physical walk, do not follow symbolic links to directories.
+-Only effective in combination with \-R.
+-This option cannot be mixed with `\-\-restore'.
++Only effective in combination with \-R or \-\-restore.
+ .TP 4
+ .I \-v, \-\-version
+ Print the version of setfacl and exit.
+diff --git a/test/Makemodule.am b/test/Makemodule.am
+index e1d715d..89497af 100644
+--- a/test/Makemodule.am
++++ b/test/Makemodule.am
+@@ -8,15 +8,20 @@ TESTS = \
+ test/getfacl-recursive.test \
+ test/malformed-restore.test \
+ test/misc.test \
+- test/sbits-restore.test \
+ test/setfacl-X.test \
+- test/utf8-filenames.test \
+ test/root/getfacl.test \
+ test/root/permissions.test \
+ test/root/restore.test \
+ test/root/setfacl.test \
+ $(XFAIL_TESTS)
+
++if UNSAFE_RESTORE_WARNINGS
++TESTS += \
++ test/restore-symlinks.test \
++ test/sbits-restore.test \
++ test/utf8-filenames.test
++endif
++
+ EXTRA_DIST += \
+ test/make-tree \
+ test/malformed-restore-double-owner.acl \
+diff --git a/test/restore-symlinks.test b/test/restore-symlinks.test
+new file mode 100644
+index 0000000..9eb055e
+--- /dev/null
++++ b/test/restore-symlinks.test
+@@ -0,0 +1,26 @@
++ $ mkdir d
++ $ touch d/f
++ $ ln -s f d/l
++ $ getfacl -RP d > physical
++ $ getfacl -RL d > logical
++
++Restoring a physical backup should work:
++
++ $ setfacl --restore=physical --physical
++
++Restoring a backup non-physically should produce the following warning:
++
++ $ setfacl --restore=physical
++ > Warning: option --restore=file is unsafe without option -P (--physical) as it traverses symbolic links in pathnames
++
++
++Restoring a logical backup can fail:
++
++ $ setfacl --restore=logical --physical
++ > setfacl: d/l: Too many levels of symbolic links
++
++Malformed restore file
++
++ $ echo "# owner: root" > dump
++ $ setfacl --restore=dump 2>&1
++ >setfacl: dump: No filename found in line 0, aborting
+diff --git a/test/root/restore.test b/test/root/restore.test
+index c85097c..504a525 100644
+--- a/test/root/restore.test
++++ b/test/root/restore.test
+@@ -20,7 +20,7 @@ Cry immediately if we are not running as root.
+ >
+ $ chown bin passwd
+ $ chmod u+s passwd
+- $ setfacl --restore passwd.acl
++ $ setfacl --restore passwd.acl --physical
+ $ ls -dl passwd | awk '{print $1 " " $3 " " $4}' | sed 's/\\.//g'
+ > -rwsr-xr-x root root
+
+diff --git a/test/sbits-restore.test b/test/sbits-restore.test
+index 5899b0a..3d048ea 100644
+--- a/test/sbits-restore.test
++++ b/test/sbits-restore.test
+@@ -12,7 +12,7 @@ Ensure setting of SUID/SGID/sticky via --restore works
+ $ mkdir d
+ $ touch d/g
+ $ touch d/u
+- $ setfacl --restore d.acl
++ $ setfacl --restore=d.acl --physical
+ $ ls -dl d | awk '{print $1}' | sed 's/\\.$//g'
+ > drwxr-xr-t
+ $ ls -dl d/u | awk '{print $1}' | sed 's/\\.$//g'
+diff --git a/test/utf8-filenames.test b/test/utf8-filenames.test
+index dae0308..ad5b9dc 100644
+--- a/test/utf8-filenames.test
++++ b/test/utf8-filenames.test
+@@ -9,6 +9,6 @@ NAME_MAX.
+ $ mkdir -p $UPATH/$UPATH/$UPATH
+ $ touch $UPATH/$UPATH/$UPATH/$UPATH
+ $ getfacl $UPATH/$UPATH/$UPATH/$UPATH > utf8-filenames.acl
+- $ setfacl --restore=utf8-filenames.acl
++ $ setfacl --restore=utf8-filenames.acl --physical
+ $ rm -R $UPATH
+ $ rm utf8-filenames.acl
+diff --git a/tools/setfacl.c b/tools/setfacl.c
+index 6c504a7..070cb73 100644
+--- a/tools/setfacl.c
++++ b/tools/setfacl.c
+@@ -35,16 +35,19 @@
+ #include "parse.h"
+ #include "do_set.h"
+ #include "walk_tree.h"
++#include "openat2.h"
++#include "fchmodat_compat.h"
+
+ #define POSIXLY_CORRECT_STR "POSIXLY_CORRECT"
+
+ /* '-' stands for `process non-option arguments in loop' */
+ #if !POSIXLY_CORRECT
+ # define CMD_LINE_OPTIONS "-:bkndvhm:M:x:X:RLP"
+-# define CMD_LINE_SPEC "[-bkndRLP] { -m|-M|-x|-X ... } file ..."
++# define CMD_LINE_SPEC1 "[-bkndRLP] { -m|-M|-x|-X ... } file ..."
++# define CMD_LINE_SPEC2 "[-P] --restore=file"
+ #endif
+ #define POSIXLY_CMD_LINE_OPTIONS "-:bkndvhm:M:x:X:"
+-#define POSIXLY_CMD_LINE_SPEC "[-bknd] {-m|-M|-x|-X ... } file ..."
++#define POSIXLY_CMD_LINE_SPEC1 "[-bknd] {-m|-M|-x|-X ... } file ..."
+
+ static const struct option long_options[] = {
+ #if !POSIXLY_CORRECT
+@@ -117,7 +120,8 @@ has_any_of_type(
+ int
+ restore(
+ FILE *file,
+- const char *filename)
++ const char *filename,
++ enum walk_flags walk_flags)
+ {
+ char *path_p;
+ struct stat st;
+@@ -128,6 +132,12 @@ restore(
+ int lineno = 0, backup_line;
+ int error, status = 0;
+ int chmod_required = 0;
++ int dirfd = -1;
++ char *dirname = NULL, *pathname;
++ int at_flags = (walk_flags & WALK_TREE_PHYSICAL) ? AT_SYMLINK_NOFOLLOW : 0;
++#ifdef UNSAFE_RESTORE_WARNINGS
++ static int non_physical_restore_warning;
++#endif
+
+ memset(&st, 0, sizeof(st));
+
+@@ -177,15 +187,75 @@ restore(
+ goto getout;
+ }
+
+- error = stat(path_p, &st);
+- if (opt_test && error != 0) {
++#ifdef UNSAFE_RESTORE_WARNINGS
++ if (!(walk_flags & WALK_TREE_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;
++ }
++#endif
++
++ /* find the last pathname component */
++ pathname = path_p + strlen(path_p);
++ while (pathname > path_p && pathname[-1] == '/')
++ pathname--;
++ while (pathname > path_p && pathname[-1] != '/')
++ pathname--;
++
++ if ((walk_flags & WALK_TREE_PHYSICAL) && pathname != path_p) {
++ dirname = malloc(pathname - path_p + 1);
++ if (dirname == NULL) {
++ fprintf(stderr, "%s: %s\n",
++ progname, strerror(errno));
++ status = 1;
++ goto getout;
++ }
++ memcpy(dirname, path_p, pathname - path_p);
++ dirname[pathname - path_p] = '\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,
++ xquote(dirname, "\n\r"),
++ strerror(errno));
++ status = 1;
++ goto resume;
++ }
++ } else {
++ dirfd = AT_FDCWD;
++ dirname = "";
++ pathname = path_p;
++ }
++
++ error = fstatat(dirfd, pathname, &st, at_flags);
++ if (error == 0 && S_ISLNK(st.st_mode)) {
++ errno = ELOOP;
++ error = -1;
++ }
++ if (error != 0) {
+ fprintf(stderr, "%s: %s: %s\n", progname,
+ xquote(path_p, "\n\r"), strerror(errno));
+ status = 1;
++ goto resume;
+ }
+
+ args.mode = 0;
+- error = do_set(AT_FDCWD, "", path_p, DT_UNKNOWN, WALK_TREE_PHYSICAL, &args);
++ error = do_set(dirfd, dirname, pathname, DT_UNKNOWN,
++ walk_flags | WALK_TREE_TOPLEVEL, &args);
+ if (error != 0) {
+ status = 1;
+ goto resume;
+@@ -201,7 +271,8 @@ restore(
+ st.st_gid = -1;
+ if (!opt_test &&
+ (st.st_uid != -1 || st.st_gid != -1)) {
+- if (chown(path_p, st.st_uid, st.st_gid) != 0) {
++ if (fchownat(dirfd, pathname, st.st_uid, st.st_gid,
++ at_flags) != 0) {
+ fprintf(stderr, _("%s: %s: Cannot change "
+ "owner/group: %s\n"),
+ progname, xquote(path_p, "\n\r"),
+@@ -220,7 +291,8 @@ restore(
+ if (!args.mode)
+ args.mode = st.st_mode;
+ args.mode &= (S_IRWXU | S_IRWXG | S_IRWXO);
+- if (chmod(path_p, flags | args.mode) != 0) {
++ if (fchmodat(dirfd, pathname, flags | args.mode,
++ at_flags) != 0) {
+ fprintf(stderr, _("%s: %s: Cannot change "
+ "mode: %s\n"),
+ progname, xquote(path_p, "\n\r"),
+@@ -229,6 +301,14 @@ restore(
+ }
+ }
+ resume:
++ if (dirfd != -1 && dirfd != AT_FDCWD) {
++ close(dirfd);
++ dirfd = -1;
++ }
++ if (dirname && *dirname) {
++ free(dirname);
++ dirname = NULL;
++ }
+ if (path_p) {
+ free(path_p);
+ path_p = NULL;
+@@ -267,6 +347,12 @@ void help(void)
+ progname, VERSION);
+ printf(_("Usage: %s %s\n"),
+ progname, cmd_line_spec);
++#if !POSIXLY_CORRECT
++ if (!posixly_correct) {
++ printf(" %s %s\n",
++ progname, CMD_LINE_SPEC2);
++ }
++#endif
+ printf(_(
+ " -m, --modify=acl modify the current ACL(s) of file(s)\n"
+ " -M, --modify-file=file read ACL entries to modify from file\n"
+@@ -330,6 +416,9 @@ int next_file(const char *arg, seq_t seq)
+
+ int main(int argc, char *argv[])
+ {
++ enum { UNDEFINED_MODE, SET_MODE, RESTORE_MODE } mode = UNDEFINED_MODE;
++ char **restore_args = NULL;
++ int opt_restore_count = 0;
+ int opt;
+ int saw_files = 0;
+ int status = 0, status2;
+@@ -344,16 +433,16 @@ int main(int argc, char *argv[])
+
+ #if POSIXLY_CORRECT
+ cmd_line_options = POSIXLY_CMD_LINE_OPTIONS;
+- cmd_line_spec = _(POSIXLY_CMD_LINE_SPEC);
++ cmd_line_spec = POSIXLY_CMD_LINE_SPEC1;
+ #else
+ if (getenv(POSIXLY_CORRECT_STR))
+ posixly_correct = 1;
+ if (!posixly_correct) {
+ cmd_line_options = CMD_LINE_OPTIONS;
+- cmd_line_spec = _(CMD_LINE_SPEC);
++ cmd_line_spec = CMD_LINE_SPEC1;
+ } else {
+ cmd_line_options = POSIXLY_CMD_LINE_OPTIONS;
+- cmd_line_spec = _(POSIXLY_CMD_LINE_SPEC);
++ cmd_line_spec = POSIXLY_CMD_LINE_SPEC1;
+ }
+ #endif
+
+@@ -383,6 +472,9 @@ int main(int argc, char *argv[])
+
+ switch (opt) {
+ case 'b': /* remove all extended entries */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (seq_append_cmd(seq, CMD_REMOVE_EXTENDED_ACL,
+ ACL_TYPE_ACCESS) ||
+ seq_append_cmd(seq, CMD_REMOVE_ACL,
+@@ -391,20 +483,32 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'k': /* remove default ACL */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (seq_append_cmd(seq, CMD_REMOVE_ACL,
+ ACL_TYPE_DEFAULT))
+ ERRNO_ERROR(1);
+ break;
+
+ case 'n': /* do not recalculate mask */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ opt_recalculate = -1;
+ break;
+
+ case 'r': /* force recalculate mask */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ opt_recalculate = 1;
+ break;
+
+ case 'd': /* operations apply to default ACL */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ opt_promote = 1;
+ break;
+
+@@ -440,6 +544,9 @@ int main(int argc, char *argv[])
+ goto set_modify_delete;
+
+ set_modify_delete:
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (!posixly_correct)
+ parse_mode |= SEQ_PARSE_DEFAULT;
+ if (opt_promote)
+@@ -498,6 +605,9 @@ int main(int argc, char *argv[])
+ goto set_modify_delete_from_file;
+
+ set_modify_delete_from_file:
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (!posixly_correct)
+ parse_mode |= SEQ_PARSE_DEFAULT;
+ if (opt_promote)
+@@ -551,6 +661,9 @@ int main(int argc, char *argv[])
+
+
+ case '\1': /* file argument */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if (seq_empty(seq))
+ goto synopsis;
+ saw_files = 1;
+@@ -561,36 +674,31 @@ int main(int argc, char *argv[])
+ break;
+
+ case 'B': /* restore ACL backup */
+- saw_files = 1;
+-
+- if (strcmp(optarg, "-") == 0)
+- file = stdin;
+- else {
+- file = fopen(optarg, "r");
+- if (file == NULL) {
+- fprintf(stderr, "%s: %s: %s\n",
+- progname,
+- xquote(optarg, "\n\r"),
+- strerror(errno));
+- status = 2;
+- goto cleanup;
+- }
+- }
+-
+- status = restore(file,
+- (file == stdin) ? NULL : optarg);
+-
+- if (file != stdin)
+- fclose(file);
+- if (status != 0)
++ if (mode == SET_MODE)
++ goto synopsis;
++ mode = RESTORE_MODE;
++ opt_restore_count++;
++ restore_args = realloc(restore_args,
++ opt_restore_count * sizeof(*restore_args));
++ if (!restore_args) {
++ fprintf(stderr, "%s: %s\n", progname, strerror(errno));
++ status = 1;
+ goto cleanup;
++ }
++ restore_args[opt_restore_count - 1] = optarg;
+ break;
+
+ case 'R': /* recursive */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ walk_flags |= WALK_TREE_RECURSIVE;
+ break;
+
+ case 'L': /* follow symlinks */
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ walk_flags |= WALK_TREE_LOGICAL;
+ walk_flags &= ~WALK_TREE_PHYSICAL;
+ break;
+@@ -636,7 +744,42 @@ int main(int argc, char *argv[])
+ seq_delete_cmd(seq, seq_remove_default_acl_cmd);
+ }
+ }
++
++ if (mode == RESTORE_MODE) {
++ if (walk_flags & WALK_TREE_LOGICAL)
++ goto synopsis;
++
++ for (opt = 0; opt < opt_restore_count; opt++) {
++ if (strcmp(restore_args[opt], "-") == 0)
++ file = stdin;
++ else {
++ file = fopen(restore_args[opt], "r");
++ if (file == NULL) {
++ fprintf(stderr, "%s: %s: %s\n",
++ progname,
++ xquote(restore_args[opt], "\n\r"),
++ strerror(errno));
++ status = 2;
++ goto cleanup;
++ }
++ }
++
++ status2 = restore(file,
++ (file == stdin) ? NULL : restore_args[opt],
++ walk_flags);
++
++ if (file != stdin)
++ fclose(file);
++ if (status == 0)
++ status = status2;
++ }
++ free(restore_args);
++ }
++
+ while (optind < argc) {
++ if (mode == RESTORE_MODE)
++ goto synopsis;
++ mode = SET_MODE;
+ if(!seq)
+ goto synopsis;
+ if (seq_empty(seq))
+@@ -647,7 +790,7 @@ int main(int argc, char *argv[])
+ if (status == 0)
+ status = status2;
+ }
+- if (!saw_files)
++ if (mode == SET_MODE && !saw_files)
+ goto synopsis;
+
+ goto cleanup;
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,213 @@
+From b1a2f75859542ccef4afe60647af42fd9572bd31 Mon Sep 17 00:00:00 2001
+From: Andreas Gruenbacher <agruenba@redhat.com>
+Date: Mon, 8 Jun 2026 13:47:24 +0200
+Subject: [PATCH] harden chacl
+
+Harden chacl by making sure it doesn't follow any symbolic links other than
+the ones specified on the command line. This is done by resolving pathnames
+relative to directory file descriptors and switching to symlink-safe functions
+(acl_get_file() -> acl_get_file_at(), acl_set_file() -> acl_set_file_at(),
+opendir() -> openat() + fdopendir()).
+
+Fixes: CVE-2026-54370 ("acl < 2.4.0 TOCTOU Symlink Traversal via getfacl/setfacl")
+
+CVE: CVE-2026-54370
+Upstream-Status: Backport [https://cgit.git.savannah.nongnu.org/cgit/acl.git/commit/?id=601cc884a548ae9e9d246ae749e54b3272e4b1d7]
+
+Signed-off-by: Andreas Gruenbacher <agruenba@redhat.com>
+(cherry picked from commit 601cc884a548ae9e9d246ae749e54b3272e4b1d7)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ tools/chacl.c | 108 +++++++++++++++++++++++++++++++++++---------------
+ 1 file changed, 77 insertions(+), 31 deletions(-)
+
+diff --git a/tools/chacl.c b/tools/chacl.c
+index 8fff875..3d57413 100644
+--- a/tools/chacl.c
++++ b/tools/chacl.c
+@@ -21,6 +21,7 @@
+ #include <stdlib.h>
+ #include <string.h>
+ #include <unistd.h>
++#include <fcntl.h>
+ #include <getopt.h>
+ #include <libgen.h>
+ #include <stdio.h>
+@@ -34,8 +35,10 @@
+
+ static int acl_delete_file (const char * path, acl_type_t type);
+ static int list_acl(char *file);
+-static int set_acl(acl_t acl, acl_t dacl, const char *fname);
+-static int walk_dir(acl_t acl, acl_t dacl, const char *fname);
++static int set_acl(acl_t acl, acl_t dacl, int dirfd, const char *dname,
++ const char *fname, unsigned int depth);
++static int walk_dir(acl_t acl, acl_t dacl, int dirfd, const char *dname,
++ const char *fname, unsigned int depth);
+
+ static char *program;
+ static int rflag;
+@@ -197,7 +200,7 @@ main(int argc, char *argv[])
+
+ /* place acls on files */
+ for (; optind < argc; optind++)
+- failed += set_acl(acl, dacl, argv[optind]);
++ failed += set_acl(acl, dacl, AT_FDCWD, "", argv[optind], 0);
+
+ if (acl)
+ acl_free(acl);
+@@ -221,7 +224,8 @@ acl_delete_file(const char *path, acl_type_t type)
+ acl_entry_t entry;
+ acl_tag_t tag;
+
+- acl = acl_get_file(path, ACL_TYPE_ACCESS);
++ acl = acl_get_file_at(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW,
++ ACL_TYPE_ACCESS);
+ if (!acl)
+ return -1;
+ error = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+@@ -237,9 +241,12 @@ acl_delete_file(const char *path, acl_type_t type)
+ error = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
+ }
+ if (!error)
+- error = acl_set_file(path, ACL_TYPE_ACCESS, acl);
++ error = acl_set_file_at(AT_FDCWD, path,
++ AT_SYMLINK_NOFOLLOW,
++ ACL_TYPE_ACCESS, acl);
+ } else
+- error = acl_delete_def_file(path);
++ error = acl_delete_def_file_at(AT_FDCWD, path,
++ AT_SYMLINK_NOFOLLOW);
+ return(error);
+ }
+
+@@ -292,63 +299,102 @@ list_acl(char *file)
+ }
+
+ static int
+-set_acl(acl_t acl, acl_t dacl, const char *fname)
++set_acl(acl_t acl, acl_t dacl, int dirfd, const char *dname, const char *fname,
++ unsigned int depth)
+ {
++ static char *aname;
+ int failed = 0;
+
++ if (aname) {
++ free(aname);
++ aname = NULL;
++ }
++ if (asprintf(&aname, "%s%s", dname, fname) == -1) {
++ fprintf(stderr, _("%s: malloc failed: %s\n"),
++ program, strerror(errno));
++ exit(1);
++ }
++
+ if (rflag)
+- failed += walk_dir(acl, dacl, fname);
++ failed += walk_dir(acl, dacl, dirfd, dname, fname, depth);
+
+ /* set regular acl */
+- if (acl && acl_set_file(fname, ACL_TYPE_ACCESS, acl) == -1) {
+- fprintf(stderr, _("%s: cannot set access acl on \"%s\": %s\n"),
+- program, fname, strerror(errno));
+- failed++;
++ if (acl && acl_set_file_at(dirfd, fname, AT_SYMLINK_NOFOLLOW,
++ ACL_TYPE_ACCESS, acl) == -1) {
++ int saved_errno = errno;
++ struct stat st;
++
++ if (!depth || errno != ENOTSUP ||
++ fstatat(dirfd, fname, &st, AT_SYMLINK_NOFOLLOW) == -1 ||
++ !S_ISLNK(st.st_mode)) {
++ fprintf(stderr, _("%s: cannot set access acl on \"%s\": %s\n"),
++ program, aname, strerror(saved_errno));
++ failed++;
++ }
+ }
+ /* set default acl */
+- if (dacl && acl_set_file(fname, ACL_TYPE_DEFAULT, dacl) == -1) {
+- fprintf(stderr, _("%s: cannot set default acl on \"%s\": %s\n"),
+- program, fname, strerror(errno));
+- failed++;
++ if (dacl && acl_set_file_at(dirfd, fname, AT_SYMLINK_NOFOLLOW,
++ ACL_TYPE_DEFAULT, dacl) == -1) {
++ int saved_errno = errno;
++ struct stat st;
++
++ if (!depth || errno != ENOTSUP ||
++ fstatat(dirfd, fname, &st, AT_SYMLINK_NOFOLLOW) == -1 ||
++ !S_ISLNK(st.st_mode)) {
++ fprintf(stderr, _("%s: cannot set default acl on \"%s\": %s\n"),
++ program, aname, strerror(saved_errno));
++ failed++;
++ }
+ }
+
+ return(failed);
+ }
+
+ static int
+-walk_dir(acl_t acl, acl_t dacl, const char *fname)
++walk_dir(acl_t acl, acl_t dacl, int dirfd, const char *dname, const char *fname,
++ unsigned int depth)
+ {
+ int failed = 0;
+ DIR *dir;
+ struct dirent *d;
+- char *name;
++ char *dname2;
++ int fd;
++
++ depth++;
+
+- if ((dir = opendir(fname)) == NULL) {
+- if (errno != ENOTDIR) {
+- fprintf(stderr, _("%s: opendir failed: %s\n"),
++ fd = openat(dirfd, fname, O_DIRECTORY | O_NOFOLLOW);
++ if (fd == -1) {
++ if (errno != ENOTDIR && errno != ELOOP) {
++ fprintf(stderr, _("%s: openat failed: %s\n"),
+ program, strerror(errno));
+ return(1);
+ }
+ return(0); /* got a file, not an error */
+ }
++ if ((dir = fdopendir(fd)) == NULL) {
++ fprintf(stderr, _("%s: opendir failed: %s\n"),
++ program, strerror(errno));
++ return(1);
++ }
++
++ dname2 = malloc(strlen(dname) + strlen(fname) + 2);
++ if (dname2 == NULL) {
++ fprintf(stderr, _("%s: malloc failed: %s\n"),
++ program, strerror(errno));
++ exit(1);
++ }
++ sprintf(dname2, "%s%s/", dname, fname);
+
+ while ((d = readdir(dir)) != NULL) {
+ /* skip "." and ".." entries */
+ if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0)
+ continue;
+
+- name = malloc(strlen(fname) + strlen(d->d_name) + 2);
+- if (name == NULL) {
+- fprintf(stderr, _("%s: malloc failed: %s\n"),
+- program, strerror(errno));
+- exit(1);
+- }
+- sprintf(name, "%s/%s", fname, d->d_name);
+-
+- failed += set_acl(acl, dacl, name);
+- free(name);
++ failed += set_acl(acl, dacl, fd, dname2, d->d_name, depth);
+ }
+ closedir(dir);
+
++ free(dname2);
++
+ return(failed);
+ }
+--
+2.35.6
+
@@ -24,6 +24,14 @@ SRC_URI = "${SAVANNAH_GNU_MIRROR}/acl/${BP}.tar.gz \
file://CVE-2026-54369_p1.patch \
file://CVE-2026-54369_p2.patch \
file://CVE-2026-54369_p3.patch \
+ file://CVE-2026-54370-dependent_p1.patch \
+ file://CVE-2026-54370-dependent_p2.patch \
+ file://CVE-2026-54370-dependent_p3.patch \
+ file://CVE-2026-54370-dependent_p4.patch \
+ file://CVE-2026-54370_p1.patch \
+ file://CVE-2026-54370_p2.patch \
+ file://CVE-2026-54370_p3.patch \
+ file://CVE-2026-54370_p4.patch \
file://run-ptest \
"
SRC_URI[sha256sum] = "5f2bdbad629707aa7d85c623f994aa8a1d2dec55a73de5205bac0bf6058a2f7c"