changeset 12039:eacd308b94de

lstat: fix Solaris 9 bug lstat("file/",buf) mistakenly succeeded. * lib/lstat.c (lstat): Also check for trailing slash on non-symlink, non-directories. Use stat module to simplify logic. * doc/posix-functions/lstat.texi (lstat): Document it. * modules/lstat-tests (Depends-on): Add errno, same-inode. (configure.ac): Check for symlink. * tests/test-lstat.c (main): Add more tests. Signed-off-by: Eric Blake <ebb9@byu.net>
author Eric Blake <ebb9@byu.net>
date Thu, 17 Sep 2009 15:55:24 -0600
parents ac2a6371d78c
children e8108d5c7ca7
files ChangeLog doc/posix-functions/lstat.texi lib/lstat.c modules/lstat-tests tests/test-lstat.c
diffstat 5 files changed, 159 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
 2009-09-19  Eric Blake  <ebb9@byu.net>
 
+	lstat: fix Solaris 9 bug
+	* lib/lstat.c (lstat): Also check for trailing slash on
+	non-symlink, non-directories.  Use stat module to simplify logic.
+	* doc/posix-functions/lstat.texi (lstat): Document it.
+	* modules/lstat-tests (Depends-on): Add errno, same-inode.
+	(configure.ac): Check for symlink.
+	* tests/test-lstat.c (main): Add more tests.
+
 	stat: add as dependency to other modules
 	* modules/chown (Depends-on): Add stat.
 	* modules/euidaccess (Depends-on): Likewise.
--- a/doc/posix-functions/lstat.texi
+++ b/doc/posix-functions/lstat.texi
@@ -9,8 +9,13 @@
 Portability problems fixed by Gnulib:
 @itemize
 @item
-When the argument ends in a slash, some platforms don't dereference the
-argument.
+For symlinks, when the argument ends in a slash, some platforms don't
+dereference the argument:
+Solaris 9.
+@item
+On some platforms, @code{lstat("file/",buf)} succeeds instead of
+failing with @code{ENOTDIR}.
+Solaris 9.
 @item
 On Windows platforms (excluding Cygwin), symlinks are not supported, so
 @code{lstat} does not exist.
@@ -22,4 +27,12 @@
 On platforms where @code{off_t} is a 32-bit type, @code{lstat} may not
 correctly report the size of files or block devices larger than 2 GB.  The fix
 is to use the @code{AC_SYS_LARGEFILE} macro.
+@item
+On Windows platforms (excluding Cygwin), @code{st_ino} is always 0.
+@item
+Because of the definition of @code{struct stat}, it is not possible to
+portably replace @code{stat} via an object-like macro.  Therefore,
+expressions such as @code{(islnk ? lstat : stat) (name, buf)} are not
+portable, and should instead be written @code{islnk ? lstat (name,
+buf) : stat (name, buf)}.
 @end itemize
--- a/lib/lstat.c
+++ b/lib/lstat.c
@@ -1,6 +1,7 @@
 /* Work around a bug of lstat on some systems
 
-   Copyright (C) 1997-1999, 2000-2006, 2008 Free Software Foundation, Inc.
+   Copyright (C) 1997-1999, 2000-2006, 2008-2009 Free Software
+   Foundation, Inc.
 
    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
@@ -19,7 +20,7 @@
 
 #include <config.h>
 
-/* Get the original definition of open.  It might be defined as a macro.  */
+/* Get the original definition of lstat.  It might be defined as a macro.  */
 #define __need_system_sys_stat_h
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -56,27 +57,27 @@
   size_t len;
   int lstat_result = orig_lstat (file, sbuf);
 
-  if (lstat_result != 0 || !S_ISLNK (sbuf->st_mode))
+  if (lstat_result != 0)
     return lstat_result;
 
+  /* This replacement file can blindly check against '/' rather than
+     using the ISSLASH macro, because all platforms with '\\' either
+     lack symlinks (mingw) or have working lstat (cygwin) and thus do
+     not compile this file.  0 len should have already been filtered
+     out above, with a failure return of ENOENT.  */
   len = strlen (file);
-  if (len == 0 || file[len - 1] != '/')
+  if (file[len - 1] != '/' || S_ISDIR (sbuf->st_mode))
     return 0;
 
-  /* FILE refers to a symbolic link and the name ends with a slash.
-     Call stat() to get info about the link's referent.  */
-
-  /* If stat fails, then we do the same.  */
-  if (stat (file, sbuf) != 0)
-    return -1;
-
-  /* If FILE references a directory, return 0.  */
-  if (S_ISDIR (sbuf->st_mode))
-    return 0;
-
-  /* Here, we know stat succeeded and FILE references a non-directory.
-     But it was specified via a name including a trailing slash.
-     Fail with errno set to ENOTDIR to indicate the contradiction.  */
-  errno = ENOTDIR;
-  return -1;
+  /* At this point, a trailing slash is only permitted on
+     symlink-to-dir; but it should have found information on the
+     directory, not the symlink.  Call stat() to get info about the
+     link's referent.  Our replacement stat guarantees valid results,
+     even if the symlink is not pointing to a directory.  */
+  if (!S_ISLNK (sbuf->st_mode))
+    {
+      errno = ENOTDIR;
+      return -1;
+    }
+  return stat (file, sbuf);
 }
