Subject: bug#10639: BUG REPORT coreutils-8.15 Solaris 10 64bit

From: Paul Eggert <[email-masked]>
Date: Wed, 01 Feb 2012 23:07:22 -0800

On 02/01/2012 02:22 AM, Bruno Haible wrote:
> I find it overkill to change the code for HP-UX and NonStop systems
> when the report is about Solaris. Also I think the structure of the loop
> is not the problem; it is the code's reaction to
>   acl("x", ACE_GETACL, 4, 0x3432A16E0)            Err#-1

The patch you sent looks like it'll fix that particular bug, but while
looking into this I discovered so many bugs in this area, mostly
hard-to-test race conditions and overflows, that I thought it better
to rewrite the affected code than to try to fix each bug one at a time.
I didn't write this up very well, and my first cut at doing this could
stand some improvements of its own.  Here's a revised patch that tries
to do a better job at all this.  (If it's any consolation, this patch
makes the code simpler -- 186 lines shorter....)

diff --git a/ChangeLog b/ChangeLog
index f80c7dd..ec16bef 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,89 @@
+2012-02-01  Paul Eggert  <[email-masked]>
+
+	acl: fix several problems with ACLs
+	Problem with infinite loop on Solaris 10 + vxfs reported by Bill
+	Jones in <http://debbugs.gnu.org/cgi/bugreport.cgi?bug=10639>.
+	Looking into the code, I saw several closely related issues:
+
+	 - There's a race condition in this kind of code:
+
+	     n = acl (f, GETACLCNT, 0, NULL);
+	     [ allocate an array A of size N ]
+	     if (acl (f, GETACL, n, a) == n)
+	       return ok;
+
+	   The ACL can grow in size between the first and second calls to
+	   'acl', which means that the second 'acl' returns a truncated
+	   ACL but thinks that it has the whole thing.  To avoid this, the
+	   following pattern should be used instead:
+
+	     n = acl (f, GETACLCNT, 0, NULL);
+	     [ allocate an array A of size N + 1 ]
+	     n1 = acl (f, GETACL, n + 1, a);
+	     if (0 < n1 && n1 <= n)
+	       return ok;
+
+	 - There were several instances of this pattern:
+
+	     for (;;) {
+	       n = acl (f, GETACLCNT, 0, NULL);
+	       [ allocate an array A of size N ]
+	       if (acl (f, GETACL, n, a) == n)
+		 break;
+	     }
+
+	   This loop might never terminate if some other process is constantly
+	   manipulating the file's ACL.  The loop should be rewritten to
+	   terminate.
+
+	 - The acl (... GETACLNT ...) call is merely an optimization; its value
+	   is merely a hint as to how big to make the array.  A better
+	   optimization is to avoid the acl (... GETACLNT ...)  call entirely,
+	   and just guess a reasonably-big size, growing the size and trying
+	   again if it's not large enough.  This guarantees termination, and
+	   saves a system call.
+
+	 - With this approach, for ports like HP-UX that have an upper bound
+	   for the ACL length, there's no longer any need to loop reading the
+	   ACL.  Just read it once and you're done reading it.
+
+	 - For ports like Solaris that do need to loop (because the ACL length
+	   is not known a priori), it's faster to allocate a reasonably-sized
+	   buffer on the stack and use that, and to allocate something on the
+	   heap only if the ACL is unusually large.  This avoids malloc in the
+	   usual case.
+
+	 - The code that calculated sizes of these arrays did not check for
+	   overflow in size calculations; it should.
+
+	 - There are some memory leaks.  For example, in qcopy_acl, if acl
+	   (src_name, GETACLCNT, 0, NULL) < 0 && errno == EPERM, then the
+	   storage for ace_entries leaks.  Similarly, if acl (src_name,
+	   ACE_GETACL, ace_count, ace_entries) returns -1, the storage for
+	   ace_entries leaks.
+
+	 - In qset_acl, there's sometimes no need to read the entire ACL
+	   to determine the convention; this can save system calls when
+	   looking at very large ACLs.
+
+	Rather than fix these bugs one at a time I thought it more efficient
+	to rewrite the affected code, as follows:
+	* lib/acl-internal.h (GETACLCNT): Remove; no longer needed.
+	Include <limits.h>.
+	(MIN): New macro.
+	* lib/copy-acl.c (qcopy_acl): Don't bother to count ACL entries
+	before getting them.  Instead, just get them, and make sure that
+	the number of entries gotten is less than the number requested.
+	This avoids a race condition and omits a system call in the usual
+	case.  Before, it could have been the case that we asked for and
+	received N entries, but these were the first N of more than N
+	actual entries, if the ACL was modified while we were getting it.
+	With this change, there is no need to use GETACLCNT or similar ops.
+	Check for size-calculation overflow.
+	Avoid need for dynamic allocation if possible.
+	* lib/file-has-acl.c (file_has_acl): Likewise.
+	* lib/set-mode-acl.c (qset_acl): Likewise.
+
 2012-01-31  Karl Berry  <[email-masked]>
 
 	setstate doc: typo.
