On 2025-11-26 09:05, Pavel Cahyna wrote:
I think though that the latter shows that the errno reported by the
openat2 module should be more consistent. I.e. that ENOTCAPABLE should
be translated to EXDEV.

Thanks, good idea. I installed the attached.

From d1aeb7388926e045bdec0f7934c5522c4745f02c Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Wed, 26 Nov 2025 13:08:34 -0800
Subject: [PATCH] =?UTF-8?q?openat2:=20don=E2=80=99t=20fail=20with=20ENOTCA?=
 =?UTF-8?q?PABLE=20on=20FreeBSD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Problem reported by Pavel Cahyna in:
https://lists.gnu.org/r/bug-gnulib/2025-11/msg00257.html
* lib/openat2.c (ENOTCAPABLE): Default to 0.
(do_openat2, openat2): Map FreeBSD’s ENOTCAPABLE to GNU/Linux’s EXDEV.
* tests/test-openat2.c (do_prepare_symlinks, do_test_resolve)
(do-test-basic): Add related tests.
---
 ChangeLog            | 10 ++++++++++
 lib/openat2.c        | 15 +++++++++++++--
 tests/test-openat2.c | 27 +++++++++++++++++++++++++++
 3 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index e6c596a9cf..8019977c73 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2025-11-26  Paul Eggert  <[email protected]>
+
+	openat2: don’t fail with ENOTCAPABLE on FreeBSD
+	Problem reported by Pavel Cahyna in:
+	https://lists.gnu.org/r/bug-gnulib/2025-11/msg00257.html
+	* lib/openat2.c (ENOTCAPABLE): Default to 0.
+	(do_openat2, openat2): Map FreeBSD’s ENOTCAPABLE to GNU/Linux’s EXDEV.
+	* tests/test-openat2.c (do_prepare_symlinks, do_test_resolve)
+	(do_test_basic): Add related tests.
+
 2025-11-25  Paul Eggert  <[email protected]>
 
 	threads-h: have mtx_lock join the throng
diff --git a/lib/openat2.c b/lib/openat2.c
index d616b80a55..d8087a0d2a 100644
--- a/lib/openat2.c
+++ b/lib/openat2.c
@@ -60,6 +60,10 @@
 # define E2BIG EINVAL
 #endif
 
+#ifndef ENOTCAPABLE /* A FreeBSD error number.  */
+# define ENOTCAPABLE 0
+#endif
+
 #ifndef PATH_MAX
 # define PATH_MAX IDX_MAX
 #endif
@@ -333,6 +337,8 @@ do_openat2 (int *fd, char const *filename,
           if (subfd < 0)
             {
               int openerr = negative_errno ();
+              if (O_RESOLVE_BENEATH && openerr == -ENOTCAPABLE)
+                return -EXDEV;
               if (! ((openerr == -_GL_OPENAT_ESYMLINK)
                      | (!!(subflags & O_DIRECTORY) & (openerr == -ENOTDIR))))
                 return openerr;
@@ -553,16 +559,21 @@ openat2 (int dfd, char const *filename,
       char resolve = how->resolve;
       mode_t mode = how->mode;
 
+      int fd;
+
       /* For speed use openat if it suffices, though it is unlikely a
          caller would use openat2 when openat's simpler API would do.  */
       if (O_RESOLVE_BENEATH ? !(resolve & ~RESOLVE_BENEATH) : !resolve)
         {
           if (resolve & RESOLVE_BENEATH)
             flags |= O_RESOLVE_BENEATH;
-          return openat (dfd, filename, flags, mode);
+          fd = openat (dfd, filename, flags, mode);
+          if (O_RESOLVE_BENEATH && fd < 0 && errno == ENOTCAPABLE)
+            errno = EXDEV;
+          return fd;
         }
 
-      int fd = dfd;
+      fd = dfd;
       char stackbuf[256];
       char *buf = stackbuf;
 
diff --git a/tests/test-openat2.c b/tests/test-openat2.c
index 12830a0bc9..da2dde415e 100644
--- a/tests/test-openat2.c
+++ b/tests/test-openat2.c
@@ -98,6 +98,7 @@ do_prepare_symlinks ()
         |- valid_link -> some_file
         |- subdir/
            |- some_file
+           |- aunt_link -> ../some_file
   */
 
   ASSERT (symlinkat ("subdir", dfd, "dirlink") == 0);
@@ -107,6 +108,7 @@ do_prepare_symlinks ()
   ASSERT (symlinkat ("some_file/invalid", dfd, "invalid_link") == 0);
   ASSERT (symlinkat ("some_file", dfd, "valid_link") == 0);
   ASSERT (mkdirat (dfd, "subdir", 0700) == 0);
+  ASSERT (symlinkat ("../some_file", dfd, "subdir/aunt_link") == 0);
   ASSERT (close (openat2 (dfd, "some_file",
                           (&(struct open_how) { .flags = O_CREAT,
                                                 .mode = 0600 }),
@@ -457,6 +459,16 @@ do_test_resolve (void)
   ASSERT ((errno == ENOENT) | is_nofollow_error (errno));
   ASSERT (fd == -1);
 
+  fd = openat2 (dfd,
+                "subdir/aunt_link",
+                (&(struct open_how)
+                 {
+                   .flags = O_RDONLY,
+                   .resolve = RESOLVE_BENEATH,
+                 }),
+                sizeof (struct open_how));
+  ASSERT (close (fd) == 0);
+
   {
     int subdfd = openat2 (dfd,
                           "subdir",
@@ -489,6 +501,20 @@ do_test_resolve (void)
                    }),
                   sizeof (struct open_how));
     ASSERT (close (fd) == 0);
+
+    /* Check that aunt_link cannot escape subdir.  */
+    fd = openat2 (subdfd,
+                  "aunt_link",
+                  (&(struct open_how)
+                   {
+                     .flags = O_RDONLY,
+                     .resolve = RESOLVE_BENEATH,
+                   }),
+                  sizeof (struct open_how));
+    ASSERT (errno == EXDEV);
+    ASSERT (fd == -1);
+
+    ASSERT (close (subdfd) == 0);
   }
 }
 
@@ -531,6 +557,7 @@ do_test_basic ()
   ASSERT (unlinkat (dfd, "escaping_link_2", 0) == 0);
   ASSERT (unlinkat (dfd, "invalid_link", 0) == 0);
   ASSERT (unlinkat (dfd, "some_file", 0) == 0);
+  ASSERT (unlinkat (dfd, "subdir/aunt_link", 0) == 0);
   ASSERT (unlinkat (dfd, "subdir/some_file", 0) == 0);
   ASSERT (unlinkat (dfd, "subdir", AT_REMOVEDIR) == 0);
   ASSERT (unlinkat (dfd, "valid_link", 0) == 0);
-- 
2.51.0

Reply via email to