This bug was found when using openat2 in an experimental version
of GNU tar atop an older Linux kernel.
* lib/openat2.c (do_openat2): When splicing symlink contents not at
name end, restore the slash that previously followed the symlink name.
* tests/test-openat2.c (do_test_resolve): Add tests for the bug.
---
 ChangeLog            |  7 +++++++
 lib/openat2.c        |  8 ++++++--
 tests/test-openat2.c | 22 ++++++++++++++++++++++
 3 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 925e9597ed..8f1a59c237 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,12 @@
 2025-11-14  Paul Eggert  <[email protected]>
 
+       openat2: fix symlink splicing bug
+       This bug was found when using openat2 in an experimental version
+       of GNU tar atop an older Linux kernel.
+       * lib/openat2.c (do_openat2): When splicing symlink contents not at
+       name end, restore the slash that previously followed the symlink name.
+       * tests/test-openat2.c (do_test_resolve): Add tests for the bug.
+
        openat2: port to O_DIRECTORY before O_NOFOLLOW
        * lib/openat2.c (do_openat2): Don’t assume that openat (...,
        "symlink", O_DIRECTORY | O_NOFOLLOW | ...) will fail with ELOOP.
diff --git a/lib/openat2.c b/lib/openat2.c
index ba69cf6704..9281e7a1a2 100644
--- a/lib/openat2.c
+++ b/lib/openat2.c
@@ -322,10 +322,11 @@ do_openat2 (int *fd, char const *filename,
 
           /* Open the current component, as either an internal directory or
              the final open.  Do not follow symlinks.  */
+          char eh = e[-h];
           int subflags = ((!final
                            ? O_PATHSEARCH | O_CLOEXEC | O_CLOFORK
                            : flags)
-                          | O_NOFOLLOW | (e[-h] ? O_DIRECTORY : 0));
+                          | O_NOFOLLOW | (eh ? O_DIRECTORY : 0));
           e[-h] = '\0';
           int subfd = openat (*fd, &e[-f], subflags, mode);
 
@@ -412,7 +413,10 @@ do_openat2 (int *fd, char const *filename,
                   kept = nextf;
                 }
               else
-                kept = h;
+                {
+                  e[-h] = eh;
+                  kept = h;
+                }
 
               if (ISSLASH ('\\'))
                 slink[slinklen] = '\0';  /* For IS_RELATIVE_FILE_NAME.  */
diff --git a/tests/test-openat2.c b/tests/test-openat2.c
index 08cbac0ea5..3f9dc70039 100644
--- a/tests/test-openat2.c
+++ b/tests/test-openat2.c
@@ -371,6 +371,28 @@ do_test_resolve (void)
   ASSERT (is_nofollow_error (errno));
   ASSERT (fd == -1);
 
+  fd = openat2 (dfd,
+                "dirlink/nosuch",
+                (&(struct open_how)
+                 {
+                   .flags = O_RDONLY | O_DIRECTORY,
+                   .resolve = RESOLVE_IN_ROOT,
+                 }),
+                sizeof (struct open_how));
+  ASSERT (errno == ENOENT);
+  ASSERT (fd == -1);
+
+  fd = openat2 (dfd,
+                "dirlinkslash/nosuch",
+                (&(struct open_how)
+                 {
+                   .flags = O_RDONLY | O_DIRECTORY,
+                   .resolve = RESOLVE_IN_ROOT,
+                 }),
+                sizeof (struct open_how));
+  ASSERT (errno == ENOENT);
+  ASSERT (fd == -1);
+
   /* ESCAPING_LINK links to /tmp, which escapes the temporary test
      directory.  */
   fd = openat2 (dfd,
-- 
2.51.0


Reply via email to