diff --git a/lib/acl-internal.h b/lib/acl-internal.h
index 88e5e45..1548ce1 100644
--- a/lib/acl-internal.h
+++ b/lib/acl-internal.h
@@ -26,9 +26,6 @@
 #if HAVE_SYS_ACL_H
 # include <sys/acl.h>
 #endif
-#if defined HAVE_FACL && ! defined GETACLCNT && defined ACL_CNT
-# define GETACLCNT ACL_CNT
-#endif
 
 /* On Linux, additional ACL related API is available in <acl/libacl.h>.  */
 #ifdef HAVE_ACL_LIBACL_H
@@ -55,6 +52,12 @@ extern int aclsort (int, int, struct acl *);
 # define ENOTSUP (-1)
 #endif
 
+#include <limits.h>
+
+#ifndef MIN
+# define MIN(a,b) ((a) < (b) ? (a) : (b))
+#endif
+
 #ifndef HAVE_FCHMOD
 # define HAVE_FCHMOD false
 # define fchmod(fd, mode) (-1)
diff --git a/lib/copy-acl.c b/lib/copy-acl.c
index 9b9f033..2c24b36 100644
--- a/lib/copy-acl.c
+++ b/lib/copy-acl.c
@@ -181,11 +181,25 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
      of Unixware.  The acl() call returns the access and default ACL both
      at once.  */
 # ifdef ACE_GETACL
+  enum {
+    ace_alloc_init = 4000 / sizeof (ace_t),
+    ace_alloc_max = MIN (INT_MAX, (size_t) -1 / sizeof (ace_t))
+  };
   int ace_count;
-  ace_t *ace_entries;
+  int ace_alloc = ace_alloc_init;
+  ace_t ace_buf[ace_alloc_init];
+  ace_t *ace_entries = ace_buf;
 # endif
+  void *ace_malloced = NULL;
+  enum {
+    acl_alloc_init = 4000 / sizeof (aclent_t),
+    acl_alloc_max = MIN (INT_MAX, (size_t) -1 / sizeof (aclent_t))
+  };
   int count;
-  aclent_t *entries;
+  int acl_alloc = acl_alloc_init;
+  aclent_t acl_buf[acl_alloc_init];
+  aclent_t *entries = acl_buf;
+  aclent_t *acl_malloced = NULL;
   int did_chmod;
   int saved_errno;
   int ret;
@@ -204,85 +218,71 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
      that the kernel will translate the ACL from one form to the other.
      (See in <http://docs.sun.com/app/docs/doc/819-2241/6n4huc7ia?l=en&a=view>
      the description of ENOTSUP.)  */
-  for (;;)
+  while ((ace_count = (source_desc != -1
+                       ? facl (source_desc, ACE_GETACL, ace_alloc, ace_entries)
+                       : acl (src_name, ACE_GETACL, ace_alloc, ace_entries)))
+         == ace_alloc)
     {
-      ace_count = (source_desc != -1
-                   ? facl (source_desc, ACE_GETACLCNT, 0, NULL)
-                   : acl (src_name, ACE_GETACLCNT, 0, NULL));
-
-      if (ace_count < 0)
-        {
-          if (errno == ENOSYS || errno == EINVAL)
-            {
-              ace_count = 0;
-              ace_entries = NULL;
-              break;
-            }
-          else
-            return -2;
-        }
-
-      if (ace_count == 0)
-        {
-          ace_entries = NULL;
-          break;
-        }
-
-      ace_entries = (ace_t *) malloc (ace_count * sizeof (ace_t));
-      if (ace_entries == NULL)
+      free (ace_malloced);
+      ace_alloc = (ace_alloc <= ace_alloc_max / 2
+                   ? 2 * ace_alloc
+                   : ace_alloc_max);
+      ace_entries = ace_malloced = (ace_count == ace_alloc_max
+                                    ? NULL
+                                    : malloc (ace_alloc * sizeof (ace_t)));
+      if (! ace_malloced)
         {
           errno = ENOMEM;
           return -2;
         }
-
-      if ((source_desc != -1
-           ? facl (source_desc, ACE_GETACL, ace_count, ace_entries)
-           : acl (src_name, ACE_GETACL, ace_count, ace_entries))
-          == ace_count)
-        break;
-      /* Huh? The number of ACL entries changed since the last call.
-         Repeat.  */
     }
