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