--- a/modules/lstat-tests
+++ b/modules/lstat-tests
@@ -2,8 +2,11 @@
 tests/test-lstat.c
 
 Depends-on:
+errno
+same-inode
 
 configure.ac:
+AC_CHECK_FUNCS_ONCE([symlink])
 
 Makefile.am:
 TESTS += test-lstat
--- a/tests/test-lstat.c
+++ b/tests/test-lstat.c
@@ -1,5 +1,5 @@
 /* Test of lstat() function.
-   Copyright (C) 2008 Free Software Foundation, Inc.
+   Copyright (C) 2008, 2009 Free Software Foundation, Inc.
 
    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
@@ -14,24 +14,129 @@
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
-/* Written by Simon Josefsson, 2008.  */
+/* Written by Simon Josefsson, 2008; and Eric Blake, 2009.  */
 
 #include <config.h>
 
 #include <sys/stat.h>
 
+#include <fcntl.h>
+#include <errno.h>
 #include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "same-inode.h"
+
+#if !HAVE_SYMLINK
+# define symlink(a,b) (-1)
+#endif
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+	{                                                                    \
+	  fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__);  \
+	  fflush (stderr);                                                   \
+	  abort ();                                                          \
+	}                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-lstat.t"
 
 int
-main (int argc, char **argv)
+main ()
 {
-  struct stat buf;
+  struct stat st1;
+  struct stat st2;
+
+  /* Remove any leftovers from a previous partial run.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  /* Test for common directories.  */
+  ASSERT (lstat (".", &st1) == 0);
+  ASSERT (lstat ("./", &st2) == 0);
+  ASSERT (SAME_INODE (st1, st2));
+  ASSERT (S_ISDIR (st1.st_mode));
+  ASSERT (S_ISDIR (st2.st_mode));
+  ASSERT (lstat ("/", &st1) == 0);
+  ASSERT (lstat ("///", &st2) == 0);
+  ASSERT (SAME_INODE (st1, st2));
+  ASSERT (S_ISDIR (st1.st_mode));
+  ASSERT (S_ISDIR (st2.st_mode));
+  ASSERT (lstat ("..", &st1) == 0);
+  ASSERT (S_ISDIR (st1.st_mode));
+
+  /* Test for error conditions.  */
+  errno = 0;
+  ASSERT (lstat ("", &st1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (lstat ("nosuch", &st1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (lstat ("nosuch/", &st1) == -1);
+  ASSERT (errno == ENOENT);
+
+  ASSERT (close (creat (BASE "file", 0600)) == 0);
+  ASSERT (lstat (BASE "file", &st1) == 0);
+  ASSERT (S_ISREG (st1.st_mode));
+  errno = 0;
+  ASSERT (lstat (BASE "file/", &st1) == -1);
+  ASSERT (errno == ENOTDIR);
 
-  if (lstat ("/", &buf) != 0)
+  /* Now for some symlink tests, where supported.  We set up:
+     link1 -> directory
+     link2 -> file
+     link3 -> dangling
+     link4 -> loop
+     then test behavior both with and without trailing slash.
+  */
+  if (symlink (".", BASE "link1") != 0)
     {
-      perror ("lstat");
-      return 1;
+      ASSERT (unlink (BASE "file") == 0);
+      fputs ("skipping test: symlinks not supported on this filesystem\n",
+             stderr);
+      return 77;
     }
+  ASSERT (symlink (BASE "file", BASE "link2") == 0);
+  ASSERT (symlink (BASE "nosuch", BASE "link3") == 0);
+  ASSERT (symlink (BASE "link4", BASE "link4") == 0);
+
+  ASSERT (lstat (BASE "link1", &st1) == 0);
+  ASSERT (S_ISLNK (st1.st_mode));
+  ASSERT (lstat (BASE "link1/", &st1) == 0);
+  ASSERT (stat (BASE "link1", &st2) == 0);
+  ASSERT (S_ISDIR (st1.st_mode));
+  ASSERT (S_ISDIR (st2.st_mode));
+  ASSERT (SAME_INODE (st1, st2));
+
+  ASSERT (lstat (BASE "link2", &st1) == 0);
+  ASSERT (S_ISLNK (st1.st_mode));
+  errno = 0;
+  ASSERT (lstat (BASE "link2/", &st1) == -1);
+  ASSERT (errno == ENOTDIR);
+
+  ASSERT (lstat (BASE "link3", &st1) == 0);
+  ASSERT (S_ISLNK (st1.st_mode));
+  errno = 0;
+  ASSERT (lstat (BASE "link3/", &st1) == -1);
+  ASSERT (errno == ENOENT);
+
+  ASSERT (lstat (BASE "link4", &st1) == 0);
+  ASSERT (S_ISLNK (st1.st_mode));
+  errno = 0;
+  ASSERT (lstat (BASE "link4/", &st1) == -1);
+  ASSERT (errno == ELOOP);
+
+  /* Cleanup.  */
+  ASSERT (unlink (BASE "file") == 0);
+  ASSERT (unlink (BASE "link1") == 0);
+  ASSERT (unlink (BASE "link2") == 0);
+  ASSERT (unlink (BASE "link3") == 0);
+  ASSERT (unlink (BASE "link4") == 0);
 
   return 0;
 }