-# endif
 
-  for (;;)
+  if (ace_count < 0)
     {
-      count = (source_desc != -1
-               ? facl (source_desc, GETACLCNT, 0, NULL)
-               : acl (src_name, GETACLCNT, 0, NULL));
-
-      if (count < 0)
+      int ace_errno = errno;
+      if (! (ace_errno == ENOSYS || ace_errno == EINVAL))
         {
-          if (errno == ENOSYS || errno == ENOTSUP || errno == EOPNOTSUPP)
-            {
-              count = 0;
-              entries = NULL;
-              break;
-            }
-          else
-            return -2;
+          free (ace_malloced);
+          errno = ace_errno;
+          return -2;
         }
+      ace_count = 0;
+    }
 
-      if (count == 0)
-        {
-          entries = NULL;
-          break;
-        }
+# endif
 
-      entries = (aclent_t *) malloc (count * sizeof (aclent_t));
-      if (entries == NULL)
+  while ((count = (source_desc != -1
+                   ? facl (source_desc, GETACL, acl_alloc, entries)
+                   : acl (src_name, GETACL, acl_alloc, entries)))
+         == acl_alloc)
+    {
+      free (acl_malloced);
+      acl_alloc = (acl_alloc <= acl_alloc_max / 2
+                   ? 2 * acl_alloc
+                   : acl_alloc_max);
+      entries = acl_malloced = (count == acl_alloc_max
+                                ? NULL
+                                : malloc (acl_alloc * sizeof (aclent_t)));
+      if (! acl_malloced)
         {
+          free (ace_malloced);
           errno = ENOMEM;
           return -2;
         }
+    }
 
-      if ((source_desc != -1
-           ? facl (source_desc, GETACL, count, entries)
-           : acl (src_name, GETACL, count, entries))
-          == count)
-        break;
-      /* Huh? The number of ACL entries changed since the last call.
-         Repeat.  */
+  if (count < 0)
+    {
+      int acl_errno = errno;
+      if (! (acl_errno == ENOSYS || acl_errno == ENOTSUP
+             || acl_errno == EOPNOTSUPP))
+        {
+          free (ace_malloced);
+          free (acl_malloced);
+          errno = acl_errno;
+          return -2;
+        }
+      count = 0;
     }
 
   /* Is there an ACL of either kind?  */
@@ -290,7 +290,11 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
   if (ace_count == 0)
 # endif
     if (count == 0)
-      return qset_acl (dst_name, dest_desc, mode);
+      {
+        free (ace_malloced);
+        free (acl_malloced);
+        return qset_acl (dst_name, dest_desc, mode);
+      }
 
   did_chmod = 0; /* set to 1 once the mode bits in 0777 have been set */
   saved_errno = 0; /* the first non-ignorable error code */
@@ -324,7 +328,7 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
       else
         did_chmod = 1;
     }
-  free (entries);
+  free (acl_malloced);
 
 # ifdef ACE_GETACL
   if (ace_count > 0)
@@ -340,7 +344,7 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
             saved_errno = 0;
         }
     }
-  free (ace_entries);
+  free (ace_malloced);
 # endif
 
   if (MODE_INSIDE_ACL
@@ -366,77 +370,44 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
 #elif USE_ACL && HAVE_GETACL /* HP-UX */
 
   int count;
-  struct acl_entry entries[NACLENTRIES];
+  struct acl_entry entries[NACLENTRIES + 1];
 # if HAVE_ACLV_H
   int aclv_count;
-  struct acl aclv_entries[NACLVENTRIES];
+  struct acl aclv_entries[NACLVENTRIES + 1];
 # endif
   int did_chmod;
   int saved_errno;
   int ret;
 
-  for (;;)
-    {
-      count = (source_desc != -1
-               ? fgetacl (source_desc, 0, NULL)
-               : getacl (src_name, 0, NULL));
+  count = (source_desc != -1
+           ? fgetacl (source_desc, NACLENTRIES + 1, entries)
+           : getacl (src_name, NACLENTRIES + 1, entries));
 
-      if (count < 0)
-        {
-          if (errno == ENOSYS || errno == EOPNOTSUPP || errno == ENOTSUP)
-            {
-              count = 0;
-              break;
-            }
-          else
-            return -2;
-        }
-
-      if (count == 0)
-        break;
-
-      if (count > NACLENTRIES)
-        /* If NACLENTRIES cannot be trusted, use dynamic memory allocation.  */
-        abort ();
-
-      if ((source_desc != -1
-           ? fgetacl (source_desc, count, entries)
-           : getacl (src_name, count, entries))
-          == count)
-        break;
-      /* Huh? The number of ACL entries changed since the last call.
-         Repeat.  */
+  if (count < 0)
+    {
+      if (! (errno == ENOSYS || errno == EOPNOTSUPP || errno == ENOTSUP))
+        return -2;
+      count = 0;
     }
 
-# if HAVE_ACLV_H
-  for (;;)
-    {
-      aclv_count = acl ((char *) src_name, ACL_CNT, NACLVENTRIES, aclv_entries);
+  if (NACLENTRIES < count)
+    /* If NACLENTRIES cannot be trusted, use dynamic memory allocation.  */
+    abort ();
 
-      if (aclv_count < 0)
-        {
-          if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL)
-            {
-              count = 0;
-              break;
-            }
-          else
-            return -2;
-        }
+# if HAVE_ACLV_H
+  aclv_count = acl ((char *) src_name, ACL_GET, NACLVENTRIES + 1, aclv_entries);
 
-      if (aclv_count == 0)
-        break;
+  if (aclv_count < 0)
+    {
+      if (! (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL))
+        return -2;
+      aclv_count = 0;
+    }
 
-      if (aclv_count > NACLVENTRIES)
-        /* If NACLVENTRIES cannot be trusted, use dynamic memory allocation.  */
-        abort ();
+  if (NACLVENTRIES < aclv_count)
+    /* If NACLVENTRIES cannot be trusted, use dynamic memory allocation.  */
+    abort ();
 
-      if (acl ((char *) src_name, ACL_GET, aclv_count, aclv_entries)
-          == aclv_count)
-        break;
-      /* Huh? The number of ACL entries changed since the last call.
-         Repeat.  */
-    }
 # endif
 
   if (count == 0)
@@ -546,37 +517,16 @@ qcopy_acl (const char *src_name, int source_desc, const char *dst_name,
 
 #elif USE_ACL && HAVE_ACLSORT /* NonStop Kernel */
 
-  int count;
-  struct acl entries[NACLENTRIES];
+  struct acl entries[NACLENTRIES + 1];
+  int count = acl ((char *) src_name, ACL_GET, NACLENTRIES + 1, entries);
   int ret;
 
-  for (;;)
-    {
-      count = acl ((char *) src_name, ACL_CNT, NACLENTRIES, NULL);
-
-      if (count < 0)
-        {
-          if (0)
-            {
-              count = 0;
-              break;
-            }
-          else
-            return -2;
-        }
-
-      if (count == 0)
-        break;
-
-      if (count > NACLENTRIES)
-        /* If NACLENTRIES cannot be trusted, use dynamic memory allocation.  */
-        abort ();
+  if (count < 0)
+    return -2;
 
-      if (acl ((char *) src_name, ACL_GET, count, entries) == count)
-        break;
-      /* Huh? The number of ACL entries changed since the last call.
-         Repeat.  */
-    }
+  if (NACLENTRIES < count)
+    /* If NACLENTRIES cannot be trusted, use dynamic memory allocation.  */
+    abort ();
 
   if (count == 0)
     return qset_acl (dst_name, dest_desc, mode);
diff --git a/lib/file-has-acl.c b/lib/file-has-acl.c
index b7c1484..41e750f 100644
--- a/lib/file-has-acl.c
+++ b/lib/file-has-acl.c
@@ -518,7 +518,7 @@ file_has_acl (char const *name, struct stat const *sb)
         return ACL_NOT_WELL_SUPPORTED (errno) ? 0 : -1;
       return ret;
 
-# elif HAVE_FACL && defined GETACLCNT /* Solaris, Cygwin, not HP-UX */
+# elif HAVE_FACL && defined GETACL /* Solaris, Cygwin, not HP-UX */
 
 #  if defined ACL_NO_TRIVIAL
 
@@ -532,107 +532,34 @@ file_has_acl (char const *name, struct stat const *sb)
       /* Solaris 2.5 through Solaris 10, Cygwin, and contemporaneous versions
          of Unixware.  The acl() call returns the access and default ACL both
          at once.  */
-      int count;
-      {
-        aclent_t *entries;
-
-        for (;;)
-          {
-            count = acl (name, GETACLCNT, 0, NULL);
-
-            if (count < 0)
-              {
-                if (errno == ENOSYS || errno == ENOTSUP)
-                  break;
-                else
-                  return -1;
-              }
-
-            if (count == 0)
-              break;
-
-            /* Don't use MIN_ACL_ENTRIES:  It's set to 4 on Cygwin, but Cygwin
-               returns only 3 entries for files with no ACL.  But this is safe:
-               If there are more than 4 entries, there cannot be only the
-               "user::", "group::", "other:", and "mask:" entries.  */
-            if (count > 4)
-              return 1;
-
-            entries = (aclent_t *) malloc (count * sizeof (aclent_t));
-            if (entries == NULL)
-              {
-                errno = ENOMEM;
-                return -1;
-              }
-            if (acl (name, GETACL, count, entries) == count)
-              {
-                if (acl_nontrivial (count, entries))
-                  {
-                    free (entries);
-                    return 1;
-                  }
-                free (entries);
-                break;
-              }
-            /* Huh? The number of ACL entries changed since the last call.
-               Repeat.  */
-            free (entries);
-          }
-      }
+      aclent_t entries[5];
+      int count = acl (name, GETACL, 5, entries);
+      if (0 <= count)
+        return acl_nontrivial (count, entries);
+      if (! (errno == ENOSYS || errno == ENOTSUP))
+        return -1;
 
 #   ifdef ACE_GETACL
+
       /* Solaris also has a different variant of ACLs, used in ZFS and NFSv4
-         file systems (whereas the other ones are used in UFS file systems).  */
-      {
-        ace_t *entries;
-
-        for (;;)
-          {
-            count = acl (name, ACE_GETACLCNT, 0, NULL);
-
-            if (count < 0)
-              {
-                if (errno == ENOSYS || errno == EINVAL)
-                  break;
-                else
-                  return -1;
-              }
-
-            if (count == 0)
-              break;
-
-            /* In the old (original Solaris 10) convention:
-               If there are more than 3 entries, there cannot be only the
-               ACE_OWNER, ACE_GROUP, ACE_OTHER entries.
-               In the newer Solaris 10 and Solaris 11 convention:
-               If there are more than 6 entries, there cannot be only the
-               ACE_OWNER, ACE_GROUP, ACE_EVERYONE entries, each once with
-               NEW_ACE_ACCESS_ALLOWED_ACE_TYPE and once with
-               NEW_ACE_ACCESS_DENIED_ACE_TYPE.  */
-            if (count > 6)
-              return 1;
-
-            entries = (ace_t *) malloc (count * sizeof (ace_t));
-            if (entries == NULL)
-              {
-                errno = ENOMEM;
-                return -1;
-              }
-            if (acl (name, ACE_GETACL, count, entries) == count)
-              {
-                if (acl_ace_nontrivial (count, entries))
-                  {
-                    free (entries);
-                    return 1;
-                  }
-                free (entries);
-                break;
-              }
-            /* Huh? The number of ACL entries changed since the last call.
-               Repeat.  */
-            free (entries);
-          }
-      }
+         file systems (whereas the other ones are used in UFS file systems).
+
+         In the old (original Solaris 10) convention:
+         If there are more than 3 entries, there cannot be only the
+         ACE_OWNER, ACE_GROUP, ACE_OTHER entries.
+         In the newer Solaris 10 and Solaris 11 convention:
+         If there are more than 6 entries, there cannot be only the
+         ACE_OWNER, ACE_GROUP, ACE_EVERYONE entries, each once with
+         NEW_ACE_ACCESS_ALLOWED_ACE_TYPE and once with
+         NEW_ACE_ACCESS_DENIED_ACE_TYPE.  */
+
+      ace_t entries[7];
+      int count = acl (name, ACE_GETACL, 7, entries);
+      if (0 <= count)
+        return acl_ace_nontrivial (count, entries);
+      if (! (errno == ENOSYS || errno == EINVAL))
+        return -1;
+
 #   endif
 
       return 0;
@@ -640,87 +567,48 @@ file_has_acl (char const *name, struct stat const *sb)
 
 # elif HAVE_GETACL /* HP-UX */
 
-      for (;;)
-        {
-          int count;
-          struct acl_entry entries[NACLENTRIES];
-
-          count = getacl (name, 0, NULL);
-
-          if (count < 0)
-            {
-              /* ENOSYS is seen on newer HP-UX versions.
-                 EOPNOTSUPP is typically seen on NFS mounts.
-                 ENOTSUP was seen on Quantum StorNext file systems (cvfs).  */
-              if (errno == ENOSYS || errno == EOPNOTSUPP || errno == ENOTSUP)
-                break;
-              else
-                return -1;
-            }
-
-          if (count == 0)
-            return 0;
-
-          if (count > NACLENTRIES)
-            /* If NACLENTRIES cannot be trusted, use dynamic memory
-               allocation.  */
-            abort ();
+      struct acl_entry entries[4];
+      int count = getacl (name, 4, entries);
 
+      if (count < 0)
+        {
+          /* ENOSYS is seen on newer HP-UX versions.
+             EOPNOTSUPP is typically seen on NFS mounts.
+             ENOTSUP was seen on Quantum StorNext file systems (cvfs).  */
+          if (! (errno == ENOSYS || errno == EOPNOTSUPP || errno == ENOTSUP))
+            return -1;
+        }
+      else if (count == 0)
+        return 0;
+      elif (count > 3)
+        {
           /* If there are more than 3 entries, there cannot be only the
              (uid,%), (%,gid), (%,%) entries.  */
-          if (count > 3)
-            return 1;
-
-          if (getacl (name, count, entries) == count)
-            {
-              struct stat statbuf;
-
-              if (stat (name, &statbuf) < 0)
-                return -1;
-
-              return acl_nontrivial (count, entries, &statbuf);
-            }
-          /* Huh? The number of ACL entries changed since the last call.
-             Repeat.  */
+          return 1;
         }
-
-#  if HAVE_ACLV_H /* HP-UX >= 11.11 */
-
-      for (;;)
+      else
         {
-          int count;
-          struct acl entries[NACLVENTRIES];
-
-          count = acl ((char *) name, ACL_CNT, NACLVENTRIES, entries);
+          struct stat statbuf;
 
-          if (count < 0)
-            {
-              /* EOPNOTSUPP is seen on NFS in HP-UX 11.11, 11.23.
-                 EINVAL is seen on NFS in HP-UX 11.31.  */
-              if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL)
-                break;
-              else
-                return -1;
-            }
-
-          if (count == 0)
-            return 0;
+          if (stat (name, &statbuf) < 0)
+            return -1;
 
-          if (count > NACLVENTRIES)
-            /* If NACLVENTRIES cannot be trusted, use dynamic memory
-               allocation.  */
-            abort ();
+          return acl_nontrivial (count, entries, &statbuf);
+        }
 
-          /* If there are more than 4 entries, there cannot be only the
-             four base ACL entries.  */
-          if (count > 4)
-            return 1;
+#  if HAVE_ACLV_H /* HP-UX >= 11.11 */
 
-          if (acl ((char *) name, ACL_GET, count, entries) == count)
-            return aclv_nontrivial (count, entries);
-          /* Huh? The number of ACL entries changed since the last call.
-             Repeat.  */
-        }
+      {
+        struct acl entries[5];
+        int count = acl ((char *) name, ACL_GET, 5, entries);
+        if (0 <= count)
+          return aclv_nontrivial (count, entries);
+
+        /* EOPNOTSUPP is seen on NFS in HP-UX 11.11, 11.23.
+           EINVAL is seen on NFS in HP-UX 11.31.  */
+        if (! (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL))
+          return -1;
+      }
 
 #  endif
 
@@ -798,38 +686,12 @@ file_has_acl (char const *name, struct stat const *sb)
 # elif HAVE_ACLSORT /* NonStop Kernel */
 
       int count;
-      struct acl entries[NACLENTRIES];
-
-      for (;;)
-        {
-          count = acl ((char *) name, ACL_CNT, NACLENTRIES, NULL);
-
-          if (count < 0)
-            {
-              if (errno == ENOSYS || errno == ENOTSUP)
-                break;
-              else
-                return -1;
-            }
-
-          if (count == 0)
-            return 0;
-
-          if (count > NACLENTRIES)
-            /* If NACLENTRIES cannot be trusted, use dynamic memory
-               allocation.  */
-            abort ();
-
-          /* If there are more than 4 entries, there cannot be only the
-             four base ACL entries.  */
-          if (count > 4)
-            return 1;
-
-          if (acl ((char *) name, ACL_GET, count, entries) == count)
-            return acl_nontrivial (count, entries);
-          /* Huh? The number of ACL entries changed since the last call.
-             Repeat.  */
-        }
+      struct acl entries[5];
+      int count = acl ((char *) name, ACL_GET, 5, entries);
+      if (0 <= count)
+        return acl_nontrivial (count, entries);
+      if (! (errno == ENOSYS || errno == ENOTSUP))
+        return -1;
 
 # endif
     }
diff --git a/lib/set-mode-acl.c b/lib/set-mode-acl.c
index a81b321..a1df9be 100644
--- a/lib/set-mode-acl.c
+++ b/lib/set-mode-acl.c
@@ -197,7 +197,7 @@ qset_acl (char const *name, int desc, mode_t mode)
   return chmod_or_fchmod (name, desc, mode);
 #  endif
 
-# elif HAVE_FACL && defined GETACLCNT /* Solaris, Cygwin, not HP-UX */
+# elif HAVE_FACL && defined GETACL /* Solaris, Cygwin, not HP-UX */
 
   int done_setacl = 0;
 
@@ -214,30 +214,23 @@ qset_acl (char const *name, int desc, mode_t mode)
   int convention;
 
   {
-    int count;
-    ace_t *entries;
+    enum {
+      alloc_init = 4000 / sizeof (ace_t),
+      alloc_max = MIN (INT_MAX, (size_t) -1 / sizeof (ace_t))
+    };
+    int alloc = alloc_init;
+    ace_t buf[alloc_init];
+    ace_t *entries = buf;
+    ace_t *malloced = NULL;
 
     for (;;)
       {
-        if (desc != -1)
-          count = facl (desc, ACE_GETACLCNT, 0, NULL);
-        else
-          count = acl (name, ACE_GETACLCNT, 0, NULL);
+        int count = (desc != -1
+                     ? facl (desc, ACE_GETACL, alloc, entries)
+                     : acl (name, ACE_GETACL, alloc, entries));
         if (count <= 0)
-          {
-            convention = -1;
-            break;
-          }
-        entries = (ace_t *) malloc (count * sizeof (ace_t));
-        if (entries == NULL)
-          {
-            errno = ENOMEM;
-            return -1;
-          }
-        if ((desc != -1
-             ? facl (desc, ACE_GETACL, count, entries)
-             : acl (name, ACE_GETACL, count, entries))
-            == count)
+          convention = -1;
+        else
           {
             int i;
 
@@ -248,12 +241,18 @@ qset_acl (char const *name, int desc, mode_t mode)
                   convention = 1;
                   break;
                 }
-            free (entries);
-            break;
           }
-        /* Huh? The number of ACL entries changed since the last call.
-           Repeat.  */
-        free (entries);
+        free (malloced);
+        if (count < alloc || convention != 0)
+          break;
+        alloc = alloc <= alloc_max / 2 ? 2 * alloc : alloc_max;
+        entries = malloced =
+          count == alloc_max ? NULL : malloc (alloc * sizeof *entries);
+        if (! malloced)
+          {
+            errno = ENOMEM;
+            return -1;
+          }
       }
   }